Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4bd48263d | |||
| 5c40052d39 | |||
| 7b858dfd69 | |||
| a4bd547023 | |||
| db03ec31b1 | |||
| 9b285735b8 | |||
| 691280797a | |||
| f84de25302 | |||
| e554f1e0ad | |||
| 62a4a2ed70 | |||
| caad74c73d | |||
| 917993d8fb | |||
| 1451db39e6 | |||
| f13a2a6c06 | |||
| ce39fb6675 | |||
| 02a926b74a | |||
| 54ba710ea0 | |||
| 2653d12e55 | |||
| f94d6d51cf | |||
| a0116f9aec | |||
| aaa8ee9d53 | |||
| ab0f6af4b9 | |||
| 7669d0933f | |||
| 50433fe265 | |||
| 42aade29eb | |||
| 9f26759b8d | |||
| 9e642c6354 | |||
| cca19f36c2 | |||
| 17894880e0 | |||
| ef6fc82c39 | |||
| 0c83ae2408 | |||
| 92490731e7 | |||
| 0ce93e366e | |||
| 0d031875f6 | |||
| e6103e4c43 | |||
| e32c042c8d |
@@ -36,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-22.04]
|
||||
platform: [arc-ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
|
||||
@@ -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/
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
@@ -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,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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -26,3 +26,4 @@ tracing.workspace = true
|
||||
|
||||
nym-sdk = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-lp = { workspace = true }
|
||||
|
||||
@@ -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
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -41,4 +41,4 @@ windows = { version = "0.61", features = [
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-test.workspace = true
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-cli"
|
||||
version = "1.1.74"
|
||||
version = "1.1.76"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
— 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>
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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 +0,0 @@
|
||||
test-results/
|
||||
@@ -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)
|
||||
Generated
-1119
File diff suppressed because it is too large
Load Diff
@@ -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" },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user