From 55b26ff7cc22d55d7b29c27a3d02c07906be2bdb Mon Sep 17 00:00:00 2001 From: Erica Windisch Date: Fri, 3 Jul 2026 10:59:43 -0400 Subject: [PATCH 01/11] wasm: make moq-net usable from a browser subscriber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small changes finish the wasm portability moq-net is already architected for (web-async, kio, web-transport-trait's MaybeSend/MaybeSync): 1. session.rs: cfg-split the SessionInner dyn-erasure wrapper. Native keeps Send + Sync (byte-for-byte the original); on wasm the bounds are dropped so a web_sys::WebTransport-backed Session (!Sync, !Send futures) can satisfy impl SessionInner for S. MaybeSend/MaybeSync aren't auto-traits so they can't be dyn bounds — hence the cfg split. 2. Swap real-path tokio::time -> web_async::time (session bandwidth loop, stats ticker, lite/publisher probe, model/track group-cache Instant, ietf/control timeout). tokio::time::Instant::now() panics on wasm (no clock) and its timers need a runtime absent under spawn_local; web_async::time is a 1:1 drop-in (tokio on native, wasmtimer on wasm). Tests keep tokio::time (the pause/advance mock clock isn't re-exported). Requires web-async 0.1.4. model/time.rs Timescale generation stays on tokio::time (producer-side; a subscriber only decodes Timescales). Native build + 336 tests pass; wasm verified via a downstream browser subscriber. --- Cargo.lock | 1 + Cargo.toml | 2 +- rs/moq-net/src/ietf/control.rs | 2 +- rs/moq-net/src/lite/publisher.rs | 6 +++--- rs/moq-net/src/model/track.rs | 8 ++++---- rs/moq-net/src/session.rs | 29 ++++++++++++++++++++++++++++- rs/moq-net/src/stats.rs | 4 ++-- 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b19aa76dc..265135d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,7 @@ version = "0.1.14" dependencies = [ "bytes", "futures", + "getrandom 0.3.4", "kio", "num_enum", "rand 0.10.2", diff --git a/Cargo.toml b/Cargo.toml index d8f6508da..e3630094c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ qmux = { version = "0.2", default-features = false } serde = { version = "1", features = ["derive"] } tokio = "1.48" -web-async = { version = "0.1.3", features = ["tracing"] } +web-async = { version = "0.1.4", features = ["tracing"] } web-transport-iroh = "0.6" web-transport-noq = "0.2.0" web-transport-proto = "0.6" diff --git a/rs/moq-net/src/ietf/control.rs b/rs/moq-net/src/ietf/control.rs index af6593aef..db27c97ba 100644 --- a/rs/moq-net/src/ietf/control.rs +++ b/rs/moq-net/src/ietf/control.rs @@ -35,7 +35,7 @@ impl Control { /// Allocate the next request_id, blocking until MAX_REQUEST_ID allows it. pub async fn next_request_id(&self) -> Result { - let timeout = tokio::time::sleep(std::time::Duration::from_secs(10)); + let timeout = web_async::time::sleep(std::time::Duration::from_secs(10)); tokio::pin!(timeout); loop { diff --git a/rs/moq-net/src/lite/publisher.rs b/rs/moq-net/src/lite/publisher.rs index 882d80d8c..5aa668d40 100644 --- a/rs/moq-net/src/lite/publisher.rs +++ b/rs/moq-net/src/lite/publisher.rs @@ -110,8 +110,8 @@ impl Publisher { const PROBE_MAX_AGE: Duration = Duration::from_secs(10); const PROBE_MAX_DELTA: f64 = 0.25; - let mut last_sent: Option<(u64, tokio::time::Instant)> = None; - let mut interval = tokio::time::interval(PROBE_INTERVAL); + let mut last_sent: Option<(u64, web_async::time::Instant)> = None; + let mut interval = web_async::time::interval(PROBE_INTERVAL); loop { tokio::select! { @@ -139,7 +139,7 @@ impl Publisher { if should_send { let rtt = session.stats().rtt().map(|d| d.as_millis() as u64); stream.writer.encode(&lite::Probe { bitrate, rtt }).await?; - last_sent = Some((bitrate, tokio::time::Instant::now())); + last_sent = Some((bitrate, web_async::time::Instant::now())); } } } diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index 7df0fdab6..a3b23ff96 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -60,7 +60,7 @@ impl Track { #[derive(Default)] struct State { /// Groups in arrival order. `None` entries are tombstones for evicted groups. - groups: VecDeque>, + groups: VecDeque>, duplicates: HashSet, offset: usize, max_sequence: Option, @@ -174,7 +174,7 @@ impl State { /// non-max_sequence group (everything after it arrived even later). /// When max_sequence is at the front, we skip past it and tombstone expired groups /// behind it. - fn evict_expired(&mut self, now: tokio::time::Instant) { + fn evict_expired(&mut self, now: web_async::time::Instant) { for slot in self.groups.iter_mut() { let Some((group, created_at)) = slot else { continue }; @@ -246,7 +246,7 @@ impl TrackProducer { return Err(Error::Duplicate); } - let now = tokio::time::Instant::now(); + let now = web_async::time::Instant::now(); state.max_sequence = Some(state.max_sequence.unwrap_or(0).max(group.sequence)); state.groups.push_back(Some((group.clone(), now))); state.evict_expired(now); @@ -269,7 +269,7 @@ impl TrackProducer { let group = Group { sequence }.produce(); - let now = tokio::time::Instant::now(); + let now = web_async::time::Instant::now(); state.duplicates.insert(sequence); state.max_sequence = Some(sequence); state.groups.push_back(Some((group.clone(), now))); diff --git a/rs/moq-net/src/session.rs b/rs/moq-net/src/session.rs index 744325c16..d00869cd4 100644 --- a/rs/moq-net/src/session.rs +++ b/rs/moq-net/src/session.rs @@ -113,7 +113,7 @@ async fn run_send_bandwidth_inner(session: &S, return; } - let mut interval = tokio::time::interval(POLL_INTERVAL); + let mut interval = web_async::time::interval(POLL_INTERVAL); loop { tokio::select! { biased; @@ -136,11 +136,27 @@ async fn run_send_bandwidth_inner(session: &S, } // We use a wrapper type that is dyn-compatible to remove the generic bounds from Session. +// +// hyprstream fork (#484): on native we keep the original `Send + Sync` bounds; on +// wasm we drop them so a browser `web_sys::WebTransport`-backed Session (which is +// `!Sync`, and whose futures are `!Send`) can satisfy the bound. web-transport-trait's +// own `Session` trait is already `MaybeSend + MaybeSync` (empty on wasm), so this only +// relaxes moq-net's over-tight dyn-erasure wrapper — it does not weaken anything native. +// `MaybeSend`/`MaybeSync` aren't auto-traits, so they can't appear in a `dyn` bound; +// hence the cfg split rather than a single `MaybeSend`-bounded declaration. +#[cfg(not(target_family = "wasm"))] trait SessionInner: Send + Sync { fn close(&self, code: u32, reason: &str); fn closed(&self) -> Pin + Send + '_>>; } +#[cfg(target_family = "wasm")] +trait SessionInner { + fn close(&self, code: u32, reason: &str); + fn closed(&self) -> Pin + '_>>; +} + +#[cfg(not(target_family = "wasm"))] impl SessionInner for S { fn close(&self, code: u32, reason: &str) { S::close(self, code, reason); @@ -150,3 +166,14 @@ impl SessionInner for S { Box::pin(async move { S::closed(self).await.to_string() }) } } + +#[cfg(target_family = "wasm")] +impl SessionInner for S { + fn close(&self, code: u32, reason: &str) { + S::close(self, code, reason); + } + + fn closed(&self) -> Pin + '_>> { + Box::pin(async move { S::closed(self).await.to_string() }) + } +} diff --git a/rs/moq-net/src/stats.rs b/rs/moq-net/src/stats.rs index 8f980df36..6aa7d12f1 100644 --- a/rs/moq-net/src/stats.rs +++ b/rs/moq-net/src/stats.rs @@ -1235,8 +1235,8 @@ async fn run_publisher( drop(shared); } - let mut ticker = tokio::time::interval(interval); - ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut ticker = web_async::time::interval(interval); + ticker.set_missed_tick_behavior(web_async::time::MissedTickBehavior::Delay); loop { ticker.tick().await; From a5c56eca71fc8c5824c160958114a906c356b415 Mon Sep 17 00:00:00 2001 From: Erica Windisch Date: Fri, 3 Jul 2026 11:25:27 -0400 Subject: [PATCH 02/11] feat(wasm): Timescale timestamp generation on wasm via wasmtimer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The producer path panicked on wasm: TIME_ANCHOR used std::time::Instant::now() + SystemTime::now() (both panic on wasm32 — no clock), Timescale::now() used tokio::time::Instant, and From called into_std(). cfg-split the anchor's clock types: native uses std::time (unchanged, still tokio-stubbable via tokio::time::pause in tests); wasm uses wasmtimer::std:: {Instant, SystemTime} (backed by performance.now()/Date.now()). Timescale::now() picks tokio (native) vs the wasmtimer monotonic clock (wasm); the tokio->Instant From impl is now native-only. Native behavior is byte-for-byte unchanged. With this, a browser can PRODUCE moq streams (not just subscribe): Timescale stamps groups from a real wall clock. Native build + tests unchanged (378 pass, 0 fail). --- rs/moq-net/Cargo.toml | 5 +++++ rs/moq-net/src/model/time.rs | 39 +++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 0b1ae664b..9aea3cc25 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -29,3 +29,8 @@ tokio = { workspace = true, features = ["macros", "io-util", "sync", "test-util" tracing = "0.1" web-async = { workspace = true } web-transport-trait = { workspace = true } + +# Timestamp generation on wasm32: std's clock panics (no clock), so route +# through wasmtimer (performance.now()/Date.now()). See model/time.rs. +[target.'cfg(target_family = "wasm")'.dependencies] +wasmtimer = "0.4" diff --git a/rs/moq-net/src/model/time.rs b/rs/moq-net/src/model/time.rs index 53eae274b..b0fd07511 100644 --- a/rs/moq-net/src/model/time.rs +++ b/rs/moq-net/src/model/time.rs @@ -4,7 +4,15 @@ use crate::Error; use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; use std::sync::LazyLock; -use std::time::{SystemTime, UNIX_EPOCH}; + +// Portable monotonic/wall clock types for timestamp generation. Native uses real +// `std::time` (unchanged); wasm uses `wasmtimer`, because `std::time::Instant`/ +// `SystemTime::now()` panic on `wasm32-unknown-unknown` (no clock). wasmtimer +// implements the same API on `performance.now()` / `Date.now()`. +#[cfg(not(target_family = "wasm"))] +use std::time::{Instant as ClockInstant, SystemTime as ClockSystemTime}; +#[cfg(target_family = "wasm")] +use wasmtimer::std::{Instant as ClockInstant, SystemTime as ClockSystemTime}; /// A timestamp representing the presentation time in milliseconds. /// @@ -189,8 +197,17 @@ impl Timescale { /// Current time as a timestamp, derived from [`tokio::time::Instant::now`] so /// it honors `tokio::time::pause` in tests. pub fn now() -> Self { - // We use tokio so it can be stubbed for testing. - tokio::time::Instant::now().into() + // Native: tokio so it can be stubbed by `tokio::time::pause` in tests. + #[cfg(not(target_family = "wasm"))] + { + tokio::time::Instant::now().into() + } + // Wasm: the wasmtimer monotonic clock (`performance.now()`); tokio's + // clock panics on wasm32. + #[cfg(target_family = "wasm")] + { + ClockInstant::now().into() + } } /// Convert this timestamp to a different scale. @@ -289,17 +306,18 @@ impl std::ops::SubAssign for Timescale { } // There's no zero Instant, so we need to use a reference point. -static TIME_ANCHOR: LazyLock<(std::time::Instant, SystemTime)> = LazyLock::new(|| { +static TIME_ANCHOR: LazyLock<(ClockInstant, ClockSystemTime)> = LazyLock::new(|| { // To deter nerds trying to use timestamp as wall clock time, we subtract a random amount of time from the anchor. // This will make our timestamps appear to be late; just enough to be annoying and obscure our clock drift. // This will also catch bad implementations that assume unrelated broadcasts are synchronized. let jitter = std::time::Duration::from_millis(rand::rng().random_range(0..69_420)); - (std::time::Instant::now(), SystemTime::now() - jitter) + (ClockInstant::now(), ClockSystemTime::now() - jitter) }); -// Convert an Instant to a Unix timestamp -impl From for Timescale { - fn from(instant: std::time::Instant) -> Self { +// Convert an Instant to a Unix timestamp. `ClockInstant`/`ClockSystemTime` are +// `std::time` on native and `wasmtimer::std` on wasm — same logic either way. +impl From for Timescale { + fn from(instant: ClockInstant) -> Self { let (anchor_instant, anchor_system) = *TIME_ANCHOR; // Conver the instant to a SystemTime. @@ -311,13 +329,16 @@ impl From for Timescale { // Convert the SystemTime to a Unix timestamp in nanoseconds. // We'll then convert that to the desired scale. system - .duration_since(UNIX_EPOCH) + .duration_since(ClockSystemTime::UNIX_EPOCH) .expect("dude your clock is earlier than 1970") .try_into() .expect("dude your clock is later than 2116") } } +// Native only: `now()` uses the tokio mock clock (`tokio::time::pause`) here so +// tests can stub it. On wasm, `now()` converts a `ClockInstant` directly. +#[cfg(not(target_family = "wasm"))] impl From for Timescale { fn from(instant: tokio::time::Instant) -> Self { instant.into_std().into() From 961326c8dca3566a2d544326aaf0ef3e3cfc38b6 Mon Sep 17 00:00:00 2001 From: Erica Windisch Date: Fri, 3 Jul 2026 11:31:05 -0400 Subject: [PATCH 03/11] test(wasm): wasm-bindgen-test for model produce/consume + Timescale clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests/wasm.rs (cfg wasm32) exercising the model layer on wasm32: - timescale_now_is_sane_and_monotonic: Time::now() returns a real post-2020, monotonic wall-clock time — proves the wasmtimer producer clock works (this panicked before the producer fix). - produce_consume_frame_roundtrip: in-process Broadcast produce -> consume of a frame, covering both directions without a WebTransport session. Run via cargo (NOT wasm-pack, which builds the crate's native-only lib unit tests — they use tokio::spawn and don't compile on wasm): CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' cargo test --test wasm -p moq-net --target wasm32-unknown-unknown Result: 2 passed, 0 failed (Node). getrandom's wasm backend + wasm-bindgen-test are wasm dev-dependencies only (not forced on downstream consumers). --- Cargo.lock | 53 +++++++++++++++++++++++++++++++++- rs/moq-net/Cargo.toml | 7 +++++ rs/moq-net/tests/wasm.rs | 62 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 rs/moq-net/tests/wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 265135d0d..b9e0884bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3933,6 +3933,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4248,7 +4258,7 @@ version = "0.1.14" dependencies = [ "bytes", "futures", - "getrandom 0.3.4", + "getrandom 0.4.3", "kio", "num_enum", "rand 0.10.2", @@ -4257,6 +4267,8 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "wasm-bindgen-test", + "wasmtimer", "web-async", "web-transport-trait", ] @@ -8545,6 +8557,45 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" + [[package]] name = "wasm-streams" version = "0.5.0" diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 9aea3cc25..960133dc4 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -34,3 +34,10 @@ web-transport-trait = { workspace = true } # through wasmtimer (performance.now()/Date.now()). See model/time.rs. [target.'cfg(target_family = "wasm")'.dependencies] wasmtimer = "0.4" + +# wasm model-layer tests (tests/wasm.rs). getrandom's wasm backend is enabled +# only here (dev), so it isn't forced on downstream consumers — they select +# their own backend in the leaf binary. +[target.'cfg(target_family = "wasm")'.dev-dependencies] +wasm-bindgen-test = "0.3" +getrandom = { version = "0.4", features = ["wasm_js"] } diff --git a/rs/moq-net/tests/wasm.rs b/rs/moq-net/tests/wasm.rs new file mode 100644 index 000000000..c19151900 --- /dev/null +++ b/rs/moq-net/tests/wasm.rs @@ -0,0 +1,62 @@ +//! wasm32 model-layer tests. +//! +//! moq-net's model layer (Origin/Broadcast/Track/Group/Frame) is transport- +//! independent, so it can be exercised in-process on wasm without a +//! WebTransport session — covering both directions (produce + consume) plus the +//! wasm timestamp clock (`wasmtimer`) that the producer path depends on. +//! +//! Run (bypassing `wasm-pack test`, which builds the crate's native-only lib +//! unit tests too — they use `tokio::spawn` and don't compile on wasm): +//! +//! ```sh +//! CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ +//! RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' \ +//! cargo test --test wasm -p moq-net --target wasm32-unknown-unknown +//! ``` +//! +//! Runs under Node (default) — `performance.now()` / `Date.now()` back +//! `wasmtimer` there just as in a browser; these model-layer tests need no +//! WebTransport. Add `wasm_bindgen_test_configure!(run_in_browser)` to run under +//! headless Chrome (the subscriber's real environment) once chromedriver is set. +#![cfg(target_arch = "wasm32")] + +use bytes::Bytes; +use moq_net::{Broadcast, Time, Track}; +use wasm_bindgen_test::*; + +/// The producer timestamp clock works on wasm: `Time::now()` (which flows through +/// the `wasmtimer` wall clock) returns a sane, non-decreasing wall-clock time. +/// On the old code this panicked (`std::time` has no clock on wasm32). +#[wasm_bindgen_test] +fn timescale_now_is_sane_and_monotonic() { + let a = Time::now(); + let b = Time::now(); + + // A real post-2020 wall-clock time in ms (2020-01-01 = 1_577_836_800_000). + // Proves the wasmtimer Date.now() anchor actually resolved. + assert!( + a.as_millis() > 1_577_836_800_000, + "timestamp before 2020 — wall clock not working: {}", + a.as_millis() + ); + // Monotonic non-decreasing (the wasmtimer performance.now() monotonic clock). + assert!(b >= a, "time went backwards: {} < {}", b.as_millis(), a.as_millis()); +} + +/// Bidirectional model round-trip in-process on wasm: produce a track + frame, +/// then consume it back. Exercises the produce path (which stamps groups via the +/// wasm `web_async::time` clock) and the consume path together. +#[wasm_bindgen_test] +async fn produce_consume_frame_roundtrip() { + let mut broadcast = Broadcast::new().produce(); + let mut track = broadcast.create_track(Track::new("stream")).unwrap(); + let consumer = broadcast.consume(); + let mut sub = consumer.subscribe_track(&Track::new("stream")).unwrap(); + + // Producer side: write a frame (creates a group, timestamped via the wasm clock). + track.write_frame(Bytes::from_static(b"hello-wasm")).unwrap(); + + // Consumer side: read it back. + let frame = sub.read_frame().await.unwrap(); + assert_eq!(frame.as_deref(), Some(&b"hello-wasm"[..]), "frame did not round-trip"); +} From 7a2fc05cdbb810595b71d7e97039328357facd1d Mon Sep 17 00:00:00 2001 From: Erica Windisch Date: Fri, 3 Jul 2026 22:10:46 -0400 Subject: [PATCH 04/11] cleanup: tidy SessionInner comment for upstream --- rs/moq-net/src/session.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rs/moq-net/src/session.rs b/rs/moq-net/src/session.rs index d00869cd4..bb10c2e17 100644 --- a/rs/moq-net/src/session.rs +++ b/rs/moq-net/src/session.rs @@ -137,13 +137,10 @@ async fn run_send_bandwidth_inner(session: &S, // We use a wrapper type that is dyn-compatible to remove the generic bounds from Session. // -// hyprstream fork (#484): on native we keep the original `Send + Sync` bounds; on -// wasm we drop them so a browser `web_sys::WebTransport`-backed Session (which is -// `!Sync`, and whose futures are `!Send`) can satisfy the bound. web-transport-trait's -// own `Session` trait is already `MaybeSend + MaybeSync` (empty on wasm), so this only -// relaxes moq-net's over-tight dyn-erasure wrapper — it does not weaken anything native. -// `MaybeSend`/`MaybeSync` aren't auto-traits, so they can't appear in a `dyn` bound; -// hence the cfg split rather than a single `MaybeSend`-bounded declaration. +// Native keeps the `Send + Sync` bounds; wasm drops them so a browser +// `web_sys::WebTransport`-backed session (`!Sync`, with `!Send` futures) can +// satisfy the bound. `MaybeSend`/`MaybeSync` aren't auto-traits and can't appear +// in a `dyn` bound, hence the cfg split. #[cfg(not(target_family = "wasm"))] trait SessionInner: Send + Sync { fn close(&self, code: u32, reason: &str); From a0f6a58b9c0f5d65473a09c51d44e08de084d399 Mon Sep 17 00:00:00 2001 From: Erica Windisch Date: Fri, 3 Jul 2026 22:41:48 -0400 Subject: [PATCH 05/11] chore: pin wasm deps via workspace inheritance; fix now() doc - Move wasmtimer/wasm-bindgen-test/getrandom to [workspace.dependencies] and inherit with { workspace = true }, per the /rs workspace convention. - Update Time::now() doc comment to describe both the native tokio path and the wasm wasmtimer-backed clock. --- Cargo.toml | 3 +++ rs/moq-net/Cargo.toml | 6 +++--- rs/moq-net/src/model/time.rs | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e3630094c..62abc0ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,8 +76,11 @@ moq-token = { version = "0.6", path = "rs/moq-token" } moq-video = { version = "0.0.6", path = "rs/moq-video" } qmux = { version = "0.2", default-features = false } +getrandom = "0.4" serde = { version = "1", features = ["derive"] } tokio = "1.48" +wasm-bindgen-test = "0.3" +wasmtimer = "0.4" web-async = { version = "0.1.4", features = ["tracing"] } web-transport-iroh = "0.6" web-transport-noq = "0.2.0" diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 960133dc4..f0ac464fa 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -33,11 +33,11 @@ web-transport-trait = { workspace = true } # Timestamp generation on wasm32: std's clock panics (no clock), so route # through wasmtimer (performance.now()/Date.now()). See model/time.rs. [target.'cfg(target_family = "wasm")'.dependencies] -wasmtimer = "0.4" +wasmtimer = { workspace = true } # wasm model-layer tests (tests/wasm.rs). getrandom's wasm backend is enabled # only here (dev), so it isn't forced on downstream consumers — they select # their own backend in the leaf binary. [target.'cfg(target_family = "wasm")'.dev-dependencies] -wasm-bindgen-test = "0.3" -getrandom = { version = "0.4", features = ["wasm_js"] } +wasm-bindgen-test = { workspace = true } +getrandom = { workspace = true, features = ["wasm_js"] } diff --git a/rs/moq-net/src/model/time.rs b/rs/moq-net/src/model/time.rs index b0fd07511..4b95bc520 100644 --- a/rs/moq-net/src/model/time.rs +++ b/rs/moq-net/src/model/time.rs @@ -194,8 +194,10 @@ impl Timescale { self.0.into_inner() == 0 } - /// Current time as a timestamp, derived from [`tokio::time::Instant::now`] so - /// it honors `tokio::time::pause` in tests. + /// Current time as a timestamp. On native targets it is derived from + /// [`tokio::time::Instant::now`] so it honors `tokio::time::pause` in tests; + /// on wasm it uses the wasmtimer-backed monotonic clock, which has no + /// `tokio::time::pause` support. pub fn now() -> Self { // Native: tokio so it can be stubbed by `tokio::time::pause` in tests. #[cfg(not(target_family = "wasm"))] From 93b4874d66949fe748ad74944de5efbd7683479e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 3 Jul 2026 21:51:34 -0700 Subject: [PATCH 06/11] simplify moq-net wasm time handling --- Cargo.lock | 5 +--- Cargo.toml | 3 +-- rs/moq-net/Cargo.toml | 7 +---- rs/moq-net/src/model/time.rs | 40 ++++----------------------- rs/moq-net/src/session.rs | 33 ++++------------------- rs/moq-net/tests/wasm.rs | 52 ++++++++++++++++++------------------ 6 files changed, 39 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9e0884bf..2af914e04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4268,7 +4268,6 @@ dependencies = [ "tokio", "tracing", "wasm-bindgen-test", - "wasmtimer", "web-async", "web-transport-trait", ] @@ -8619,15 +8618,13 @@ dependencies = [ "js-sys", "parking_lot", "pin-utils", - "slab", "wasm-bindgen", ] [[package]] name = "web-async" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f56ac33e792583916a8021e43e8a7e0987f5df7abc8f8afd72fcc361048755" +source = "git+https://github.com/kixelated/web-rs?rev=3ef469078a50a3417ecd921cda6d7a27de7f98ff#3ef469078a50a3417ecd921cda6d7a27de7f98ff" dependencies = [ "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 62abc0ce8..e4fd19211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,8 +80,7 @@ getrandom = "0.4" serde = { version = "1", features = ["derive"] } tokio = "1.48" wasm-bindgen-test = "0.3" -wasmtimer = "0.4" -web-async = { version = "0.1.4", features = ["tracing"] } +web-async = { git = "https://github.com/kixelated/web-rs", rev = "3ef469078a50a3417ecd921cda6d7a27de7f98ff", package = "web-async", features = ["tracing"] } web-transport-iroh = "0.6" web-transport-noq = "0.2.0" web-transport-proto = "0.6" diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index f0ac464fa..30ca4aabc 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -30,13 +30,8 @@ tracing = "0.1" web-async = { workspace = true } web-transport-trait = { workspace = true } -# Timestamp generation on wasm32: std's clock panics (no clock), so route -# through wasmtimer (performance.now()/Date.now()). See model/time.rs. -[target.'cfg(target_family = "wasm")'.dependencies] -wasmtimer = { workspace = true } - # wasm model-layer tests (tests/wasm.rs). getrandom's wasm backend is enabled -# only here (dev), so it isn't forced on downstream consumers — they select +# only here (dev), so it isn't forced on downstream consumers. They select # their own backend in the leaf binary. [target.'cfg(target_family = "wasm")'.dev-dependencies] wasm-bindgen-test = { workspace = true } diff --git a/rs/moq-net/src/model/time.rs b/rs/moq-net/src/model/time.rs index 4b95bc520..08555c050 100644 --- a/rs/moq-net/src/model/time.rs +++ b/rs/moq-net/src/model/time.rs @@ -5,14 +5,7 @@ use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; use std::sync::LazyLock; -// Portable monotonic/wall clock types for timestamp generation. Native uses real -// `std::time` (unchanged); wasm uses `wasmtimer`, because `std::time::Instant`/ -// `SystemTime::now()` panic on `wasm32-unknown-unknown` (no clock). wasmtimer -// implements the same API on `performance.now()` / `Date.now()`. -#[cfg(not(target_family = "wasm"))] -use std::time::{Instant as ClockInstant, SystemTime as ClockSystemTime}; -#[cfg(target_family = "wasm")] -use wasmtimer::std::{Instant as ClockInstant, SystemTime as ClockSystemTime}; +use web_async::time::{Instant as ClockInstant, SystemTime as ClockSystemTime, UNIX_EPOCH}; /// A timestamp representing the presentation time in milliseconds. /// @@ -194,22 +187,9 @@ impl Timescale { self.0.into_inner() == 0 } - /// Current time as a timestamp. On native targets it is derived from - /// [`tokio::time::Instant::now`] so it honors `tokio::time::pause` in tests; - /// on wasm it uses the wasmtimer-backed monotonic clock, which has no - /// `tokio::time::pause` support. + /// Current time as a timestamp. pub fn now() -> Self { - // Native: tokio so it can be stubbed by `tokio::time::pause` in tests. - #[cfg(not(target_family = "wasm"))] - { - tokio::time::Instant::now().into() - } - // Wasm: the wasmtimer monotonic clock (`performance.now()`); tokio's - // clock panics on wasm32. - #[cfg(target_family = "wasm")] - { - ClockInstant::now().into() - } + ClockInstant::now().into() } /// Convert this timestamp to a different scale. @@ -316,8 +296,7 @@ static TIME_ANCHOR: LazyLock<(ClockInstant, ClockSystemTime)> = LazyLock::new(|| (ClockInstant::now(), ClockSystemTime::now() - jitter) }); -// Convert an Instant to a Unix timestamp. `ClockInstant`/`ClockSystemTime` are -// `std::time` on native and `wasmtimer::std` on wasm — same logic either way. +// Convert an Instant to a Unix timestamp. impl From for Timescale { fn from(instant: ClockInstant) -> Self { let (anchor_instant, anchor_system) = *TIME_ANCHOR; @@ -331,22 +310,13 @@ impl From for Timescale { // Convert the SystemTime to a Unix timestamp in nanoseconds. // We'll then convert that to the desired scale. system - .duration_since(ClockSystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) .expect("dude your clock is earlier than 1970") .try_into() .expect("dude your clock is later than 2116") } } -// Native only: `now()` uses the tokio mock clock (`tokio::time::pause`) here so -// tests can stub it. On wasm, `now()` converts a `ClockInstant` directly. -#[cfg(not(target_family = "wasm"))] -impl From for Timescale { - fn from(instant: tokio::time::Instant) -> Self { - instant.into_std().into() - } -} - impl Decode for Timescale { fn decode(r: &mut R, version: crate::Version) -> Result { let v = VarInt::decode(r, version)?; diff --git a/rs/moq-net/src/session.rs b/rs/moq-net/src/session.rs index bb10c2e17..3ab30f81a 100644 --- a/rs/moq-net/src/session.rs +++ b/rs/moq-net/src/session.rs @@ -1,5 +1,6 @@ -use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; +use web_async::MaybeSendBoxFuture; use web_transport_trait::Stats; use crate::{BandwidthConsumer, BandwidthProducer, Error, Version}; @@ -136,41 +137,17 @@ async fn run_send_bandwidth_inner(session: &S, } // We use a wrapper type that is dyn-compatible to remove the generic bounds from Session. -// -// Native keeps the `Send + Sync` bounds; wasm drops them so a browser -// `web_sys::WebTransport`-backed session (`!Sync`, with `!Send` futures) can -// satisfy the bound. `MaybeSend`/`MaybeSync` aren't auto-traits and can't appear -// in a `dyn` bound, hence the cfg split. -#[cfg(not(target_family = "wasm"))] -trait SessionInner: Send + Sync { +trait SessionInner: web_transport_trait::MaybeSend + web_transport_trait::MaybeSync { fn close(&self, code: u32, reason: &str); - fn closed(&self) -> Pin + Send + '_>>; + fn closed(&self) -> MaybeSendBoxFuture<'_, String>; } -#[cfg(target_family = "wasm")] -trait SessionInner { - fn close(&self, code: u32, reason: &str); - fn closed(&self) -> Pin + '_>>; -} - -#[cfg(not(target_family = "wasm"))] -impl SessionInner for S { - fn close(&self, code: u32, reason: &str) { - S::close(self, code, reason); - } - - fn closed(&self) -> Pin + Send + '_>> { - Box::pin(async move { S::closed(self).await.to_string() }) - } -} - -#[cfg(target_family = "wasm")] impl SessionInner for S { fn close(&self, code: u32, reason: &str) { S::close(self, code, reason); } - fn closed(&self) -> Pin + '_>> { + fn closed(&self) -> MaybeSendBoxFuture<'_, String> { Box::pin(async move { S::closed(self).await.to_string() }) } } diff --git a/rs/moq-net/tests/wasm.rs b/rs/moq-net/tests/wasm.rs index c19151900..f59e49793 100644 --- a/rs/moq-net/tests/wasm.rs +++ b/rs/moq-net/tests/wasm.rs @@ -2,11 +2,11 @@ //! //! moq-net's model layer (Origin/Broadcast/Track/Group/Frame) is transport- //! independent, so it can be exercised in-process on wasm without a -//! WebTransport session — covering both directions (produce + consume) plus the -//! wasm timestamp clock (`wasmtimer`) that the producer path depends on. +//! WebTransport session. This covers both directions (produce + consume) plus +//! the wasm timestamp clock that the producer path depends on. //! //! Run (bypassing `wasm-pack test`, which builds the crate's native-only lib -//! unit tests too — they use `tokio::spawn` and don't compile on wasm): +//! unit tests too. They use `tokio::spawn` and don't compile on wasm): //! //! ```sh //! CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ @@ -14,8 +14,8 @@ //! cargo test --test wasm -p moq-net --target wasm32-unknown-unknown //! ``` //! -//! Runs under Node (default) — `performance.now()` / `Date.now()` back -//! `wasmtimer` there just as in a browser; these model-layer tests need no +//! Runs under Node (default). `performance.now()` / `Date.now()` back the +//! clock there just as in a browser; these model-layer tests need no //! WebTransport. Add `wasm_bindgen_test_configure!(run_in_browser)` to run under //! headless Chrome (the subscriber's real environment) once chromedriver is set. #![cfg(target_arch = "wasm32")] @@ -25,22 +25,22 @@ use moq_net::{Broadcast, Time, Track}; use wasm_bindgen_test::*; /// The producer timestamp clock works on wasm: `Time::now()` (which flows through -/// the `wasmtimer` wall clock) returns a sane, non-decreasing wall-clock time. +/// the web_async clock) returns a sane, non-decreasing wall-clock time. /// On the old code this panicked (`std::time` has no clock on wasm32). #[wasm_bindgen_test] fn timescale_now_is_sane_and_monotonic() { - let a = Time::now(); - let b = Time::now(); + let a = Time::now(); + let b = Time::now(); - // A real post-2020 wall-clock time in ms (2020-01-01 = 1_577_836_800_000). - // Proves the wasmtimer Date.now() anchor actually resolved. - assert!( - a.as_millis() > 1_577_836_800_000, - "timestamp before 2020 — wall clock not working: {}", - a.as_millis() - ); - // Monotonic non-decreasing (the wasmtimer performance.now() monotonic clock). - assert!(b >= a, "time went backwards: {} < {}", b.as_millis(), a.as_millis()); + // A real post-2020 wall-clock time in ms (2020-01-01 = 1_577_836_800_000). + // Proves the web_async wall clock actually resolved. + assert!( + a.as_millis() > 1_577_836_800_000, + "timestamp before 2020. wall clock not working: {}", + a.as_millis() + ); + // Monotonic non-decreasing. + assert!(b >= a, "time went backwards: {} < {}", b.as_millis(), a.as_millis()); } /// Bidirectional model round-trip in-process on wasm: produce a track + frame, @@ -48,15 +48,15 @@ fn timescale_now_is_sane_and_monotonic() { /// wasm `web_async::time` clock) and the consume path together. #[wasm_bindgen_test] async fn produce_consume_frame_roundtrip() { - let mut broadcast = Broadcast::new().produce(); - let mut track = broadcast.create_track(Track::new("stream")).unwrap(); - let consumer = broadcast.consume(); - let mut sub = consumer.subscribe_track(&Track::new("stream")).unwrap(); + let mut broadcast = Broadcast::new().produce(); + let mut track = broadcast.create_track(Track::new("stream")).unwrap(); + let consumer = broadcast.consume(); + let mut sub = consumer.subscribe_track(&Track::new("stream")).unwrap(); - // Producer side: write a frame (creates a group, timestamped via the wasm clock). - track.write_frame(Bytes::from_static(b"hello-wasm")).unwrap(); + // Producer side: write a frame (creates a group, timestamped via the wasm clock). + track.write_frame(Bytes::from_static(b"hello-wasm")).unwrap(); - // Consumer side: read it back. - let frame = sub.read_frame().await.unwrap(); - assert_eq!(frame.as_deref(), Some(&b"hello-wasm"[..]), "frame did not round-trip"); + // Consumer side: read it back. + let frame = sub.read_frame().await.unwrap(); + assert_eq!(frame.as_deref(), Some(&b"hello-wasm"[..]), "frame did not round-trip"); } From 118147430389b65d7d5cbe6f6170cc56cdae5c26 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 4 Jul 2026 06:45:54 -0700 Subject: [PATCH 07/11] use published web-async 0.1.5 --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2af914e04..e5eef0019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8623,8 +8623,9 @@ dependencies = [ [[package]] name = "web-async" -version = "0.1.4" -source = "git+https://github.com/kixelated/web-rs?rev=3ef469078a50a3417ecd921cda6d7a27de7f98ff#3ef469078a50a3417ecd921cda6d7a27de7f98ff" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba1426dcf56093a94d9415ba4c016e45c1323679501e7309f1919ab32f08206" dependencies = [ "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index e4fd19211..28881989d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ getrandom = "0.4" serde = { version = "1", features = ["derive"] } tokio = "1.48" wasm-bindgen-test = "0.3" -web-async = { git = "https://github.com/kixelated/web-rs", rev = "3ef469078a50a3417ecd921cda6d7a27de7f98ff", package = "web-async", features = ["tracing"] } +web-async = { version = "0.1.5", features = ["tracing"] } web-transport-iroh = "0.6" web-transport-noq = "0.2.0" web-transport-proto = "0.6" From 22dc1c805fc971f0eb11b968bb0604d4dbbcd06a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 4 Jul 2026 06:53:21 -0700 Subject: [PATCH 08/11] use web async instant conversion --- rs/moq-net/src/model/time.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rs/moq-net/src/model/time.rs b/rs/moq-net/src/model/time.rs index 08555c050..5dc163927 100644 --- a/rs/moq-net/src/model/time.rs +++ b/rs/moq-net/src/model/time.rs @@ -5,8 +5,6 @@ use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; use std::sync::LazyLock; -use web_async::time::{Instant as ClockInstant, SystemTime as ClockSystemTime, UNIX_EPOCH}; - /// A timestamp representing the presentation time in milliseconds. /// /// The underlying implementation supports any scale, but everything uses milliseconds by default. @@ -189,7 +187,7 @@ impl Timescale { /// Current time as a timestamp. pub fn now() -> Self { - ClockInstant::now().into() + web_async::time::Instant::now().into() } /// Convert this timestamp to a different scale. @@ -288,17 +286,20 @@ impl std::ops::SubAssign for Timescale { } // There's no zero Instant, so we need to use a reference point. -static TIME_ANCHOR: LazyLock<(ClockInstant, ClockSystemTime)> = LazyLock::new(|| { +static TIME_ANCHOR: LazyLock<(web_async::time::Instant, web_async::time::SystemTime)> = LazyLock::new(|| { // To deter nerds trying to use timestamp as wall clock time, we subtract a random amount of time from the anchor. // This will make our timestamps appear to be late; just enough to be annoying and obscure our clock drift. // This will also catch bad implementations that assume unrelated broadcasts are synchronized. let jitter = std::time::Duration::from_millis(rand::rng().random_range(0..69_420)); - (ClockInstant::now(), ClockSystemTime::now() - jitter) + ( + web_async::time::Instant::now(), + web_async::time::SystemTime::now() - jitter, + ) }); // Convert an Instant to a Unix timestamp. -impl From for Timescale { - fn from(instant: ClockInstant) -> Self { +impl From for Timescale { + fn from(instant: web_async::time::Instant) -> Self { let (anchor_instant, anchor_system) = *TIME_ANCHOR; // Conver the instant to a SystemTime. @@ -310,7 +311,7 @@ impl From for Timescale { // Convert the SystemTime to a Unix timestamp in nanoseconds. // We'll then convert that to the desired scale. system - .duration_since(UNIX_EPOCH) + .duration_since(web_async::time::UNIX_EPOCH) .expect("dude your clock is earlier than 1970") .try_into() .expect("dude your clock is later than 2116") From 83134a651384e4bf18817203f5ea2adc5d422d2e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 4 Jul 2026 10:01:45 -0700 Subject: [PATCH 09/11] sort Cargo dependencies --- Cargo.toml | 4 ++-- rs/moq-net/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 28881989d..636db333c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,8 @@ rust-version = "1.85" [workspace.dependencies] flate2 = "1" + +getrandom = "0.4" hang = { version = "0.19", path = "rs/hang" } kio = { version = "0.4", path = "rs/kio" } moq-audio = { version = "0.0.7", path = "rs/moq-audio" } @@ -75,8 +77,6 @@ moq-srt = { version = "0.1.0", path = "rs/moq-srt" } moq-token = { version = "0.6", path = "rs/moq-token" } moq-video = { version = "0.0.6", path = "rs/moq-video" } qmux = { version = "0.2", default-features = false } - -getrandom = "0.4" serde = { version = "1", features = ["derive"] } tokio = "1.48" wasm-bindgen-test = "0.3" diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 30ca4aabc..a0bb7c2e7 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -34,5 +34,5 @@ web-transport-trait = { workspace = true } # only here (dev), so it isn't forced on downstream consumers. They select # their own backend in the leaf binary. [target.'cfg(target_family = "wasm")'.dev-dependencies] -wasm-bindgen-test = { workspace = true } getrandom = { workspace = true, features = ["wasm_js"] } +wasm-bindgen-test = { workspace = true } From 69984e022215161dff00081292352cd836aa403a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 4 Jul 2026 10:02:53 -0700 Subject: [PATCH 10/11] scope wasm test dependencies --- Cargo.toml | 3 --- rs/moq-net/Cargo.toml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 636db333c..b51971219 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,8 +58,6 @@ rust-version = "1.85" [workspace.dependencies] flate2 = "1" - -getrandom = "0.4" hang = { version = "0.19", path = "rs/hang" } kio = { version = "0.4", path = "rs/kio" } moq-audio = { version = "0.0.7", path = "rs/moq-audio" } @@ -79,7 +77,6 @@ moq-video = { version = "0.0.6", path = "rs/moq-video" } qmux = { version = "0.2", default-features = false } serde = { version = "1", features = ["derive"] } tokio = "1.48" -wasm-bindgen-test = "0.3" web-async = { version = "0.1.5", features = ["tracing"] } web-transport-iroh = "0.6" web-transport-noq = "0.2.0" diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index a0bb7c2e7..b1ea12b71 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -34,5 +34,5 @@ web-transport-trait = { workspace = true } # only here (dev), so it isn't forced on downstream consumers. They select # their own backend in the leaf binary. [target.'cfg(target_family = "wasm")'.dev-dependencies] -getrandom = { workspace = true, features = ["wasm_js"] } -wasm-bindgen-test = { workspace = true } +getrandom = { version = "0.4", features = ["wasm_js"] } +wasm-bindgen-test = "0.3" From b0b15d462a4e5579b5375f238c5d6dc36b452ef6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 4 Jul 2026 10:10:26 -0700 Subject: [PATCH 11/11] ignore wasm entropy dependency in shear --- rs/moq-net/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index b1ea12b71..e73bd2c86 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -12,6 +12,9 @@ rust-version.workspace = true keywords = ["quic", "http3", "webtransport", "media", "live"] categories = ["multimedia", "network-programming", "web-programming"] +[package.metadata.cargo-shear] +ignored = ["getrandom"] + [features] # Legacy no-op: serde is now unconditional (stats publishing requires it). serde = []