Compare commits

...

13 Commits

Author SHA1 Message Date
mfahampshire 01f1a7c3d0 Merge branch 'develop' into max/mixfetch-edge-runtime-debug 2026-04-17 15:15:18 +00:00
mfahampshire d2a7199b07 revert changes to package.json + yarn.lock 2026-04-17 11:23:28 +01:00
mfahampshire 06ec97c08f Add gitignore - do not push local test results to remote 2026-04-15 18:39:46 +01:00
mfahampshire b5d20199a0 Add Playwright CI tests for mix-fetch internal-dev harness
Per-PR smoke test (ci-sdk-wasm.yml): loads WASM runtimes and inits
MixFetch on Chromium, Firefox, and WebKit.

Nightly stress test (nightly-mix-fetch-stress.yml): connects to mainnet
via random gateway, runs 10 mixed-size fetches, asserts >= 80% pass.
2026-04-15 18:39:44 +01:00
mfahampshire 9790033f7b Fix deprecated wasm_bindgen init call in internal-dev worker 2026-04-15 18:32:59 +01:00
mfahampshire 3dd1658c08 Downgrade misleading late-packet log from error to warning in mix-fetch 2026-04-15 18:32:59 +01:00
mfahampshire 9d40039f88 Simplify internal-dev test harness 2026-04-15 18:32:59 +01:00
mfahampshire 9f013aaab0 Revert formatting changes to make PR less noisy 2026-04-15 18:32:59 +01:00
mfahampshire b0f5945ba8 Fix erroneous versioning bump + version in internal-dev 2026-04-15 18:32:58 +01:00
mfahampshire e656157284 Make internal-dev less debug specific
- Evergreen the drip test
- Add setup config to MixFetch instance
2026-04-15 18:32:58 +01:00
mfahampshire 114e9b2f15 Version bump 2026-04-15 18:32:58 +01:00
mfahampshire 2274dd4eeb Fix cross-runtime panic in mix-fetch when timed-out requests receive
late error injection
- Add early return in reject() when request is already gone from Rust
  map
- Replace panic with log + return in SendError and InjectData
2026-04-15 18:32:58 +01:00
mfahampshire be5cdb1ebe Debug 2026-04-15 18:32:58 +01:00
23 changed files with 2070 additions and 512 deletions
+24
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
}
+119 -77
View File
@@ -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
&mdash; 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>
+203 -165
View File
@@ -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
View File
@@ -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": {
+3 -3
View File
@@ -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",
+86 -123
View File
@@ -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();
+10 -6
View File
@@ -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;
};
+1
View File
@@ -0,0 +1 @@
test-results/
+81
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+13
View File
@@ -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" },
},
],
});
+71
View File
@@ -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);
});