Compare commits

..

36 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu f4bd48263d Apply cargo fmt 2026-06-08 15:32:57 +03:00
rachyandco 5c40052d39 Bugfix: set connection fd callback on LP registration socket
The LP registration client opened its TCP connection to the gateway
  control port with a plain TcpStream::connect, without invoking the
  connection_fd_callback used by the websocket registration path. On
  Linux the callback sets SO_MARK (fwmark) so the daemon's own firewall
  allows the connection during the connecting state; without it the
  unmarked SYN matched no allow rule and was rejected locally with
  ECONNREFUSED. The daemon blamed the gateway, blacklisted it, and
  cycled through every gateway the same way, making it impossible to
  connect when LP registration is enabled.

  Plumb the existing connection_fd_callback from the builder config
  through RegistrationClientConfig into the LP client via an optional
  dialer, which creates the socket, applies the callback before
  connect(), and only then dials. The fd callback must run before
  connect so the SYN itself carries the mark.

  The exit gateway needs no dialer: its registration is forwarded
  through the entry gateway's nested session and never opens its own
  TCP connection.
2026-06-05 23:36:52 +02:00
Jędrzej Stuczyński 7b858dfd69 chore: LP registration adjustments (#6845)
* remove mixnet fallback for LP registration

* change LP registration timeouts and introduce exchange timeout

* remove fallback client construction and disable mixnet via LP registration
2026-06-02 16:28:57 +01:00
Jack Wampler a4bd547023 Handle Rate Limit Challenge Response (#6825)
rotate urls on HTTP response error indicating API rate limiting
2026-05-26 14:58:45 -06:00
Andy Duplain db03ec31b1 Merge pull request #6812 from nymtech/cherry-pick/nym-583-corrupt-db-windows
NYM-583: Avoid corrupted database on Windows.
2026-05-21 14:44:39 +01:00
Andy Duplain 9b285735b8 NYM-583: Avoid corrupted database on Windows.
NYM-583: Avoid corrupted database on Windows.
2026-05-21 14:23:20 +01:00
Simon Wicky 691280797a back to v8 on non sdk client (#6771) 2026-05-13 18:24:50 +02:00
benedettadavico f84de25302 update changelog 2026-05-06 07:16:42 +02:00
benedettadavico e554f1e0ad bump versions 2026-04-28 15:02:40 +02:00
benedetta davico 62a4a2ed70 Merge pull request #6710 from nymtech/bdq/versioning-fix 2026-04-28 10:01:52 +02:00
benedetta davico caad74c73d Merge pull request #6713 from nymtech/bdq/nym-binaries-ci
update CI runners
2026-04-27 15:39:44 +02:00
benedettadavico 917993d8fb clean 2026-04-27 12:17:31 +02:00
benedettadavico 1451db39e6 warn 2026-04-27 11:27:41 +02:00
benedettadavico f13a2a6c06 change to warn level 2026-04-27 10:45:42 +02:00
benedetta davico ce39fb6675 Update publish-nym-binaries.yml 2026-04-27 10:20:10 +02:00
benedettadavico 02a926b74a addressing comments 2026-04-27 10:10:08 +02:00
benedetta davico 54ba710ea0 Change CI platform from ubuntu-22.04 to arc-ubuntu-22.04 2026-04-27 09:33:57 +02:00
benedettadavico 2653d12e55 fix ipr msg, and unit tests 2026-04-24 16:07:49 +02:00
benedettadavico f94d6d51cf adding debugging traces 2026-04-24 14:11:19 +02:00
Andrej Mihajlov a0116f9aec Merge pull request #6708 from nymtech/am/lazy-init-dns
Only init SHARED_CLIENT if requested
2026-04-23 18:36:57 +02:00
Tommy Verrall aaa8ee9d53 Revert "Merge remote-tracking branch 'origin/develop' into chore/eslint-9-flat-config-migration"
This reverts commit ab0f6af4b9, reversing
changes made to cca19f36c2.
2026-04-23 15:42:27 +01:00
Tommy Verrall ab0f6af4b9 Merge remote-tracking branch 'origin/develop' into chore/eslint-9-flat-config-migration 2026-04-23 15:40:01 +01:00
Tommy Verrall 7669d0933f Migrate ESLint config to flat config for ESLint 9
Dependabot bumped eslint to ^9 across the lint-scoped TS packages but did
not migrate the legacy .eslintrc.* configs, breaking CI lint on develop.

Behavior preserved: yarn lint passes locally with the same effective rule
coverage as the pre-bump setup. Pre-existing warnings in nym-wallet and
mui-theme are unchanged. Orphan .eslintrc files in sdk/typescript outside
the lerna lint scope are left untouched.
2026-04-23 15:39:11 +01:00
Andrej Mihajlov 50433fe265 Only init SHARED_CLIENT if requested 2026-04-23 16:29:02 +02:00
benedettadavico 42aade29eb more v9 fixes 2026-04-23 13:28:17 +02:00
benedettadavico 9f26759b8d v9 bugfix 2026-04-23 13:28:17 +02:00
benedettadavico 9e642c6354 v9 bugfix 2026-04-23 13:28:16 +02:00
mfahampshire cca19f36c2 Remove unused header (#6699) 2026-04-22 09:35:59 +00:00
Merve 17894880e0 Changelog urda (#6698)
* test identity

* changelog update
2026-04-22 08:41:12 +00:00
benedetta davico ef6fc82c39 Merge pull request #6696 from nymtech/release/2026.8-urda
final merge todevelop
2026-04-21 13:14:23 +02:00
benedettadavico 0c83ae2408 duplicate description 2026-04-21 12:06:23 +02:00
benedetta davico 92490731e7 Merge pull request #6691 from nymtech/merge/release/2026.8-urda
Merge release/2026.8 urda
2026-04-20 14:28:34 +02:00
benedettadavico 0ce93e366e Merge branch 'release/2026.8-urda' into develop 2026-04-20 14:10:23 +02:00
benedettadavico 0d031875f6 merge conflicts 2026-04-20 14:07:38 +02:00
benedettadavico e6103e4c43 update changelog 2026-04-20 13:58:11 +02:00
benedettadavico e32c042c8d version bump 2026-04-17 11:03:03 +02:00
54 changed files with 978 additions and 2206 deletions
@@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-22.04]
platform: [arc-ubuntu-22.04]
runs-on: ${{ matrix.platform }}
env:
-24
View File
@@ -54,30 +54,6 @@ 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
@@ -1,61 +0,0 @@
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/
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: arc-ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
+28
View File
@@ -4,6 +4,34 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.9-venaco] (2026-05-06)
- Fix for v9 IPR ([#6710])
- Only init SHARED_CLIENT if requested ([#6708])
- Fixes to crates and CI ([#6686])
- Return ipv6 addresses as well ([#6684])
- Fix invalid ticket spend ([#6683])
- Block non-public IPR/NR checks ([#6670])
[#6710]: https://github.com/nymtech/nym/pull/6710
[#6708]: https://github.com/nymtech/nym/pull/6708
[#6686]: https://github.com/nymtech/nym/pull/6686
[#6684]: https://github.com/nymtech/nym/pull/6684
[#6683]: https://github.com/nymtech/nym/pull/6683
[#6670]: https://github.com/nymtech/nym/pull/6670
## [2026.8-urda] (2026-04-20)
- Include all gateways in the returned list ([#6649])
- Optimize GW probe in NS agent ([#6636])
- Max/sdk docrs ([#6566])
- Max/sdk stream wrapper ([#6320])
[#6649]: https://github.com/nymtech/nym/pull/6649
[#6636]: https://github.com/nymtech/nym/pull/6636
[#6566]: https://github.com/nymtech/nym/pull/6566
[#6320]: https://github.com/nymtech/nym/pull/6320
## [2026.7-tola] (2026-04-07)
- Simon/ecash contract serde fix ([#6634])
Generated
+10 -9
View File
@@ -5275,7 +5275,7 @@ dependencies = [
[[package]]
name = "mix-fetch-wasm"
version = "1.4.4"
version = "1.4.3"
dependencies = [
"async-trait",
"futures",
@@ -5635,7 +5635,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.77"
version = "1.1.79"
dependencies = [
"anyhow",
"async-trait",
@@ -5880,7 +5880,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.74"
version = "1.1.76"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5963,7 +5963,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.74"
version = "1.1.76"
dependencies = [
"bs58",
"clap",
@@ -7028,6 +7028,7 @@ dependencies = [
"bytes",
"futures",
"nym-ip-packet-requests",
"nym-lp",
"nym-sdk",
"thiserror 2.0.12",
"tokio",
@@ -7372,7 +7373,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.75"
version = "1.1.77"
dependencies = [
"addr",
"anyhow",
@@ -7422,7 +7423,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.29.0"
version = "1.31.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -7974,7 +7975,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.74"
version = "1.1.76"
dependencies = [
"bs58",
"clap",
@@ -8242,7 +8243,7 @@ dependencies = [
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"tracing-test",
"windows 0.61.3",
]
@@ -8772,7 +8773,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.39"
version = "0.1.41"
dependencies = [
"anyhow",
"bytes",
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.74"
version = "1.1.76"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.74"
version = "1.1.76"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
+10
View File
@@ -25,6 +25,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
async fn close(&self) {}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -56,6 +58,10 @@ where
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
async fn close(&self) {
self.storage.close().await;
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -75,4 +81,8 @@ impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
async fn close(&self) {
(**self).close().await;
}
}
@@ -1023,6 +1023,16 @@ where
let encryption_keys = init_res.client_keys.encryption_keypair();
let identity_keys = init_res.client_keys.identity_keypair();
let credential_store_for_close = credential_store.clone();
let close_credential_token = shutdown_tracker.clone_shutdown_token();
shutdown_tracker.try_spawn_named(
async move {
close_credential_token.cancelled().await;
credential_store_for_close.close().await;
},
"CredentialStorage::close_on_shutdown",
);
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
let bandwidth_controller = self
@@ -11,11 +11,17 @@ use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path};
use std::{io, path::Path, time::Duration};
use time::OffsetDateTime;
use tracing::{error, info, trace};
use url::Url;
/// Maximum rename retry attempts when the database file is temporarily locked.
const ARCHIVE_MAX_RETRY_ATTEMPTS: u8 = 15;
/// Delay between archive rename retry attempts.
const ARCHIVE_RETRY_DELAY: Duration = Duration::from_millis(200);
async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
@@ -74,13 +80,58 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
};
let renamed = db_path.with_extension(new_extension);
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
// On Windows, sqlx may release its OS file handles asynchronously after
// pool.close() returns, briefly keeping the file locked
// (ERROR_SHARING_VIOLATION, os error 32). Retry with a short delay to
// give the OS time to flush the remaining handles.
for attempt in 0..ARCHIVE_MAX_RETRY_ATTEMPTS {
match tokio::fs::rename(db_path, &renamed).await {
Ok(()) => return Ok(()),
Err(e) if is_file_locked_error(&e) && (attempt + 1) < ARCHIVE_MAX_RETRY_ATTEMPTS => {
trace!(
"Database file is temporarily locked, retrying archive \
(attempt {}/{}): {e}",
attempt + 1,
ARCHIVE_MAX_RETRY_ATTEMPTS
);
tokio::time::sleep(ARCHIVE_RETRY_DELAY).await;
}
Err(e) => {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
return Err(e);
}
}
}
// Reached only when every attempt was blocked by a file lock.
error!(
"Failed to rename corrupt database file after {} attempts: {} to {}",
ARCHIVE_MAX_RETRY_ATTEMPTS,
db_path.display(),
renamed.display()
);
Err(io::Error::other(
"corrupt database archive blocked by persistent file lock",
))
}
/// Returns `true` when the IO error indicates a temporary file lock held by another handle
/// within the same process. Only meaningful on Windows; always `false` elsewhere.
fn is_file_locked_error(e: &io::Error) -> bool {
#[cfg(windows)]
{
// ERROR_SHARING_VIOLATION = 32, ERROR_LOCK_VIOLATION = 33
matches!(e.raw_os_error(), Some(32) | Some(33))
}
#[cfg(not(windows))]
{
let _ = e;
false
}
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -337,6 +337,8 @@ impl ReplyStorageBackend for Backend {
}
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
self.stop_client_use().await
let result = self.stop_client_use().await;
self.shutdown().await;
result
}
}
+5 -3
View File
@@ -48,6 +48,7 @@ where
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
error!("failed to start the storage session - {err}");
self.backend.stop_storage_session().await.ok();
return;
}
@@ -55,10 +56,11 @@ where
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
info!("Data flush is complete")
error!("failed to flush our reply-related data to the persistent storage: {err}");
self.backend.stop_storage_session().await.ok();
return;
}
info!("Data flush is complete");
if let Err(err) = self.backend.stop_storage_session().await {
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
+54 -10
View File
@@ -1152,7 +1152,10 @@ impl ApiClientCore for Client {
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
let client = self
.reqwest_client
.as_ref()
.unwrap_or_else(|| &*SHARED_CLIENT);
Ok(
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
.await
@@ -1162,12 +1165,24 @@ impl ApiClientCore for Client {
#[cfg(not(target_arch = "wasm32"))]
let response = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
let client = self
.reqwest_client
.as_ref()
.unwrap_or_else(|| &*SHARED_CLIENT);
client.execute(req).await
};
match response {
Ok(resp) => return Ok(resp),
Ok(resp) => {
// Check if the response includes a rate limit error from the vercel API
if is_http_rate_limit_err(&resp) {
warn!("encountered vercel rate limit error for {}", url.as_str());
// if we have multiple urls, update to the next
self.maybe_rotate_hosts(Some(url.clone()));
}
return Ok(resp);
}
Err(err) => {
#[cfg(target_arch = "wasm32")]
let is_network_err = err.is_timeout();
@@ -1220,17 +1235,39 @@ impl ApiClientCore for Client {
}
}
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
/// Check for Rate Limit challenge response from the vercel API
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
let status = resp.status() == StatusCode::FORBIDDEN;
let header = resp
.headers()
.get(VERCEL_CHALLENGE_HEADER)
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
let content_type = resp
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok())
.is_some_and(|mime_type| {
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
});
status && header && content_type
}
#[cfg(not(target_arch = "wasm32"))]
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
/// This functions attempts to check the error returned by reqwest to see if
/// rotating host informtion (for clients with mutliple hosts defined) could be
/// helpful. This looks for situations where the error could plausibly be caused
/// by a network adversary, or where rotating to an equival hostname might help.
/// This functions attempts to check the error returned by reqwest to see if rotating host
/// information (for clients with multiple hosts defined) could be helpful. This looks for
/// situations where the error could plausibly be caused by a network adversary, or where rotating
/// to an equivalent hostname might help.
///
/// For example --> NetworkUnreachable will not be helped by rotating domains,
/// but ConnectionReset might be caused by a network adversary blocking by SNI
/// which could possibly benefit from rotating domains.
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
/// rotating domains.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
if err.is_timeout() {
@@ -1691,6 +1728,13 @@ where
decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url: Box::new(url) })
} else if is_http_rate_limit_err(&res) {
Err(HttpClientError::EndpointFailure {
url: Box::new(url),
status,
headers: Box::new(headers),
error: String::from("received vercel rate limit challenge response"),
})
} else {
let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure {
+8
View File
@@ -12,6 +12,14 @@ pub mod v7;
pub mod v8;
pub mod v9;
/// Highest IPR protocol version that is allowed to be sent as a **non-stream** mixnet payload
/// (i.e. not wrapped in `LpFrameKind::SphinxStream`).
pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
/// First IPR protocol version that **requires** the SphinxStream (LP) transport for non-stream
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
// version 3: initial version
// version 4: IPv6 support
// version 5: Add severity level to info response
@@ -57,6 +57,34 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2026.8-urda`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.8-urda)
- [`nym-node`](nodes/nym-node.mdx) version `1.30.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2026-04-21T10:11:28.164080002Z
Build Version: 1.30.0
Commit SHA: 0c83ae2408ea9efcc1abae613711c6e4e16148ad
Commit Date: 2026-04-21T12:06:23.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
cargo Profile: release
```
### Features
- [Include all gateways in the returned list](https://github.com/nymtech/nym/pull/6649): Ensures gateway APIs return the full gateway set instead of filtered results, improving consistency across services.
- [Max/sdk stream wrapper](https://github.com/nymtech/nym/pull/6320): Introduces a Rust SDK stream abstraction enabling IPR-based mixnet communication and client-side streaming support.
- [Max/sdk docrs](https://github.com/nymtech/nym/pull/6566): Updates SDK documentation to reflect the new stream-based architecture and IPR communication model.
### Refactors & Maintenance
- [Optimize GW probe in NS agent](https://github.com/nymtech/nym/pull/6636): Refactors gateway probe integration from subprocess execution to library usage, removing CLI dependency and improving typed communication between agent and probe.
## `v2026.7-tola`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.7-tola)
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
Binary Name: nym-node
Build Timestamp: 2026-04-08T10:31:49.141185557Z
Build Version: 1.29.0
Commit SHA: 97068b2aace6626e3dff6864a26e7b1ff35f5725
Commit Date: 2026-04-07T15:51:44.000000000+02:00
Build Timestamp: 2026-04-21T10:11:28.164080002Z
Build Version: 1.30.0
Commit SHA: 0c83ae2408ea9efcc1abae613711c6e4e16148ad
Commit Date: 2026-04-21T12:06:23.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.77"
version = "1.1.79"
authors.workspace = true
edition = "2021"
license = "GPL-3.0"
+1
View File
@@ -26,3 +26,4 @@ tracing.workspace = true
nym-sdk = { workspace = true }
nym-ip-packet-requests = { workspace = true }
nym-lp = { workspace = true }
+1 -1
View File
@@ -11,4 +11,4 @@ pub use error::Error;
pub use listener::{IprListener, MixnetMessageOutcome};
// Re-export the currently used version
pub use nym_ip_packet_requests::v9 as current;
pub use nym_ip_packet_requests::v8 as current;
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-node"
version = "1.29.0"
version = "1.31.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0"
@@ -97,6 +97,8 @@ impl BuilderConfig {
exit: self.exit_node.clone(),
mode: self.mode,
lp_registration_config: self.lp_registration_config,
#[cfg(unix)]
connection_fd_callback: self.connection_fd_callback.clone(),
}
}
@@ -143,7 +143,6 @@ impl RegistrationClientBuilder {
config,
bandwidth_controller,
cancel_token: self.config.cancel_token.clone(),
fallback_client_builder: Some(self),
})
}
}
+40 -23
View File
@@ -18,13 +18,12 @@ use rand09::{CryptoRng, RngCore, SeedableRng};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio_util::sync::CancellationToken;
use tracing::warn;
pub struct LpBasedRegistrationClient {
pub(crate) config: RegistrationClientConfig,
pub(crate) bandwidth_controller: Box<dyn BandwidthTicketProvider>,
pub(crate) cancel_token: CancellationToken,
// While we allow a fallback, we need to be able to build it
pub(crate) fallback_client_builder: Option<RegistrationClientBuilder>,
}
impl LpBasedRegistrationClient {
@@ -81,6 +80,32 @@ impl LpBasedRegistrationClient {
self.config.lp_registration_config,
);
// Open the entry connection through a socket that has the connection
// fd callback applied before connecting (sets SO_MARK on Linux), so
// the connection is allowed through the VPN firewall during the
// connecting state.
#[cfg(unix)]
{
let fd_callback = self.config.connection_fd_callback.clone();
entry_client.set_dialer(Arc::new(move |addr| {
let fd_callback = fd_callback.clone();
Box::pin(async move {
let socket = if addr.is_ipv4() {
tokio::net::TcpSocket::new_v4()
} else {
tokio::net::TcpSocket::new_v6()
}
.map_err(|err| {
nym_lp::transport::LpTransportError::connection_failure(err.to_string())
})?;
fd_callback(std::os::fd::AsRawFd::as_raw_fd(&socket));
socket.connect(addr).await.map_err(|err| {
nym_lp::transport::LpTransportError::connection_failure(err.to_string())
})
})
}));
}
// Perform handshake with entry gateway (outer session now established)
entry_client.perform_handshake().await.map_err(|source| {
RegistrationClientError::EntryGatewayRegisterLp {
@@ -162,15 +187,11 @@ impl LpBasedRegistrationClient {
self.register_wg_with_rng(&mut rng).await
}
pub(crate) async fn register(mut self) -> Result<RegistrationResult, RegistrationClientError> {
let fallback = self.fallback_client_builder.take();
async fn register_inner(mut self) -> Result<RegistrationResult, RegistrationClientError> {
match &self.config.mode {
RegistrationMode::Mixnet => {
if let Some(fallback) = fallback {
register_with_fallback(fallback).await
} else {
Err(RegistrationClientError::UnsupportedMode)
}
// mixnet registration is not supported for LP
Err(RegistrationClientError::UnsupportedMode)
}
RegistrationMode::Wireguard => {
let lp_registration_result = self
@@ -182,15 +203,9 @@ impl LpBasedRegistrationClient {
// Everything went fine
Some(Ok(res)) => Ok(res),
// LP reg failed, try fallback if we have one
Some(Err(e)) => {
tracing::error!("LP registration failed : {e}");
if let Some(fallback) = fallback {
tracing::info!("Registering with fallback");
register_with_fallback(fallback).await
} else {
Err(e)
}
Err(e)
}
// Cancelled registration
@@ -199,12 +214,14 @@ impl LpBasedRegistrationClient {
}
}
}
}
async fn register_with_fallback(
client_builder: RegistrationClientBuilder,
) -> Result<RegistrationResult, RegistrationClientError> {
// This is forcefully building a mixnet based client
let fallback_client = client_builder.build_mixnet().await?;
fallback_client.register().await
pub(crate) async fn register(mut self) -> Result<RegistrationResult, RegistrationClientError> {
let timeout = self.config.lp_registration_config.exchange_timeout;
tokio::time::timeout(timeout, self.register_inner())
.await
.unwrap_or_else(|timeout| {
warn!("timed out while attempting to complete LP registration");
Err(RegistrationClientError::Timeout(timeout))
})
}
}
+5
View File
@@ -19,4 +19,9 @@ pub struct RegistrationClientConfig {
pub(crate) exit: NymNodeWithKeys,
pub(crate) mode: RegistrationMode,
pub(crate) lp_registration_config: LpRegistrationConfig,
/// Callback invoked with the raw fd of sockets opened for registration,
/// before connecting. Used to set `SO_MARK` on Linux so the connection is
/// allowed through the VPN firewall during the connecting state.
#[cfg(unix)]
pub(crate) connection_fd_callback: std::sync::Arc<dyn Fn(std::os::fd::RawFd) + Send + Sync>,
}
+53 -16
View File
@@ -33,6 +33,19 @@ use std::time::Duration;
use tokio::net::TcpStream;
use tracing::{debug, warn};
/// Custom dialer used to open the connection to the gateway.
///
/// Allows the caller to configure the socket before the connection is
/// initiated, e.g. set `SO_MARK` on Linux so the connection is allowed
/// through the VPN firewall during the connecting state.
pub type LpDialer<S> = Arc<
dyn Fn(
SocketAddr,
) -> futures::future::BoxFuture<'static, std::result::Result<S, LpTransportError>>
+ Send
+ Sync,
>;
/// LP (Lewes Protocol) registration client for direct gateway connections.
///
/// This client uses a persistent TCP connection model where a single TCP
@@ -70,6 +83,11 @@ pub struct LpRegistrationClient<S = TcpStream> {
/// Persistent TCP stream for the connection.
/// Opened on first use, closed after registration.
stream: Option<S>,
/// Optional custom dialer used to open the connection, allowing socket
/// configuration (e.g. `SO_MARK`) before the connection is initiated.
/// Falls back to `S::connect` when unset.
dialer: Option<LpDialer<S>>,
}
impl<S> LpRegistrationClient<S>
@@ -115,9 +133,18 @@ where
transport_session: None,
config,
stream: None,
dialer: None,
}
}
/// Sets a custom dialer used to open the connection to the gateway.
///
/// Allows socket configuration (e.g. setting `SO_MARK` on Linux so the
/// connection is allowed through the VPN firewall) before connecting.
pub fn set_dialer(&mut self, dialer: LpDialer<S>) {
self.dialer = Some(dialer);
}
/// Attempt to use this `LpRegistrationClient` as transport for `NestedSession`
pub fn as_nested_connection(&mut self, exit_address: SocketAddr) -> NestedConnection<'_, S> {
NestedConnection {
@@ -209,22 +236,32 @@ where
self.gateway_lp_address
);
let mut stream = tokio::time::timeout(
self.config.connect_timeout,
S::connect(self.gateway_lp_address),
)
.await
.map_err(|_| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source: LpTransportError::ConnectionFailure(format!(
"Connection timeout after {:?}",
self.config.connect_timeout
)),
})?
.map_err(|source| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source,
})?;
let connect_result = match &self.dialer {
Some(dialer) => {
tokio::time::timeout(self.config.connect_timeout, dialer(self.gateway_lp_address))
.await
}
None => {
tokio::time::timeout(
self.config.connect_timeout,
S::connect(self.gateway_lp_address),
)
.await
}
};
let mut stream = connect_result
.map_err(|_| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source: LpTransportError::ConnectionFailure(format!(
"Connection timeout after {:?}",
self.config.connect_timeout
)),
})?
.map_err(|source| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source,
})?;
// Set TCP_NODELAY for low latency
stream
+27 -24
View File
@@ -28,40 +28,43 @@ use std::time::Duration;
/// - Optimize for latency over throughput (small messages)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LpRegistrationConfig {
/// TCP connection timeout (nym-102).
/// TCP connection timeout.
///
/// Maximum time to wait for TCP connection establishment.
/// Default: 10 seconds.
/// Default: 5 seconds.
pub connect_timeout: Duration,
/// Noise protocol handshake timeout (nym-102).
/// KKT/PSQ protocol handshake timeout.
///
/// Maximum time to wait for Noise handshake completion (all round-trips).
/// Default: 15 seconds.
/// Maximum time to wait for KKT/PSQ handshake completion with the entry (all round-trips).
/// Default: 8 seconds.
pub handshake_timeout: Duration,
/// Registration request/response timeout (nym-102).
/// Registration request/response timeout.
///
/// Maximum time to wait for registration request send + response receive.
/// Includes credential verification on gateway side.
/// Default: 30 seconds.
/// Default: 8 seconds.
pub registration_timeout: Duration,
/// Maximum time for the whole exchange (handshake + registration).
/// Default: 20 seconds.
pub exchange_timeout: Duration,
/// Forward packet send/receive timeout.
///
/// Maximum time to wait for forward packet send + response receive via entry gateway.
/// Covers the entire round-trip through entry to exit gateway and back.
/// Default: 30 seconds.
/// Maximum time to wait for forward packet to get sent via entry gateway.
/// Default: 3 seconds.
pub forward_timeout: Duration,
/// Enable TCP_NODELAY (disable Nagle's algorithm) (nym-104).
/// Enable TCP_NODELAY (disable Nagle's algorithm).
///
/// When true, disables Nagle's algorithm for lower latency.
/// Recommended for registration messages which are small and latency-sensitive.
/// Default: true.
pub tcp_nodelay: bool,
/// TCP keepalive duration (nym-104).
/// TCP keepalive duration.
///
/// When Some, enables TCP keepalive with specified interval.
/// Since LP is registration-only with short-lived connections, keepalive is not needed.
@@ -72,15 +75,14 @@ pub struct LpRegistrationConfig {
impl Default for LpRegistrationConfig {
fn default() -> Self {
Self {
// nym-102: Sane timeout defaults for real network conditions
connect_timeout: Duration::from_secs(10),
handshake_timeout: Duration::from_secs(15),
registration_timeout: Duration::from_secs(30),
forward_timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(5),
handshake_timeout: Duration::from_secs(8),
registration_timeout: Duration::from_secs(8),
exchange_timeout: Duration::from_secs(20),
forward_timeout: Duration::from_secs(3),
// nym-104: Optimized for registration-only protocol
tcp_nodelay: true, // Lower latency for small messages
tcp_keepalive: None, // Not needed for ephemeral connections
tcp_nodelay: true,
tcp_keepalive: None,
}
}
}
@@ -93,10 +95,11 @@ mod tests {
fn test_default_config() {
let config = LpRegistrationConfig::default();
assert_eq!(config.connect_timeout, Duration::from_secs(10));
assert_eq!(config.handshake_timeout, Duration::from_secs(15));
assert_eq!(config.registration_timeout, Duration::from_secs(30));
assert_eq!(config.forward_timeout, Duration::from_secs(30));
assert_eq!(config.connect_timeout, Duration::from_secs(5));
assert_eq!(config.handshake_timeout, Duration::from_secs(8));
assert_eq!(config.registration_timeout, Duration::from_secs(8));
assert_eq!(config.forward_timeout, Duration::from_secs(3));
assert_eq!(config.exchange_timeout, Duration::from_secs(20));
assert!(config.tcp_nodelay);
assert_eq!(config.tcp_keepalive, None);
}
+1 -1
View File
@@ -41,4 +41,4 @@ windows = { version = "0.61", features = [
[dev-dependencies]
tempfile.workspace = true
tracing-subscriber.workspace = true
tracing-test.workspace = true
+96 -27
View File
@@ -3,8 +3,9 @@
use std::{
io,
ops::{Deref, DerefMut},
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
@@ -26,10 +27,8 @@ const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 20;
/// Delay between file checks
const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100);
/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217).
/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed.
#[derive(Debug, Clone)]
pub struct SqlitePoolGuard {
#[derive(Debug)]
struct SqlitePoolGuardInner {
/// Path to sqlite database file.
database_path: PathBuf,
@@ -37,6 +36,18 @@ pub struct SqlitePoolGuard {
connection_pool: sqlx::SqlitePool,
}
/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217).
/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed.
///
/// This type is cheaply [`Clone`]-able: all clones share the same underlying pool and the same
/// reference count. The `Drop` impl only emits a warning when the **last** reference is dropped
/// without an explicit [`close`](Self::close) call, so it is safe to clone this guard temporarily
/// (e.g. to pass into a spawned task) without triggering spurious warnings.
#[derive(Debug, Clone)]
pub struct SqlitePoolGuard {
inner: Arc<SqlitePoolGuardInner>,
}
impl SqlitePoolGuard {
/// Create new instance providing path to database and connection pool
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
@@ -46,46 +57,70 @@ impl SqlitePoolGuard {
.to_path_buf();
Self {
database_path,
connection_pool,
inner: Arc::new(SqlitePoolGuardInner {
database_path,
connection_pool,
}),
}
}
/// Returns database path
pub fn database_path(&self) -> &Path {
&self.database_path
&self.inner.database_path
}
/// Close udnerlying sqlite pool and wait for files to be closed before returning.
/// Close the underlying sqlite pool and wait for OS file handles to be released.
///
/// **Callers must invoke this method before dropping the guard.** The `Drop` impl does
/// not perform async cleanup; it only logs a warning when the pool was not closed
/// beforehand.
pub async fn close(&self) {
// Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database.
if !self.connection_pool.is_closed() {
tracing::info!("Closing sqlite pool: {}", self.database_path.display());
if !self.inner.connection_pool.is_closed() {
tracing::info!(
"Closing sqlite pool: {}",
self.inner.database_path.display()
);
self.close_pool_inner().await.ok();
}
}
async fn close_pool_inner(&self) -> std::io::Result<()> {
self.connection_pool.close().await;
self.inner.connection_pool.close().await;
self.wait_for_db_files_close().await.inspect_err(|e| {
tracing::error!("Failed to wait for file to close: {e}");
})
if let Err(e) = self.wait_for_db_files_close().await {
if e.kind() == std::io::ErrorKind::TimedOut {
tracing::warn!(
"Timed out waiting for OS file handles for sqlite database to be released; \
another connection to the same file may still be open. Path = {}",
self.inner.database_path.display()
);
} else {
tracing::warn!(
"Failed to wait for sqlite database file handles to be released: Path = {}. Error = {}",
self.inner.database_path.display(),
e
);
}
}
Ok(())
}
/// Returns all database files, including shm and wal files.
fn all_database_files(&self) -> Vec<PathBuf> {
let mut database_files = vec![];
let canonical_path = self
.inner
.database_path
.canonicalize()
.inspect_err(|e| {
tracing::error!(
"Failed to canonicalize path: {}. Cause: {e}",
self.database_path.display()
self.inner.database_path.display()
);
})
.unwrap_or(self.database_path.clone());
.unwrap_or(self.inner.database_path.clone());
if let Some(ext) = canonical_path.extension() {
for added_ext in ["-shm", "-wal"] {
@@ -120,34 +155,38 @@ impl SqlitePoolGuard {
}
}
impl Drop for SqlitePoolGuard {
fn drop(&mut self) {
if Arc::strong_count(&self.inner) == 1 && !self.inner.connection_pool.is_closed() {
tracing::warn!(
"SqlitePoolGuard dropped without explicit close(); path={}",
self.inner.database_path.display()
);
}
}
}
impl Deref for SqlitePoolGuard {
type Target = sqlx::SqlitePool;
fn deref(&self) -> &Self::Target {
&self.connection_pool
&self.inner.connection_pool
}
}
impl DerefMut for SqlitePoolGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.connection_pool
}
}
#[cfg(test)]
mod tests {
use sqlx::{
ConnectOptions, Executor,
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
};
use tracing_test::traced_test;
use super::*;
#[traced_test]
#[tokio::test]
async fn test_wait_close() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.init();
let temp_dir = tempfile::tempdir().unwrap();
let database_path = temp_dir.path().join("storage.db");
@@ -177,4 +216,34 @@ mod tests {
assert!(guard.close_pool_inner().await.is_ok());
tokio::fs::remove_file(database_path).await.unwrap();
}
#[traced_test]
#[tokio::test]
async fn test_clone_drop_no_warning() {
// Cloning the guard and dropping the clone should not warn because the original is still alive.
let temp_dir = tempfile::tempdir().unwrap();
let database_path = temp_dir.path().join("storage2.db");
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path.clone())
.create_if_missing(true)
.disable_statement_logging();
let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
let guard = SqlitePoolGuard::new(connection_pool);
{
let _clone = guard.clone();
assert_eq!(Arc::strong_count(&guard.inner), 2);
}
assert_eq!(Arc::strong_count(&guard.inner), 1);
assert!(!logs_contain(
"SqlitePoolGuard dropped without explicit close"
));
guard.close().await;
tokio::fs::remove_file(database_path).await.unwrap();
}
}
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch-example-parcel",
"version": "1.0.7",
"version": "1.0.6",
"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.4-rc.0 || ^1",
"@nymproject/mix-fetch": ">=1.4.2-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.4",
"version": "1.4.3",
"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.4-rc.0 || ^1",
"@nymproject/mix-fetch-wasm-node": ">=1.4.2-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.7",
"version": "1.0.6",
"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.3-rc.0 || ^1"
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
},
"devDependencies": {
"@babel/core": "^7.22.10",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch-tester-parcel",
"version": "1.0.7",
"version": "1.0.6",
"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.3-rc.0 || ^1"
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
},
"private": false,
"source": "../src/index.html"
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch",
"version": "1.4.4",
"version": "1.4.3",
"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.4-rc.0 || ^1",
"@nymproject/mix-fetch-wasm": ">=1.4.2-rc.0 || ^1",
"comlink": "^4.3.1"
},
"devDependencies": {
@@ -82,4 +82,4 @@
"private": false,
"type": "module",
"types": "./dist/esm/index.d.ts"
}
}
@@ -0,0 +1,5 @@
{
"name": "@nymproject/nym-client-wasm",
"version": "1.0.0",
"sideEffects": false
}
@@ -23,6 +23,7 @@ use crate::{
};
use futures::StreamExt;
use nym_ip_packet_requests::codec::MultiIpPacketCodec;
use nym_ip_packet_requests::{MAX_NON_STREAM_VERSION, SPHINX_STREAM_VERSION_THRESHOLD};
use nym_lp::packet::frame::{LpFrameHeader, LpFrameKind, SphinxStreamFrameAttributes};
use nym_sdk::mixnet::MixnetMessageSender;
use nym_sphinx::receiver::ReconstructedMessage;
@@ -559,8 +560,9 @@ impl MixnetListener {
///
/// # Version / transport enforcement
///
/// - LP Stream frames (`stream_id` is `Some`) **must** carry v9+ payloads.
/// - Non-stream messages (`stream_id` is `None`) **must** be v8 or lower.
/// - LP Stream frames (`stream_id` is `Some`) **must** carry payloads with version
/// `>= SPHINX_STREAM_VERSION_THRESHOLD` (see `nym_ip_packet_requests`).
/// - Non-stream messages (`stream_id` is `None`) **must** be `<= MAX_NON_STREAM_VERSION`.
///
/// Messages that violate these rules are dropped.
async fn on_ipr_message(
@@ -578,16 +580,22 @@ impl MixnetListener {
}?;
// Enforce version/transport consistency:
// - LP Stream frames must carry v9+ payloads
// - Non-stream messages must be v8 or lower
// - LP Stream frames must carry payloads at/above the SphinxStream threshold
// - Non-stream messages must be at/below the max non-stream version
let version_num = request.version().into_u8();
if stream_id.is_some() && version_num < 9 {
log::warn!("LP Stream frame contains v{version_num} payload, expected v9+; dropping",);
if stream_id.is_some() && version_num < SPHINX_STREAM_VERSION_THRESHOLD {
log::warn!(
"LP Stream frame contains v{version_num} payload, expected v{expected}+; dropping",
expected = SPHINX_STREAM_VERSION_THRESHOLD
);
return Ok(vec![]);
}
if stream_id.is_none() && version_num >= 9 {
log::warn!("Non-stream message claims v{version_num}, expected v8 or lower; dropping",);
if stream_id.is_none() && version_num > MAX_NON_STREAM_VERSION {
log::warn!(
"Non-stream message claims v{version_num}, expected v{expected} or lower; dropping",
expected = MAX_NON_STREAM_VERSION
);
return Ok(vec![]);
}
@@ -3,7 +3,7 @@
[package]
name = "nym-network-requester"
version = "1.1.75"
version = "1.1.77"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-cli"
version = "1.1.74"
version = "1.1.76"
authors.workspace = true
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nymvisor"
version = "0.1.39"
version = "0.1.41"
authors.workspace = true
edition.workspace = true
license.workspace = true
+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,8 +92,7 @@ func (ar *CurrentActiveRequests) InjectData(id types.RequestId, data []byte) {
defer ar.Unlock()
_, exists := ar.Requests[id]
if !exists {
log.Error("attempted to inject data for connection %d that no longer exists — likely already cleaned up", id)
return
panic("attempted to write to connection that doesn't exist")
}
ar.Requests[id].injector.ServerData <- data
}
@@ -116,8 +115,7 @@ func (ar *CurrentActiveRequests) SendError(id types.RequestId, err error) {
defer ar.Unlock()
_, exists := ar.Requests[id]
if !exists {
log.Error("attempted to inject error for connection %d that no longer exists — likely already cleaned up", id)
return
panic("attempted to inject error data to connection that doesn't exist")
}
ar.Requests[id].injector.RemoteError <- err
}
+75 -117
View File
@@ -3,142 +3,100 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nym MixFetch Dev</title>
<title>Nym MixFetch Demo</title>
<script src="bootstrap.js"></script>
</head>
<body>
<h1>MixFetch Dev</h1>
<h1>Mix Fetch Demo</h1>
<fieldset id="startup-controls">
<legend>Connection</legend>
<div>
<label><input type="radio" name="gateway-mode" value="default" checked />
Default Gateway (q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1)</label>
<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>
</div>
<div>
<label><input type="radio" name="gateway-mode" value="random" />
Random Gateway</label>
</div>
<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>
<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 style="margin-bottom: 10px">
<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>
<hr />
<fieldset id="fetch-controls" disabled>
<legend>Quick Fetch</legend>
<legend>Fetch Controls</legend>
<div>
<input type="text" size="55" id="fetch_payload_1" value="https://api.ipify.org?format=json" />
<button id="fetch-button-1">GET</button>
<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>
</div>
<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>
<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>
<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>
<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
</p>
</div>
<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>
<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>
<label>POST URL: </label>
<input
type="text"
size="60"
id="post_url"
value="https://jsonplaceholder.typicode.com/posts"
/>
</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>
<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 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>
<button id="post-button">Send POST Request</button>
</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>
<p>Do a POST and get it echoed back</p>
</fieldset>
<hr />
<p>
<span id="output"></span>
</p>
</body>
</html>
+159 -197
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// ─── Worker client ──────────────────────────────────────────────────────────
class WebWorkerClient {
worker = null;
@@ -21,254 +19,218 @@ class WebWorkerClient {
this.worker = new Worker('./worker.js');
this.worker.onmessage = (ev) => {
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;
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;
}
case 'MixFetchReady':
onMixFetchReady();
break;
case 'MixFetchError':
onMixFetchError(ev.data.args.error);
break;
case 'StressTestFetchResult':
onStressTestFetchResult(ev.data.args);
break;
}
};
}
startMixFetch = (preferredGateway, setupOpts) => {
this.worker.postMessage({ kind: 'StartMixFetch', args: { preferredGateway, setupOpts } });
};
startMixFetch = (preferredGateway) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'StartMixFetch',
args: {
preferredGateway,
},
});
}
doFetch = (target) => {
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 },
});
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
};
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,
},
});
}
}
// ─── Startup ────────────────────────────────────────────────────────────────
let client = null;
const DEFAULT_GATEWAY = 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
const DEFAULT_GATEWAY = "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1";
async function main() {
client = new WebWorkerClient();
// 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 startButton = document.querySelector('#start-mixfetch');
startButton.onclick = function () {
const gatewayMode = document.querySelector('input[name="gateway-mode"]:checked').value;
const preferredGateway = gatewayMode === 'default' ? DEFAULT_GATEWAY : undefined;
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),
};
startButton.disabled = true;
document.querySelectorAll('input[name="gateway-mode"]').forEach(r => r.disabled = true);
updateStatus('Starting...', 'orange');
document.querySelector('#start-mixfetch').disabled = true;
document.querySelectorAll('input[name="gateway-mode"]').forEach((r) => (r.disabled = true));
updateStatus('mixfetch-status', 'Starting...');
displayLog(`Starting MixFetch with ${gatewayMode} gateway${preferredGateway ? ` (${preferredGateway})` : ''}...`, 'info');
client.startMixFetch(preferredGateway);
}
// Sync the stress-test Go timeout to match the configured request timeout
document.getElementById('stress-go-timeout').value = setupOpts.requestTimeoutMs;
const fetchButton1 = document.querySelector('#fetch-button-1');
fetchButton1.onclick = function () {
doFetch(1);
}
console.log(`Starting MixFetch (${gatewayMode} gateway${preferredGateway ? `: ${preferredGateway}` : ''})...`);
console.log('Setup options:', setupOpts);
client.startMixFetch(preferredGateway, setupOpts);
};
const fetchButton2 = document.querySelector('#fetch-button-2');
fetchButton2.onclick = function () {
doFetch(2);
}
document.querySelector('#fetch-button-1').onclick = () => doFetch(1);
document.querySelector('#fetch-button-2').onclick = () => doFetch(2);
const fetch10Button = document.querySelector('#fetch-10-concurrent');
fetch10Button.onclick = function () {
doFetch10Concurrent();
}
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);
};
const postButton = document.querySelector('#post-button');
postButton.onclick = function () {
doPost();
}
}
// ─── UI helpers ─────────────────────────────────────────────────────────────
function updateStatus(elementId, text) {
document.getElementById(elementId).textContent = text;
function updateStatus(text, color) {
const status = document.getElementById('mixfetch-status');
status.textContent = text;
status.style.color = color;
}
function onMixFetchReady() {
updateStatus('mixfetch-status', 'Ready');
updateStatus('Ready', 'green');
document.getElementById('fetch-controls').disabled = false;
console.log('MixFetch ready!');
displayLog('MixFetch is ready!', 'info');
}
function onMixFetchError(error) {
updateStatus('mixfetch-status', 'Error: ' + error);
updateStatus('Error: ' + error, 'red');
document.querySelector('#start-mixfetch').disabled = false;
document.querySelectorAll('input[name="gateway-mode"]').forEach((r) => (r.disabled = false));
console.error('MixFetch error:', error);
document.querySelectorAll('input[name="gateway-mode"]').forEach(r => r.disabled = false);
displayLog('MixFetch error: ' + 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 url = document.getElementById(`fetch_payload_${id}`).value;
appendFetchLog(`GET ${url}`);
console.log(`GET ${url}`);
await client.doFetch(url);
const payload = document.getElementById(`fetch_payload_${id}`).value;
await client.doFetch(payload)
displaySend(`[${id}] clicked the button and the payload is: ${payload}...`);
}
// ─── Stress test ────────────────────────────────────────────────────────────
async function doFetch10Concurrent() {
const baseUrl = 'https://jsonplaceholder.typicode.com/posts/';
displaySend('Starting 10 concurrent requests to posts/1-10...');
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 = [];
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,
});
}
for (let i = 1; i <= 10; i++) {
const url = `${baseUrl}${i}`;
displaySend(`[${i}] Sending request to ${url}`);
requests.push(client.doFetch(url));
}
return requests;
await Promise.all(requests);
displaySend('All 10 concurrent requests dispatched!');
}
let stressTest = null;
async function doPost() {
const url = document.getElementById('post_url').value;
const body = document.getElementById('post_body').value;
function onStressTestFetchResult(result) {
if (!stressTest) return;
displaySend(`[POST] Sending POST request to ${url}`);
displaySend(`[POST] Body: ${body}`);
stressTest.results.push(result);
await client.doPost(url, body);
}
const progress = `${stressTest.results.length}/${stressTest.count}`;
const tag = `#${result.id} ${result.label}`;
/**
* 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);
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})`);
}
const colors = {
info: 'gray',
error: 'red',
warn: 'orange',
debug: 'purple',
};
updateStatus('stress-test-status', progress);
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);
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;
logDiv.appendChild(paragraph);
document.getElementById('output').appendChild(logDiv);
}
console.log(`=== COMPLETE: ${totalElapsed}s | OK ${succeeded}/${stressTest.count} | Failed ${failed}/${stressTest.count} ===`);
/**
* 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);
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}`);
}
}
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);
updateStatus('stress-test-status',
`Done: ${succeeded}/${stressTest.count} OK, ${failed} failed (${totalElapsed}s)`
);
document.querySelector('#stress-test-button').disabled = false;
stressTest = null;
}
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);
}
main();
+121 -120
View File
@@ -1,11 +1,11 @@
{
"name": "mix-fetch-internal-dev",
"name": "create-wasm-app",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mix-fetch-internal-dev",
"name": "create-wasm-app",
"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.4",
"version": "1.4.2",
"license": "Apache-2.0"
},
"node_modules/@discoveryjs/json-ext": {
@@ -141,14 +141,14 @@
}
},
"node_modules/@jsonjoy.com/fs-core": {
"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==",
"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==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.1",
"@jsonjoy.com/fs-node-utils": "4.57.1",
"@jsonjoy.com/fs-node-builtins": "4.56.10",
"@jsonjoy.com/fs-node-utils": "4.56.10",
"thingies": "^2.5.0"
},
"engines": {
@@ -163,15 +163,15 @@
}
},
"node_modules/@jsonjoy.com/fs-fsa": {
"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==",
"version": "4.56.10",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz",
"integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@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-core": "4.56.10",
"@jsonjoy.com/fs-node-builtins": "4.56.10",
"@jsonjoy.com/fs-node-utils": "4.56.10",
"thingies": "^2.5.0"
},
"engines": {
@@ -186,17 +186,17 @@
}
},
"node_modules/@jsonjoy.com/fs-node": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz",
"integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==",
"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==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@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",
"@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",
"glob-to-regex.js": "^1.0.0",
"thingies": "^2.5.0"
},
@@ -212,9 +212,9 @@
}
},
"node_modules/@jsonjoy.com/fs-node-builtins": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz",
"integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==",
"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==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -229,15 +229,15 @@
}
},
"node_modules/@jsonjoy.com/fs-node-to-fsa": {
"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==",
"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==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-fsa": "4.57.1",
"@jsonjoy.com/fs-node-builtins": "4.57.1",
"@jsonjoy.com/fs-node-utils": "4.57.1"
"@jsonjoy.com/fs-fsa": "4.56.10",
"@jsonjoy.com/fs-node-builtins": "4.56.10",
"@jsonjoy.com/fs-node-utils": "4.56.10"
},
"engines": {
"node": ">=10.0"
@@ -251,13 +251,13 @@
}
},
"node_modules/@jsonjoy.com/fs-node-utils": {
"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==",
"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==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.1"
"@jsonjoy.com/fs-node-builtins": "4.56.10"
},
"engines": {
"node": ">=10.0"
@@ -271,13 +271,13 @@
}
},
"node_modules/@jsonjoy.com/fs-print": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.1.tgz",
"integrity": "sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==",
"version": "4.56.10",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz",
"integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-utils": "4.57.1",
"@jsonjoy.com/fs-node-utils": "4.56.10",
"tree-dump": "^1.1.0"
},
"engines": {
@@ -292,14 +292,14 @@
}
},
"node_modules/@jsonjoy.com/fs-snapshot": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.1.tgz",
"integrity": "sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==",
"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==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/buffers": "^17.65.0",
"@jsonjoy.com/fs-node-utils": "4.57.1",
"@jsonjoy.com/fs-node-utils": "4.56.10",
"@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.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -879,9 +879,9 @@
}
},
"node_modules/@types/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT"
},
@@ -1332,9 +1332,9 @@
}
},
"node_modules/baseline-browser-mapping": {
"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==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1414,9 +1414,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -1434,11 +1434,11 @@
],
"license": "MIT",
"dependencies": {
"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"
"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"
},
"bin": {
"browserslist": "cli.js"
@@ -1522,9 +1522,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001784",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
"integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
"version": "1.0.30001774",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
"dev": true,
"funding": [
{
@@ -1878,9 +1878,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"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==",
"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==",
"dev": true,
"license": "ISC"
},
@@ -1895,9 +1895,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2766,9 +2766,9 @@
}
},
"node_modules/is-network-error": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
"integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2894,9 +2894,9 @@
}
},
"node_modules/launch-editor": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz",
"integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==",
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz",
"integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2952,20 +2952,20 @@
}
},
"node_modules/memfs": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.1.tgz",
"integrity": "sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==",
"version": "4.56.10",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz",
"integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@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/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/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.36",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
@@ -3287,9 +3287,9 @@
"license": "MIT"
},
"node_modules/path-to-regexp": {
"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==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true,
"license": "MIT"
},
@@ -3311,9 +3311,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3337,9 +3337,9 @@
}
},
"node_modules/pkijs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
"integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz",
"integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4142,9 +4142,9 @@
}
},
"node_modules/tapable": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
"integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4156,9 +4156,9 @@
}
},
"node_modules/terser": {
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -4175,15 +4175,16 @@
}
},
"node_modules/terser-webpack-plugin": {
"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==",
"version": "5.3.16",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"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": {
@@ -4209,9 +4210,9 @@
}
},
"node_modules/thingies": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz",
"integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4423,9 +4424,9 @@
}
},
"node_modules/webpack": {
"version": "5.105.4",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
"version": "5.105.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz",
"integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4435,11 +4436,11 @@
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.16.0",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.20.0",
"enhanced-resolve": "^5.19.0",
"es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -4451,9 +4452,9 @@
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.17",
"terser-webpack-plugin": "^5.3.16",
"watchpack": "^2.5.1",
"webpack-sources": "^3.3.4"
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -4716,9 +4717,9 @@
"license": "MIT"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"license": "MIT",
"engines": {
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "mix-fetch-internal-dev",
"name": "create-wasm-app",
"version": "0.1.0",
"description": "Internal dev/stress-test harness for mix-fetch WASM",
"description": "create an app to fetch data through the mixnet",
"main": "index.js",
"bin": {
"create-wasm-app": ".bin/create-wasm-app.js"
@@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/nymtech/nym.git"
"url": "git+https://github.com/rustwasm/create-wasm-app.git"
},
"keywords": [
"webassembly",
+123 -86
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,9 +36,11 @@ 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;
@@ -62,20 +64,83 @@ function sendError(error) {
}
async function logFetchResult(res) {
let text = await res.text();
console.log(`${res.status} ${res.statusText} (${text.length} bytes)`);
console.log(text);
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)
self.postMessage({
kind: 'DisplayString',
args: { rawString: text },
args: {
rawString: text,
},
});
}
// For custom MixFetchConfig (specific network requester, debug overrides),
// see setupMixFetchWithConfig() and the MixFetchConfig / MixFetchConfigOpts types.
async function wasm_bindgenSetup() {
const preferredGateway = "6qQYb4ArXANU6HJDxzH4PFCUqYb39Dae2Gem2KpxescM";
const validator = 'https://qa-nym-api.qa.nymte.ch/api';
async function nativeSetup(preferredGateway, setupOpts = {}) {
// 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) {
sendLog('Setting up MixFetch...');
if (preferredGateway) {
sendLog(`Using preferred gateway: ${preferredGateway}`);
@@ -83,25 +148,17 @@ async function nativeSetup(preferredGateway, setupOpts = {}) {
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: disablePoisson },
coverTraffic: { disableLoopCoverTrafficStream: disableCover },
};
traffic: {disableMainPoissonPacketDistribution: true},
coverTraffic: {disableLoopCoverTrafficStream: true},
}
const mixFetchOverride = {
requestTimeoutMs,
};
requestTimeoutMs: 60000
}
const opts = {
forceTls,
clientId,
forceTls: true,
clientId: "my-client",
clientOverride: noCoverTrafficOverride,
mixFetchOverride,
};
@@ -110,19 +167,16 @@ async function nativeSetup(preferredGateway, setupOpts = {}) {
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, setupOpts) {
async function startMixFetch(preferredGateway) {
sendLog('Instantiating MixFetch...');
try {
await nativeSetup(preferredGateway, setupOpts);
await nativeSetup(preferredGateway);
mixFetchReady = true;
sendLog('MixFetch client running!');
sendReady();
@@ -139,7 +193,7 @@ async function handleFetchPayload(target) {
}
const url = target;
const args = { mode: "unsafe-ignore-cors" };
const args = {mode: "unsafe-ignore-cors"};
try {
sendLog(`Fetching: ${url}`);
@@ -152,50 +206,40 @@ async function handleFetchPayload(target) {
}
}
async function handleStressTestFetch(id, url, label) {
async function handlePostPayload(url, body) {
if (!mixFetchReady) {
sendLog('MixFetch not ready yet', 'error');
return;
}
const tag = `[stress #${id} ${label}]`;
const start = performance.now();
const args = { mode: "unsafe-ignore-cors" };
const args = {
method: 'POST',
mode: "unsafe-ignore-cors",
headers: {
'Content-Type': 'application/json',
},
body: body,
};
try {
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,
},
});
sendLog(`POST request to: ${url}`);
sendLog(`POST body: ${body}`);
const mixFetchRes = await mixFetch(url, args);
sendLog('POST completed');
await logFetchResult(mixFetchRes);
} catch (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) },
});
sendLog('POST request failure: ' + e, 'error');
console.error("mix fetch POST request failure: ", 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, setupOpts } = event.data.args;
await startMixFetch(preferredGateway, setupOpts);
const { preferredGateway } = event.data.args;
await startMixFetch(preferredGateway);
break;
}
case 'FetchPayload': {
@@ -203,17 +247,9 @@ function setupMessageHandler() {
await handleFetchPayload(target);
break;
}
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);
case 'PostPayload': {
const { url, body } = event.data.args;
await handlePostPayload(url, body);
break;
}
}
@@ -221,30 +257,31 @@ 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() {
@@ -252,7 +289,7 @@ async function main() {
// load rust WASM package
sendLog('Loading Rust WASM...');
await wasm_bindgen({ module_or_path: RUST_WASM_URL });
await wasm_bindgen(RUST_WASM_URL);
sendLog('Loaded Rust WASM');
// load go WASM package
@@ -265,7 +302,7 @@ async function main() {
setupRsGoBridge();
goWasmSetLogging("trace");
goWasmSetLogging("trace")
// Set up message handler (MixFetch will be started on demand)
setupMessageHandler();
@@ -274,4 +311,4 @@ async function main() {
}
// Let's get started!
main();
main();
+6 -10
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, console_warn};
use nym_wasm_utils::{console_error, console_log};
use rand::{thread_rng, RngCore};
use std::collections::HashMap;
use std::sync::Arc;
@@ -69,8 +69,7 @@ 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 no longer exists — likely already cleaned up by Go timeout");
return;
console_error!("attempted to reject request {id}, but it seems to have never existed?")
}
goWasmInjectConnError(id.to_string(), err.to_string())
@@ -80,15 +79,12 @@ 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 - this is likely just a retransmitted fragment that
// arrived after the original
// if there's no data and the socket is closed, we're all good because our local must have already closed
if !data.data.is_empty() || !data.header.local_socket_closed {
console_warn!(
"received data for request {id} which is no longer active \
(likely a retransmitted packet for an already-completed request)"
);
console_error!("attempted to send data for request {id}, however it no longer exists. Has it been aborted?");
}
// TODO: if it doesn't exist here, make sure to clear Go's memory too
return;
};
-1
View File
@@ -1 +0,0 @@
test-results/
-81
View File
@@ -1,81 +0,0 @@
# 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
@@ -1,13 +0,0 @@
{
"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"
}
}
@@ -1,53 +0,0 @@
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
@@ -1,71 +0,0 @@
// 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([]);
});
@@ -1,87 +0,0 @@
// 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);
});