Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01f1a7c3d0 | |||
| d2a7199b07 | |||
| 06ec97c08f | |||
| b5d20199a0 | |||
| 9790033f7b | |||
| 3dd1658c08 | |||
| 9d40039f88 | |||
| 9f013aaab0 | |||
| b0f5945ba8 | |||
| e656157284 | |||
| 114e9b2f15 | |||
| 2274dd4eeb | |||
| be5cdb1ebe |
@@ -54,6 +54,30 @@ jobs:
|
||||
- name: "Build"
|
||||
run: make sdk-wasm-build
|
||||
|
||||
- name: "Build mix-fetch WASM (debug)"
|
||||
run: |
|
||||
make -C wasm/mix-fetch/go-mix-conn build-debug-dev
|
||||
make -C wasm/mix-fetch build-rust-debug
|
||||
|
||||
- name: "Build mix-fetch internal-dev harness"
|
||||
working-directory: wasm/mix-fetch/internal-dev
|
||||
run: npm install && npm run build
|
||||
|
||||
- name: "Install Playwright browsers"
|
||||
working-directory: wasm/mix-fetch/tests
|
||||
run: npm install && npx playwright install --with-deps # --with-deps assumes Ubuntu/Debian, see note in wasm/mix-fetch/tests/README.md
|
||||
|
||||
- name: "Smoke-test mix-fetch internal-dev (headless)"
|
||||
working-directory: wasm/mix-fetch/tests
|
||||
run: npm run test:smoke
|
||||
|
||||
- name: Upload Playwright traces on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mix-fetch-playwright-traces
|
||||
path: wasm/mix-fetch/tests/test-results/
|
||||
|
||||
- name: "Test"
|
||||
run: make sdk-wasm-test
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: nightly-mix-fetch-stress
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stress:
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli
|
||||
|
||||
- name: "Build mix-fetch WASM (debug)"
|
||||
run: |
|
||||
make -C wasm/mix-fetch/go-mix-conn build-debug-dev
|
||||
make -C wasm/mix-fetch build-rust-debug
|
||||
|
||||
- name: "Build internal-dev harness"
|
||||
working-directory: wasm/mix-fetch/internal-dev
|
||||
run: npm install && npm run build
|
||||
|
||||
- name: "Install Playwright browsers"
|
||||
working-directory: wasm/mix-fetch/tests
|
||||
run: npm install && npx playwright install --with-deps
|
||||
|
||||
- name: "Stress-test mix-fetch through mainnet"
|
||||
working-directory: wasm/mix-fetch/tests
|
||||
run: npm run test:stress
|
||||
|
||||
- name: Upload Playwright traces on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mix-fetch-stress-traces
|
||||
path: wasm/mix-fetch/tests/test-results/
|
||||
Generated
+1
-1
@@ -5275,7 +5275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-example-parcel",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "parcel build --no-cache --no-content-hash",
|
||||
@@ -8,9 +8,9 @@
|
||||
"start": "parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1",
|
||||
"@nymproject/mix-fetch": ">=1.4.4-rc.0 || ^1",
|
||||
"parcel": "^2.9.3"
|
||||
},
|
||||
"private": false,
|
||||
"source": "src/index.html"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-node",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.4",
|
||||
"description": "This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
@@ -28,7 +28,7 @@
|
||||
"tsc": "tsc --noEmit true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch-wasm-node": ">=1.4.2-rc.0 || ^1",
|
||||
"@nymproject/mix-fetch-wasm-node": ">=1.4.4-rc.0 || ^1",
|
||||
"comlink": "^4.3.1",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
@@ -68,4 +68,4 @@
|
||||
},
|
||||
"private": false,
|
||||
"types": "./dist/cjs/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-tester-webpack",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "webpack serve --progress --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
|
||||
"@nymproject/mix-fetch": ">=1.4.3-rc.0 || ^1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-tester-parcel",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "npx parcel build --no-cache --no-content-hash",
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "npx parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
|
||||
"@nymproject/mix-fetch": ">=1.4.3-rc.0 || ^1"
|
||||
},
|
||||
"private": false,
|
||||
"source": "../src/index.html"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.4",
|
||||
"description": "This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
@@ -34,7 +34,7 @@
|
||||
"tsc": "tsc --noEmit true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch-wasm": ">=1.4.2-rc.0 || ^1",
|
||||
"@nymproject/mix-fetch-wasm": ">=1.4.4-rc.0 || ^1",
|
||||
"comlink": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -82,4 +82,4 @@
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"types": "./dist/esm/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.3"
|
||||
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>"]
|
||||
version = "1.4.4"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/nymtech/nym"
|
||||
|
||||
@@ -92,7 +92,8 @@ func (ar *CurrentActiveRequests) InjectData(id types.RequestId, data []byte) {
|
||||
defer ar.Unlock()
|
||||
_, exists := ar.Requests[id]
|
||||
if !exists {
|
||||
panic("attempted to write to connection that doesn't exist")
|
||||
log.Error("attempted to inject data for connection %d that no longer exists — likely already cleaned up", id)
|
||||
return
|
||||
}
|
||||
ar.Requests[id].injector.ServerData <- data
|
||||
}
|
||||
@@ -115,7 +116,8 @@ func (ar *CurrentActiveRequests) SendError(id types.RequestId, err error) {
|
||||
defer ar.Unlock()
|
||||
_, exists := ar.Requests[id]
|
||||
if !exists {
|
||||
panic("attempted to inject error data to connection that doesn't exist")
|
||||
log.Error("attempted to inject error for connection %d that no longer exists — likely already cleaned up", id)
|
||||
return
|
||||
}
|
||||
ar.Requests[id].injector.RemoteError <- err
|
||||
}
|
||||
|
||||
@@ -3,100 +3,142 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nym MixFetch Demo</title>
|
||||
<title>Nym MixFetch Dev</title>
|
||||
<script src="bootstrap.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Mix Fetch Demo</h1>
|
||||
<h1>MixFetch Dev</h1>
|
||||
|
||||
<fieldset id="startup-controls">
|
||||
<legend>MixFetch Configuration</legend>
|
||||
<p>
|
||||
You can either use the default Gateway/NR combo run by us, or have
|
||||
MixFetch choose a random one on startup.
|
||||
</p>
|
||||
<div style="margin-bottom: 10px">
|
||||
<label>
|
||||
<input type="radio" name="gateway-mode" value="default" checked />
|
||||
Default Gateway (q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1)
|
||||
</label>
|
||||
<legend>Connection</legend>
|
||||
<div>
|
||||
<label><input type="radio" name="gateway-mode" value="default" checked />
|
||||
Default Gateway (q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1)</label>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
<label>
|
||||
<input type="radio" name="gateway-mode" value="random" />
|
||||
Random Gateway
|
||||
</label>
|
||||
<div>
|
||||
<label><input type="radio" name="gateway-mode" value="random" />
|
||||
Random Gateway</label>
|
||||
</div>
|
||||
<button id="start-mixfetch">Start MixFetch</button>
|
||||
<span id="mixfetch-status" style="margin-left: 10px; color: gray"
|
||||
>Not started</span
|
||||
>
|
||||
</fieldset>
|
||||
<details style="margin-top: 8px">
|
||||
<summary style="cursor: pointer; font-size: 0.9em; color: #555">Advanced Options</summary>
|
||||
<div style="margin-top: 6px; padding: 8px; background: #f9f9f9; font-size: 0.9em">
|
||||
<div>
|
||||
<label><input type="checkbox" id="opt-force-tls" checked /> Force TLS</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>Client ID: <input type="text" id="opt-client-id" size="20" /></label>
|
||||
<span style="font-size: 0.85em; color: #888">(randomised on load to avoid ID reuse)</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label><input type="checkbox" id="opt-disable-poisson" checked /> Disable Poisson packet distribution</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label><input type="checkbox" id="opt-disable-cover" checked /> Disable cover traffic</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>Request timeout (ms): <input type="number" id="opt-request-timeout" value="60000" min="1000" max="300000" style="width: 80px" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
<div style="margin-top: 8px">
|
||||
<button id="start-mixfetch">Start MixFetch</button>
|
||||
<span id="mixfetch-status" style="margin-left: 10px; color: gray">Not started</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="fetch-controls" disabled>
|
||||
<legend>Fetch Controls</legend>
|
||||
<legend>Quick Fetch</legend>
|
||||
<div>
|
||||
<label>Target Host 1: </label>
|
||||
<input
|
||||
type="text"
|
||||
size="60"
|
||||
id="fetch_payload_1"
|
||||
value="https://api.ipify.org?format=json"
|
||||
/>
|
||||
<button id="fetch-button-1">Fetch 1</button>
|
||||
<input type="text" size="55" id="fetch_payload_1" value="https://api.ipify.org?format=json" />
|
||||
<button id="fetch-button-1">GET</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>Target Host 2: </label>
|
||||
<input
|
||||
type="text"
|
||||
size="60"
|
||||
id="fetch_payload_2"
|
||||
value="https://api6.ipify.org?format=json"
|
||||
/>
|
||||
<button id="fetch-button-2">Fetch 2</button>
|
||||
<div style="margin-top: 4px">
|
||||
<input type="text" size="55" id="fetch_payload_2" value="https://api6.ipify.org?format=json" />
|
||||
<button id="fetch-button-2">GET</button>
|
||||
</div>
|
||||
<p>
|
||||
Note: if you're hammering these endpoints and you start to get timeouts
|
||||
(or you've been reloading the page a lot) then change the target hosts
|
||||
to e.g. http://ipv4.icanhazip.com and https://ipv6.icanhazip.com/
|
||||
</p>
|
||||
<pre id="fetch-log" style="background: #f5f5f5; padding: 8px; margin-top: 8px; max-height: 150px; overflow-y: auto; font-size: 0.85em; white-space: pre-wrap; display: none"></pre>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>Stress Test</h3>
|
||||
<div>
|
||||
<button id="fetch-10-concurrent">
|
||||
Send 10 Concurrent Requests (posts/1-10)
|
||||
</button>
|
||||
<p>
|
||||
This does what is says on the tin - sends 10 fetch requests to
|
||||
https://jsonplaceholder.typicode.com/posts/ 1 through 10
|
||||
<label>Requests:</label>
|
||||
<input type="number" id="stress-test-count" value="20" min="1" max="200" style="width: 60px" />
|
||||
<label>Mode:</label>
|
||||
<select id="stress-test-mode">
|
||||
<option value="uniform">Uniform (same URL, incremented ID)</option>
|
||||
<option value="mixed" selected>Mixed sizes</option>
|
||||
<option value="drip">Slow drip (timeout boundary)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="stress-uniform-opts" style="display: none; margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<div>
|
||||
<label>Base URL:</label>
|
||||
<input type="text" size="50" id="stress-test-url" value="https://jsonplaceholder.typicode.com/posts/" />
|
||||
</div>
|
||||
<p style="margin: 4px 0 0; font-size: 0.85em; color: #666">
|
||||
Request ID (1..N) appended to the URL. All requests are the same size.
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<label>POST URL: </label>
|
||||
<input
|
||||
type="text"
|
||||
size="60"
|
||||
id="post_url"
|
||||
value="https://jsonplaceholder.typicode.com/posts"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>POST Body (JSON): </label>
|
||||
<textarea id="post_body" rows="3" cols="60">
|
||||
{"title": "Test Post", "body": "Hello from MixFetch!", "userId": 1}</textarea
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<button id="post-button">Send POST Request</button>
|
||||
</div>
|
||||
<p>Do a POST and get it echoed back</p>
|
||||
</fieldset>
|
||||
|
||||
<hr />
|
||||
<p>
|
||||
<span id="output"></span>
|
||||
</p>
|
||||
<div id="stress-mixed-opts" style="margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<p style="margin: 0 0 4px; font-size: 0.85em; color: #666">
|
||||
Each request randomly assigned a size via <code>httpbin.org/bytes/{n}</code>.
|
||||
Tests how the mixnet handles asymmetric payload sizes concurrently.
|
||||
</p>
|
||||
<table style="font-size: 0.9em; border-collapse: collapse">
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>tiny</b></td><td>128 B</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>small</b></td><td>1 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>medium</b></td><td>10 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>large</b></td><td>100 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>xlarge</b></td><td>1 MB</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="stress-drip-opts" style="display: none; margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<p style="margin: 0 0 4px; font-size: 0.85em; color: #666">
|
||||
Uses <code>httpbin.org/drip</code> to slowly drip bytes over a set duration,
|
||||
keeping connections alive for a controlled time. Profiles are computed from
|
||||
the Go timeout value below, so they always straddle the configured boundary
|
||||
— some finish well before the timeout, others are mid-transfer when it fires.
|
||||
</p>
|
||||
<table style="font-size: 0.9em; border-collapse: collapse">
|
||||
<tr>
|
||||
<td style="padding: 1px 10px 1px 0"><b>safe</b></td>
|
||||
<td>~50% of timeout</td>
|
||||
<td style="padding-left: 10px; color: #666">well under the limit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 1px 10px 1px 0"><b>boundary</b></td>
|
||||
<td>~92% of timeout</td>
|
||||
<td style="padding-left: 10px; color: #666">server-side OK, but mixnet latency pushes it over</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 1px 10px 1px 0"><b>over</b></td>
|
||||
<td>~108% of timeout</td>
|
||||
<td style="padding-left: 10px; color: #666">exceeds the timeout on its own</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 1px 10px 1px 0"><b>slow-start</b></td>
|
||||
<td>~17% delay + ~83% drip</td>
|
||||
<td style="padding-left: 10px; color: #666">late first byte, then long transfer</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 8px">
|
||||
<label>Go timeout (ms):</label>
|
||||
<input type="number" id="stress-go-timeout" value="60000" min="500" max="120000" style="width: 80px" />
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 8px">
|
||||
<button id="stress-test-button">Run Stress Test</button>
|
||||
<span id="stress-test-status" style="margin-left: 10px"></span>
|
||||
</div>
|
||||
<div id="stress-tracker" style="margin-top: 8px; font-family: monospace; font-size: 0.85em; display: none"></div>
|
||||
</fieldset>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// ─── Worker client ──────────────────────────────────────────────────────────
|
||||
|
||||
class WebWorkerClient {
|
||||
worker = null;
|
||||
|
||||
@@ -19,218 +21,254 @@ class WebWorkerClient {
|
||||
this.worker = new Worker('./worker.js');
|
||||
|
||||
this.worker.onmessage = (ev) => {
|
||||
if (ev.data && ev.data.kind) {
|
||||
switch (ev.data.kind) {
|
||||
case 'DisplayString':
|
||||
const { rawString } = ev.data.args;
|
||||
displayReceivedRawString(rawString)
|
||||
break;
|
||||
case 'Log':
|
||||
const { message, level } = ev.data.args;
|
||||
displayLog(message, level);
|
||||
break;
|
||||
case 'MixFetchReady':
|
||||
onMixFetchReady();
|
||||
break;
|
||||
case 'MixFetchError':
|
||||
const { error } = ev.data.args;
|
||||
onMixFetchError(error);
|
||||
break;
|
||||
if (!ev.data || !ev.data.kind) return;
|
||||
switch (ev.data.kind) {
|
||||
case 'DisplayString':
|
||||
appendFetchLog(ev.data.args.rawString);
|
||||
console.log('[mixfetch response]', ev.data.args.rawString);
|
||||
break;
|
||||
case 'Log': {
|
||||
const { message, level } = ev.data.args;
|
||||
const fn = level === 'error' ? console.error
|
||||
: level === 'warn' ? console.warn
|
||||
: console.log;
|
||||
fn(`[worker/${level}]`, message);
|
||||
break;
|
||||
}
|
||||
case 'MixFetchReady':
|
||||
onMixFetchReady();
|
||||
break;
|
||||
case 'MixFetchError':
|
||||
onMixFetchError(ev.data.args.error);
|
||||
break;
|
||||
case 'StressTestFetchResult':
|
||||
onStressTestFetchResult(ev.data.args);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
startMixFetch = (preferredGateway) => {
|
||||
if (!this.worker) {
|
||||
console.error('Could not send message because worker does not exist');
|
||||
return;
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
kind: 'StartMixFetch',
|
||||
args: {
|
||||
preferredGateway,
|
||||
},
|
||||
});
|
||||
}
|
||||
startMixFetch = (preferredGateway, setupOpts) => {
|
||||
this.worker.postMessage({ kind: 'StartMixFetch', args: { preferredGateway, setupOpts } });
|
||||
};
|
||||
|
||||
doFetch = (target) => {
|
||||
if (!this.worker) {
|
||||
console.error('Could not send message because worker does not exist');
|
||||
return;
|
||||
this.worker.postMessage({ kind: 'FetchPayload', args: { target } });
|
||||
};
|
||||
|
||||
setGoTimeout = (timeoutMs) => {
|
||||
this.worker.postMessage({ kind: 'SetGoTimeout', args: { timeoutMs } });
|
||||
};
|
||||
|
||||
doStressTest = (requests) => {
|
||||
for (const req of requests) {
|
||||
this.worker.postMessage({
|
||||
kind: 'StressTestFetch',
|
||||
args: { id: req.id, url: req.url, label: req.label },
|
||||
});
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
kind: 'FetchPayload',
|
||||
args: {
|
||||
target,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
doPost = (url, body) => {
|
||||
if (!this.worker) {
|
||||
console.error('Could not send message because worker does not exist');
|
||||
return;
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
kind: 'PostPayload',
|
||||
args: {
|
||||
url,
|
||||
body,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let client = null;
|
||||
// ─── Startup ────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_GATEWAY = "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1";
|
||||
let client = null;
|
||||
const DEFAULT_GATEWAY = 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
|
||||
async function main() {
|
||||
client = new WebWorkerClient();
|
||||
|
||||
const startButton = document.querySelector('#start-mixfetch');
|
||||
startButton.onclick = function () {
|
||||
// Randomise client ID on each load to avoid storage/state collisions
|
||||
document.getElementById('opt-client-id').value =
|
||||
'client-' + Math.random().toString(36).slice(2, 8);
|
||||
|
||||
document.querySelector('#start-mixfetch').onclick = () => {
|
||||
const gatewayMode = document.querySelector('input[name="gateway-mode"]:checked').value;
|
||||
const preferredGateway = gatewayMode === 'default' ? DEFAULT_GATEWAY : undefined;
|
||||
|
||||
startButton.disabled = true;
|
||||
document.querySelectorAll('input[name="gateway-mode"]').forEach(r => r.disabled = true);
|
||||
updateStatus('Starting...', 'orange');
|
||||
const setupOpts = {
|
||||
forceTls: document.getElementById('opt-force-tls').checked,
|
||||
clientId: document.getElementById('opt-client-id').value,
|
||||
disablePoisson: document.getElementById('opt-disable-poisson').checked,
|
||||
disableCover: document.getElementById('opt-disable-cover').checked,
|
||||
requestTimeoutMs: parseInt(document.getElementById('opt-request-timeout').value, 10),
|
||||
};
|
||||
|
||||
displayLog(`Starting MixFetch with ${gatewayMode} gateway${preferredGateway ? ` (${preferredGateway})` : ''}...`, 'info');
|
||||
client.startMixFetch(preferredGateway);
|
||||
}
|
||||
document.querySelector('#start-mixfetch').disabled = true;
|
||||
document.querySelectorAll('input[name="gateway-mode"]').forEach((r) => (r.disabled = true));
|
||||
updateStatus('mixfetch-status', 'Starting...');
|
||||
|
||||
const fetchButton1 = document.querySelector('#fetch-button-1');
|
||||
fetchButton1.onclick = function () {
|
||||
doFetch(1);
|
||||
}
|
||||
// Sync the stress-test Go timeout to match the configured request timeout
|
||||
document.getElementById('stress-go-timeout').value = setupOpts.requestTimeoutMs;
|
||||
|
||||
const fetchButton2 = document.querySelector('#fetch-button-2');
|
||||
fetchButton2.onclick = function () {
|
||||
doFetch(2);
|
||||
}
|
||||
console.log(`Starting MixFetch (${gatewayMode} gateway${preferredGateway ? `: ${preferredGateway}` : ''})...`);
|
||||
console.log('Setup options:', setupOpts);
|
||||
client.startMixFetch(preferredGateway, setupOpts);
|
||||
};
|
||||
|
||||
const fetch10Button = document.querySelector('#fetch-10-concurrent');
|
||||
fetch10Button.onclick = function () {
|
||||
doFetch10Concurrent();
|
||||
}
|
||||
document.querySelector('#fetch-button-1').onclick = () => doFetch(1);
|
||||
document.querySelector('#fetch-button-2').onclick = () => doFetch(2);
|
||||
|
||||
const postButton = document.querySelector('#post-button');
|
||||
postButton.onclick = function () {
|
||||
doPost();
|
||||
}
|
||||
const stressModeSelect = document.getElementById('stress-test-mode');
|
||||
stressModeSelect.onchange = function () {
|
||||
document.getElementById('stress-uniform-opts').style.display = this.value === 'uniform' ? 'block' : 'none';
|
||||
document.getElementById('stress-mixed-opts').style.display = this.value === 'mixed' ? 'block' : 'none';
|
||||
document.getElementById('stress-drip-opts').style.display = this.value === 'drip' ? 'block' : 'none';
|
||||
};
|
||||
|
||||
document.querySelector('#stress-test-button').onclick = () => {
|
||||
const count = parseInt(document.getElementById('stress-test-count').value, 10);
|
||||
const mode = document.getElementById('stress-test-mode').value;
|
||||
const goTimeoutMs = parseInt(document.getElementById('stress-go-timeout').value, 10);
|
||||
|
||||
document.querySelector('#stress-test-button').disabled = true;
|
||||
updateStatus('stress-test-status', 'Running...');
|
||||
client.setGoTimeout(goTimeoutMs);
|
||||
|
||||
const requests = generateStressRequests(count, mode, goTimeoutMs);
|
||||
stressTest = {
|
||||
count,
|
||||
startTime: performance.now(),
|
||||
results: [],
|
||||
};
|
||||
|
||||
console.log(`=== STRESS TEST: ${count} requests, ${mode} mode, timeout=${goTimeoutMs}ms ===`);
|
||||
|
||||
if (mode === 'mixed' || mode === 'drip') {
|
||||
const breakdown = {};
|
||||
for (const req of requests) breakdown[req.label] = (breakdown[req.label] || 0) + 1;
|
||||
console.log('Profiles:', breakdown);
|
||||
}
|
||||
|
||||
client.doStressTest(requests);
|
||||
};
|
||||
}
|
||||
|
||||
function updateStatus(text, color) {
|
||||
const status = document.getElementById('mixfetch-status');
|
||||
status.textContent = text;
|
||||
status.style.color = color;
|
||||
// ─── UI helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function updateStatus(elementId, text) {
|
||||
document.getElementById(elementId).textContent = text;
|
||||
}
|
||||
|
||||
function onMixFetchReady() {
|
||||
updateStatus('Ready', 'green');
|
||||
updateStatus('mixfetch-status', 'Ready');
|
||||
document.getElementById('fetch-controls').disabled = false;
|
||||
displayLog('MixFetch is ready!', 'info');
|
||||
console.log('MixFetch ready!');
|
||||
}
|
||||
|
||||
function onMixFetchError(error) {
|
||||
updateStatus('Error: ' + error, 'red');
|
||||
updateStatus('mixfetch-status', 'Error: ' + error);
|
||||
document.querySelector('#start-mixfetch').disabled = false;
|
||||
document.querySelectorAll('input[name="gateway-mode"]').forEach(r => r.disabled = false);
|
||||
displayLog('MixFetch error: ' + error, 'error');
|
||||
document.querySelectorAll('input[name="gateway-mode"]').forEach((r) => (r.disabled = false));
|
||||
console.error('MixFetch error:', error);
|
||||
}
|
||||
|
||||
// ─── Quick fetch ────────────────────────────────────────────────────────────
|
||||
|
||||
function appendFetchLog(text) {
|
||||
const log = document.getElementById('fetch-log');
|
||||
log.style.display = 'block';
|
||||
const ts = new Date().toISOString().substr(11, 12);
|
||||
log.textContent += `${ts} ${text}\n`;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
async function doFetch(id) {
|
||||
const payload = document.getElementById(`fetch_payload_${id}`).value;
|
||||
await client.doFetch(payload)
|
||||
|
||||
displaySend(`[${id}] clicked the button and the payload is: ${payload}...`);
|
||||
const url = document.getElementById(`fetch_payload_${id}`).value;
|
||||
appendFetchLog(`GET ${url}`);
|
||||
console.log(`GET ${url}`);
|
||||
await client.doFetch(url);
|
||||
}
|
||||
|
||||
async function doFetch10Concurrent() {
|
||||
const baseUrl = 'https://jsonplaceholder.typicode.com/posts/';
|
||||
displaySend('Starting 10 concurrent requests to posts/1-10...');
|
||||
// ─── Stress test ────────────────────────────────────────────────────────────
|
||||
|
||||
const STRESS_PROFILES = [
|
||||
{ label: 'tiny', bytes: 128 },
|
||||
{ label: 'small', bytes: 1024 },
|
||||
{ label: 'medium', bytes: 10240 },
|
||||
{ label: 'large', bytes: 102400 },
|
||||
{ label: 'xlarge', bytes: 1048576 },
|
||||
];
|
||||
|
||||
function buildDripProfiles(timeoutSec) {
|
||||
return [
|
||||
{ label: 'safe', dripDuration: Math.round(timeoutSec * 0.50), dripDelay: 0, dripBytes: 100 },
|
||||
{ label: 'boundary', dripDuration: Math.round(timeoutSec * 0.92), dripDelay: 0, dripBytes: 100 },
|
||||
{ label: 'over', dripDuration: Math.round(timeoutSec * 1.08), dripDelay: 0, dripBytes: 100 },
|
||||
{ label: 'slow-start', dripDuration: Math.round(timeoutSec * 0.83), dripDelay: Math.round(timeoutSec * 0.17), dripBytes: 100 },
|
||||
];
|
||||
}
|
||||
|
||||
function generateStressRequests(count, mode, timeoutMs) {
|
||||
const requests = [];
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const url = `${baseUrl}${i}`;
|
||||
displaySend(`[${i}] Sending request to ${url}`);
|
||||
requests.push(client.doFetch(url));
|
||||
if (mode === 'uniform') {
|
||||
const baseUrl = document.getElementById('stress-test-url').value;
|
||||
for (let i = 1; i <= count; i++) {
|
||||
requests.push({ id: i, url: `${baseUrl}${i}`, label: 'uniform', bytes: null });
|
||||
}
|
||||
} else if (mode === 'drip') {
|
||||
const dripProfiles = buildDripProfiles(timeoutMs / 1000);
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const p = dripProfiles[Math.floor(Math.random() * dripProfiles.length)];
|
||||
requests.push({
|
||||
id: i,
|
||||
url: `https://httpbin.org/drip?duration=${p.dripDuration}&numbytes=${p.dripBytes}&delay=${p.dripDelay}&code=200`,
|
||||
label: p.label,
|
||||
bytes: p.dripBytes,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const p = STRESS_PROFILES[Math.floor(Math.random() * STRESS_PROFILES.length)];
|
||||
requests.push({
|
||||
id: i,
|
||||
url: `https://httpbin.org/bytes/${p.bytes}`,
|
||||
label: p.label,
|
||||
bytes: p.bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
let stressTest = null;
|
||||
|
||||
function onStressTestFetchResult(result) {
|
||||
if (!stressTest) return;
|
||||
|
||||
stressTest.results.push(result);
|
||||
|
||||
const progress = `${stressTest.results.length}/${stressTest.count}`;
|
||||
const tag = `#${result.id} ${result.label}`;
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`[${tag}] ${result.status} OK ${result.elapsed}s ${result.textLength}B (${progress})`);
|
||||
} else {
|
||||
console.error(`[${tag}] FAIL ${result.elapsed}s ${result.error} (${progress})`);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
displaySend('All 10 concurrent requests dispatched!');
|
||||
}
|
||||
updateStatus('stress-test-status', progress);
|
||||
|
||||
async function doPost() {
|
||||
const url = document.getElementById('post_url').value;
|
||||
const body = document.getElementById('post_body').value;
|
||||
if (stressTest.results.length === stressTest.count) {
|
||||
const totalElapsed = ((performance.now() - stressTest.startTime) / 1000).toFixed(2);
|
||||
const succeeded = stressTest.results.filter((r) => r.ok).length;
|
||||
const failed = stressTest.results.filter((r) => !r.ok).length;
|
||||
|
||||
displaySend(`[POST] Sending POST request to ${url}`);
|
||||
displaySend(`[POST] Body: ${body}`);
|
||||
console.log(`=== COMPLETE: ${totalElapsed}s | OK ${succeeded}/${stressTest.count} | Failed ${failed}/${stressTest.count} ===`);
|
||||
|
||||
await client.doPost(url, body);
|
||||
}
|
||||
if (failed > 0) {
|
||||
const failures = stressTest.results.filter((r) => !r.ok);
|
||||
for (const f of failures) {
|
||||
console.log(` FAIL #${f.id} ${f.label} (${f.elapsed}s): ${f.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display log messages from MixFetch. Colors based on level.
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {string} level - 'info', 'error', 'warn', or 'debug'
|
||||
*/
|
||||
function displayLog(message, level) {
|
||||
let timestamp = new Date().toISOString().substr(11, 12);
|
||||
|
||||
const colors = {
|
||||
info: 'gray',
|
||||
error: 'red',
|
||||
warn: 'orange',
|
||||
debug: 'purple',
|
||||
};
|
||||
|
||||
let logDiv = document.createElement('div');
|
||||
let paragraph = document.createElement('p');
|
||||
paragraph.setAttribute('style', `color: ${colors[level] || 'gray'}`);
|
||||
let paragraphContent = document.createTextNode(timestamp + ' [' + level.toUpperCase() + '] ' + message);
|
||||
paragraph.appendChild(paragraphContent);
|
||||
|
||||
logDiv.appendChild(paragraph);
|
||||
document.getElementById('output').appendChild(logDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display messages that have been sent up the websocket. Colours them blue.
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
function displaySend(message) {
|
||||
let timestamp = new Date().toISOString().substr(11, 12);
|
||||
|
||||
let sendDiv = document.createElement('div');
|
||||
let paragraph = document.createElement('p');
|
||||
paragraph.setAttribute('style', 'color: blue');
|
||||
let paragraphContent = document.createTextNode(timestamp + ' sent >>> ' + message);
|
||||
paragraph.appendChild(paragraphContent);
|
||||
|
||||
sendDiv.appendChild(paragraph);
|
||||
document.getElementById('output').appendChild(sendDiv);
|
||||
}
|
||||
|
||||
function displayReceivedRawString(raw) {
|
||||
let timestamp = new Date().toISOString().substr(11, 12);
|
||||
let receivedDiv = document.createElement('div');
|
||||
let paragraph = document.createElement('p');
|
||||
paragraph.setAttribute('style', 'color: green');
|
||||
let paragraphContent = document.createTextNode(timestamp + ' received >>> ' + raw);
|
||||
paragraph.appendChild(paragraphContent);
|
||||
receivedDiv.appendChild(paragraph);
|
||||
document.getElementById('output').appendChild(receivedDiv);
|
||||
updateStatus('stress-test-status',
|
||||
`Done: ${succeeded}/${stressTest.count} OK, ${failed} failed (${totalElapsed}s)`
|
||||
);
|
||||
document.querySelector('#stress-test-button').disabled = false;
|
||||
stressTest = null;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
+120
-121
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "create-wasm-app",
|
||||
"name": "mix-fetch-internal-dev",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-wasm-app",
|
||||
"name": "mix-fetch-internal-dev",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"../go-mix-conn/build": {},
|
||||
"../pkg": {
|
||||
"name": "@nymproject/mix-fetch-wasm",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.4",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
@@ -141,14 +141,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-core": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz",
|
||||
"integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.1.tgz",
|
||||
"integrity": "sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"thingies": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -163,15 +163,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-fsa": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz",
|
||||
"integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.1.tgz",
|
||||
"integrity": "sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-core": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-core": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"thingies": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -186,17 +186,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-node": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz",
|
||||
"integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz",
|
||||
"integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-core": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-print": "4.56.10",
|
||||
"@jsonjoy.com/fs-snapshot": "4.56.10",
|
||||
"@jsonjoy.com/fs-core": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"@jsonjoy.com/fs-print": "4.57.1",
|
||||
"@jsonjoy.com/fs-snapshot": "4.57.1",
|
||||
"glob-to-regex.js": "^1.0.0",
|
||||
"thingies": "^2.5.0"
|
||||
},
|
||||
@@ -212,9 +212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-node-builtins": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz",
|
||||
"integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz",
|
||||
"integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -229,15 +229,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-node-to-fsa": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz",
|
||||
"integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.1.tgz",
|
||||
"integrity": "sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-fsa": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10"
|
||||
"@jsonjoy.com/fs-fsa": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
@@ -251,13 +251,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-node-utils": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz",
|
||||
"integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.1.tgz",
|
||||
"integrity": "sha512-vp+7ZzIB8v43G+GLXTS4oDUSQmhAsRz532QmmWBbdYA20s465JvwhkSFvX9cVTqRRAQg+vZ7zWDaIEh0lFe2gw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10"
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
@@ -271,13 +271,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-print": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz",
|
||||
"integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.1.tgz",
|
||||
"integrity": "sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"tree-dump": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -292,14 +292,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonjoy.com/fs-snapshot": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz",
|
||||
"integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.1.tgz",
|
||||
"integrity": "sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/buffers": "^17.65.0",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"@jsonjoy.com/json-pack": "^17.65.0",
|
||||
"@jsonjoy.com/util": "^17.65.0"
|
||||
},
|
||||
@@ -869,9 +869,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
||||
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -879,9 +879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||
"integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1332,9 +1332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
|
||||
"integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1414,9 +1414,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1434,11 +1434,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
"electron-to-chromium": "^1.5.328",
|
||||
"node-releases": "^2.0.36",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -1522,9 +1522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001774",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
||||
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
|
||||
"version": "1.0.30001784",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
|
||||
"integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1878,9 +1878,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.302",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||
"version": "1.5.330",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz",
|
||||
"integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1895,9 +1895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2766,9 +2766,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
|
||||
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
|
||||
"integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2894,9 +2894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz",
|
||||
"integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==",
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz",
|
||||
"integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2952,20 +2952,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "4.56.10",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz",
|
||||
"integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.1.tgz",
|
||||
"integrity": "sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jsonjoy.com/fs-core": "4.56.10",
|
||||
"@jsonjoy.com/fs-fsa": "4.56.10",
|
||||
"@jsonjoy.com/fs-node": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-to-fsa": "4.56.10",
|
||||
"@jsonjoy.com/fs-node-utils": "4.56.10",
|
||||
"@jsonjoy.com/fs-print": "4.56.10",
|
||||
"@jsonjoy.com/fs-snapshot": "4.56.10",
|
||||
"@jsonjoy.com/fs-core": "4.57.1",
|
||||
"@jsonjoy.com/fs-fsa": "4.57.1",
|
||||
"@jsonjoy.com/fs-node": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-builtins": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-to-fsa": "4.57.1",
|
||||
"@jsonjoy.com/fs-node-utils": "4.57.1",
|
||||
"@jsonjoy.com/fs-print": "4.57.1",
|
||||
"@jsonjoy.com/fs-snapshot": "4.57.1",
|
||||
"@jsonjoy.com/json-pack": "^1.11.0",
|
||||
"@jsonjoy.com/util": "^1.9.0",
|
||||
"glob-to-regex.js": "^1.0.1",
|
||||
@@ -3114,9 +3114,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"version": "2.0.36",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
||||
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3287,9 +3287,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3311,9 +3311,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3337,9 +3337,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkijs": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz",
|
||||
"integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
|
||||
"integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -4142,9 +4142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
|
||||
"integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4156,9 +4156,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.46.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
||||
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
||||
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
@@ -4175,16 +4175,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
|
||||
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4210,9 +4209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/thingies": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz",
|
||||
"integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz",
|
||||
"integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4424,9 +4423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.105.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz",
|
||||
"integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==",
|
||||
"version": "5.105.4",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
|
||||
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4436,11 +4435,11 @@
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.28.1",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.19.0",
|
||||
"enhanced-resolve": "^5.20.0",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
@@ -4452,9 +4451,9 @@
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.16",
|
||||
"terser-webpack-plugin": "^5.3.17",
|
||||
"watchpack": "^2.5.1",
|
||||
"webpack-sources": "^3.3.3"
|
||||
"webpack-sources": "^3.3.4"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
@@ -4717,9 +4716,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "create-wasm-app",
|
||||
"name": "mix-fetch-internal-dev",
|
||||
"version": "0.1.0",
|
||||
"description": "create an app to fetch data through the mixnet",
|
||||
"description": "Internal dev/stress-test harness for mix-fetch WASM",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"create-wasm-app": ".bin/create-wasm-app.js"
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rustwasm/create-wasm-app.git"
|
||||
"url": "git+https://github.com/nymtech/nym.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webassembly",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const RUST_WASM_URL = "mix_fetch_wasm_bg.wasm"
|
||||
const GO_WASM_URL = "go_conn.wasm"
|
||||
const RUST_WASM_URL = "mix_fetch_wasm_bg.wasm";
|
||||
const GO_WASM_URL = "go_conn.wasm";
|
||||
|
||||
importScripts('mix_fetch_wasm.js');
|
||||
importScripts('wasm_exec.js');
|
||||
@@ -36,11 +36,9 @@ const {
|
||||
disconnectMixFetch,
|
||||
setupMixFetchWithConfig,
|
||||
mix_fetch_initialised,
|
||||
finish_mixnet_connection
|
||||
finish_mixnet_connection,
|
||||
} = wasm_bindgen;
|
||||
|
||||
let client = null;
|
||||
let tester = null;
|
||||
const go = new Go(); // Defined in wasm_exec.js
|
||||
var goWasm;
|
||||
let mixFetchReady = false;
|
||||
@@ -64,83 +62,20 @@ function sendError(error) {
|
||||
}
|
||||
|
||||
async function logFetchResult(res) {
|
||||
console.log(res)
|
||||
let text = await res.text()
|
||||
console.log("HEADERS: ", ...res.headers)
|
||||
console.log("STATUS: ", res.status)
|
||||
console.log("STATUS TEXT: ", res.statusText)
|
||||
console.log("OK: ", res.ok)
|
||||
console.log("TYPE: ", res.type)
|
||||
console.log("URL: ", res.url)
|
||||
console.log("BODYUSED: ", res.bodyUsed)
|
||||
console.log("REDIRECTED: ", res.redirected)
|
||||
console.log("TEXT: ", text)
|
||||
let text = await res.text();
|
||||
console.log(`${res.status} ${res.statusText} (${text.length} bytes)`);
|
||||
console.log(text);
|
||||
|
||||
self.postMessage({
|
||||
kind: 'DisplayString',
|
||||
args: {
|
||||
rawString: text,
|
||||
},
|
||||
args: { rawString: text },
|
||||
});
|
||||
}
|
||||
|
||||
async function wasm_bindgenSetup() {
|
||||
const preferredGateway = "6qQYb4ArXANU6HJDxzH4PFCUqYb39Dae2Gem2KpxescM";
|
||||
const validator = 'https://qa-nym-api.qa.nymte.ch/api';
|
||||
// For custom MixFetchConfig (specific network requester, debug overrides),
|
||||
// see setupMixFetchWithConfig() and the MixFetchConfig / MixFetchConfigOpts types.
|
||||
|
||||
// local
|
||||
const mixFetchNetworkRequesterAddress = "2o47bhnXWna6VEyt4mXMGQQAbXfpKmX7BkjkxUz8uQVi.6uQGnCqSczpXwh86NdbsCoDDXuqZQM9Uwko8GE7uC9g8@6qQYb4ArXANU6HJDxzH4PFCUqYb39Dae2Gem2KpxescM";
|
||||
// const mixFetchNetworkRequesterAddress= "GqiGWmKRCbGQFSqH88BzLKijvZgipnqhmbNFsmkZw84t.4L8sXFuAUyUYyHZYgMdM3AtiusKnYUft6Pd8e41rrCHA@6qQYb4ArXANU6HJDxzH4PFCUqYb39Dae2Gem2KpxescM";
|
||||
|
||||
// STEP 1. construct config
|
||||
// those are just some examples, there are obviously more permutations;
|
||||
// note, the extra optional argument is of the following type:
|
||||
// /*
|
||||
// export interface MixFetchConfigOpts {
|
||||
// id?: string;
|
||||
// nymApi?: string;
|
||||
// nyxd?: string;
|
||||
// debug?: DebugWasm;
|
||||
// }
|
||||
// */
|
||||
//
|
||||
// const debug = no_cover_debug()
|
||||
//
|
||||
// #1
|
||||
// const config = new MixFetchConfig(mixFetchNetworkRequesterAddress, { id: 'my-awesome-mix-fetch-client', nymApi: validator, debug: debug} );
|
||||
// #2
|
||||
// const config = new MixFetchConfig(mixFetchNetworkRequesterAddress, { nymApi: validator, debug: debug} );
|
||||
// #3
|
||||
// const config = new MixFetchConfig(mixFetchNetworkRequesterAddress, { id: 'my-awesome-mix-fetch-client' } );
|
||||
//
|
||||
// #4
|
||||
const differentDebug = default_debug()
|
||||
const updatedTraffic = differentDebug.traffic;
|
||||
updatedTraffic.use_extended_packet_size = true
|
||||
updatedTraffic.average_packet_delay_ms = 666;
|
||||
differentDebug.traffic = updatedTraffic;
|
||||
|
||||
const config = new MixFetchConfig(mixFetchNetworkRequesterAddress, {debug: differentDebug});
|
||||
//
|
||||
// // STEP 2. setup the client
|
||||
// // note, the extra optional argument is of the following type:
|
||||
// /*
|
||||
// export interface MixFetchOptsSimple {
|
||||
// preferredGateway?: string;
|
||||
// storagePassphrase?: string;
|
||||
// }
|
||||
// */
|
||||
// #1
|
||||
await setupMixFetchWithConfig(config)
|
||||
//
|
||||
// #2
|
||||
// await setupMixFetchWithConfig(config, { storagePassphrase: "foomp" })
|
||||
//
|
||||
// #3
|
||||
// await setupMixFetchWithConfig(config, { storagePassphrase: "foomp", preferredGateway })
|
||||
}
|
||||
|
||||
async function nativeSetup(preferredGateway) {
|
||||
async function nativeSetup(preferredGateway, setupOpts = {}) {
|
||||
sendLog('Setting up MixFetch...');
|
||||
if (preferredGateway) {
|
||||
sendLog(`Using preferred gateway: ${preferredGateway}`);
|
||||
@@ -148,17 +83,25 @@ async function nativeSetup(preferredGateway) {
|
||||
sendLog('Using random gateway selection');
|
||||
}
|
||||
|
||||
const {
|
||||
forceTls = true,
|
||||
clientId = 'client-' + Math.random().toString(36).slice(2, 8),
|
||||
disablePoisson = true,
|
||||
disableCover = true,
|
||||
requestTimeoutMs = 60000,
|
||||
} = setupOpts;
|
||||
|
||||
const noCoverTrafficOverride = {
|
||||
traffic: {disableMainPoissonPacketDistribution: true},
|
||||
coverTraffic: {disableLoopCoverTrafficStream: true},
|
||||
}
|
||||
traffic: { disableMainPoissonPacketDistribution: disablePoisson },
|
||||
coverTraffic: { disableLoopCoverTrafficStream: disableCover },
|
||||
};
|
||||
const mixFetchOverride = {
|
||||
requestTimeoutMs: 60000
|
||||
}
|
||||
requestTimeoutMs,
|
||||
};
|
||||
|
||||
const opts = {
|
||||
forceTls: true,
|
||||
clientId: "my-client",
|
||||
forceTls,
|
||||
clientId,
|
||||
clientOverride: noCoverTrafficOverride,
|
||||
mixFetchOverride,
|
||||
};
|
||||
@@ -167,16 +110,19 @@ async function nativeSetup(preferredGateway) {
|
||||
opts.preferredGateway = preferredGateway;
|
||||
}
|
||||
|
||||
sendLog(
|
||||
`Setup config: forceTls=${forceTls}, clientId=${clientId}, disablePoisson=${disablePoisson}, disableCover=${disableCover}, timeout=${requestTimeoutMs}ms`
|
||||
);
|
||||
sendLog('Calling setupMixFetch...');
|
||||
await setupMixFetch(opts);
|
||||
sendLog('setupMixFetch completed');
|
||||
}
|
||||
|
||||
async function startMixFetch(preferredGateway) {
|
||||
async function startMixFetch(preferredGateway, setupOpts) {
|
||||
sendLog('Instantiating MixFetch...');
|
||||
|
||||
try {
|
||||
await nativeSetup(preferredGateway);
|
||||
await nativeSetup(preferredGateway, setupOpts);
|
||||
mixFetchReady = true;
|
||||
sendLog('MixFetch client running!');
|
||||
sendReady();
|
||||
@@ -193,7 +139,7 @@ async function handleFetchPayload(target) {
|
||||
}
|
||||
|
||||
const url = target;
|
||||
const args = {mode: "unsafe-ignore-cors"};
|
||||
const args = { mode: "unsafe-ignore-cors" };
|
||||
|
||||
try {
|
||||
sendLog(`Fetching: ${url}`);
|
||||
@@ -206,40 +152,50 @@ async function handleFetchPayload(target) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePostPayload(url, body) {
|
||||
async function handleStressTestFetch(id, url, label) {
|
||||
if (!mixFetchReady) {
|
||||
sendLog('MixFetch not ready yet', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const args = {
|
||||
method: 'POST',
|
||||
mode: "unsafe-ignore-cors",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body,
|
||||
};
|
||||
const tag = `[stress #${id} ${label}]`;
|
||||
const start = performance.now();
|
||||
const args = { mode: "unsafe-ignore-cors" };
|
||||
|
||||
try {
|
||||
sendLog(`POST request to: ${url}`);
|
||||
sendLog(`POST body: ${body}`);
|
||||
const mixFetchRes = await mixFetch(url, args);
|
||||
sendLog('POST completed');
|
||||
await logFetchResult(mixFetchRes);
|
||||
sendLog(`${tag} Fetching: ${url}`);
|
||||
const res = await mixFetch(url, args);
|
||||
const text = await res.text();
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
sendLog(`${tag} ${res.status} OK in ${elapsed}s (${text.length} bytes)`);
|
||||
self.postMessage({
|
||||
kind: 'StressTestFetchResult',
|
||||
args: {
|
||||
id,
|
||||
label,
|
||||
ok: true,
|
||||
status: res.status,
|
||||
elapsed,
|
||||
textLength: text.length,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
sendLog('POST request failure: ' + e, 'error');
|
||||
console.error("mix fetch POST request failure: ", e);
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
sendLog(`${tag} FAILED in ${elapsed}s: ${e}`, 'error');
|
||||
self.postMessage({
|
||||
kind: 'StressTestFetchResult',
|
||||
args: { id, label, ok: false, elapsed, error: String(e) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupMessageHandler() {
|
||||
self.onmessage = async event => {
|
||||
self.onmessage = async (event) => {
|
||||
if (event.data && event.data.kind) {
|
||||
switch (event.data.kind) {
|
||||
case 'StartMixFetch': {
|
||||
const { preferredGateway } = event.data.args;
|
||||
await startMixFetch(preferredGateway);
|
||||
const { preferredGateway, setupOpts } = event.data.args;
|
||||
await startMixFetch(preferredGateway, setupOpts);
|
||||
break;
|
||||
}
|
||||
case 'FetchPayload': {
|
||||
@@ -247,9 +203,17 @@ function setupMessageHandler() {
|
||||
await handleFetchPayload(target);
|
||||
break;
|
||||
}
|
||||
case 'PostPayload': {
|
||||
const { url, body } = event.data.args;
|
||||
await handlePostPayload(url, body);
|
||||
case 'SetGoTimeout': {
|
||||
const { timeoutMs } = event.data.args;
|
||||
sendLog(`Setting Go-side request timeout to ${timeoutMs}ms`);
|
||||
self.__go_rs_bridge__.goWasmSetMixFetchRequestTimeout(timeoutMs);
|
||||
break;
|
||||
}
|
||||
case 'StressTestFetch': {
|
||||
const { id, url, label } = event.data.args;
|
||||
// NOT awaited — each request runs independently,
|
||||
// just like separate callers in a real app
|
||||
handleStressTestFetch(id, url, label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -257,31 +221,30 @@ function setupMessageHandler() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// TODO: look into https://www.aaron-powell.com/posts/2019-02-08-golang-wasm-5-compiling-with-webpack/
|
||||
async function loadGoWasm() {
|
||||
const resp = await fetch(GO_WASM_URL);
|
||||
|
||||
if ('instantiateStreaming' in WebAssembly) {
|
||||
const wasmObj = await WebAssembly.instantiateStreaming(resp, go.importObject)
|
||||
goWasm = wasmObj.instance
|
||||
go.run(goWasm)
|
||||
const wasmObj = await WebAssembly.instantiateStreaming(resp, go.importObject);
|
||||
goWasm = wasmObj.instance;
|
||||
go.run(goWasm);
|
||||
} else {
|
||||
const bytes = await resp.arrayBuffer()
|
||||
const wasmObj = await WebAssembly.instantiate(bytes, go.importObject)
|
||||
goWasm = wasmObj.instance
|
||||
go.run(goWasm)
|
||||
const bytes = await resp.arrayBuffer();
|
||||
const wasmObj = await WebAssembly.instantiate(bytes, go.importObject);
|
||||
goWasm = wasmObj.instance;
|
||||
go.run(goWasm);
|
||||
}
|
||||
}
|
||||
|
||||
function setupRsGoBridge() {
|
||||
// (note: reason for intermediate `__rs_go_bridge__` object is to decrease global scope bloat
|
||||
// and to discourage users from trying to call those methods directly)
|
||||
self.__rs_go_bridge__ = {}
|
||||
self.__rs_go_bridge__.send_client_data = send_client_data
|
||||
self.__rs_go_bridge__.start_new_mixnet_connection = start_new_mixnet_connection
|
||||
self.__rs_go_bridge__.mix_fetch_initialised = mix_fetch_initialised
|
||||
self.__rs_go_bridge__.finish_mixnet_connection = finish_mixnet_connection
|
||||
self.__rs_go_bridge__ = {};
|
||||
self.__rs_go_bridge__.send_client_data = send_client_data;
|
||||
self.__rs_go_bridge__.start_new_mixnet_connection = start_new_mixnet_connection;
|
||||
self.__rs_go_bridge__.mix_fetch_initialised = mix_fetch_initialised;
|
||||
self.__rs_go_bridge__.finish_mixnet_connection = finish_mixnet_connection;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -289,7 +252,7 @@ async function main() {
|
||||
|
||||
// load rust WASM package
|
||||
sendLog('Loading Rust WASM...');
|
||||
await wasm_bindgen(RUST_WASM_URL);
|
||||
await wasm_bindgen({ module_or_path: RUST_WASM_URL });
|
||||
sendLog('Loaded Rust WASM');
|
||||
|
||||
// load go WASM package
|
||||
@@ -302,7 +265,7 @@ async function main() {
|
||||
|
||||
setupRsGoBridge();
|
||||
|
||||
goWasmSetLogging("trace")
|
||||
goWasmSetLogging("trace");
|
||||
|
||||
// Set up message handler (MixFetch will be started on demand)
|
||||
setupMessageHandler();
|
||||
@@ -311,4 +274,4 @@ async function main() {
|
||||
}
|
||||
|
||||
// Let's get started!
|
||||
main();
|
||||
main();
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::go_bridge::{goWasmCloseRemoteSocket, goWasmInjectConnError, goWasmInj
|
||||
use crate::RequestId;
|
||||
use nym_ordered_buffer::OrderedMessageBuffer;
|
||||
use nym_socks5_requests::SocketData;
|
||||
use nym_wasm_utils::{console_error, console_log};
|
||||
use nym_wasm_utils::{console_error, console_log, console_warn};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -69,7 +69,8 @@ impl ActiveRequests {
|
||||
let mut guard = self.inner.lock().await;
|
||||
let old = guard.remove(&id);
|
||||
if old.is_none() {
|
||||
console_error!("attempted to reject request {id}, but it seems to have never existed?")
|
||||
console_error!("attempted to reject request {id}, but it no longer exists — likely already cleaned up by Go timeout");
|
||||
return;
|
||||
}
|
||||
|
||||
goWasmInjectConnError(id.to_string(), err.to_string())
|
||||
@@ -79,12 +80,15 @@ impl ActiveRequests {
|
||||
let id = data.header.connection_id;
|
||||
let mut guard = self.inner.lock().await;
|
||||
let Some(req) = guard.get_mut(&id) else {
|
||||
// if there's no data and the socket is closed, we're all good because our local must have already closed
|
||||
// if there's no data and the socket is closed, we're all good because our local
|
||||
// must have already closed - this is likely just a retransmitted fragment that
|
||||
// arrived after the original
|
||||
if !data.data.is_empty() || !data.header.local_socket_closed {
|
||||
console_error!("attempted to send data for request {id}, however it no longer exists. Has it been aborted?");
|
||||
console_warn!(
|
||||
"received data for request {id} which is no longer active \
|
||||
(likely a retransmitted packet for an already-completed request)"
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: if it doesn't exist here, make sure to clear Go's memory too
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test-results/
|
||||
@@ -0,0 +1,81 @@
|
||||
# mix-fetch Playwright Tests
|
||||
|
||||
Automated browser tests for the mix-fetch internal-dev harness. Tests run against the webpack-built `internal-dev/dist/` served locally.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
WASM build artifacts must exist (Go first, then Rust). For local dev, use the debug targets:
|
||||
|
||||
```bash
|
||||
# Builds to go-mix-conn/build/
|
||||
make -C wasm/mix-fetch/go-mix-conn build-debug-dev
|
||||
# Builds to pkg/ (needs Go bindings)
|
||||
make -C wasm/mix-fetch build-rust-debug
|
||||
```
|
||||
|
||||
CI uses the same debug targets.
|
||||
|
||||
Build the internal-dev webpack bundle:
|
||||
|
||||
```bash
|
||||
cd wasm/mix-fetch/internal-dev && npm install && npm run build
|
||||
```
|
||||
|
||||
Install Playwright and browsers:
|
||||
|
||||
```bash
|
||||
cd wasm/mix-fetch/tests && npm install && npx playwright install --with-deps
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# Smoke tests (all browsers, WASM load + MixFetch init)
|
||||
npm run test:smoke
|
||||
|
||||
# Stress tests (all browsers, stresstest on mainnet)
|
||||
npm run test:stress
|
||||
|
||||
# Single browser
|
||||
npx playwright test --project=smoke-chromium
|
||||
npx playwright test --project=stress-firefox
|
||||
```
|
||||
|
||||
## Test tiers
|
||||
|
||||
### Smoke (`smoke.spec.mjs`)
|
||||
|
||||
Verifies the internal-dev harness loads in a headless browser: Rust WASM + Go WASM initialise, the worker signals readiness, MixFetch connects to a random Entry Gateway, and no console errors are emitted. Runs on Chromium, Firefox, and WebKit.
|
||||
|
||||
- **CI workflow**: `ci-sdk-wasm.yml`
|
||||
- **Trigger**: every PR that touches `wasm/**`, `clients/client-core/**`, `common/**`, or the workflow itself
|
||||
- **Timeout**: 1 minute
|
||||
|
||||
### Stress (`stress.spec.mjs`)
|
||||
|
||||
Connects to mainnet via a random Entry Gateway, fires 10 concurrent mixed-size fetches through the mixnet, and asserts >= 80% succeed. Runs on all three browsers.
|
||||
|
||||
- **CI workflow**: `nightly-mix-fetch-stress.yml`
|
||||
- **Trigger**: daily at 03:00 UTC via cron, also available via `workflow_dispatch` for manual runs
|
||||
- **Timeout**: 2 minutes per browser, 2 retries
|
||||
|
||||
## Arch/Manjaro note
|
||||
|
||||
Playwright's WebKit is built for Ubuntu 24.04 and links against specific soname versions that don't match Arch's (e.g. `libicu*.so.74` vs `.so.78`, `libxml2.so.2` vs `.so.16`). `playwright install --with-deps` also fails because it uses `apt-get`.
|
||||
|
||||
Chromium and Firefox work without any workarounds. To skip WebKit locally:
|
||||
|
||||
```bash
|
||||
npx playwright test --project=smoke-chromium --project=smoke-firefox
|
||||
npx playwright test --project=stress-chromium --project=stress-firefox
|
||||
```
|
||||
|
||||
All three browsers work on CI (Ubuntu runners with `--with-deps`).
|
||||
|
||||
> **TODO**: investigate getting WebKit running on Arch/Manjaro (soname symlinks or alternative Playwright WebKit build).
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add Playwright CI for `wasm/client/` (nym-client-wasm) via the chat-app examples after WASM cleanup
|
||||
- [ ] Add Playwright CI for other SDK examples once stale dependencies are resolved
|
||||
- [ ] Consider WebKit system deps in CI runner setup (currently relies on `playwright install --with-deps` on Ubuntu)
|
||||
Generated
+1119
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mix-fetch-playwright-tests",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:smoke": "playwright test --project=smoke-chromium --project=smoke-firefox --project=smoke-webkit",
|
||||
"test:stress": "playwright test --project=stress-chromium --project=stress-firefox --project=stress-webkit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.52.0",
|
||||
"serve": "^14.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
timeout: 60_000,
|
||||
retries: 1,
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
webServer: {
|
||||
command: "npx serve ../internal-dev/dist -l 8001 --no-clipboard",
|
||||
port: 8001,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "smoke-chromium",
|
||||
testMatch: "smoke.spec.mjs",
|
||||
use: { browserName: "chromium" },
|
||||
},
|
||||
{
|
||||
name: "smoke-firefox",
|
||||
testMatch: "smoke.spec.mjs",
|
||||
use: { browserName: "firefox" },
|
||||
},
|
||||
{
|
||||
name: "smoke-webkit",
|
||||
testMatch: "smoke.spec.mjs",
|
||||
use: { browserName: "webkit" },
|
||||
},
|
||||
{
|
||||
name: "stress-chromium",
|
||||
testMatch: "stress.spec.mjs",
|
||||
timeout: 120_000,
|
||||
retries: 2,
|
||||
use: { browserName: "chromium" },
|
||||
},
|
||||
{
|
||||
name: "stress-firefox",
|
||||
testMatch: "stress.spec.mjs",
|
||||
timeout: 120_000,
|
||||
retries: 2,
|
||||
use: { browserName: "firefox" },
|
||||
},
|
||||
{
|
||||
name: "stress-webkit",
|
||||
testMatch: "stress.spec.mjs",
|
||||
timeout: 120_000,
|
||||
retries: 2,
|
||||
use: { browserName: "webkit" },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// Smoke test: verify the internal-dev harness loads both WASM runtimes
|
||||
// and successfully initialises a MixFetch connection to mainnet.
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
function waitForConsole(page, predicate, timeoutMs = 60_000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(`Timed out waiting for console message (${timeoutMs}ms)`)
|
||||
),
|
||||
timeoutMs
|
||||
);
|
||||
page.on("console", function handler(msg) {
|
||||
if (predicate(msg.text())) {
|
||||
clearTimeout(timer);
|
||||
page.removeListener("console", handler);
|
||||
resolve(msg.text());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("internal-dev harness loads and MixFetch initialises", async ({
|
||||
page,
|
||||
}) => {
|
||||
const errors = [];
|
||||
|
||||
// Forward worker lifecycle + errors to test output
|
||||
page.on("console", (msg) => {
|
||||
const text = msg.text();
|
||||
if (msg.type() === "error") {
|
||||
if (!text.includes("favicon.ico")) {
|
||||
errors.push(text);
|
||||
}
|
||||
console.log(`[ERROR] ${text}`);
|
||||
} else if (
|
||||
text.includes("Worker") ||
|
||||
text.includes("MixFetch") ||
|
||||
text.includes("Setting up") ||
|
||||
text.includes("gateway")
|
||||
) {
|
||||
console.log(text);
|
||||
}
|
||||
});
|
||||
|
||||
page.on("pageerror", (err) => {
|
||||
errors.push(`pageerror: ${err.message}`);
|
||||
});
|
||||
|
||||
const workerReady = waitForConsole(
|
||||
page,
|
||||
(text) => text.includes("Worker ready"),
|
||||
30_000
|
||||
);
|
||||
await page.goto("http://localhost:8001");
|
||||
await workerReady;
|
||||
|
||||
// Init MixFetch with a random gateway
|
||||
const mixFetchReady = waitForConsole(
|
||||
page,
|
||||
(text) => text.includes("MixFetch ready!"),
|
||||
120_000
|
||||
);
|
||||
await page.check('input[name="gateway-mode"][value="random"]');
|
||||
await page.click("#start-mixfetch");
|
||||
await mixFetchReady;
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
// Stress test: connect to mainnet via a random Entry Gateway and run concurrent fetches.
|
||||
// Pass criteria: >= 80% of requests succeed.
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const STRESS_COUNT = 10;
|
||||
const MIN_SUCCESS_RATE = 0.8;
|
||||
|
||||
function waitForConsole(page, predicate, timeoutMs = 60_000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(`Timed out waiting for console message (${timeoutMs}ms)`)
|
||||
),
|
||||
timeoutMs
|
||||
);
|
||||
page.on("console", function handler(msg) {
|
||||
if (predicate(msg.text())) {
|
||||
clearTimeout(timer);
|
||||
page.removeListener("console", handler);
|
||||
resolve(msg.text());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("stress test: mixed-size fetches through mainnet", async ({ page }) => {
|
||||
// Forward warnings, errors, and worker lifecycle messages to test output
|
||||
page.on("console", (msg) => {
|
||||
const text = msg.text();
|
||||
if (msg.type() === "warning" || msg.type() === "error") {
|
||||
console.log(`[${msg.type().toUpperCase()}] ${text}`);
|
||||
} else if (
|
||||
text.includes("Worker") ||
|
||||
text.includes("MixFetch") ||
|
||||
text.includes("stress") ||
|
||||
text.includes("COMPLETE") ||
|
||||
text.includes("FAIL")
|
||||
) {
|
||||
console.log(text);
|
||||
}
|
||||
});
|
||||
|
||||
const workerReady = waitForConsole(
|
||||
page,
|
||||
(text) => text.includes("Worker ready"),
|
||||
30_000
|
||||
);
|
||||
await page.goto("http://localhost:8001");
|
||||
await workerReady;
|
||||
|
||||
const mixFetchReady = waitForConsole(
|
||||
page,
|
||||
(text) => text.includes("MixFetch ready!"),
|
||||
120_000
|
||||
);
|
||||
await page.check('input[name="gateway-mode"][value="random"]');
|
||||
await page.click("#start-mixfetch");
|
||||
await mixFetchReady;
|
||||
|
||||
const stressComplete = waitForConsole(
|
||||
page,
|
||||
(text) => text.includes("=== COMPLETE:"),
|
||||
90_000
|
||||
);
|
||||
await page.fill("#stress-test-count", String(STRESS_COUNT));
|
||||
await page.selectOption("#stress-test-mode", "mixed");
|
||||
await page.click("#stress-test-button");
|
||||
const completionMsg = await stressComplete;
|
||||
|
||||
const match = completionMsg.match(/OK (\d+)\/(\d+)/);
|
||||
expect(
|
||||
match,
|
||||
`Could not parse completion message: ${completionMsg}`
|
||||
).toBeTruthy();
|
||||
|
||||
const [, succeeded, total] = match;
|
||||
const successRate = parseInt(succeeded, 10) / parseInt(total, 10);
|
||||
|
||||
console.log(
|
||||
`Stress test result: ${succeeded}/${total} succeeded (${(
|
||||
successRate * 100
|
||||
).toFixed(0)}%)`
|
||||
);
|
||||
expect(successRate).toBeGreaterThanOrEqual(MIN_SUCCESS_RATE);
|
||||
});
|
||||
Reference in New Issue
Block a user