From 759e9f66619ff635522a502360da9c66b43ed52c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 13 Oct 2026 21:26:34 -0700 Subject: [PATCH 1/6] Claim quest/m1/net-async-feature Co-Authored-By: GPT-6 From d5edec51996d93cbd70388d0807665a7331ad40d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 13 Oct 2026 22:00:58 -0700 Subject: [PATCH 2/6] Gate moq-net async helpers and sessions behind a default feature Co-Authored-By: GPT-6 --- doc/lib/rs/moq-net.md | 1 + quest/m1/README.md | 1 - quest/m1/net-async-feature.md | 24 -- quest/m1/time/README.md | 1 - quest/m1/time/net.md | 9 +- rs/justfile | 5 + rs/moq-net/Cargo.toml | 21 +- rs/moq-net/README.md | 6 + rs/moq-net/src/auth.rs | 66 ++++- rs/moq-net/src/coding/codes.rs | 1 + rs/moq-net/src/coding/decode.rs | 17 ++ rs/moq-net/src/coding/encode.rs | 15 ++ rs/moq-net/src/coding/mod.rs | 8 + rs/moq-net/src/coding/varint.rs | 4 + rs/moq-net/src/coding/version.rs | 1 + rs/moq-net/src/epoch.rs | 2 + rs/moq-net/src/error.rs | 4 +- rs/moq-net/src/goaway.rs | 34 ++- rs/moq-net/src/ietf/auth.rs | 15 +- rs/moq-net/src/ietf/mod.rs | 18 +- rs/moq-net/src/ietf/peer.rs | 3 +- rs/moq-net/src/ietf/token.rs | 6 +- rs/moq-net/src/lib.rs | 39 ++- rs/moq-net/src/lite/compress.rs | 3 + rs/moq-net/src/lite/mod.rs | 11 +- rs/moq-net/src/lite/setup.rs | 2 + rs/moq-net/src/model/bandwidth.rs | 23 +- rs/moq-net/src/model/broadcast.rs | 76 +++++- rs/moq-net/src/model/follow.rs | 3 +- rs/moq-net/src/model/frame.rs | 18 +- rs/moq-net/src/model/group.rs | 47 +++- rs/moq-net/src/model/mod.rs | 5 +- rs/moq-net/src/model/origin.rs | 181 ++++++++++++- rs/moq-net/src/model/resume.rs | 45 +++- rs/moq-net/src/model/track.rs | 241 +++++++++++++++++- rs/moq-net/src/path/mod.rs | 1 + rs/moq-net/src/setup.rs | 15 ++ rs/moq-net/src/stats.rs | 21 +- rs/moq-net/src/time.rs | 5 + rs/moq-net/src/util.rs | 7 +- rs/moq-net/src/version.rs | 2 + rs/moq-net/tests/announce_covering_route.rs | 2 + rs/moq-net/tests/announce_to_serve.rs | 2 + rs/moq-net/tests/auth.rs | 2 + rs/moq-net/tests/broadcast_close.rs | 2 + rs/moq-net/tests/catalog_acquisition.rs | 2 + rs/moq-net/tests/datagram.rs | 2 + rs/moq-net/tests/dial_split_horizon.rs | 2 + rs/moq-net/tests/drained_claim.rs | 2 + rs/moq-net/tests/empty_group.rs | 2 + rs/moq-net/tests/extensions.rs | 2 + rs/moq-net/tests/fetch_cancel.rs | 2 + rs/moq-net/tests/fetch_from_feed.rs | 2 + rs/moq-net/tests/fetch_only.rs | 2 + rs/moq-net/tests/finished_broadcast_mock.rs | 2 + rs/moq-net/tests/finite_relay.rs | 2 + rs/moq-net/tests/goaway.rs | 2 + rs/moq-net/tests/group_charge.rs | 2 + rs/moq-net/tests/group_order.rs | 2 + rs/moq-net/tests/head_fetch.rs | 2 + rs/moq-net/tests/history_groups.rs | 2 + rs/moq-net/tests/idle_cancel_refetch.rs | 2 + rs/moq-net/tests/idle_session.rs | 2 + rs/moq-net/tests/late_lower_group.rs | 2 + rs/moq-net/tests/loom.rs | 2 + rs/moq-net/tests/mesh_withdraw.rs | 2 + rs/moq-net/tests/model.rs | 48 ++++ rs/moq-net/tests/multi_cdn.rs | 2 + rs/moq-net/tests/quiet_catalog.rs | 2 + rs/moq-net/tests/rejoin.rs | 2 + rs/moq-net/tests/replacement.rs | 2 + rs/moq-net/tests/request_caps.rs | 2 + rs/moq-net/tests/restart.rs | 2 + rs/moq-net/tests/route_change.rs | 2 + rs/moq-net/tests/session_close.rs | 2 + rs/moq-net/tests/session_panic.rs | 2 + .../tests/settled_end_survives_abort.rs | 2 + rs/moq-net/tests/subscribe_tracks.rs | 2 + .../tests/subscription_end_integrity.rs | 2 + rs/moq-net/tests/track_abort_over_session.rs | 2 + rs/moq-net/tests/track_info_cancel.rs | 2 + rs/moq-net/tests/track_stream_demand.rs | 2 + rs/moq-net/tests/track_tail.rs | 2 + rs/moq-net/tests/unannounce_release.rs | 2 + rs/moq-net/tests/untimed.rs | 2 + rs/moq-net/tests/wasm.rs | 2 + rs/moq-net/tests/wide_sequence.rs | 2 + sh/rs/net-features.sh | 38 +++ 88 files changed, 1067 insertions(+), 116 deletions(-) delete mode 100644 quest/m1/net-async-feature.md create mode 100644 rs/moq-net/tests/model.rs create mode 100644 sh/rs/net-features.sh diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index 58e54483a6e..d49c3d9d494 100644 --- a/doc/lib/rs/moq-net.md +++ b/doc/lib/rs/moq-net.md @@ -21,6 +21,7 @@ What you use it for, beyond what the concept page already describes: - **Follow restarts.** `consumer.follow(path)` reduces the routes covering a path to the one a request resolves (the most specific) and yields its start, update, restart, and end. A restart says another publisher instance now serves the path: request it again. A broadcast already resolved stays on the old instance until dropped or its route goes. - **One track per name.** Concurrent subscriptions share one request and producer. The application owns sequence numbers: a producer that replaces an ended one gets `Error::Duplicate` from an append that would reuse a sequence the name already wrote, so it starts with `create_group` at an explicit sequence. Only a new broadcast starts over. Readers of an aborted track resume onto the replacement only when the broadcast announces an epoch; otherwise their subscription ends with the error. - **Publish only while someone is watching.** `demand()` on a track, group, or broadcast says whether a subscriber is attached, which is how capture and transcode skip work nobody asked for. Holding a broadcast consumer is not demand. Holding a track consumer is, so drop one you are not reading. A shared fetch stays up until its last reader leaves. +- **Use the model without a runtime.** Disable default features on `moq-net` to use its `poll_*` APIs and origin driver with caller-supplied time. The default `async` feature adds async helpers, `time::run`, and both wire sessions. - **You drive the session, or `moq-tokio` does.** `connect` and `accept` return a session plus a driver that never reads the clock itself. `moq_net::time::run` polls it on tokio or in the browser. `moq-tokio` and `moq-wasm` hide that. A custom transport implements `moq_net::transport::poll`. - **A session caps what its peer can hold.** `session::Limits` (via `Client::with_limits` and `Server::with_limits`) bounds announces and subscriptions per session; the defaults suit a relay mesh, so lower them for untrusted peers. Past either, the session closes with `TOO_MANY_REQUESTS`. On moq-transport drafts 14 to 16 they also size the request-ID window, which every request counts against, so very low limits can starve it. Peer-declared lengths are capped before they are buffered. - **Authorization is in band.** On moq-lite 07 (`moq-lite-07-wip`, opt-in) and on moq-transport draft-17+ with the [MoQ Auth extension](/draft/moq-auth), `session.auth()` presents tokens without reconnecting and watches the grant the peer sent back. A server verifies tokens itself by taking `handshake.auth().requests()` before `ok()`. `authorize(&grant)` narrows or widens a live session on any version: what falls outside resets with `Unauthorized` and the session stays up. A client that publishes outside its grant closes the session, naming the path. diff --git a/quest/m1/README.md b/quest/m1/README.md index 0601bc4d708..4ccb3df7f0e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -80,7 +80,6 @@ above them. - [Untimed decisions](/quest/m1/untimed-decisions.md) - the maintainer decides whether moq-archive keeps refusing untimed tracks, and whether a malformed FETCH object ends its track - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed -- [moq-net's async feature](/quest/m1/net-async-feature.md) - async helpers and both sessions sit behind a default `async` feature, and CI builds moq-net without it; Controlled time's net migration requires it - [Bump web-transport-iroh for the capsule close](/quest/m1/iroh-capsule-bump.md) - condition: moq-dev/web-transport#419 ships in a release, then moq's iroh HTTP/3 client reports a peer's close capsule - [Early drops](/quest/m1/subscribe-drop-early.md) - Rust and JS publishers drop a sequence the moment they give it up, and forward an upstream drop - [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - every stream group in a lite subscription arrives or is dropped by name, and lite-07 drops its stream count for it diff --git a/quest/m1/net-async-feature.md b/quest/m1/net-async-feature.md deleted file mode 100644 index 7db2ad4d715..00000000000 --- a/quest/m1/net-async-feature.md +++ /dev/null @@ -1,24 +0,0 @@ -# [S] moq-net's async helpers sit behind a feature - -## Goal - -moq-net's async helper methods sit behind an `async` cargo feature, and a CI -lane builds and tests the crate without it. Without the feature, both -sessions (lite and IETF) are compiled out entirely, leaving the model's -explicit-time interface for Controlled time. - -## Plan - -- The feature is on by default, so Rust callers see no change. -- Gate the sessions whole rather than requiring a sans-IO rewrite. This - stays in m1 because [Controlled time](/quest/m1/time/net.md) requires it, - independently of the browser backend selection. -- The model's `run_front`/`serve_front` (`model/origin.rs`) are core driver - logic, not helpers, so they stay outside the feature; they need no runtime - to build. Production WASM may retain async Rust. Rewriting these drivers - or reimplementing helpers with JS Promises is not part of this quest. -- The lane runs at least the tests that do not exercise the helpers; tests - that do stay behind the feature. - -Public API: moq-net's async helpers and sessions move behind a default -feature, so a `default-features = false` caller loses them. Wire: none. diff --git a/quest/m1/time/README.md b/quest/m1/time/README.md index edaaa51e181..0ac1e468f54 100644 --- a/quest/m1/time/README.md +++ b/quest/m1/time/README.md @@ -108,4 +108,3 @@ cleanup and the global lint ratchet belong to - [More tests under load](/quest/m1/test-flakes-2/README.md) - the per-flake fixes this line makes systemic - [Rust-backed @moq/net](/quest/m2/rs2ts/README.md) - Rust and JS clocks must agree across either browser backend -- [The async feature](/quest/m1/net-async-feature.md) - the core keeps explicit instants; ambient time stays behind `async` diff --git a/quest/m1/time/net.md b/quest/m1/time/net.md index 2e4eaeaa8ed..7cef980c8b1 100644 --- a/quest/m1/time/net.md +++ b/quest/m1/time/net.md @@ -24,9 +24,8 @@ arm timers only through `moq-clock`: Breaking; it lands on `main` after the crate (see [the line's README](/quest/m1/time/README.md)). The ambient functions sit behind moq-net's `async` feature; the core takes -explicit instants. Decided 2026-10-10 from review: this waits for -[the async feature](/quest/m1/net-async-feature.md), so the -migration lands gated rather than exposing ambient time to the sans-IO core. +explicit instants. The migration preserves that boundary rather than exposing +ambient time to the sans-IO core. Coordinate clock origin, units, and controlled-time behavior with the [browser backend](/quest/m2/rs2ts/README.md) if it has landed. Source-level `Instant` mappings depend on the selected backend; WASM retains Rust time types. @@ -60,7 +59,3 @@ instant has no general conversion back: `time::Instant`, `moq_mux::Clock`, and moq-mux's `flush`/`update` family among them (`container::Producer`, `catalog::Estimator`, `rate::Control`, the codec and import producers). The PR enumerates each break and migrates its in-tree callers. Wire: none. - -## Required - -- [The async feature](/quest/m1/net-async-feature.md) - the feature the ambient functions sit behind diff --git a/rs/justfile b/rs/justfile index d48da66ed91..aea81851904 100644 --- a/rs/justfile +++ b/rs/justfile @@ -208,6 +208,7 @@ c-tests: # Compile the workspace at the feature extremes. features: + just rs net-features cargo check --locked --workspace {{ no_fuzz }} --all-targets --no-default-features cargo hack check --locked -p moq-tokio --each-feature --exclude-features _transport --features aws-lc-rs cargo check --locked -p moq-relay --no-default-features @@ -220,6 +221,10 @@ features: RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace {{ no_fuzz }} --all-features --no-deps bash sh/rs/nextest.sh cargo nextest run --locked --workspace {{ no_fuzz }} --all-targets --all-features +# Compile and test the explicit-time model without async helpers or sessions. +net-features: + bash sh/rs/net-features.sh + # Compile the media crates at supported feature boundaries and prove expensive # native/render dependencies are absent when their features are excluded. media-features: diff --git a/rs/moq-net/Cargo.toml b/rs/moq-net/Cargo.toml index 13c72b870c9..ac00e89123e 100644 --- a/rs/moq-net/Cargo.toml +++ b/rs/moq-net/Cargo.toml @@ -16,15 +16,19 @@ categories = ["multimedia", "network-programming", "web-programming"] ignored = ["getrandom"] [features] +default = ["async"] +# Runtime-backed helpers and Lite/IETF sessions; explicit-time model drivers stay available. +async = ["dep:futures", "dep:web-async"] + # Exposes the wire codecs to the `fuzz/` harness and the announce, codec, varint, and frame benches, # and session views of an origin to the viewers bench, through the hidden `fuzz` module. Off by # default and hidden from the docs. -fuzz = [] +fuzz = ["async"] [dependencies] arrayvec = "0.7" bytes = { workspace = true } -futures = { workspace = true } +futures = { workspace = true, optional = true } kio = { workspace = true } moq-pattern = { workspace = true, features = ["serde"] } num_enum = { workspace = true } @@ -34,10 +38,15 @@ slab = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["js"] } +web-async = { workspace = true, optional = true } + +# The browser instant shim is needed even by explicit-time drivers. +[target.'cfg(target_family = "wasm")'.dependencies] web-async = { workspace = true } [dev-dependencies] criterion = { workspace = true } +futures = { workspace = true } # The tests' executor, with simulated time. moq-net-sim = { path = "sim" } serde_json = { workspace = true } @@ -71,6 +80,7 @@ required-features = ["fuzz"] [[bench]] name = "origin" harness = false +required-features = ["async"] # Reaches the crate-private session view of an origin through the hidden `fuzz` module. [[bench]] @@ -105,26 +115,32 @@ required-features = ["fuzz"] [[bench]] name = "group" harness = false +required-features = ["async"] [[bench]] name = "track" harness = false +required-features = ["async"] [[bench]] name = "priority" harness = false +required-features = ["async"] [[bench]] name = "stats" harness = false +required-features = ["async"] [[bench]] name = "session" harness = false +required-features = ["async"] [[bench]] name = "admission" harness = false +required-features = ["async"] [[bench]] name = "request-ids" @@ -140,3 +156,4 @@ required-features = ["fuzz"] [[bench]] name = "auth" harness = false +required-features = ["async"] diff --git a/rs/moq-net/README.md b/rs/moq-net/README.md index 2f54ddfe0da..6d81ccf8935 100644 --- a/rs/moq-net/README.md +++ b/rs/moq-net/README.md @@ -15,6 +15,12 @@ Live media is built on top of this layer using something like [hang](https://git - **Groups**: A sequential collection of frames, usually starting with a keyframe. - **Frame**: A timed chunk of data. +The default `async` feature includes Lite/IETF sessions, async convenience methods, +and the announcement `Stream` adapter. +Use `default-features = false` for the model's `poll_*` APIs and origin driver, +polled with explicit time without a native runtime. This excludes sessions and +`time::run`; the origin's core serving logic remains available. + ## Examples - [Publishing a chat track](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/examples/chat.rs) diff --git a/rs/moq-net/src/auth.rs b/rs/moq-net/src/auth.rs index cf036f7d98f..08368918cb4 100644 --- a/rs/moq-net/src/auth.rs +++ b/rs/moq-net/src/auth.rs @@ -2,9 +2,9 @@ //! //! Each side of a session presents the credential its connection //! already carried (the URL, a client certificate, or nothing) right after -//! setup, and learns the [`Grant`] it earned. [`Session::auth`](crate::Session::auth) +//! setup, and learns the [`Grant`] it earned. `Session::auth` //! returns the [`Handle`]: [`grant`](Handle::grant) is the union of every token -//! this side presented, and [`add`](Handle::add) presents another without +//! this side presented, and `add` presents another without //! reconnecting. Dropping the returned [`Token`] withdraws it. //! //! The other direction, answering the peer's tokens, is automatic: the session @@ -16,7 +16,7 @@ //! //! moq-transport draft-17+ carries the same exchange when both sides negotiate the //! MoQ Auth extension. Older versions, and peers that do not negotiate it, carry no -//! AUTH exchange: there the grant stays `None` and [`add`](Handle::add) fails with +//! AUTH exchange: there the grant stays `None` and `add` fails with //! [`Error::Unsupported`]. moq-lite carries a grant's patterns as they are; //! moq-transport carries namespace prefixes, so it refuses a grant that is not a //! union of subtrees rather than widen it. @@ -31,6 +31,7 @@ use bytes::Bytes; use crate::{Error, Patterns, Result, SessionError, time::Instant}; /// Classify malformed AUTH messages separately from transport and local failures. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn protocol_error(err: Error) -> Error { match err { Error::Decode(_) @@ -88,6 +89,7 @@ impl Grant { /// The paths both grants allow, lapsing at the earlier expiry. /// /// Fails closed: an intersection too large to hold grants nothing, never more. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn intersect(&self, other: &Self) -> Self { let both = |a: &Patterns, b: &Patterns| { a.intersect(b).unwrap_or_else(|err| { @@ -118,6 +120,7 @@ pub(crate) struct State { supported: bool, /// Set once the session ends; every waiter resolves with it. closed: Option, + #[cfg_attr(not(feature = "async"), allow(dead_code))] next_id: u64, /// Every token this side presented that has not ended. tokens: BTreeMap, @@ -139,12 +142,15 @@ pub(crate) struct State { /// One presented token. pub(crate) struct Slot { + #[cfg_attr(not(feature = "async"), allow(dead_code))] token: Bytes, /// Presented by the session itself at setup, so enforcement waits on it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] setup: bool, /// The latest AUTH_OK, `None` before the first and after the token ends. grant: Option, /// The first reply: `Ok` for an AUTH_OK, the refusal otherwise. + #[cfg_attr(not(feature = "async"), allow(dead_code))] answered: Option>, /// Why the token ended, once it has. ended: Option, @@ -159,8 +165,10 @@ enum Acceptor { #[default] Undecided, /// The app answers every token. + #[cfg_attr(not(feature = "async"), allow(dead_code))] App(Pending), /// The session answers from its own origin handles. + #[cfg_attr(not(feature = "async"), allow(dead_code))] Default, } @@ -187,6 +195,7 @@ impl State { /// The union's and the limit's patterns for `direction`, each `None` while /// unrestricted. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn parts(&self, direction: Direction) -> (Option<&Patterns>, Option<&Patterns>) { // The peer's grant names what we may do; the limit names what the peer may, // so what we send is what the peer may subscribe to, and the other way around. @@ -203,6 +212,7 @@ impl State { } /// What `direction` allows right now. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn permit(&self, direction: Direction) -> Permit { let (granted, limit) = self.parts(direction); Permit { @@ -212,6 +222,7 @@ impl State { } /// Whether `direction` allows `path` right now, without copying the patterns. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn allows(&self, direction: Direction, path: &str) -> bool { let (granted, limit) = self.parts(direction); granted.is_none_or(|granted| granted.matches(path)) && limit.is_none_or(|limit| limit.matches(path)) @@ -222,6 +233,7 @@ impl State { /// union of its tokens) and the limit this side set on the peer, each `None` /// while unrestricted. #[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Permit { granted: Option, limit: Option, @@ -229,18 +241,21 @@ pub(crate) struct Permit { impl Permit { /// Whether both allow `path`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn matches(&self, path: &str) -> bool { self.granted.as_ref().is_none_or(|granted| granted.matches(path)) && self.within_limit(path) } /// Whether the limit alone allows `path`: for what the peer offers us, which our /// own grant does not decide. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn within_limit(&self, path: &str) -> bool { self.limit.as_ref().is_none_or(|limit| limit.matches(path)) } /// Whether this allows everything `other` does, judged part by part, so a `false` /// may be spurious but a `true` never is. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn covers(&self, other: &Self) -> bool { let covers = |this: &Option, other: &Option| match (this, other) { (None, _) => true, @@ -251,7 +266,7 @@ impl Permit { } } -/// The session's auth handle, returned by [`Session::auth`](crate::Session::auth). +/// The session's auth handle, returned by `Session::auth`. /// /// Cheap to clone; every clone shares the session's tokens. #[derive(Clone)] @@ -261,6 +276,7 @@ pub struct Handle { impl Handle { /// A handle for a session that speaks AUTH (`supported`) or never will. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(supported: bool) -> Self { Self { state: kio::Shared::new(State { @@ -272,6 +288,7 @@ impl Handle { /// Whether this side speaks AUTH: false on a version without it, on a side that does /// not offer it, and once the peer turns out not to. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn supported(&self) -> bool { self.state.lock().supported } @@ -292,6 +309,7 @@ impl Handle { /// [`Error::Unsupported`] when the version or the peer carries no AUTH (or the /// peer takes no tokens in band), the peer's refusal as /// [`Error::Session`], or the session's close. + #[cfg(feature = "async")] pub async fn add(&self, token: impl Into) -> Result { let token = self.present(token.into(), false)?; kio::wait(|waiter| token.poll_answered(waiter)).await?; @@ -299,6 +317,7 @@ impl Handle { } /// Queue a token for the driver to present. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn present(&self, token: Bytes, setup: bool) -> Result { let mut state = self.state.lock(); if !state.supported { @@ -360,6 +379,7 @@ impl Handle { /// Decide who answers the peer's tokens: the app if it took the requests, /// otherwise the session itself (`None`). Idempotent. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn acceptor(&self) -> Option { let mut state = self.state.lock(); match &state.acceptor { @@ -373,6 +393,7 @@ impl Handle { } /// The next token to open a stream for, or `Ready(None)` once the session ends. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_opening(&self, waiter: &kio::Waiter) -> Poll> { let mut state = ready_or!(self.state.poll(waiter, |state| { match state.opening.is_empty() && state.closed.is_none() { @@ -396,6 +417,7 @@ impl Handle { } /// Whether the token's handle dropped, so its stream should close. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_withdrawn(&self, id: u64, waiter: &kio::Waiter) -> Poll<()> { let _ = ready_or!(self.state.poll(waiter, |state| match state.tokens.get(&id) { Some(slot) if !slot.withdrawn => Poll::Pending, @@ -405,6 +427,7 @@ impl Handle { } /// Record an AUTH_OK for the token. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn granted(&self, id: u64, grant: Grant) { // One parseable line per AUTH_OK, so the grant the peer actually sent is observable // without an API; the interop harness checks it against the token it minted. @@ -427,6 +450,7 @@ impl Handle { /// Record an AUTH_ERROR for the token: a reply that grants nothing, so a refused /// setup token leaves an empty union rather than an unknown (unrestricted) one. /// The driver still ends the token with [`ended`](Self::ended). + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn refused(&self, id: u64) { let mut state = self.state.lock(); if state.tokens.contains_key(&id) { @@ -436,6 +460,7 @@ impl Handle { /// End the token: refused or revoked by the peer, withdrawn by us, or its /// stream lost. A token that was never answered records `err` as its answer. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn ended(&self, id: u64, err: Error) { let mut state = self.state.lock(); let Some(slot) = state.tokens.get_mut(&id) else { @@ -453,6 +478,7 @@ impl Handle { /// Resolve once every token the session presented at setup has its first /// reply, so enforcement never waits on a token the app added later. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_setup_answered(&self, waiter: &kio::Waiter) -> Poll<()> { let _ = ready_or!(self.state.poll(waiter, |state| { match state.tokens.values().any(|slot| slot.setup && slot.answered.is_none()) { @@ -488,6 +514,7 @@ impl Handle { /// The union once it changed since `epoch` last saw it, advancing `epoch`. It /// only ever changes into `Some`: no answer yet is the initial state. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_union(&self, epoch: &mut u64, waiter: &kio::Waiter) -> Poll> { let state = ready_or!(self.poll_epoch(*epoch, waiter)); *epoch = state.epoch; @@ -496,6 +523,7 @@ impl Handle { /// What `direction` allows once it may have changed since `epoch` last saw it, /// advancing `epoch`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_permit(&self, direction: Direction, epoch: &mut u64, waiter: &kio::Waiter) -> Poll { let state = ready_or!(self.poll_epoch(*epoch, waiter)); *epoch = state.epoch; @@ -503,12 +531,14 @@ impl Handle { } /// The limit once it may have changed since `epoch` last saw it, advancing `epoch`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_limit(&self, epoch: &mut u64, waiter: &kio::Waiter) -> Poll> { let state = ready_or!(self.poll_epoch(*epoch, waiter)); *epoch = state.epoch; Poll::Ready(state.limit.clone()) } + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn poll_epoch(&self, seen: u64, waiter: &kio::Waiter) -> Poll> { self.state.poll(waiter, |state| match state.epoch != seen { true => Poll::Ready(()), @@ -519,12 +549,14 @@ impl Handle { /// Whether `direction` allows `path` right now: the union (allowing everything /// while it is `None`, before an answer or on a version without AUTH) and the /// limit both. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn allows(&self, direction: Direction, path: &str) -> bool { self.state.read().allows(direction, path) } /// Whether the limit alone allows `direction` at `path`: for a route the peer /// offers us, which our own grant does not decide. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn within_limit(&self, direction: Direction, path: &str) -> bool { let state = self.state.read(); let (_, limit) = state.parts(direction); @@ -533,6 +565,7 @@ impl Handle { /// The peer turned out not to negotiate AUTH: fail every token as unsupported and /// close the requests, leaving the union unknown. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn unsupported(&self) { let mut state = self.state.lock(); state.supported = false; @@ -550,6 +583,7 @@ impl Handle { /// End the session: fail every pending token, end every watch, and close the /// requests. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn close(&self, err: Error) { let mut state = self.state.lock(); if state.closed.is_some() { @@ -586,6 +620,7 @@ impl Token { /// Wait for the token to end: the peer's revocation as [`Error::Session`], /// [`Error::Cancel`] when the peer closed it without one, or the session's /// close. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { kio::wait(|waiter| { let state = ready_or!(self.state.poll(waiter, |state| match state.tokens.get(&self.id) { @@ -598,6 +633,7 @@ impl Token { .await } + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn poll_answered(&self, waiter: &kio::Waiter) -> Poll> { let state = ready_or!(self.state.poll(waiter, |state| match state.tokens.get(&self.id) { Some(slot) if slot.answered.is_none() => Poll::Pending, @@ -711,6 +747,7 @@ impl Watch { /// # Errors /// /// See [`poll_changed`](Self::poll_changed). + #[cfg(feature = "async")] pub async fn changed(&mut self) -> Result> { kio::wait(|waiter| self.poll_changed(waiter)).await } @@ -730,6 +767,7 @@ impl Requests { } /// Wait for the next token the peer presents, or `None` once the session ends. + #[cfg(feature = "async")] pub async fn next(&mut self) -> Option { kio::wait(|waiter| self.poll_next(waiter)).await } @@ -761,6 +799,7 @@ impl Pending { } /// Queue the token for the application, or refuse it when closed or full. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn push(&self, request: Request) { let mut state = self.state.lock(); // Dropping the request refuses it as unanswered. @@ -802,7 +841,9 @@ impl Pending { /// What the acceptor writes on one peer token's stream. pub(crate) enum Reply { + #[cfg_attr(not(feature = "async"), allow(dead_code))] Grant(Grant), + #[cfg_attr(not(feature = "async"), allow(dead_code))] Refuse { code: SessionError, reason: String }, } @@ -821,6 +862,7 @@ pub(crate) struct Issue { /// /// Dropping it settles [`Issued::closed`] with the session's error, unless the task /// already recorded why the stream ended, so no exit path leaves the acceptor waiting. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Serving { pub issue: kio::Shared, handle: Handle, @@ -828,6 +870,7 @@ pub(crate) struct Serving { impl Serving { /// A fresh stream's shared state, settled by the session `handle` if the task drops. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(handle: Handle) -> Self { Self { issue: kio::Shared::default(), @@ -836,6 +879,7 @@ impl Serving { } /// Record why the stream ended on the peer's side; the first reason wins. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn end(&self, err: Error) { self.issue.lock().peer.get_or_insert(err); } @@ -857,6 +901,7 @@ pub struct Request { } impl Request { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(token: Bytes, issue: kio::Shared) -> Self { Self { token, @@ -875,6 +920,7 @@ impl Request { } /// Wait for the peer to withdraw this unanswered token or close the session. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { closed(self.issue.as_ref().expect("unanswered request")).await } @@ -935,11 +981,13 @@ impl Issued { /// Wait for the peer to withdraw the token ([`Error::Cancel`]), or for its /// stream or the session to end. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { closed(&self.issue).await } } +#[cfg(feature = "async")] async fn closed(issue: &kio::Shared) -> Error { kio::wait(|waiter| { let issue = ready_or!(issue.poll(waiter, |issue| { @@ -962,6 +1010,7 @@ impl Drop for Issued { /// Which way media flows, from this side's view. #[derive(Clone, Copy)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) enum Direction { /// This side sends to the peer: our grant's `publish`, and the limit's `subscribe`. Publish, @@ -975,6 +1024,7 @@ pub(crate) enum Direction { /// Create it at the request's first check and hold it to the end, so a change /// landing in between is never missed. Cheap to poll on every wakeup: the path is /// only re-matched when the union or the limit changes. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Gate { handle: Handle, path: crate::PathOwned, @@ -983,6 +1033,7 @@ pub(crate) struct Gate { } impl Gate { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(handle: Handle, path: crate::PathOwned, direction: Direction) -> Self { Self { handle, @@ -995,6 +1046,7 @@ impl Gate { /// Resolve once the session no longer allows the path. A union that is still /// `None` (no answer yet, or a version without AUTH) and a session never limited /// allow everything. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_denied(&mut self, waiter: &kio::Waiter) -> Poll<()> { loop { let permit = ready_or!(self.handle.poll_permit(self.direction, &mut self.epoch, waiter)); @@ -1007,6 +1059,7 @@ impl Gate { /// The default acceptor's answer to the peer's connection credential: the grant /// the session's origin handles allow, within the limit as it changes. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct DefaultGrant { handle: Handle, base: Grant, @@ -1017,6 +1070,7 @@ pub(crate) struct DefaultGrant { } impl DefaultGrant { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(handle: Handle, base: Grant) -> Self { let limit = handle.state.read().limit.clone(); Self { @@ -1029,6 +1083,7 @@ impl DefaultGrant { } /// The grant to send next: the first, then each one a new limit changes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll(&mut self, waiter: &kio::Waiter) -> Poll { while let Poll::Ready(limit) = self.handle.poll_limit(&mut self.epoch, waiter) { self.limit = limit; @@ -1047,6 +1102,7 @@ impl DefaultGrant { /// The close reason naming a broadcast published outside the grant. [`Error`] carries /// no payload, so the path travels here, in the session's own terms. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn unauthorized_reason(path: &crate::Path) -> String { format!("unauthorized: {path}") } @@ -1061,6 +1117,7 @@ pub(crate) fn unauthorized_reason(path: &crate::Path) -> String { /// publication. Only the dialing side enforces: a server's publish origin is everything /// the peer may read, not what it intends to push. #[derive(Default)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Enforce { epoch: u64, permit: Option, @@ -1072,6 +1129,7 @@ pub(crate) struct Enforce { impl Enforce { /// Resolve with the first broadcast announced outside the union, or `None` once the /// origin ends. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll( &mut self, handle: &Handle, diff --git a/rs/moq-net/src/coding/codes.rs b/rs/moq-net/src/coding/codes.rs index af5f8280729..c12ec1abe53 100644 --- a/rs/moq-net/src/coding/codes.rs +++ b/rs/moq-net/src/coding/codes.rs @@ -13,6 +13,7 @@ use crate::{Error, StreamError, ietf, lite}; /// /// Encoding and decoding live on one trait so a peer cannot be told a code from one table /// and read against another: implement both halves or neither. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub trait StreamCodes { /// The code to reset a stream, or send STOP_SENDING, with. fn encode_stream_code(&self, err: &StreamError) -> u32; diff --git a/rs/moq-net/src/coding/decode.rs b/rs/moq-net/src/coding/decode.rs index fa33535ef6b..df93fcde506 100644 --- a/rs/moq-net/src/coding/decode.rs +++ b/rs/moq-net/src/coding/decode.rs @@ -6,6 +6,7 @@ use super::{BoundsExceeded, Form, varint}; /// Read the value from a [`Decoder`] using the given version. /// /// If [DecodeError::Short] is returned, the caller should try again with more data. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub trait Decode: Sized { /// Decode the value from the front of the decoder. fn decode(r: &mut Decoder<'_>, version: V) -> Result; @@ -107,6 +108,7 @@ pub enum DecodeError { impl DecodeError { /// A complete frame cannot be extended by reading more stream bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn complete(self) -> Self { match self { Self::Short => Self::InvalidValue, @@ -126,6 +128,7 @@ impl From for DecodeError { /// A read either consumes exactly what it returns or fails and consumes nothing, so a /// [`DecodeError::Short`] can be retried once more bytes arrive. #[derive(Debug, Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub struct Decoder<'a> { buf: &'a [u8], form: Form, @@ -133,26 +136,31 @@ pub struct Decoder<'a> { impl<'a> Decoder<'a> { /// Read `buf`, with varints in the given form. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn new(buf: &'a [u8], form: Form) -> Self { Self { buf, form } } /// The varint form this decoder reads. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn form(&self) -> Form { self.form } /// The number of unread bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn remaining(&self) -> usize { self.buf.len() } /// Whether every byte has been read. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn is_empty(&self) -> bool { self.buf.is_empty() } /// Read `len` raw bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn slice(&mut self, len: usize) -> Result<&'a [u8], DecodeError> { let Some((head, rest)) = self.buf.split_at_checked(len) else { return Err(DecodeError::Short); @@ -162,27 +170,32 @@ impl<'a> Decoder<'a> { } /// Read every remaining byte. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn rest(&mut self) -> &'a [u8] { std::mem::take(&mut self.buf) } /// Split off the next `len` bytes as their own decoder, e.g. a size-prefixed body. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn sub(&mut self, len: usize) -> Result { Ok(Self::new(self.slice(len)?, self.form)) } /// Read a single byte. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn u8(&mut self) -> Result { Ok(self.slice(1)?[0]) } /// Read a big-endian `u16`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn u16(&mut self) -> Result { let b = self.slice(2)?; Ok(u16::from_be_bytes([b[0], b[1]])) } /// Read a byte that must be 0 or 1. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn bool(&mut self) -> Result { match self.u8()? { 0 => Ok(false), @@ -194,6 +207,7 @@ impl<'a> Decoder<'a> { /// Read a varint. #[cfg_attr(target_arch = "wasm32", inline)] #[cfg_attr(not(target_arch = "wasm32"), inline(always))] + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn varint(&mut self) -> Result { let (value, rest) = varint::read(self.buf, self.form)?; self.buf = rest; @@ -201,11 +215,13 @@ impl<'a> Decoder<'a> { } /// Read an optional varint: 0 is `None`, and `n + 1` is `Some(n)`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn varint_opt(&mut self) -> Result, DecodeError> { Ok(self.varint()?.checked_sub(1)) } /// Read a varint length, then that many raw bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn bytes(&mut self) -> Result<&'a [u8], DecodeError> { let start = self.buf; let len = usize::try_from(self.varint()?).map_err(|_| DecodeError::BoundsExceeded)?; @@ -213,6 +229,7 @@ impl<'a> Decoder<'a> { } /// Read a varint length, then that many bytes of UTF-8. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn string(&mut self) -> Result { Ok(String::from_utf8(self.bytes()?.to_vec())?) } diff --git a/rs/moq-net/src/coding/encode.rs b/rs/moq-net/src/coding/encode.rs index a5eca879125..34bfa547a5a 100644 --- a/rs/moq-net/src/coding/encode.rs +++ b/rs/moq-net/src/coding/encode.rs @@ -36,6 +36,7 @@ impl From for EncodeError { } /// Write the value to an [`Encoder`] using the given version. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub trait Encode { /// Encode the value to the given encoder, which may hold partial output on error. fn encode(&self, w: &mut Encoder<'_>, version: V) -> Result<(), EncodeError>; @@ -67,6 +68,7 @@ impl<'a> Encoder<'a> { } /// The varint form this encoder writes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn form(&self) -> Form { self.form } @@ -77,16 +79,19 @@ impl<'a> Encoder<'a> { } /// Write a single byte. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn u8(&mut self, v: u8) { self.buf.push(v); } /// Write a big-endian `u16`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn u16(&mut self, v: u16) { self.buf.extend_from_slice(&v.to_be_bytes()); } /// Write a bool as a 0 or 1 byte. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn bool(&mut self, v: bool) { self.buf.push(v as u8); } @@ -99,6 +104,7 @@ impl<'a> Encoder<'a> { } /// Write an optional varint: `None` as 0, and `Some(n)` as `n + 1`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn varint_opt(&mut self, v: Option) -> Result<(), EncodeError> { let v = match v { Some(v) => v.checked_add(1).ok_or(EncodeError::TooLarge)?, @@ -108,6 +114,7 @@ impl<'a> Encoder<'a> { } /// Write a varint length, then the raw bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn bytes(&mut self, v: &[u8]) -> Result<(), EncodeError> { self.varint(v.len() as u64)?; self.slice(v); @@ -115,6 +122,7 @@ impl<'a> Encoder<'a> { } /// Write a varint length, then the UTF-8 bytes. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn string(&mut self, v: &str) -> Result<(), EncodeError> { self.bytes(v.as_bytes()) } @@ -122,6 +130,7 @@ impl<'a> Encoder<'a> { /// Reserve a varint size prefix for the body written next; [`Self::fill`] sizes it. /// /// One byte is reserved, which most bodies fit, so they never move. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn prefix_varint(&mut self) -> Prefix { self.buf.push(0); Prefix { @@ -131,6 +140,7 @@ impl<'a> Encoder<'a> { } /// Reserve a big-endian `u16` size prefix for the body written next; [`Self::fill`] sizes it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn prefix_u16(&mut self) -> Prefix { self.buf.extend_from_slice(&[0, 0]); Prefix { @@ -140,11 +150,13 @@ impl<'a> Encoder<'a> { } /// The number of bytes written since `prefix` was reserved. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn since(&self, prefix: &Prefix) -> usize { self.buf.len() - prefix.body } /// Drop a reserved prefix and everything written since, as if neither was written. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn discard(&mut self, prefix: Prefix) { let start = match prefix.kind { PrefixKind::Varint => prefix.body - 1, @@ -154,6 +166,7 @@ impl<'a> Encoder<'a> { } /// Size a reserved prefix to everything written since. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn fill(&mut self, prefix: Prefix) -> Result<(), EncodeError> { let body = prefix.body; let end = self.buf.len(); @@ -188,6 +201,7 @@ impl<'a> Encoder<'a> { /// A size prefix reserved ahead of a body, sized by [`Encoder::fill`] once it is written. #[must_use = "an unfilled prefix leaves a zero size on the wire"] #[derive(Debug)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub struct Prefix { /// Where the body starts, just past the reserved bytes. body: usize, @@ -195,6 +209,7 @@ pub struct Prefix { } #[derive(Debug)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] enum PrefixKind { Varint, U16, diff --git a/rs/moq-net/src/coding/mod.rs b/rs/moq-net/src/coding/mod.rs index 263cc221d3a..b7ba715ed30 100644 --- a/rs/moq-net/src/coding/mod.rs +++ b/rs/moq-net/src/coding/mod.rs @@ -3,20 +3,27 @@ mod codes; mod decode; mod encode; +#[cfg(feature = "async")] mod reader; +#[cfg(feature = "async")] mod stream; pub mod varint; mod version; +#[cfg(feature = "async")] mod writer; +#[cfg(feature = "async")] pub use codes::*; pub use decode::*; pub use encode::*; +#[cfg(feature = "async")] pub use reader::*; +#[cfg(feature = "async")] pub use stream::*; pub use varint::BoundsExceeded; pub(crate) use varint::Form; pub use version::*; +#[cfg(feature = "async")] pub use writer::*; /// Decode from the front of a test buffer with `decode`, advancing it past what was read. @@ -36,6 +43,7 @@ pub(crate) fn decode_buf + Copy, T>( /// Decode one varint from the front of a test buffer, advancing it past what was read. #[cfg(test)] +#[cfg(feature = "async")] pub(crate) fn decode_varint + Copy>(buf: &mut B, version: V) -> Result { decode_buf(buf, version, |r, _| r.varint()) } diff --git a/rs/moq-net/src/coding/varint.rs b/rs/moq-net/src/coding/varint.rs index 222e5b25ceb..982fd6802fc 100644 --- a/rs/moq-net/src/coding/varint.rs +++ b/rs/moq-net/src/coding/varint.rs @@ -168,6 +168,7 @@ fn write_leading_ones(value: u64, out: &mut Vec) { /// Decode a varint in `form` from the front of `buf`, returning it and the rest of `buf`. #[cfg_attr(target_arch = "wasm32", inline)] #[cfg_attr(not(target_arch = "wasm32"), inline(always))] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(super) fn read(buf: &[u8], form: Form) -> Result<(u64, &[u8]), DecodeError> { match form { Form::Quic => read_quic(buf), @@ -199,6 +200,7 @@ fn read_quic(buf: &[u8]) -> Result<(u64, &[u8]), DecodeError> { #[cfg_attr(target_arch = "wasm32", inline)] #[cfg_attr(not(target_arch = "wasm32"), inline(always))] +#[cfg_attr(not(feature = "async"), allow(dead_code))] fn read_leading_ones(buf: &[u8], seven: bool) -> Result<(u64, &[u8]), DecodeError> { let Some((&first, rest)) = buf.split_first() else { return Err(DecodeError::Short); @@ -286,11 +288,13 @@ pub fn encode_quic(value: u64, w: &mut W) -> Result<(), Encode /// Map a signed value onto the unsigned range: `(n << 1) ^ (n >> 63)`. /// /// Small magnitudes stay small (-1 -> 1, 1 -> 2, -2 -> 3, ...), and all of `i64` fits. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const fn zigzag(signed: i64) -> u64 { ((signed << 1) ^ (signed >> 63)) as u64 } /// The inverse of [`zigzag`]. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const fn unzigzag(value: u64) -> i64 { ((value >> 1) as i64) ^ -((value & 1) as i64) } diff --git a/rs/moq-net/src/coding/version.rs b/rs/moq-net/src/coding/version.rs index 705912ef010..4d89150a8e6 100644 --- a/rs/moq-net/src/coding/version.rs +++ b/rs/moq-net/src/coding/version.rs @@ -40,6 +40,7 @@ impl fmt::Debug for Version { /// A list of versions in preferred order. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub struct Versions(Vec); impl Decode for Versions { diff --git a/rs/moq-net/src/epoch.rs b/rs/moq-net/src/epoch.rs index f47b2d1117a..ee23e564b76 100644 --- a/rs/moq-net/src/epoch.rs +++ b/rs/moq-net/src/epoch.rs @@ -35,12 +35,14 @@ impl Epoch { impl Epoch { /// The UUID's 16 bytes, as the wire carries them. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn to_bytes(&self) -> [u8; 16] { // Parsing enforces the canonical form, so this cannot fail. uuid::Uuid::parse_str(&self.0).unwrap().into_bytes() } /// Parse the wire's 16 bytes, refusing anything but a UUIDv7 with the RFC variant. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn from_bytes(bytes: &[u8]) -> Result { let uuid = uuid::Uuid::from_slice(bytes).map_err(|_| InvalidEpoch)?; uuid.hyphenated().to_string().parse() diff --git a/rs/moq-net/src/error.rs b/rs/moq-net/src/error.rs index 56f590d0f1e..cf20c5af1b6 100644 --- a/rs/moq-net/src/error.rs +++ b/rs/moq-net/src/error.rs @@ -36,7 +36,7 @@ pub enum SessionError { /// The peer went past what the session allows: a request ID at or past the /// MAX_REQUEST_ID we advertised (moq-transport drafts 14 to 16), or more announcements - /// or subscriptions than its [`crate::session::Limits`]. + /// or subscriptions than its `crate::session::Limits`. #[error("too many requests")] TooManyRequests, @@ -418,7 +418,7 @@ pub enum Error { TooManyParameters, /// The peer already holds as many requests, announcements, or subscriptions as this - /// session allows (see [`crate::session::Limits`]). + /// session allows (see `crate::session::Limits`). #[error("too many requests")] TooManyRequests, diff --git a/rs/moq-net/src/goaway.rs b/rs/moq-net/src/goaway.rs index 652b9f7eeba..fde643024c2 100644 --- a/rs/moq-net/src/goaway.rs +++ b/rs/moq-net/src/goaway.rs @@ -1,8 +1,8 @@ //! GOAWAY, the graceful drain signal, split into a [Producer] and [Consumer] handle. //! //! A session sends its own GOAWAY through the [Producer] from -//! [`Session::drain`](crate::Session::drain), and observes the peer's on the -//! [Consumer] from [`Session::draining`](crate::Session::draining). A session +//! `Session::drain`, and observes the peer's on the +//! [Consumer] from `Session::draining`. A session //! sends at most one, which is why [`Producer::send`] consumes the handle. //! //! Draining works the same on a version with no GOAWAY message: the deadline is @@ -86,7 +86,7 @@ impl Goaway { /// Sends this session's single GOAWAY. /// -/// Obtained from [`Session::drain`](crate::Session::drain). +/// Obtained from `Session::drain`. pub struct Producer { trigger: kio::Producer>, /// Whether this side may name a redirect URI. A moq-transport client may not, @@ -99,7 +99,7 @@ impl Producer { /// /// The session must stay driven for the message to reach the wire. With /// [`Goaway::timeout`] set, the driver force-closes the session once the - /// deadline passes, so awaiting [`Session::closed`](crate::Session::closed) + /// deadline passes, so awaiting `Session::closed` /// is how a caller observes the drain finishing either way. That holds on a /// version with no GOAWAY message too, where the deadline is all the peer gets. /// @@ -135,7 +135,7 @@ impl Producer { /// Observes the peer's GOAWAY. /// -/// Obtained from [`Session::draining`](crate::Session::draining). Cheap to +/// Obtained from `Session::draining`. Cheap to /// clone; every clone reports the same GOAWAY. #[derive(Clone)] pub struct Consumer { @@ -145,7 +145,7 @@ pub struct Consumer { impl Consumer { /// The peer's GOAWAY, or `None` if it has not sent one. /// - /// The synchronous read behind [`recv`](Self::recv), for callers that only + /// The synchronous read behind [`poll`](Self::poll), for callers that only /// want to know whether the session is draining. pub fn peek(&self) -> Option { self.state.read().clone() @@ -167,6 +167,7 @@ impl Consumer { } /// Wait for the peer's GOAWAY, or `None` if the session closes without one. + #[cfg(feature = "async")] pub async fn recv(&self) -> Option { kio::wait(|waiter| self.poll(waiter)).await.ok() } @@ -179,6 +180,7 @@ impl Consumer { /// why the flag is not merely a read of the channel), and a wakeup for the /// per-source tasks, which have to react to a peer that drains and then goes quiet. #[derive(Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct GoingAway { flag: Arc, received: Consumer, @@ -186,11 +188,13 @@ pub(crate) struct GoingAway { impl GoingAway { /// Mark the session as going away, returning whether this was the first time. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn set(&self) -> bool { !self.flag.swap(true, Ordering::AcqRel) } /// Whether a GOAWAY has been received. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn is_set(&self) -> bool { self.flag.load(Ordering::Acquire) } @@ -200,6 +204,7 @@ impl GoingAway { /// /// Stays `Ready` once set, so a caller must treat what it does in response as /// idempotent rather than as an edge. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn poll(&self, waiter: &kio::Waiter) -> Poll<()> { // Presence only, rather than [`Consumer::poll`]: this stays ready for the rest // of the session, and every wakeup of every per-source task would otherwise @@ -234,6 +239,7 @@ impl Default for GoingAway { /// The halves handed to a protocol driver's `start()`. #[derive(Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Protocol { /// Awaited by the driver's send path; firing means "encode and send GOAWAY now". trigger: kio::Consumer>, @@ -252,6 +258,7 @@ impl Protocol { /// require the session be closed over (draft-19 sect 10.4). Keeping the first /// payload instead would leave an observer acting on a URI the peer has since /// replaced, with no way to tell. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn record(&self, goaway: Goaway) -> Result<()> { if !self.going_away.set() { return Err(Error::ProtocolViolation); @@ -266,6 +273,7 @@ impl Protocol { /// /// `None` if the trigger was dropped without firing (the session is closing /// without a drain), so nothing should be sent. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn poll_triggered(&self, waiter: &kio::Waiter) -> Poll> { match self.trigger.poll(waiter, |state| match &**state { Some(goaway) => Poll::Ready(goaway.clone()), @@ -283,6 +291,7 @@ impl Protocol { /// The draft makes the deadline the sender's promise, not the peer's, so the /// driver arms this after the message hits the wire rather than making the /// caller hold a handle and await it. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Enforce { session: S, /// The armed deadline, or `None` when the GOAWAY carried no timeout (ready @@ -291,6 +300,7 @@ pub(crate) struct Enforce { } impl Enforce { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn new(runtime: &crate::time::Clock, session: S, timeout: Option) -> Self { Self { session, @@ -299,6 +309,7 @@ impl Enforce { } /// Ready once the session closes on its own or the deadline passes (closing it). + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> { let Some((deadline, timeout)) = &mut self.deadline else { return Poll::Ready(()); @@ -320,6 +331,7 @@ impl Enforce { /// Enforce a sent GOAWAY's deadline: close the session once it passes. /// /// The async form of [`Enforce`], for drivers that are still `async` themselves. +#[cfg(feature = "async")] pub(crate) async fn enforce( runtime: &crate::time::Clock, session: &mut S, @@ -330,12 +342,13 @@ pub(crate) async fn enforce( } /// The halves held by the public [`crate::Session`]. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Handle { - /// Cloned into each [`Producer`] handed out by [`crate::Session::drain`]. + /// Cloned into each [`Producer`] handed out by `crate::Session::drain`. trigger: kio::Producer>, /// Whether this side may name a redirect URI in its own GOAWAY. redirect: bool, - /// Handed out by [`crate::Session::draining`]. + /// Handed out by `crate::Session::draining`. consumer: Consumer, } @@ -343,6 +356,7 @@ impl Handle { /// Create the session-side handle and its protocol-side counterpart. /// /// `redirect` is whether this side may name a URI in its own GOAWAY. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn new(redirect: bool) -> (Self, Protocol) { let trigger = kio::Producer::new(None); let received = kio::Producer::new(None); @@ -371,6 +385,7 @@ impl Handle { /// A send half. Handing one out is always allowed; sending twice is what /// [`Producer::send`] refuses, so a caller never has to ask whether the slot /// is still free before deciding how to shut down. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn producer(&self) -> Producer { Producer { trigger: self.trigger.clone(), @@ -379,6 +394,7 @@ impl Handle { } /// A fresh handle on the receive half. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn consumer(&self) -> Consumer { self.consumer.clone() } @@ -474,6 +490,7 @@ mod tests { /// The consumer reports the recorded GOAWAY both synchronously and by polling. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn consumer_observes_the_recorded_goaway() { let (handle, protocol) = Handle::new(true); let consumer = handle.consumer(); @@ -492,6 +509,7 @@ mod tests { /// A session that closes without a GOAWAY resolves `recv` rather than parking /// forever, so a caller watching for one can stop. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn recv_resolves_when_the_session_closes() { let (handle, protocol) = Handle::new(true); let consumer = handle.consumer(); diff --git a/rs/moq-net/src/ietf/auth.rs b/rs/moq-net/src/ietf/auth.rs index f24452f7c3d..53ae67d2c59 100644 --- a/rs/moq-net/src/ietf/auth.rs +++ b/rs/moq-net/src/ietf/auth.rs @@ -14,11 +14,15 @@ use std::time::Duration; use bytes::Bytes; use crate::auth::{Grant, Handle, Issue, Reply, Request, Serving}; -use crate::coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder, Stream}; +use crate::coding::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder}; use crate::{Error, Path, Pattern, Patterns, SessionError}; +#[cfg(feature = "async")] +use super::Control; use super::namespace::{decode_namespace, encode_namespace}; -use super::{Control, Message, RequestId, Version, cluster, peer}; +use super::{Message, RequestId, Version, cluster, peer}; +#[cfg(feature = "async")] +use crate::coding::Stream; /// AUTH Setup Option: the sender speaks this extension. Even, so the value is a bare /// varint. @@ -228,6 +232,7 @@ pub(super) fn peer_grant( /// Present every token this side adds, one AUTH request each, once the peer's SETUP /// says it negotiated the extension. Never returns: a session without it just fails /// the tokens as unsupported. +#[cfg(feature = "async")] pub(super) async fn run_present( runtime: crate::time::Clock, session: S, @@ -293,6 +298,7 @@ where } /// One token's Auth request. +#[cfg(feature = "async")] struct Present { runtime: crate::time::Clock, session: S, @@ -302,8 +308,10 @@ struct Present { id: u64, } +#[cfg(feature = "async")] impl Present { /// Send the token, then track its grant until either side ends it, resolving with why. + #[cfg(feature = "async")] async fn run(&self, token: Bytes) -> Error { let request_id = match self.control.next_request_id(&self.runtime).await { Ok(id) => id, @@ -400,6 +408,7 @@ fn decode_reply(body: &super::Body, version: Version) -> Result( stream: &mut Stream, ) -> Result, Error> { @@ -422,6 +431,7 @@ pub(super) struct Serve { impl Serve { /// Answer one Auth request. The stream lives as long as the token. + #[cfg(feature = "async")] pub(super) async fn run( self, mut stream: Stream, @@ -474,6 +484,7 @@ impl Serve { /// Write the acceptor's replies until either side ends the token. `default` feeds /// the default acceptor's grant into the outbox as it changes. +#[cfg(feature = "async")] async fn serve_issue( runtime: &crate::time::Clock, issue: &kio::Shared, diff --git a/rs/moq-net/src/ietf/mod.rs b/rs/moq-net/src/ietf/mod.rs index 50f2fcdce83..aaf83a76562 100644 --- a/rs/moq-net/src/ietf/mod.rs +++ b/rs/moq-net/src/ietf/mod.rs @@ -4,12 +4,17 @@ //! //! You should not use this module directly; see [crate] for the high-level API. +// Private codec entry points remain testable when their session callers are absent. +#![cfg_attr(not(feature = "async"), allow(dead_code, unused_imports))] + #[macro_use] mod parameters; pub mod active_count; +#[cfg(feature = "async")] mod adapter; pub(crate) mod auth; pub mod cluster; +#[cfg(feature = "async")] mod control; mod datagram; pub(crate) mod error; @@ -26,19 +31,25 @@ mod priority; mod properties; mod publish; mod publish_namespace; +#[cfg(feature = "async")] mod publisher; mod request; +#[cfg(feature = "async")] mod request_stream; +#[cfg(feature = "async")] mod session; pub mod solicit; mod subscribe; mod subscribe_namespace; +#[cfg(feature = "async")] mod subscriber; pub(crate) mod token; mod track; mod version; +#[cfg(feature = "async")] use control::Control; +#[cfg(feature = "async")] pub(crate) use control::initial_max_request_id; pub use datagram::*; pub use fetch::*; @@ -51,16 +62,19 @@ pub use parameters::*; pub use properties::Properties; pub use publish::*; pub use publish_namespace::*; +#[cfg(feature = "async")] use publisher::*; pub use request::*; +#[cfg(feature = "async")] pub use session::*; pub use subscribe::*; pub use subscribe_namespace::*; // includes PublishBlocked +#[cfg(feature = "async")] use subscriber::*; pub use track::*; pub use version::Version; -#[cfg(any(test, feature = "fuzz"))] +#[cfg(all(feature = "async", any(test, feature = "fuzz")))] pub(crate) fn request_ids(window: u64, ids: &[u64]) -> usize { let control = Control::new(None, false).with_window(window, false, Version::Draft16); let held: Vec<_> = ids @@ -70,7 +84,7 @@ pub(crate) fn request_ids(window: u64, ids: &[u64]) -> usize { held.len() } -#[cfg(any(test, feature = "fuzz"))] +#[cfg(all(feature = "async", any(test, feature = "fuzz")))] pub(crate) fn request_history(ids: &[u64]) -> usize { let control = Control::new(None, false); ids.iter().filter(|id| control.accept(RequestId(**id)).is_ok()).count() diff --git a/rs/moq-net/src/ietf/peer.rs b/rs/moq-net/src/ietf/peer.rs index c85f53e70b2..81b751f0515 100644 --- a/rs/moq-net/src/ietf/peer.rs +++ b/rs/moq-net/src/ietf/peer.rs @@ -57,6 +57,7 @@ impl PeerSetup { /// The peer MUST send exactly one, so this resolves once that stream is read. Waits /// forever if it never does; the caller is a session task, cancelled when the driver /// drops. + #[cfg(feature = "async")] pub async fn get(&self) -> Peer { let slot = self .0 @@ -69,7 +70,7 @@ impl PeerSetup { } } -#[cfg(test)] +#[cfg(all(test, feature = "async"))] mod tests { use super::*; diff --git a/rs/moq-net/src/ietf/token.rs b/rs/moq-net/src/ietf/token.rs index 615508d279f..9dd600cb312 100644 --- a/rs/moq-net/src/ietf/token.rs +++ b/rs/moq-net/src/ietf/token.rs @@ -5,9 +5,12 @@ //! type selects. We advertise no `MAX_AUTH_TOKEN_CACHE_SIZE`, so its default of 0 means //! no alias is ever registered and every token arrives by value. +#[cfg(feature = "async")] +use crate::coding::Stream; + use crate::{ DecodeError, Error, SessionError, - coding::{Decode as _, Decoder, EncodeError, Encoder, Stream}, + coding::{Decode as _, Decoder, EncodeError, Encoder}, setup::Token, }; @@ -76,6 +79,7 @@ pub fn unique(tokens: &[RequestToken]) -> Result<(), DecodeError> { /// /// Section 8.9 rejects the message rather than the session. Every request starts with its /// Request ID, so it is read from the undecoded `body`. +#[cfg(feature = "async")] pub(super) async fn reject( mut stream: Stream, kind: request::Kind, diff --git a/rs/moq-net/src/lib.rs b/rs/moq-net/src/lib.rs index c57d0cd9f52..63ff8b119ce 100644 --- a/rs/moq-net/src/lib.rs +++ b/rs/moq-net/src/lib.rs @@ -5,9 +5,17 @@ //! At session setup it negotiates one of two wire protocols: the simplified `moq-lite` //! protocol (the default) or the full IETF `moq-transport` protocol. //! +//! ## Features +//! The default `async` feature enables async helpers, the announcement `Stream` adapter, +//! `time::run`, and +//! the Lite/IETF sessions (`Client`, `Server`, `Session`, and the session `Driver`). +//! With `default-features = false`, use the model's `poll_*` methods and +//! [`origin::Driver`] with explicit time. Origin serving stays available without +//! a native runtime; no executor or clock is chosen by the model. +//! //! ## API //! The API is built around Producer/Consumer pairs, with the hierarchy: -//! - [origin::Consumer]: A collection of [broadcast::Consumer]s, produced by one or more [Session]s. +//! - [origin::Consumer]: A collection of [broadcast::Consumer]s, produced by one or more sessions. //! - [broadcast::Consumer]: A collection of [track::Consumer]s, produced by a single publisher. //! - [track::Consumer]: A collection of [group::Info]s, delivered out-of-order until expired. //! - [group::Info]: A collection of [frame::Info]s, delivered in order until cancelled. @@ -17,8 +25,8 @@ //! `announce`) that owns the short `Producer` / `Consumer` / `Info` names. //! //! Traffic counters for the levels above live in [`stats`]: build a [`stats::Registry`] -//! and hand each session a [`stats::Session`] via [`Client::with_stats`] / -//! [`Server::with_stats`]. Publishing the counters as MoQ broadcasts lives in the +//! and hand each session a [`stats::Session`] via `Client::with_stats` / +//! `Server::with_stats`. Publishing the counters as MoQ broadcasts lives in the //! `moq-stats` crate. //! //! ## Compatibility @@ -48,12 +56,12 @@ //! //! ## Driving and time //! Session and origin drivers never spawn tasks or read the clock. -//! Explicitly minting an [`Epoch`] uses the wall clock and secure randomness. [`Client::connect`] and -//! [`Server::accept`] take an initial [`time::Instant`] and return -//! `(Session, Driver)`. Poll the [`Driver`] with the current instant and a +//! Explicitly minting an [`Epoch`] uses the wall clock and secure randomness. `Client::connect` and +//! `Server::accept` take an initial [`time::Instant`] and return +//! `(Session, Driver)`. Poll the `Driver` with the current instant and a //! [`kio::Waiter`], then wake on external activity or at the deadline it -//! returns; [`time::run`] does exactly that on tokio or the browser. The last -//! [`Session`] drop requests closure; dropping the driver cancels it. +//! returns; `time::run` does exactly that on tokio or the browser. The last +//! `Session` drop requests closure; dropping the driver cancels it. //! //! [`origin::Producer::new`] also returns a producer and driver. Its driver runs //! route changes, serving, linger, teardown, and the origin's cache expiration. @@ -74,38 +82,47 @@ #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] pub mod auth; +#[cfg(feature = "async")] mod client; mod coding; +#[cfg(feature = "async")] mod driver; mod epoch; mod error; pub mod goaway; // Not part of the public API: compiled only for the crate's own tests and for the // `fuzz/` harness. -#[cfg(any(test, feature = "fuzz"))] +#[cfg(all(feature = "async", any(test, feature = "fuzz")))] #[doc(hidden)] pub mod fuzz; mod ietf; mod lite; mod model; pub mod path; +#[cfg(feature = "async")] mod priority; +#[cfg(feature = "async")] mod recv; pub mod setup; +#[cfg(feature = "async")] mod tail; -#[cfg(test)] +#[cfg(all(test, feature = "async"))] mod test_interop; mod util; mod version; +#[cfg(feature = "async")] pub mod server; +#[cfg(feature = "async")] pub mod session; pub mod stats; pub mod time; pub mod transport; +#[cfg(feature = "async")] pub use client::*; pub use coding::{BoundsExceeded, DecodeError, EncodeError, varint}; +#[cfg(feature = "async")] pub use driver::Driver; pub use epoch::{Epoch, InvalidEpoch}; pub use error::*; @@ -113,7 +130,9 @@ pub use error::*; pub use lite::Role; pub use model::*; pub use path::{AsPath, InvalidPattern, Path, PathOwned, Pattern, Patterns}; +#[cfg(feature = "async")] pub use server::Server; +#[cfg(feature = "async")] pub use session::Session; pub use version::*; diff --git a/rs/moq-net/src/lite/compress.rs b/rs/moq-net/src/lite/compress.rs index 42ddfb14dfe..f640660c320 100644 --- a/rs/moq-net/src/lite/compress.rs +++ b/rs/moq-net/src/lite/compress.rs @@ -624,6 +624,7 @@ mod tests { /// decodes the same hex, so a codec change on either side breaks both. const GOLDEN: &str = "001600000a726f6f6d2f612f63616d0000029111a2220000000d0102036d6963000101b33301000208000101c044440100010101000c02010162000201c055550100"; + #[cfg(feature = "async")] fn golden() -> Vec { use crate::fuzz::Announced; vec![ @@ -636,6 +637,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn golden_stream_is_pinned() { let encoded = crate::fuzz::encode_announces(&golden(), true); let hex: String = encoded.iter().map(|b| format!("{b:02x}")).collect(); @@ -648,6 +650,7 @@ mod tests { const JS_LITERAL: &str = "001600000a726f6f6d2f612f63616d0000029111a2220000001600000a726f6f6d2f612f6d6963000002b333a2220000020a000002c04444a22200000101010013000006726f6f6d2f62000002c05555a2220000"; #[test] + #[cfg(feature = "async")] fn js_literal_stream_decodes() { let bytes: Vec = (0..JS_LITERAL.len()) .step_by(2) diff --git a/rs/moq-net/src/lite/mod.rs b/rs/moq-net/src/lite/mod.rs index b4cad690160..afb18fdbc8e 100644 --- a/rs/moq-net/src/lite/mod.rs +++ b/rs/moq-net/src/lite/mod.rs @@ -4,6 +4,9 @@ //! //! Specification: [] +// Private codec entry points remain testable when their session callers are absent. +#![cfg_attr(not(feature = "async"), allow(dead_code, unused_imports))] + mod announce; mod auth; mod compress; @@ -17,13 +20,16 @@ mod info; pub mod message; mod parameters; mod probe; +#[cfg(feature = "async")] mod publisher; +#[cfg(feature = "async")] mod session; mod setup; mod stream; mod subscribe; +#[cfg(feature = "async")] mod subscriber; -#[cfg(test)] +#[cfg(all(test, feature = "async"))] pub(crate) mod test_transport; mod track; mod version; @@ -42,12 +48,15 @@ pub use info::*; pub use message::Message; pub use parameters::*; pub use probe::*; +#[cfg(feature = "async")] use publisher::*; +#[cfg(feature = "async")] pub(super) use session::*; #[allow(unused_imports)] pub use setup::*; pub use stream::*; pub use subscribe::*; +#[cfg(feature = "async")] use subscriber::*; #[allow(unused_imports)] pub use track::*; diff --git a/rs/moq-net/src/lite/setup.rs b/rs/moq-net/src/lite/setup.rs index a9d566c27e8..fc34f9467f9 100644 --- a/rs/moq-net/src/lite/setup.rs +++ b/rs/moq-net/src/lite/setup.rs @@ -341,6 +341,7 @@ mod tests { /// draft makes such a publisher reset any Probe Stream a subscriber opens. It /// must advertise `None` so the subscriber never opens one. #[test] + #[cfg(feature = "async")] fn detect_reports_nothing_without_stats() { use crate::lite::test_transport::{SinkSession, SinkStats}; let session = SinkSession::new(Default::default()).with_stats(SinkStats::default()); @@ -350,6 +351,7 @@ mod tests { /// Either metric alone is enough to report, since the two PROBE fields are /// independent on the wire. #[test] + #[cfg(feature = "async")] fn detect_reports_with_either_metric() { use crate::lite::test_transport::{SinkSession, SinkStats}; diff --git a/rs/moq-net/src/model/bandwidth.rs b/rs/moq-net/src/model/bandwidth.rs index d3f2782a781..72f1b600304 100644 --- a/rs/moq-net/src/model/bandwidth.rs +++ b/rs/moq-net/src/model/bandwidth.rs @@ -105,6 +105,7 @@ impl Producer { } /// A handle that mints consumers without being one, so holding it is not demand. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn weak(&self) -> Weak { Weak(self.state.weak()) } @@ -118,12 +119,14 @@ impl Producer { } /// Block until the channel is closed, returning the cause. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { self.state.closed().await; self.close_error() } /// Block until there are no active consumers. + #[cfg(feature = "async")] pub async fn unused(&self) -> Result<()> { kio::wait(|waiter| self.poll_unused(waiter)).await } @@ -139,6 +142,7 @@ impl Producer { } /// Block until there is at least one active consumer. + #[cfg(feature = "async")] pub async fn used(&self) -> Result<()> { kio::wait(|waiter| self.poll_used(waiter)).await } @@ -168,10 +172,12 @@ impl Default for Producer { /// Mints [`Consumer`]s on demand without counting as one, nor keeping the channel open. #[derive(Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Weak(kio::ProducerWeak); impl Weak { /// A new consumer, which reports the closure once the producer is gone. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn consume(&self) -> Consumer { Consumer { inner: Inner::Whole(self.0.consume()), @@ -201,7 +207,7 @@ pub struct Allocator { impl Allocator { /// Divide `estimate`, normally a connection's - /// [`Session::send_bandwidth`](crate::Session::send_bandwidth). + /// `Session::send_bandwidth`. pub fn new(estimate: Consumer) -> Self { Self { estimate, @@ -313,7 +319,7 @@ pub struct Reservation { impl Reservation { /// This reservation's slice right now. /// - /// Stateless, unlike [`Consumer::changed`], which carries a cursor over what it last + /// Stateless, unlike [`Consumer::poll_changed`], which carries a cursor over what it last /// reported and so needs a handle of its own. pub fn peek(&self) -> Option { self.share.grant() @@ -620,6 +626,7 @@ impl Consumer { /// /// Returns an error once the producer is closed or dropped, so a caller can /// stop watching. See [`poll_changed`](Self::poll_changed). + #[cfg(feature = "async")] pub async fn changed(&mut self) -> Result> { kio::wait(|waiter| self.poll_changed(waiter)).await } @@ -730,6 +737,7 @@ mod tests { /// The headline case: two encoders on one connection must not each target the /// whole estimate. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn concurrent_tracks_split_the_estimate() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -782,6 +790,7 @@ mod tests { /// Demand transitions have to wake a parked share, not just change what a later /// `peek` would see; the encoder is sitting in a `select!` on `changed`. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_share_wakes_when_a_sibling_goes_idle() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -807,8 +816,10 @@ mod tests { /// fresh `poll` can't tell you: a lost wakeup still looks correct on the next poll /// and only shows up as a task that never runs again. #[derive(Default)] + #[cfg(feature = "async")] struct Woken(std::sync::atomic::AtomicBool); + #[cfg(feature = "async")] impl std::task::Wake for Woken { fn wake(self: std::sync::Arc) { self.wake_by_ref(); @@ -819,6 +830,7 @@ mod tests { } } + #[cfg(feature = "async")] impl Woken { /// A flag and its waiter. Both are returned because a [`kio::WaiterList`] /// holds only a `Weak`, so a waiter dropped at the end of the calling @@ -839,6 +851,7 @@ mod tests { /// it, and a poll that returned the value without arming anything would park the /// encoder with nothing left to wake it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_unchanged_slice_keeps_watching_the_estimate() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -865,6 +878,7 @@ mod tests { /// The same, for the other input: a sibling's demand. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_parked_share_is_woken_by_sibling_demand() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -890,6 +904,7 @@ mod tests { /// A share follows the estimate's own lifecycle: unavailable while disconnected /// (hold the current rate), terminal once the session is gone for good. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_share_follows_the_estimate_lifecycle() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -933,6 +948,7 @@ mod tests { /// tracks that have *closed*, so a claim left behind by a track that is still /// publishing would stand for the rest of the connection. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dropping_a_reservation_releases_it() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -999,6 +1015,7 @@ mod tests { /// A reader parked on `changed` has to be woken by its own reservation moving, not /// just by the estimate or a sibling: the encoder is sitting in a `select!` on it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn update_wakes_a_parked_reader() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -1021,6 +1038,7 @@ mod tests { /// The registry is the allocator's, not a share's: a share that outlives every /// allocator reports the estimate as gone rather than keeping the channel open. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_share_outliving_the_allocator_reports_closed() { let estimate = Producer::new(); let allocator = Allocator::new(estimate.consume()); @@ -1040,6 +1058,7 @@ mod tests { /// Reporting closure as `Ok(None)` would spin any `select!` over `changed()`, /// because a closed channel is always immediately ready. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn closed_is_distinct_from_unavailable() { let producer = Producer::new(); let mut consumer = producer.consume(); diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index 1486de7607c..04a8c7cd788 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -323,7 +323,7 @@ impl Producer { /// [`track::Request::accept`] to set its info and start producing. Use this when /// the producer can't pick the track's properties (e.g. timescale) until it has /// inspected the media, the same shape as a consumer-driven - /// [`Dynamic::requested_track`]. + /// [`Dynamic::poll_requested_track`]. /// /// Subscribers wait on the name until it is accepted, so a reservation the producer /// ends up never filling has to be dropped or rejected. Ending the broadcast @@ -529,6 +529,7 @@ impl Drop for Alive { /// Whether a path request still names the source that handed out its track. #[derive(Clone, Default)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) enum RequestValidity { #[default] Unrestricted, @@ -539,10 +540,12 @@ pub(crate) enum RequestValidity { } impl RequestValidity { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn token(route: Option>) -> Self { route.map(Self::Route).unwrap_or_default() } + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn for_source(route: &crate::origin::Dynamic, path: &crate::Path) -> Self { route .request_validity(path) @@ -551,6 +554,7 @@ impl RequestValidity { } /// Capture one selection period so a pending request never revives after a replacement. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn start(&self) -> Self { match self { Self::Selected(selected) => Self::Period(selected.current(), selected.clone()), @@ -558,6 +562,7 @@ impl RequestValidity { } } + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn check(&self, waiter: &kio::Waiter) -> Result<(), crate::Error> { match self { Self::Route(route) if route.poll_closed(waiter).is_ready() => Err(crate::Error::Unroutable), @@ -572,6 +577,7 @@ impl RequestValidity { } } + #[cfg(feature = "async")] pub(crate) async fn run( &self, future: impl std::future::Future>, @@ -604,9 +610,11 @@ impl Producer { /// /// A peer's retraction and a dead session end the source the same way: a broadcast /// carries no end cause past this hop. Shared by the lite and IETF subscribers. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct SourceGuard(Producer); impl SourceGuard { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn new(producer: Producer) -> Self { Self(producer) } @@ -629,7 +637,7 @@ impl Drop for SourceGuard { /// Handles on-demand track creation for a broadcast. /// /// When a consumer requests a track that doesn't exist, the dynamic producer -/// picks up the request via [`Self::requested_track`] and either +/// picks up the request via [`Self::poll_requested_track`] and either /// [`track::Request::accept`]s it with a concrete [`track::Info`] or /// [`track::Request::reject`]s it. Dropped when no longer needed; pending requests /// are automatically aborted. @@ -724,6 +732,7 @@ impl Dynamic { } /// Block until a consumer requests a track, returning a [`track::Request`] to serve. + #[cfg(feature = "async")] pub async fn requested_track(&mut self) -> Result { kio::wait(|waiter| self.poll_requested_track(waiter)).await } @@ -739,11 +748,12 @@ impl Dynamic { } /// Block until the broadcast ends, by [`Producer::close`] or every producer dropping. + #[cfg(feature = "async")] pub async fn closed(&self) { kio::wait(|waiter| self.poll_closed(waiter)).await } - /// Poll-based variant of [`Self::closed`]. + /// Poll until the producer closes. pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> { self.alive.token.poll_closed(waiter) } @@ -761,14 +771,19 @@ use futures::FutureExt; #[allow(missing_docs)] // test-only assertion helpers impl Dynamic { pub fn assert_request(&mut self) -> track::Request { - self.requested_track() + kio::wait(|waiter| self.poll_requested_track(waiter)) .now_or_never() .expect("should not have blocked") .expect("should not have errored") } pub fn assert_no_request(&mut self) { - assert!(self.requested_track().now_or_never().is_none(), "should have blocked"); + assert!( + kio::wait(|waiter| self.poll_requested_track(waiter)) + .now_or_never() + .is_none(), + "should have blocked" + ); } } @@ -902,7 +917,7 @@ impl Consumer { /// only a read handle. Holding this handle, or the [`Consumer`] it came from, /// is not itself demand. /// - /// Demand going away is [`Demand::unused`] resolving. The broadcast going + /// Demand going away is [`Demand::poll_unused`] resolving. The broadcast going /// away is [`Error::Dropped`], which here means the upstream producer ended. pub fn demand(&self) -> Demand { Demand { @@ -912,6 +927,7 @@ impl Consumer { } /// Block until the broadcast ends, by [`Producer::close`] or every producer dropping. + #[cfg(feature = "async")] pub async fn closed(&self) { self.alive.closed().await } @@ -998,8 +1014,8 @@ impl super::WeakEntry for WeakConsumer { /// uses it to run expensive work only while someone is watching. /// /// It's a weak handle: it neither keeps the broadcast alive nor counts as -/// demand itself. Once every producer is gone, [`used`](Self::used) / -/// [`unused`](Self::unused) return [`Error::Dropped`]. +/// demand itself. Once every producer is gone, [`poll_used`](Self::poll_used) / +/// [`poll_unused`](Self::poll_unused) return [`Error::Dropped`]. #[derive(Clone)] pub struct Demand { alive: kio::ConsumerWeak<()>, @@ -1009,30 +1025,32 @@ pub struct Demand { impl Demand { /// Whether the broadcast has live demand right now. /// - /// A point-in-time snapshot with no registration; use [`Self::used`] / - /// [`Self::unused`] (or their `poll_*` forms) to wait for the edge. + /// A point-in-time snapshot with no registration; use [`Self::poll_used`] / + /// [`Self::poll_unused`] (or their `poll_*` forms) to wait for the edge. pub fn is_used(&self) -> bool { self.state.read().is_used() } /// Block until the broadcast has demand. Resolves immediately if it already /// does; returns [`Error::Dropped`] once every producer is gone. + #[cfg(feature = "async")] pub async fn used(&self) -> Result<(), Error> { kio::wait(|waiter| self.poll_used(waiter)).await } /// Block until the broadcast has no demand. Resolves immediately if it has /// none; returns [`Error::Dropped`] once every producer is gone. + #[cfg(feature = "async")] pub async fn unused(&self) -> Result<(), Error> { kio::wait(|waiter| self.poll_unused(waiter)).await } - /// Poll-based variant of [`Self::used`]. + /// Poll until a consumer is using this resource. pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll> { self.poll_demand(waiter, true) } - /// Poll-based variant of [`Self::unused`]. + /// Poll until no consumer is using this resource. pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll> { self.poll_demand(waiter, false) } @@ -1055,11 +1073,17 @@ impl Demand { #[allow(missing_docs)] // test-only assertion helpers impl Consumer { pub fn assert_not_closed(&self) { - assert!(self.closed().now_or_never().is_none(), "should not be closed"); + assert!( + kio::wait(|waiter| self.poll_closed(waiter)).now_or_never().is_none(), + "should not be closed" + ); } pub fn assert_closed(&self) { - assert!(self.closed().now_or_never().is_some(), "should be closed"); + assert!( + kio::wait(|waiter| self.poll_closed(waiter)).now_or_never().is_some(), + "should be closed" + ); } /// The tracks still open on the broadcast, whether or not anyone reads them. @@ -1077,6 +1101,7 @@ impl Consumer { #[cfg(test)] mod test { use super::*; + #[cfg(feature = "async")] use std::time::Duration; #[test] @@ -1133,6 +1158,7 @@ mod test { /// Await with a timeout so a missed demand wake fails the test instead of /// hanging it (time is paused, so the timeout fires instantly when idle). + #[cfg(feature = "async")] async fn expect(fut: impl Future) -> T { moq_net_sim::timeout(Duration::from_secs(1), fut) .await @@ -1143,6 +1169,7 @@ mod test { /// production: a live track producer alone is unused, a consumed track is /// used, and both edges wake parked waiters. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn demand_ordinary() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1175,6 +1202,7 @@ mod test { /// One read track keeps the broadcast in demand however many others are unread, and /// its last reader leaving wakes a parked `unused`. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn demand_unused_waits_for_every_track() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1194,6 +1222,7 @@ mod test { /// A consumer demand handle distinguishes lost demand from a dropped producer. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn consumer_demand_reports_dropped_producer() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1225,6 +1254,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn insert() { let mut producer = Info::new().produce(); @@ -1250,6 +1280,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn closed() { let mut producer = Info::new().produce(); let dynamic = producer.dynamic(); @@ -1278,6 +1309,7 @@ mod test { /// `close()` ends the broadcast for every clone at once, and a second close is a no-op. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_ends_every_clone() { let producer = Info::new().produce(); let clone = producer.clone(); @@ -1294,6 +1326,7 @@ mod test { /// Dropping the last producer ends the broadcast exactly like `close()`. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn drop_ends_like_close() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1303,6 +1336,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn requests() { let mut producer = Info::new().produce().dynamic(); @@ -1343,6 +1377,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn stale_producer() { let mut broadcast = Info::new().produce().dynamic(); let consumer = broadcast.consume(); @@ -1442,6 +1477,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn requested_unused() { let mut broadcast = Info::new().produce().dynamic(); let bc = broadcast.consume(); @@ -1501,6 +1537,7 @@ mod test { /// the name exists the moment the track does, so the queue entry could /// never be served under it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn create_track_fulfills_queued_request() { let producer = Info::new().produce(); let mut dynamic = producer.dynamic(); @@ -1571,6 +1608,7 @@ mod test { /// `Unroutable` for a name asked about after this point, so whoever asked earlier gets /// the same answer instead of waiting on info that can never arrive. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_resolves_a_reserved_name() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1585,6 +1623,7 @@ mod test { /// A request still queued for a handler is the same parking case reached from the /// consumer side, so it ends the same way. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_resolves_a_queued_request() { let producer = Info::new().produce(); let dynamic = producer.dynamic(); @@ -1600,6 +1639,7 @@ mod test { /// A queued request answers the same when the broadcast ends by its last handle /// dropping, even when that handle is the `Dynamic` that would have served it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dropping_the_last_handle_resolves_a_queued_request() { let dynamic = Info::new().produce().dynamic(); let consumer = dynamic.consume(); @@ -1613,6 +1653,7 @@ mod test { /// With a producer still alive, losing the last handler is not the broadcast ending: /// the queued request fails as `Dropped`. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dropping_the_last_handler_resolves_a_queued_request_dropped() { let producer = Info::new().produce(); let dynamic = producer.dynamic(); @@ -1629,6 +1670,7 @@ mod test { /// to a peer, and a retraction does not disturb subscriptions already in flight. /// Whatever the handler decides still reaches the consumer. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_leaves_a_claimed_request_to_its_handler() { let producer = Info::new().produce(); let mut dynamic = producer.dynamic(); @@ -1661,6 +1703,7 @@ mod test { /// must still reject that name so an arrival-order subscriber does not park on /// backfill that is deliberately absent from its queue. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_resolves_an_unaccepted_track_with_fetched_info() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1685,6 +1728,7 @@ mod test { /// Ending the broadcast doesn't cascade into a track someone is publishing: it keeps /// its cache and its publisher decides when it ends. Only a new lookup is refused. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_spares_a_served_track() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1704,6 +1748,7 @@ mod test { /// just gave up on. Accepting it afterwards must not resurrect the track, or a /// subscriber that was told `Unroutable` could be contradicted by a later one. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn close_leaves_a_stale_reservation_inert() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1725,6 +1770,7 @@ mod test { /// `Dropped` (a handler lost to a crashed publisher or a dead transport), never as /// `NotFound`. Only an explicit rejection may claim the track is absent. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dropping_a_reserved_request_resolves_dropped() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1740,6 +1786,7 @@ mod test { /// `track::Request::reject` carries its reason the same way, which is what lets a /// subscriber tell "no such track" from "the publisher went away". #[moq_net_sim::test] + #[cfg(feature = "async")] async fn rejecting_a_reserved_request_carries_the_reason() { let producer = Info::new().produce(); let consumer = producer.consume(); @@ -1758,6 +1805,7 @@ mod test { /// really does commit the cached handle is reclaimed rather than handed out /// cancelled. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_idle_teardown_yields_to_a_returning_viewer() { let producer = Info::new().produce(); let consumer = producer.consume(); diff --git a/rs/moq-net/src/model/follow.rs b/rs/moq-net/src/model/follow.rs index 879210e16be..21190d74503 100644 --- a/rs/moq-net/src/model/follow.rs +++ b/rs/moq-net/src/model/follow.rs @@ -51,6 +51,7 @@ impl Follow { } /// The next change to the route serving the path, or `None` once the origin closes. + #[cfg(feature = "async")] pub async fn next(&mut self) -> Option { kio::wait(|waiter| self.poll_next(waiter)).await } @@ -176,7 +177,7 @@ impl Follow { } } -#[cfg(test)] +#[cfg(all(test, feature = "async"))] mod tests { use std::time::Duration; diff --git a/rs/moq-net/src/model/frame.rs b/rs/moq-net/src/model/frame.rs index d792a5f119a..d4b6deeb89f 100644 --- a/rs/moq-net/src/model/frame.rs +++ b/rs/moq-net/src/model/frame.rs @@ -48,7 +48,7 @@ pub struct Frame { pub payload: Bytes, } -/// A reusable batch of frames, filled by [`group::Consumer::read_frames`] and drained +/// A reusable batch of frames, filled by [`group::Consumer::poll_read_frames`] and drained /// by [`group::Producer::write_frames`]. /// /// A fixed-capacity inline buffer: `N` frames of stack storage, never a heap @@ -139,17 +139,21 @@ impl Buffer { /// size fits what remains is allocated up front; past that, its buffer grows with the /// bytes received. Each frame returns its share once it completes or aborts. #[derive(Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Budget(Arc); impl Budget { /// Enough for a handful of large keyframes in flight at once. + #[cfg_attr(not(feature = "async"), allow(dead_code))] const DEFAULT: usize = 16 * 1024 * 1024; + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(bytes: usize) -> Self { Self(Arc::new(AtomicUsize::new(bytes))) } /// Take `size` bytes of the budget, or `None` if that is more than remains. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn reserve(&self, size: usize) -> Option { self.0 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |left| left.checked_sub(size)) @@ -168,6 +172,7 @@ impl Default for Budget { } /// A frame's share of a [`Budget`], returned on drop. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Reservation { budget: Budget, size: usize, @@ -271,6 +276,7 @@ impl FrameBuf { } /// A buffer for a frame of `size` bytes that allocates only as bytes arrive. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn growing(size: usize) -> Self { Self::with_growth(size, true) } @@ -548,6 +554,7 @@ impl<'a> Producer<'a> { /// frame on the group until this one is finished or aborted. /// /// Holds the frame's share of the session's [`Budget`], if it got one, until then. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct ProducerOwned { raw: Raw, _reserved: Option, @@ -562,6 +569,7 @@ impl std::ops::Deref for ProducerOwned { } impl ProducerOwned { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(group: group::Producer, buf: FrameBuf, info: Info, reserved: Option) -> Self { Self { raw: Raw { @@ -576,12 +584,14 @@ impl ProducerOwned { } /// Attach the parent group's ingress meter, so written chunks bump `bytes`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self { self.raw.stats = meter; self } /// Bytes still needed to complete the frame. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn remaining(&self) -> usize { self.raw.remaining() } @@ -594,21 +604,25 @@ impl ProducerOwned { /// to publish bytes nobody can observe yet. /// /// `coding::Reader::poll_read_frame` owns the pairing and is the only caller. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn write(&mut self, chunk: B) -> Result<()> { self.raw.write(chunk) } /// Publish what has been written so far, waking consumers parked on the group. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn notify(&self) { self.raw.group.frame_notify(); } /// Commit the frame, verifying that all bytes were written. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn finish(mut self) -> Result<()> { self.raw.finish() } /// Abort the frame (and its group) with the given error. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn abort(mut self, err: Error) -> Result<()> { self.raw.abort(err) } @@ -877,6 +891,7 @@ impl Consumer { } /// Return the next chunk of bytes since the last read. + #[cfg(feature = "async")] pub async fn read_chunk(&mut self) -> Result> { kio::wait(|waiter| self.poll_read_chunk(waiter)).await } @@ -926,6 +941,7 @@ impl Consumer { } /// Return all remaining bytes, blocking until the frame is finished. + #[cfg(feature = "async")] pub async fn read_all(&mut self) -> Result { kio::wait(|waiter| self.poll_read_all(waiter)).await } diff --git a/rs/moq-net/src/model/group.rs b/rs/moq-net/src/model/group.rs index 11783daa92d..96f112b969a 100644 --- a/rs/moq-net/src/model/group.rs +++ b/rs/moq-net/src/model/group.rs @@ -704,6 +704,7 @@ impl Producer { /// /// The declared size is the peer's claim, so the buffer is allocated up front only /// while it fits the session's `budget`; otherwise it grows with the bytes received. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn create_frame_owned( &mut self, frame: frame::Info, @@ -866,6 +867,7 @@ impl Producer { /// Abort with `err` only while nothing consumes the group, returning whether it is /// closed. Consumer creation and the check share a lock, so a reader arriving after /// [`poll_unused`](Self::poll_unused) keeps the group alive instead of reading the abort. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn abort_unused(&self, err: Error) -> bool { match self.state.write_unused() { kio::Unused::Idle(guard) => { @@ -950,11 +952,13 @@ impl Producer { } /// Whether `other` is a handle to this same group. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn is_clone(&self, other: &Self) -> bool { self.state.same_channel(&other.state) } /// Whether the group opened its first frame, or ended without one. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn is_started(&self) -> bool { self.state.read().started() } @@ -1074,6 +1078,7 @@ impl Producer { } /// Block until the group is aborted, including by eviction. Finishing does not close it. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { kio::wait(|waiter| self.poll_closed(waiter)).await } @@ -1093,6 +1098,7 @@ impl Producer { } /// Poll for the group becoming unused (every consumer dropped). + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> { self.state.poll_unused(waiter).map(|_| ()) } @@ -1141,11 +1147,13 @@ impl Demand { } /// Wait until at least one reader needs the group. + #[cfg(feature = "async")] pub async fn used(&self) -> Result<()> { kio::wait(|waiter| self.poll_used(waiter)).await } /// Wait until no reader needs the group. + #[cfg(feature = "async")] pub async fn unused(&self) -> Result<()> { kio::wait(|waiter| self.poll_unused(waiter)).await } @@ -1169,6 +1177,7 @@ impl Demand { } /// Wait until the group or fetch closes, returning its cause. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { match &self.state { DemandSource::Group(state) => state.closed().await, @@ -1516,6 +1525,7 @@ impl Consumer { /// Whether this cursor failed because its subscription max delay budget expired. #[cfg(test)] + #[cfg(feature = "async")] pub(crate) fn latency_expired(&self) -> bool { self.expired } @@ -1610,6 +1620,7 @@ impl Consumer { } /// Return a consumer for the next frame for chunked reading. + #[cfg(feature = "async")] pub async fn next_frame(&mut self) -> Result> { kio::wait(|waiter| self.poll_next_frame(waiter)).await } @@ -1676,6 +1687,7 @@ impl Consumer { } /// Read the next frame (timestamp and payload) all at once. + #[cfg(feature = "async")] pub async fn read_frame(&mut self) -> Result> { // A prefetched frame is already buffered, so the drift budget (which only judges // a read that would park) can never apply to it. @@ -1726,6 +1738,7 @@ impl Consumer { /// Fill `out` with every frame that is ready, blocking until a frame arrives or /// the group ends. Returns the current batch, empty only at the end of the group. + #[cfg(feature = "async")] pub async fn read_frames<'a, const N: usize>( &mut self, out: &'a mut frame::Buffer, @@ -1771,6 +1784,7 @@ impl Consumer { /// clean end even if the group was aborted afterwards to release its cache, while one that /// stopped short gets that abort. A prior [`Self::skip_to`] contributes to the index even /// though those frames were not read. Use [`Self::frame_count`] for the producer's total. + #[cfg(feature = "async")] pub async fn finished(&mut self) -> Result { kio::wait(|waiter| self.poll_finished(waiter)).await } @@ -1984,7 +1998,7 @@ impl Fetch { } /// A consumer's request for a single past group, handed to a handler via -/// [`track::Dynamic::requested_group`]. +/// [`track::Dynamic::poll_requested_group`]. /// /// The handler fulfills it by calling [`Self::accept`], which inserts the group /// into the track cache (resolving every [`track::Consumer::fetch_group`] that joined the @@ -2008,9 +2022,11 @@ mod test { use super::*; use crate::model::test_tracing::count_drop_warnings; use bytes::Bytes; + #[cfg(feature = "async")] use futures::FutureExt; #[test] + #[cfg(feature = "async")] fn demand_watches_readers_without_keeping_the_group_alive() { let producer = Info { sequence: 7 }.produce(); let demand = producer.demand(); @@ -2089,6 +2105,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn basic_frame_reading() { let mut producer = Info { sequence: 0 }.produce(); producer @@ -2109,6 +2126,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn read_frame_all_at_once() { let mut producer = Info { sequence: 0 }.produce(); producer @@ -2122,6 +2140,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn read_frame_preserves_timestamp() { let mut producer = Info { sequence: 0 }.produce(); let timestamp = Timestamp::from_micros(20_000).unwrap(); @@ -2137,6 +2156,7 @@ mod test { /// An untimed frame reads back untimed, and still counts as the group's first frame: /// an empty group and a group on an untimed track are different answers. #[test] + #[cfg(feature = "async")] fn an_untimed_frame_still_starts_the_group() { let mut producer = Info { sequence: 0 }.produce_untimed(); let waiter = kio::Waiter::noop(); @@ -2179,6 +2199,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn chunked_frame_reads_whole() { let mut producer = Info { sequence: 0 }.produce(); { @@ -2202,6 +2223,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn chunked_frame_streams_partial() { let mut producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2230,6 +2252,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn group_finish_returns_none() { let producer = Info { sequence: 0 }.produce(); producer.finish().unwrap(); @@ -2240,6 +2263,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn abort_propagates() { let producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2283,6 +2307,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn drop_unfinished_clears_cached_frames() { let producer = Info { sequence: 0 }.produce(); let mut writer = producer.clone(); @@ -2334,6 +2359,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn drop_finished_keeps_cached_frames() { let mut producer = Info { sequence: 0 }.produce(); producer @@ -2350,6 +2376,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn pending_then_ready() { let mut producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2367,6 +2394,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn overflow_aborts_the_group() { let mut producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2418,6 +2446,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn clone_consumer_independent() { let mut producer = Info { sequence: 0 }.produce(); producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap(); @@ -2440,6 +2469,7 @@ mod test { assert!(end.is_none()); } + #[cfg(feature = "async")] fn prefetched_consumer(pool: &cache::Pool, max_age: std::time::Duration) -> (Producer, Consumer) { let cache = cache::Track::new(pool.clone(), kio::Weak::new()); let track = track::Info::timed().with_max_age(max_age); @@ -2454,6 +2484,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn prefetch_refresh_honors_pool_expiry() { let config = cache::Config::default().with_expiry(std::time::Duration::from_secs(1)); let pool = cache::Pool::new(config); @@ -2467,6 +2498,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn prefetch_refresh_honors_track_max_age() { let config = cache::Config::default().with_expiry(std::time::Duration::from_secs(30)); let pool = cache::Pool::new(config); @@ -2482,6 +2514,7 @@ mod test { /// Reading more than one prefetch batch drains every frame in order across the /// batch boundary (the refill starts exactly where the previous batch ended). #[test] + #[cfg(feature = "async")] fn read_frame_crosses_prefetch_batches() { let n = Prefetch::CAP * 3 + 5; let mut producer = Info { sequence: 0 }.produce(); @@ -2504,6 +2537,7 @@ mod test { /// track's max age window, or the cache pool). A reader that already drained every frame /// is missing nothing, so it must see the clean end of group rather than the abort. #[test] + #[cfg(feature = "async")] fn abort_after_finish_keeps_the_clean_end_for_a_drained_reader() { let mut producer = Info { sequence: 0 }.produce(); producer @@ -2530,6 +2564,7 @@ mod test { /// abort that released the cache, and one that stopped short gets that abort. The /// producer's total stays available on `frame_count`. #[test] + #[cfg(feature = "async")] fn finished_answers_for_the_cursor() { let mut producer = Info { sequence: 0 }.produce(); producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap(); @@ -2551,6 +2586,7 @@ mod test { /// A cursor on a group aborted for overflowing its budget can never reach the end, /// so `finished` reports that abort instead of parking forever. #[test] + #[cfg(feature = "async")] fn finished_reports_a_group_too_large() { let mut producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2570,6 +2606,7 @@ mod test { /// `next_frame` drains frames a prior `read_frame` prefetched, preserving order. #[test] + #[cfg(feature = "async")] fn interleave_read_and_next_frame() { let mut producer = Info { sequence: 0 }.produce(); for i in 0..5u8 { @@ -2594,6 +2631,7 @@ mod test { /// A `read_frame` whose index sits past the buffered frames (cleared by an abort) /// must surface the error, not panic on an out-of-range `range(local..)`. #[test] + #[cfg(feature = "async")] fn read_frame_past_cleared_frames_does_not_panic() { let mut producer = Info { sequence: 0 }.produce(); producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap(); @@ -2614,6 +2652,7 @@ mod test { /// Dropping a consumer mid-batch must drop the buffered-but-untaken frames /// (exercises the `MaybeUninit` Drop path; run under miri to catch leaks/UB). #[test] + #[cfg(feature = "async")] fn drop_with_partial_batch() { let mut producer = Info { sequence: 0 }.produce(); for _ in 0..Prefetch::CAP { @@ -2632,6 +2671,7 @@ mod test { /// modified; a guard dropped untouched wakes nobody and the reader would /// stall until the frame completed. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn chunk_write_wakes_parked_reader() { let mut producer = Info { sequence: 0 }.produce(); let mut consumer = producer.consume(); @@ -2697,6 +2737,7 @@ mod test { /// A group can start partway in, so a route can serve the tail of a group whose /// head came from somewhere else. #[test] + #[cfg(feature = "async")] fn start_at_starts_the_group_later() { let mut producer = Info { sequence: 0 }.produce(); producer.start_at(3).unwrap(); @@ -2724,6 +2765,7 @@ mod test { /// Seeking to the group's first available frame is how a front's pump picks up /// the tail; a lower index clamps up rather than failing. #[test] + #[cfg(feature = "async")] fn start_at_clamps_up_to_the_first_frame() { let mut producer = Info { sequence: 0 }.produce(); producer.start_at(3).unwrap(); @@ -2742,6 +2784,7 @@ mod test { /// `end_at` ends the read cleanly at the cap, and raising it re-offers the frames /// still cached behind it. #[test] + #[cfg(feature = "async")] fn end_at_caps_and_reopens() { let mut producer = Info { sequence: 0 }.produce(); for i in 0..4u8 { @@ -2772,6 +2815,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn frame_ranges_preserve_progress_and_make_inclusion_explicit() { let mut producer = Info { sequence: 0 }.produce(); for i in 0..4u8 { @@ -2801,6 +2845,7 @@ mod test { /// An exclusive cap at 0 is the empty range: no frame is delivered, and raising /// it re-offers the held frames. #[test] + #[cfg(feature = "async")] fn end_at_zero_is_empty() { let mut producer = Info { sequence: 0 }.produce(); producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap(); diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index 1c28f154c4e..b320c4034ae 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -60,11 +60,12 @@ pub mod announce { pub use origin_impl::{Consume, Hop, Hops, InvalidHop}; // The announce-interest prefixes a scope needs on a prefix-shaped wire. +#[cfg(feature = "async")] pub(crate) use origin_impl::interest_prefixes; // The advertise-only route guard, for tests shaping the route table. -#[cfg(test)] +#[cfg(all(test, feature = "async"))] pub(crate) use origin_impl::AnnounceProducer; -#[cfg(test)] +#[cfg(all(test, feature = "async"))] pub(crate) use origin_impl::ProduceTest; diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 8a3730d9dea..806a8ca6adf 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -78,6 +78,7 @@ impl Hop { } /// Build a hop from an id read off the wire, where 0 is legal. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn from_wire(id: u64) -> Result { if id >= 1u64 << 62 { return Err(DecodeError::InvalidValue); @@ -384,9 +385,11 @@ impl Cost { /// A peer without a wire cost: the same as [`Cost::default`], so only the /// local link price counts. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const UNKNOWN: Self = Self(0); /// Add a link's static price without overflowing the wire ceiling. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn charged(self, link_cost: u64) -> Self { Self::new(self.0.saturating_add(link_cost)) } @@ -518,6 +521,7 @@ impl Route { /// /// Not part of the advertised route: an assigned identity is private selection /// state and must not be forwarded. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn with_via(mut self, via: Hop) -> Self { self.via = via; self @@ -676,6 +680,7 @@ struct ReleaseState { held: Held, hold: Duration, clock: Clock, + #[cfg_attr(not(feature = "async"), allow(dead_code))] deadline: crate::time::Deadline, } @@ -708,6 +713,7 @@ impl Release { } /// `Ready` once released, registering `waiter` until then. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn poll(&self, waiter: &kio::Waiter) -> Poll<()> { let mut state = self.0.lock().unwrap(); let Some(now) = state.clock.try_now() else { @@ -1428,6 +1434,7 @@ impl Mount { impl OriginScope { /// A view that reaches nothing. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn empty() -> Self { Self { allowed: Patterns::new(), @@ -1671,6 +1678,7 @@ impl Producer { } /// The ingress stats context this handle was tagged with. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn stats(&self) -> &stats::Session { &self.stats } @@ -1743,6 +1751,7 @@ impl Producer { /// advertises no subscribe interest (its `allowed()` is empty, so the /// subscriber issues no ANNOUNCE_PLEASE). Used to fill an unset session half /// so both the publisher and subscriber loops still run. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn empty(hop: Hop) -> Self { // No allowed prefixes means no broadcast is ever created, so nothing will // ever be queued on the detached submission handle. @@ -1861,6 +1870,7 @@ impl Producer { /// is *not* entered into the route table. Sessions answer /// [`Dynamic`] requests with one of these; the requester already holds /// the request's result channel, so the table never needs to resolve it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn create_source(&self, path: impl AsPath) -> broadcast::Producer { let path = path.as_path(); let full = self.root.join(&path).to_owned(); @@ -3426,6 +3436,7 @@ struct Watched { enum WatchProducer { Changes(kio::Producer), + #[cfg_attr(not(feature = "async"), allow(dead_code))] Selection(kio::Producer), } @@ -3492,6 +3503,7 @@ impl Selected { } /// The lifetime of path requests while this peer still selects the held instance. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Selection { shared: kio::Shared, path: PathOwned, @@ -3500,6 +3512,7 @@ pub(crate) struct Selection { } impl Selection { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn current(&self) -> kio::Consumer<()> { self.signal .read() @@ -3511,6 +3524,7 @@ impl Selection { /// `Ready` once the current period's replacement is released, registering /// `waiter` until then. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_retired(&self, waiter: &kio::Waiter) -> Poll<()> { match self.signal.poll(waiter, |selected| match &selected.retiring { Some(release) => release.poll(waiter), @@ -4262,6 +4276,7 @@ pub struct Dynamic { } impl Dynamic { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn request_validity(&self, path: &Path) -> Option { let shared = &self.announcement.shared; let mut table = shared.lock(); @@ -4325,6 +4340,7 @@ impl Dynamic { /// Retire an explicit peer withdrawal, hiding derived paths only once its /// last live advertisement at the prefix is gone. A lost session just drops. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn withdrawn(self) { self.announcement.retract(true); } @@ -4397,6 +4413,7 @@ impl Dynamic { /// /// Takes `&self` so a handler can serve from one task while another re-prices /// the route; concurrent callers each receive distinct requests. + #[cfg(feature = "async")] pub async fn requested_broadcast(&self) -> Result { kio::wait(|waiter| self.poll_requested_broadcast(waiter)).await } @@ -4476,7 +4493,7 @@ impl ServeState { /// A pending request for a broadcast to be served on demand. /// -/// Yielded by [`Dynamic::requested_broadcast`]. The requester is awaiting inside +/// Yielded by [`Dynamic::poll_requested_broadcast`]. The requester is awaiting inside /// [`Consumer::request_broadcast`]; [`accept`](Self::accept) resolves it with a live /// broadcast (which the handler keeps producing into) and [`reject`](Self::reject) resolves /// it with an error. Dropping the request without either rejects it. @@ -4576,7 +4593,7 @@ impl Requesting { /// Fixed at request time, so it distinguishes the two ways /// [`Error::Unroutable`] arises: a queued request that fails was killed by /// its serving route retracting, and the table may already hold a - /// replacement worth retrying against ([`Consumer::routed_broadcast`] does), + /// replacement worth retrying against (`Consumer::routed_broadcast` does), /// while an unqueued failure means nothing could serve the path at all. pub fn is_queued(&self) -> bool { matches!(self.inner, RequestState::Pending { .. }) @@ -4651,7 +4668,7 @@ impl kio::Task for Requesting { /// Derive a read view from a handle. /// /// Lets APIs accept either a producer or a consumer (e.g. -/// [`Client::with_publisher`](crate::Client::with_publisher), +/// `Client::with_publisher`, /// [`Request::accept`]). The blanket `&T` impl means you can /// pass by value (`foo(x)`) to hand off ownership, or by reference (`foo(&x)`) /// to keep it, without spelling out `.consume()`. @@ -4786,6 +4803,7 @@ impl Consumer { /// nobody, so the announcing session's assigned identity is what keeps an /// anonymous route from echoing back. Pass [`Hop::UNKNOWN`] for an anonymous /// peer. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn excluding(mut self, peer: Hop) -> Self { self.horizon.exclude = Some(peer); self @@ -4813,6 +4831,7 @@ impl Consumer { /// A clone whose [`announced`](Self::announced) reports only the routes a feed /// from `outer` hides, for a stream topping up that feed. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn beyond(mut self, outer: &Consumer) -> Self { self.hidden.beyond = Some( outer @@ -4826,6 +4845,7 @@ impl Consumer { } /// Set wire discovery visibility relative to this consumer's requested root. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn discovery(mut self, hidden: bool) -> Self { self.hidden.include = hidden; self.hidden.from = Some(self.root.clone()); @@ -4833,11 +4853,13 @@ impl Consumer { } /// Whether [`announced`](Self::announced) reports hidden routes too. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn includes_hidden(&self) -> bool { self.hidden.include } /// The egress stats context this handle was tagged with. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn stats(&self) -> &stats::Session { &self.stats } @@ -4864,6 +4886,7 @@ impl Consumer { /// [`announced`](Self::announced) yields nothing. Used to answer a peer's /// announce-interest for a prefix outside our scope by announcing nothing, /// rather than tearing the stream down. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn empty(&self) -> Self { Self { scope: OriginScope::empty(), @@ -5014,6 +5037,7 @@ impl Consumer { /// To resolve a broadcast rather than inspect the route, use /// [`Self::routed_broadcast`]: pairing this with [`Self::request_broadcast`] /// leaves a gap where the covering route can retract. + #[cfg(feature = "async")] pub async fn routed(&self, path: impl AsPath) -> Option { // A follower's first event is always a start. match self.follow(path).ok()?.next().await? { @@ -5068,6 +5092,7 @@ impl Consumer { /// [`Error::Unauthorized`] for a path outside this consumer's scope, /// [`Error::Closed`] once the origin closes, and any other resolution /// failure as-is. + #[cfg(feature = "async")] pub async fn routed_broadcast(&self, path: impl AsPath) -> Result { let path = path.as_path(); @@ -5335,6 +5360,7 @@ pub struct AnnounceConsumer { // Holds the waiter a `Stream` poll registered; disjoint from `state` so the // borrow never collides with the body's. + #[cfg(feature = "async")] park: kio::Park, // Held updates (see [`Config::update_hold`]): their holds by prefix, the @@ -5385,6 +5411,7 @@ impl AnnounceConsumer { state, stats, guards: HashMap::new(), + #[cfg(feature = "async")] park: kio::Park::default(), update_hold, held: HashMap::new(), @@ -5486,6 +5513,7 @@ impl AnnounceConsumer { /// repeated announcement for the same prefix is a metadata update. Returns /// None if the cursor is closed. The consumer is also a [`futures::Stream`] /// of the same events. + #[cfg(feature = "async")] pub async fn next(&mut self) -> Option { kio::wait(|waiter| self.poll_next(waiter)).await } @@ -5595,6 +5623,7 @@ impl AnnounceConsumer { } } +#[cfg(feature = "async")] impl futures::Stream for AnnounceConsumer { type Item = AnnounceEvent; @@ -5637,7 +5666,10 @@ impl AnnounceConsumer { /// The next update must be an active route at `expected`; returns it. pub fn assert_next_active(&mut self, expected: impl AsPath) -> Route { - let event = self.next().now_or_never().expect("next blocked").expect("no next"); + let event = kio::wait(|waiter| self.poll_next(waiter)) + .now_or_never() + .expect("next blocked") + .expect("no next"); Self::active(event, &expected.as_path()) } @@ -5649,7 +5681,11 @@ impl AnnounceConsumer { /// The next update must be a restart at `expected`; returns its route. pub fn assert_next_restarted(&mut self, expected: impl AsPath) -> Route { - match self.next().now_or_never().expect("next blocked").expect("no next") { + match kio::wait(|waiter| self.poll_next(waiter)) + .now_or_never() + .expect("next blocked") + .expect("no next") + { AnnounceEvent::Restart(announce) => { assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"); announce.route @@ -5660,14 +5696,18 @@ impl AnnounceConsumer { /// The next update must be a retraction at `expected`. pub fn assert_next_ended(&mut self, expected: impl AsPath) { - match self.next().now_or_never().expect("next blocked").expect("no next") { + match kio::wait(|waiter| self.poll_next(waiter)) + .now_or_never() + .expect("next blocked") + .expect("no next") + { AnnounceEvent::End(announce) => assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"), other => panic!("should be a retraction: got {other:?}"), } } pub fn assert_next_wait(&mut self) { - if let Some(event) = self.next().now_or_never() { + if let Some(event) = kio::wait(|waiter| self.poll_next(waiter)).now_or_never() { panic!("next should block: got {event:?}"); } } @@ -5706,6 +5746,7 @@ impl ProduceTest for Hop { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "async")] use crate::model::test_tracing::count_drop_warnings; use futures::FutureExt; @@ -5737,6 +5778,7 @@ mod tests { /// Ending a logical track must finish its metadata producer as well as its routes, /// without losing the source's final group or warning about an unfinished writer. #[test] + #[cfg(feature = "async")] fn a_finished_logical_track_does_not_warn() { let warns = count_drop_warnings("track::Producer dropped without finish", || { let broadcast = Arc::new(broadcast::Info::default()); @@ -5800,6 +5842,7 @@ mod tests { /// Yield to the driver until `check` passes, bounded so a bug fails instead /// of hanging. + #[cfg(feature = "async")] async fn settle(mut check: impl FnMut() -> bool) { for _ in 0..100 { if check() { @@ -5811,6 +5854,7 @@ mod tests { } /// Yield to the driver until the server's front watcher delivers a request. + #[cfg(feature = "async")] async fn queued(server: &Dynamic) -> Request { let mut request = None; settle(|| match server.poll_requested_broadcast(&kio::Waiter::noop()) { @@ -5825,6 +5869,7 @@ mod tests { } /// Yield to the driver until the subscription has its next group or its end. + #[cfg(feature = "async")] async fn next_group(subscription: &mut crate::track::Subscriber) -> Result, Error> { let mut next = None; settle(|| match subscription.poll_recv_group(&kio::Waiter::noop()) { @@ -5908,6 +5953,7 @@ mod tests { /// Hiding narrows discovery only: an exact request resolves without an opt-in. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn hidden_broadcast_resolves_by_path() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -5933,6 +5979,7 @@ mod tests { /// A request through a mount is a request for the target path: it joins the /// one front there, so the fleet-wide claim is asked once for both readers. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn mount_resolves_on_the_target_front() { let producer = origin(1).produce(); let server = producer.dynamic(".svc", Route::default()).unwrap(); @@ -5957,6 +6004,7 @@ mod tests { /// Routes under the target, and the claim covering it, present under the /// mount; what the origin holds at the mounted path itself does not. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn mount_presents_target_routes_under_the_mount() { let producer = origin(1).produce(); let _claim = producer.announce(".svc", Route::default()).unwrap(); @@ -5999,6 +6047,7 @@ mod tests { /// The handle's own patterns authorize a path through the mount, named as the /// handle names it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn mount_authorizes_the_named_path() { let producer = origin(1).produce(); let _foo = producer.publish(".svc/p1/foo", Route::default()).unwrap(); @@ -6055,6 +6104,7 @@ mod tests { /// A target at the maximum depth still presents its exact path: the `**` a /// wildcard scope carries past it can only match nothing there. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn mount_keeps_a_max_depth_target() { let producer = origin(1).produce(); let deep = vec!["d"; Path::MAX_PARTS].join("/"); @@ -6168,6 +6218,7 @@ mod tests { /// Egress through a mount counts under the path the reader named, so it /// attributes to the reader's root rather than the fleet-wide target. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn mount_egress_counts_under_the_named_path() { let registry = stats::Registry::new(stats::Config::new()); let producer = origin(1).produce(); @@ -6217,6 +6268,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn broadcast_announces_its_own_path() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6461,6 +6513,7 @@ mod tests { /// A re-price of the winning route is an update in place, never a restart. #[test] + #[cfg(feature = "async")] fn a_reprice_is_an_update() { let producer = origin(1).produce(); let mut announced = producer.consume().announced(); @@ -6479,6 +6532,7 @@ mod tests { /// on the old route until it is withdrawn, then consumers restart and a re-request /// lands on the replacement. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_equal_cost_replacement_waits_for_the_old_route() { // Pinned in `spread_hash_matches_js`: hop 11 hashes below hop 10 for this path. // Restarts are delivered at once: the hold is not what this checks. @@ -6519,6 +6573,7 @@ mod tests { /// An origin delivering restarts at once, and a way to claim `prefix` with a /// producer scoped to `scope`, from `hop` at `cost`. + #[cfg(feature = "async")] fn scoped_pool() -> (Producer, impl Fn(&str, &str, u64, u64) -> Dynamic) { let producer = Config { update_hold: Duration::ZERO, @@ -6538,6 +6593,7 @@ mod tests { /// Request `path` from `consumer` and have `server` answer it: the answer, and /// the requester's broadcast. + #[cfg(feature = "async")] async fn hold(consumer: &Consumer, path: &str, server: &Dynamic) -> (broadcast::Producer, broadcast::Consumer) { let pending = consumer.request_broadcast(path, None); let answer = broadcast::Info::new().produce(); @@ -6546,6 +6602,7 @@ mod tests { } /// Let the fronts react, then expect nothing more on `announced`. + #[cfg(feature = "async")] async fn quiet(announced: &mut AnnounceConsumer) { for _ in 0..20 { moq_net_sim::yield_now().await; @@ -6554,6 +6611,7 @@ mod tests { } /// Let the fronts react, then expect a restart of `prefix` on `announced`. + #[cfg(feature = "async")] async fn restarted(announced: &mut AnnounceConsumer, prefix: &str) -> Route { for _ in 0..20 { moq_net_sim::yield_now().await; @@ -6565,6 +6623,7 @@ mod tests { /// though the route the cursor cannot see left and so changed the prefix's /// overall winner. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_moved_path_restarts_a_scoped_cursor() { let (producer, claim) = scoped_pool(); let consumer = producer.consume(); @@ -6588,6 +6647,7 @@ mod tests { /// A requester joining the front after its cursor restarted is not covered by /// that restart: the path moving afterwards restarts the prefix again. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_moved_path_restarts_a_requester_that_joined_after_a_restart() { let (producer, claim) = scoped_pool(); let consumer = producer.consume(); @@ -6612,6 +6672,7 @@ mod tests { /// A path that moved with the prefix's own restart is settled: the prefix's winner /// moving back to a route the front saw restarts it once, and the front adds none. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_moved_path_settled_by_a_restart_stays_settled() { let (producer, claim) = scoped_pool(); let consumer = producer.consume(); @@ -6635,6 +6696,7 @@ mod tests { /// A route from before the request repricing to win both the prefix and the held /// path restarts the prefix once: the cursor's own switch covers the move. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_repriced_route_taking_a_held_path_restarts_once() { let (producer, claim) = scoped_pool(); let consumer = producer.consume(); @@ -6746,6 +6808,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn request_prefers_identified_over_cheaper_anonymous() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6767,6 +6830,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn request_avoids_a_demoted_link_even_when_cheaper() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6866,6 +6930,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn capture_change_retracts_before_reannouncing_a_presented_prefix() { let producer = origin(1).produce(); let _broad = producer.announce("room", Route::default()).unwrap(); @@ -6898,6 +6963,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn routed_broadcast_resolves_once_announced() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6922,6 +6988,7 @@ mod tests { /// A local broadcast competes on its announced cost: a cheaper route at the /// same path wins, for cursors and requests alike. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn cheaper_remote_route_beats_a_local_broadcast() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6953,6 +7020,7 @@ mod tests { /// A resolved broadcast names the epoch of the route it came through, and none /// when that route has none. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_resolved_broadcast_carries_its_route_epoch() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -6971,6 +7039,7 @@ mod tests { /// A cheaper route that appears after a front was minted takes the front over: it /// carries the same epoch, so a newcomer joins the front. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn cheaper_route_after_a_front_takes_it_over() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7001,6 +7070,7 @@ mod tests { /// own, such as a later claim on this origin: locality is the tie-break after /// cost, not the newest entry. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn local_broadcast_wins_a_tie_with_a_hopless_route() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7054,6 +7124,7 @@ mod tests { /// At equal cost the local broadcast wins. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn local_broadcast_wins_a_cost_tie() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7082,6 +7153,7 @@ mod tests { /// Unannouncing ends the front the origin served from the broadcast and /// refuses new requests at once, even before the front acts on it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn unannounce_ends_the_front() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7108,6 +7180,7 @@ mod tests { /// unannouncing leaves it on the copy it asked for, which the source can /// still answer and finish. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn unannounce_keeps_a_track_awaiting_its_info() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7139,6 +7212,7 @@ mod tests { /// A front resolved through a served route, as a relay's upstream session serves /// one: the upstream broadcast's track requests arrive on the returned handle. + #[cfg(feature = "async")] async fn served_front() -> (Dynamic, broadcast::Producer, broadcast::Dynamic, broadcast::Consumer) { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7157,6 +7231,7 @@ mod tests { /// linger after, then leaves the broadcast so it stops pinning its cache: the next /// reader asks the source afresh. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn finished_track_is_forgotten_after_the_linger() { let (_server, _upstream, mut dynamic, resolved) = served_front().await; @@ -7218,6 +7293,7 @@ mod tests { /// they all share the plain front instead of each minting one that outlives them /// for as long as the route stands (#4799). Its tracks still go with the linger. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn viewer_sessions_share_the_plain_front() { let producer = origin(1).produce(); // A prefix route, which resolves any path beneath it optimistically. @@ -7268,6 +7344,7 @@ mod tests { /// of its own. Decided per request: a peer whose hop joins a chain after it joined /// the plain front gets the filtered one on its next request. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_hop_in_a_chain_gets_its_own_front() { let producer = origin(1).produce(); let _incumbent = producer @@ -7296,6 +7373,7 @@ mod tests { /// A front ends once nothing reads it for the linger and nothing holds its /// broadcast, rather than for as long as its route stands, and lets go of its source. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_unread_front_ends_after_the_linger() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7331,6 +7409,7 @@ mod tests { /// A request racing a front's retirement never joins it as it ends: one that holds /// the front before its driver looks keeps it, and one after mints a fresh front. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_request_racing_a_front_retiring_never_joins_it_ending() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7361,6 +7440,7 @@ mod tests { /// holding it: the front sees it go unread once and lets it linger, rather than /// keeping it read forever or spinning on it while it lingers ended and unread. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_track_ending_while_read_lets_its_front_retire() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7390,6 +7470,7 @@ mod tests { /// that a closed broadcast answers at once. Nothing but the front closes it today, so /// the test does; a regression spins inside one poll and hangs the test. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_front_whose_broadcast_closes_ends() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7423,6 +7504,7 @@ mod tests { /// The filtered front a peer session gets for a chain through it goes once the peer /// stops reading it, leaving the plain front its viewers share. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_peers_filtered_front_ends_once_unread() { let producer = origin(1).produce(); let server = producer @@ -7460,6 +7542,7 @@ mod tests { /// plain front when the only route left runs through it gets that route, but its own /// view withdraws the path, so it stops serving the path back to us. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_hairpin_through_the_plain_front_is_withdrawn() { let producer = origin(1).produce(); let peer = producer.consume().excluding(origin(7)); @@ -7494,12 +7577,14 @@ mod tests { /// (a cheaper route preempting it). A reader joining mid-outage gets it too, and the /// reader already mid-group is not handed it a second time. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_takeover_serves_the_open_group_head_to_later_readers() { for dies_first in [true, false] { takeover_keeps_the_open_group_head(dies_first).await; } } + #[cfg(feature = "async")] async fn takeover_keeps_the_open_group_head(dies_first: bool) { let producer = origin(1).produce(); let first_server = producer @@ -7514,6 +7599,7 @@ mod tests { queued(&first_server).await.accept(&upstream); let resolved = pending.await.unwrap(); + #[cfg(feature = "async")] async fn read(group: &mut crate::group::Consumer) -> Vec { let frame = moq_net_sim::timeout(Duration::from_secs(1), group.read_frame()) .await @@ -7665,6 +7751,7 @@ mod tests { /// The same holds for a reader returning to a parked track: its warm cache /// does not stand in for the copy it is waiting on. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn unannounce_keeps_a_returning_reader_awaiting_its_info() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7721,6 +7808,7 @@ mod tests { /// A re-announce that lands before the front acts on the retraction reuses /// the same route entry, so the front carries on. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn reannounce_before_the_front_acts_keeps_it() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7739,6 +7827,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn local_broadcast_resolves_once_announced() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7781,6 +7870,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn duplicate_routes_aggregate_until_the_last_leaves() { let producer = origin(1).produce(); let first = producer.dynamic("live", Route::default().with_cost(3)).unwrap(); @@ -7821,6 +7911,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dynamic_route_keeps_its_producer_scope() { let producer = origin(1).produce(); let scope = Patterns::from("*/chat".parse::().unwrap()); @@ -7873,6 +7964,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn route_changes_preserve_each_cursors_visible_winner() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -7923,6 +8015,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn root_cursor_keeps_the_more_specific_covering_route() { let producer = origin(1).produce(); let broad = producer.dynamic("room", Route::default().with_cost(1)).unwrap(); @@ -7943,6 +8036,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn dynamic_accepts_a_max_depth_prefix() { let producer = origin(1).produce(); let path = (0..Path::MAX_PARTS) @@ -7975,6 +8069,7 @@ mod tests { /// The consumer is a `Stream` of the same updates as `next`. #[test] + #[cfg(feature = "async")] fn announce_consumer_is_a_stream() { use futures::StreamExt; let producer = origin(1).produce(); @@ -8027,6 +8122,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn served_route_materializes_on_demand() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8050,6 +8146,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn served_requests_coalesce() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8071,6 +8168,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn retract_rejects_pending_requests() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8093,6 +8191,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn routed_broadcast_survives_serving_route_retraction() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8181,8 +8280,10 @@ mod tests { /// hold passes. Nothing else wakes the idle driver, so the hold's own timer /// must, or the origin's clock never reaches the deadline. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn try_next_delivers_a_held_update() { // Let paused time pass and every task woken by it run. + #[cfg(feature = "async")] async fn step(by: Duration) { moq_net_sim::advance(by).await; for _ in 0..10 { @@ -8221,6 +8322,7 @@ mod tests { /// within one instance waits out the update hold, and a newer change during the /// wait replaces it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_changed_route_waits_out_the_hold() { let producer = origin(1).produce(); let peer = producer.clone().peer(); @@ -8259,6 +8361,7 @@ mod tests { /// After a retraction, a worse path to the same publisher waits out the /// quarantine. An anonymous route names no publisher, so one after it is not held. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_retraction_quarantines_a_named_publisher() { let producer = origin(1).produce(); let peer = producer.clone().peer(); @@ -8404,6 +8507,7 @@ mod tests { /// from a peer instead of a client is a different fact for the consumer. Under one /// epoch it is an update. #[test] + #[cfg(feature = "async")] fn source_change_is_an_update() { let producer = origin(1).produce(); let peer = producer.clone().peer(); @@ -8542,6 +8646,7 @@ mod tests { /// gives the requester that answer; the front must not re-ask the same route /// forever, which would spin the origin driver. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn handler_rejection_is_final() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8569,6 +8674,7 @@ mod tests { /// A refusal from the longest prefix is the answer: neither a costlier advertiser of /// that prefix nor a catch-all is asked instead. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn refusal_never_falls_through() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8596,6 +8702,7 @@ mod tests { /// A refusal from a route a local broadcast superseded while it was pending is moot: /// the request resolves to the local broadcast instead of ending. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn superseded_refusal_does_not_end_the_request() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8622,6 +8729,7 @@ mod tests { /// The same holds for a more specific remote route: the beaten route's refusal is /// moot, and the new winner is asked instead. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn refusal_from_a_beaten_route_asks_the_new_winner() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8648,6 +8756,7 @@ mod tests { /// `routed_broadcast` treats a handler's rejection as the table's verdict: /// it waits for the table to move instead of re-asking the same route. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn routed_broadcast_waits_out_a_rejection() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8677,6 +8786,7 @@ mod tests { /// Teardown rejects a parked request with `Dropped`, but a destroyed origin /// is `Closed` to `routed_broadcast`'s callers. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn routed_broadcast_reports_teardown_as_closed() { let (producer, driver) = Producer::new(Config::new(origin(1))); let consumer = producer.consume(); @@ -8699,6 +8809,7 @@ mod tests { /// A local broadcast announcing at the exact path is a table change too: a /// requester parked on a handler's rejection resolves to it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn routed_broadcast_wakes_for_a_local_broadcast() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8722,6 +8833,7 @@ mod tests { /// A track first subscribed after the front is already serving another still /// replays what its source holds, like the first track did. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn late_track_on_a_served_front_replays() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8760,6 +8872,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn most_specific_prefix_shadows() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8784,6 +8897,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn root_dynamic_serves_any_path() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8819,6 +8933,7 @@ mod tests { /// routing can send a request to the handler. A `Request` carries only a path, /// so the handler cannot tell who asked. #[test] + #[cfg(feature = "async")] fn out_of_scope_request_never_reaches_the_dynamic_handler() { let producer = origin(1).produce(); let dynamic = producer.dynamic("", Route::default()).unwrap(); @@ -8840,6 +8955,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn routed_waits_for_coverage() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8861,6 +8977,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn routed_ignores_deeper_routes() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8875,6 +8992,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn routed_accepts_a_max_depth_path() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8895,6 +9013,7 @@ mod tests { } #[test] + #[cfg(feature = "async")] fn teardown_ends_everything() { let (producer, driver) = Producer::new(Config::new(origin(1))); let consumer = producer.consume(); @@ -8930,6 +9049,7 @@ mod tests { /// One live subscription reading a track through a remote front, plus the /// bookkeeping to kill and replace its serving route. + #[cfg(feature = "async")] struct ResumeRig { producer: Producer, resolved: broadcast::Consumer, @@ -8939,9 +9059,11 @@ mod tests { incumbent_track: track::Producer, } + #[cfg(feature = "async")] impl ResumeRig { /// Announce a served route with `first` as its first hop, materialize /// "room/alice" through it with a one-group "before" track, and subscribe. + #[cfg(feature = "async")] async fn new(first: &[u64]) -> (Self, Dynamic, broadcast::Producer) { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -8999,6 +9121,7 @@ mod tests { /// content stream (the delivered group plus its successor) and prove the /// rig's subscription resumes onto it: the successor group is delivered on /// the same subscription, at the group boundary. + #[cfg(feature = "async")] async fn assert_resumes(rig: &mut ResumeRig, server: &Dynamic) { let request = queued(server).await; let replacement = broadcast::Info::new().produce(); @@ -9025,6 +9148,7 @@ mod tests { } /// Write one single-frame group and read it back through `subscription`. + #[cfg(feature = "async")] async fn deliver(track: &track::Producer, subscription: &mut track::Subscriber, payload: &'static [u8]) { let mut group = track.append_group().unwrap(); group.write_frame(crate::Timestamp::ZERO, payload).unwrap(); @@ -9037,6 +9161,7 @@ mod tests { /// route does not move the subscription, and when its own route goes it ends /// rather than resuming elsewhere. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_route_without_an_epoch_never_resumes_elsewhere() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9073,6 +9198,7 @@ mod tests { /// A newer publisher at the path wins new requests even when it costs more, while /// the subscriptions on the old one stay with it until its route goes. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_newer_epoch_leaves_the_broadcast_in_flight() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9116,6 +9242,7 @@ mod tests { /// the old front: its watcher has not noticed the replacement yet, but the /// request mints a front on the new epoch instead of joining the replaced one. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_request_racing_a_newer_epoch_skips_the_old_front() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9159,6 +9286,7 @@ mod tests { /// A newer epoch winning while the front's first request is in flight beats that /// route: its late answer is not what requesters resolve to. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_answer_racing_a_newer_epoch_resolves_the_winner() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9187,6 +9315,7 @@ mod tests { /// neither served under the new epoch nor cached for the next request, and a request /// pinned to the old epoch is refused. `answer_first` lands the old answer just /// before the change instead of after, before the front takes it. + #[cfg(feature = "async")] async fn held_request_across_an_epoch_change(answer_first: bool) { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9229,11 +9358,13 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_request_held_across_an_epoch_change_is_asked_again() { held_request_across_an_epoch_change(false).await; } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_answer_before_an_epoch_change_is_asked_again() { held_request_across_an_epoch_change(true).await; } @@ -9241,6 +9372,7 @@ mod tests { /// A refusal that lands just before an epoch change, before the front takes it, /// was the old instance's answer: the request is asked again, not ended. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_refusal_before_an_epoch_change_is_asked_again() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9264,6 +9396,7 @@ mod tests { /// A release is not undone by the epoch coming back before the front looks: a /// request held across A to B to A is asked again, not refused. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_request_held_across_an_epoch_round_trip_is_asked_again() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9295,6 +9428,7 @@ mod tests { /// first: a request after the round trip is asked again rather than joining the /// front still serving the first instance's subscribers. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn losing_an_epoch_is_another_instance() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9323,6 +9457,7 @@ mod tests { /// Announce cursors follow the newest epoch over a cheaper one, and deliver a /// change of epoch as a restart: a different broadcast. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn the_newest_epoch_is_announced_as_a_new_broadcast() { let producer = origin(1).produce(); let mut announced = producer.consume().announced(); @@ -9350,6 +9485,7 @@ mod tests { /// A peer naming one publisher instance is never handed another. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_request_for_another_epoch_is_refused() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9369,6 +9505,7 @@ mod tests { /// A route without an epoch resolves a handle without one. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_route_without_an_epoch_resolves_without_one() { let producer = origin(1).produce(); let _broadcast = producer.publish("room/alice", Route::default()).unwrap(); @@ -9383,6 +9520,7 @@ mod tests { /// The driver's completion contract: it resolves once every producer handle /// drops, however many read handles remain. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn driver_resolves_with_live_consumers() { let (producer, driver) = Producer::new(Config::new(origin(1))); let consumer = producer.consume(); @@ -9398,6 +9536,7 @@ mod tests { /// the successor is refused instead of the subscriber's samples being read on /// a different grid, and the verdict outlives the aborted logical track. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn incompatible_successor_is_refused() { for replacement in [ track::Info::default().with_timescale(crate::Timescale::MICRO), @@ -9437,6 +9576,7 @@ mod tests { /// subscription resumes through another with that epoch, whatever its first hop, /// or with none at all. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn any_route_at_the_path_resumes_the_subscription() { let cases = [ (&[10][..], &[10, 20][..]), @@ -9459,6 +9599,7 @@ mod tests { /// A route updated in place to a new first hop still serves the same broadcast: /// the subscription carries on through it, and a new request joins the front. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_first_hop_update_keeps_serving() { for first in [&[][..], &[0][..], &[10][..]] { let (mut rig, server, _source) = ResumeRig::new(first).await; @@ -9493,6 +9634,7 @@ mod tests { /// front never resumes onto a route through that peer, even one from the same /// publisher. A reader that can see the route resumes there. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn resume_never_routes_through_the_requester() { for peer in [Some(origin(7)), None] { let producer = origin(1).produce(); @@ -9562,6 +9704,7 @@ mod tests { /// The front closes with its last source, so the newcomer attaches a fresh /// one and the subscriber resolves it without parking. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn anonymous_handoff_serves_the_newcomer_immediately() { let producer = origin(1).produce(); @@ -9658,6 +9801,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn reprice_is_invisible_to_the_subscription() { let (rig, incumbent, source) = ResumeRig::new(&[10]).await; @@ -9689,6 +9833,7 @@ mod tests { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn drain_reprice_migrates_before_the_session_dies() { let (mut rig, incumbent, source) = ResumeRig::new(&[10]).await; let standby_server = rig.standby(&[10, 20]); @@ -9715,6 +9860,7 @@ mod tests { /// A second broadcast at the path without an epoch is another instance: new /// requests resolve it on a fresh front, rather than join the first one's. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_newer_local_source_gets_a_fresh_front() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9745,6 +9891,7 @@ mod tests { /// The runtime is single-threaded and the publisher's whole ending has no await in /// it, so the outcome does not depend on timing. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_finished_broadcast_concludes_in_flight_subscriptions() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9787,6 +9934,7 @@ mod tests { /// disturb subscriptions already in flight, which conclude normally with /// SUBSCRIBE_END." #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_retracted_route_concludes_in_flight_subscriptions() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9835,6 +9983,7 @@ mod tests { /// A standing route outlives the source it produced: the front ends instead of /// asking that route for the broadcast that just closed. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_closed_source_is_not_requested_again_from_its_standing_route() { let producer = origin(1).produce(); let server = producer @@ -9858,6 +10007,7 @@ mod tests { /// Cached groups stay on the front for the linger; a returning reader /// replays them and asks the source again for groups past that edge. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn origin_front_drops_the_source_when_unused() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -9920,6 +10070,7 @@ mod tests { /// of the group: parking the track reads no new group, but the upstream stays /// subscribed until the held one ends. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_held_group_continues_after_its_track_goes_unread() { let producer = origin(1).produce(); let server = producer @@ -9960,6 +10111,7 @@ mod tests { /// the reader too, rather than leaving it waiting for a route change that never /// comes. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_group_the_source_aborts_ends_while_the_track_lives() { let producer = origin(1).produce(); let broadcast = producer.publish("live", Route::default()).unwrap(); @@ -9992,6 +10144,7 @@ mod tests { /// so the publisher's `unused()` resolves far below `track::IDLE_LINGER` through /// the whole chain, and a returning reader reaches the publisher's cache afresh. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn chained_front_drops_the_source_when_unused() { let leaf = origin(1).produce(); let leaf_consumer = leaf.consume(); @@ -10086,6 +10239,7 @@ mod tests { /// of the track carries different metadata is refused: the incumbent keeps /// serving, and the refusal is never retried once the incumbent leaves. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn incompatible_local_source_keeps_the_incumbent() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -10133,6 +10287,7 @@ mod tests { /// A remote front serving "room/alice" with one read track, "video", whose first /// group was delivered. Returns the producer, the incumbent's track, and the reader. + #[cfg(feature = "async")] async fn remote_front() -> ( Producer, Dynamic, @@ -10162,6 +10317,7 @@ mod tests { /// drains the incumbent. The reader leaves: the incumbent's copy is dropped at once, /// like any unread track's, not held until the linger. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_unread_draining_track_releases_the_old_source() { let (producer, _server, _upstream, old, subscription) = remote_front().await; @@ -10185,6 +10341,7 @@ mod tests { /// takes over, and the reader returns: it gets an outcome rather than waiting on the /// dropped copy. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_returning_reader_is_not_stranded_on_a_dropped_copy() { let (producer, _server, upstream, old, mut subscription) = remote_front().await; let resolved = producer.consume().request_broadcast("room/alice", None).await.unwrap(); @@ -10241,6 +10398,7 @@ mod tests { /// A newer local source's copy of "video" is refused for its metadata while the /// incumbent keeps serving: the refused copy is let go, not kept subscribed. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_refused_copy_is_not_kept_subscribed() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -10277,6 +10435,7 @@ mod tests { /// Then both withdraw and the front ends: the track in flight carries on with the /// incumbent, never the refused copy. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn the_end_never_feeds_a_refused_copy() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -10332,6 +10491,7 @@ mod tests { /// withdraw and the front ends: the track carries on with the incumbent's copy rather /// than the unanswered one. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn the_end_keeps_the_copy_still_serving() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -10465,6 +10625,7 @@ mod tests { /// A handler holding only its `Dynamic` keeps serving once every producer /// handle drops, and the driver finishes once the handler is gone too. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_dynamic_keeps_the_driver_running() { let (producer, driver) = Producer::new(Config::new(origin(1))); let run = moq_net_sim::spawn(crate::time::run_sim(driver)); @@ -10491,6 +10652,7 @@ mod tests { /// A reader still on a retracted broadcast's track keeps its front draining, but /// never keeps the driver from finishing once nothing owns the origin. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_retracted_reader_never_holds_the_driver() { let (producer, driver) = Producer::new(Config::new(origin(1))); let run = moq_net_sim::spawn(crate::time::run_sim(driver)); @@ -10515,6 +10677,7 @@ mod tests { /// A subscriber that asked before the retraction but was not polled yet still finds /// the copy after the origin is orphaned and its driver finishes. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_orphaned_front_keeps_the_copy_for_a_pending_subscriber() { let (producer, driver) = Producer::new(Config::new(origin(1))); let run = moq_net_sim::spawn(crate::time::run_sim(driver)); @@ -10543,6 +10706,7 @@ mod tests { /// A track whose last reader leaves as its broadcast ends, before the front parks it, /// lets go of its copy: the source is not kept subscribed for nobody. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_track_unread_as_its_broadcast_ends_releases_its_source() { let producer = origin(1).produce(); let consumer = producer.consume(); @@ -10813,6 +10977,7 @@ mod tests { } /// Mint an origin whose pool reclaims idle content after `expiry`. + #[cfg(feature = "async")] fn expiring_origin(expiry: Duration) -> Producer { let pool = cache::Pool::new(cache::Config::default().with_expiry(expiry)); Config { @@ -10826,6 +10991,7 @@ mod tests { /// track's own write-driven expiry never fires and a reader parked in that group /// is never told. The driver's wall-clock sweep is the bound. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn stalled_publisher_open_group_is_reclaimed() { let expiry = Duration::from_secs(1); let origin = expiring_origin(expiry); @@ -10855,6 +11021,7 @@ mod tests { /// Reclamation is the pool's policy, not the origin's: a pool with no expiry /// window keeps idle content until byte pressure takes it, sweep or no sweep. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn sweep_respects_a_disabled_expiry() { let origin = Config { pool: cache::Pool::unbounded(), diff --git a/rs/moq-net/src/model/resume.rs b/rs/moq-net/src/model/resume.rs index dc0b54ab858..595f112f2d1 100644 --- a/rs/moq-net/src/model/resume.rs +++ b/rs/moq-net/src/model/resume.rs @@ -337,6 +337,7 @@ impl Subscriber { /// Poll for where the serving copy's live feed starts; see /// [`track::Subscriber::poll_start`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_start(&mut self, waiter: &kio::Waiter) -> Poll> { let mut reader = self.reader(); let sub = ready!(reader.poll_serving(waiter)); @@ -353,6 +354,7 @@ impl Subscriber { /// Move the local read floor to `start`, including downward. /// See [`track::Subscriber::start_at`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn start_at(&mut self, start: u64) { self.reader().start_at(start) } @@ -389,6 +391,7 @@ impl Subscriber { /// Poll for the serving copy's cache reflecting its live feed; see /// [`track::Subscriber::poll_live`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_live(&mut self, waiter: &kio::Waiter) -> Poll> { let mut reader = self.reader(); let sub = ready!(reader.poll_serving(waiter)); @@ -730,6 +733,7 @@ impl Reader { /// The serving copy's subscription, once it answered; `None` once the track ended /// without one. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn poll_serving(&mut self, waiter: &kio::Waiter) -> Poll> { self.sync(waiter); let groups = self.groups; @@ -766,6 +770,7 @@ impl Reader { /// from the mirrored subscription when it resolves; one already reading has /// to be moved, or a group it skipped stays skipped. Never below a copy's own /// resume point: groups under it were handed out by an earlier route, or never owed. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn start_at(&mut self, start: u64) { self.groups.0 = start; for copy in &mut self.copies { @@ -1088,7 +1093,9 @@ mod test { use futures::FutureExt; use super::*; - use crate::{Timestamp, broadcast}; + #[cfg(feature = "async")] + use crate::Timestamp; + use crate::broadcast; /// A route's copy of the track. fn copy() -> track::Producer { @@ -1116,6 +1123,7 @@ mod test { .unwrap() } + #[cfg(feature = "async")] fn recv(sub: &mut track::Subscriber) -> group::Consumer { sub.recv_group() .now_or_never() @@ -1124,11 +1132,13 @@ mod test { .expect("track ended") } + #[cfg(feature = "async")] fn read(group: &mut group::Consumer) -> Option> { let frame = group.read_frame().now_or_never().expect("a frame").unwrap(); frame.map(|frame| frame.payload.to_vec()) } + #[cfg(feature = "async")] fn ts(ms: u64) -> Timestamp { Timestamp::from_millis(ms).unwrap() } @@ -1174,6 +1184,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_reader_reads_the_serving_copy() { let routes = Producer::new(); let logical = logical(&routes); @@ -1191,6 +1202,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_stalled_group_continues_from_the_replacement() { let routes = Producer::new(); let logical = logical(&routes); @@ -1223,6 +1235,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_half_read_frame_continues_from_the_replacement() { let routes = Producer::new(); let logical = logical(&routes); @@ -1261,6 +1274,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_group_both_routes_deliver_is_handed_out_once() { let routes = Producer::new(); let logical = logical(&routes); @@ -1287,6 +1301,7 @@ mod test { /// floor when the reader's floor widens: an older group its route cached was /// never owed, and handing it out now would be out of order. #[test] + #[cfg(feature = "async")] fn a_widened_floor_stays_above_the_replacement_resume_point() { let routes = Producer::new(); let logical = logical(&routes); @@ -1313,6 +1328,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_replaced_route_finishes_only_the_groups_it_had() { let routes = Producer::new(); let logical = logical(&routes); @@ -1337,6 +1353,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_is_given_up_once_stale() { let routes = Producer::new(); let logical = logical(&routes); @@ -1365,6 +1382,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagrams_a_new_copy_replays_are_handed_out_once() { let routes = Producer::new(); let logical = logical(&routes); @@ -1398,6 +1416,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_parked_group_wakes_when_the_route_changes() { let routes = Producer::new(); let logical = logical(&routes); @@ -1416,6 +1435,7 @@ mod test { /// A reader holding group 2, which its quiet route never finishes, after the front /// switched to `copy`, a replacement that starts past it. + #[cfg(feature = "async")] struct Held { group: group::Consumer, copy: track::Producer, @@ -1432,9 +1452,11 @@ mod test { ), } + #[cfg(feature = "async")] impl Held { /// `groups` are the replacement's `(sequence, first timestamp)`, written before the /// switch is noticed, so its subscription has passed the held group. + #[cfg(feature = "async")] fn new(groups: &[(u64, Option)]) -> Self { let routes = Producer::new(); let logical = logical(&routes); @@ -1508,6 +1530,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_wakes_when_its_successor_is_stamped() { let mut held = Held::new(&[(3, None), (4, Some(4000))]); // Group 2 ends where group 3 begins, so this is when it is known to be 1s behind. @@ -1521,6 +1544,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_wakes_when_its_successor_aborts() { let mut held = Held::new(&[(3, None), (4, Some(4000)), (5, Some(5000))]); // Group 4 succeeds it instead, a full second behind the edge. @@ -1530,6 +1554,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_wakes_when_the_edge_passes_it() { let mut held = Held::new(&[(3, Some(3000))]); let mut newer = held.group(4); @@ -1544,6 +1569,7 @@ mod test { /// The held group sits below everything the replacement holds, so a landing at the /// top must not forget it before an older group lands as its new successor. #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_wakes_when_an_older_successor_lands() { let mut held = Held::new(&[(4, Some(4000))]); held.group(5).write_frame(ts(4050), b"b".as_ref()).unwrap(); @@ -1562,6 +1588,7 @@ mod test { /// The replacement passed the held group, so it looked lost, but its subscription /// can still deliver it out of order: that landing is the continuation. #[test] + #[cfg(feature = "async")] fn a_group_no_route_continues_wakes_when_it_lands_late() { let mut held = Held::new(&[(3, Some(3000))]); let _newer = held.group(4); @@ -1571,6 +1598,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_half_read_frame_waits_for_the_replacement_header() { for arrives in [true, false] { let routes = Producer::new(); @@ -1620,6 +1648,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn recovered_groups_and_frames_expire_against_the_new_route() { for partial in [false, true] { let routes = Producer::new(); @@ -1666,6 +1695,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_pending_fetch_wakes_and_moves_to_the_new_route() { let routes = Producer::new(); let a = copy(); @@ -1687,6 +1717,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_pending_recovery_fetch_does_not_disable_expiry() { let routes = Producer::new(); let logical = logical(&routes); @@ -1729,6 +1760,7 @@ mod test { /// A fetch answered from a group still arriving over a subscription outlives that /// delivery giving up on it as too old: the route is asked for the rest instead. #[test] + #[cfg(feature = "async")] fn a_fetched_group_outlives_its_delivery_giving_up() { let routes = Producer::new(); let a = copy(); @@ -1757,6 +1789,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn ending_the_front_releases_a_stalled_group() { let routes = Producer::new(); let logical = logical(&routes); @@ -1774,6 +1807,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_datagram_route_failure_waits_for_replacement() { let routes = Producer::new(); let logical = logical(&routes); @@ -1794,6 +1828,7 @@ mod test { /// Datagrams and groups are independent channels: reading datagrams first must not /// release a replaced copy whose buffered groups the reader has yet to read. #[test] + #[cfg(feature = "async")] fn datagram_reads_keep_unread_groups_on_a_replaced_copy() { for datagrams_first in [false, true] { let routes = Producer::new(); @@ -1816,6 +1851,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_retention_preserves_ordered_backlog() { for ordered_first in [false, true] { let routes = Producer::new(); @@ -1856,6 +1892,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_retention_obeys_changed_group_bounds() { for ordered in [false, true] { for raise_floor in [false, true] { @@ -1902,6 +1939,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_readers_release_replaced_subscriptions() { let routes = Producer::new(); let logical = logical(&routes); @@ -1933,6 +1971,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_recovered_group_keeps_its_new_copy_subscribed_during_another_switch() { let routes = Producer::new(); let logical = logical(&routes); @@ -1955,6 +1994,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn held_read_preferences_follow_the_reader() { enum Read { Group(Box), @@ -2085,6 +2125,7 @@ mod test { /// Replay every ordering of independent actions, with the return to A following /// the switch to B. The payload is the oracle, independent of route bookkeeping. #[test] + #[cfg(feature = "async")] fn route_flap_interleavings_preserve_readers() { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Event { @@ -2096,6 +2137,7 @@ mod test { Cancel, } + #[cfg(feature = "async")] fn run(events: &[Event], wake_only: bool) { let routes = Producer::new(); let logical = logical(&routes); @@ -2194,6 +2236,7 @@ mod test { } } + #[cfg(feature = "async")] fn walk(events: &mut Vec, remaining: &[Event]) { if remaining.is_empty() { for wake_only in [false, true] { diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index bbea59faf02..23f4f576963 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -199,9 +199,11 @@ enum Feed { Live, /// The upstream subscription ended with the copy still held, so how stale the cache /// is cannot be told until the route answers again. + #[cfg_attr(not(feature = "async"), allow(dead_code))] Idle, /// The route answered that its feed reaches group `from`, which the cache cannot show /// yet: until it can, the newest group cached would read as the live edge. + #[cfg_attr(not(feature = "async"), allow(dead_code))] Answered { from: u64 }, } @@ -324,6 +326,7 @@ pub(crate) struct TrackState { live_floor: Option, // The newest group cached when the track went idle: what the route's answer is // judged against, whatever lands before it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] idle_newest: Option, // Where production stopped, snapshotted when the open groups are released (an @@ -1214,6 +1217,7 @@ impl TrackState { } /// Show a [`Producer::receive_group`] group to readers, if `group` still holds its slot. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn reveal(&mut self, group: &group::Producer) { let sequence = group.sequence; let Some(slot) = self.lookup.get_mut(&sequence) else { @@ -1553,6 +1557,7 @@ impl TrackState { /// The upstream subscription ended with the copy still held: what it cached may go /// stale, so readers get nothing from it until the route answers again. Buffered /// datagrams go too, since a reader returning later must not be handed them. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn set_idle(&mut self) { self.feed = Feed::Idle; self.idle_newest = self @@ -1572,6 +1577,7 @@ impl TrackState { /// /// Readers wait until the cache shows that position: before then its newest group would /// read as the live edge, though the route already holds a newer one. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn set_live(&mut self, largest: Option) { if self.feed != Feed::Idle { return; @@ -1598,12 +1604,14 @@ impl TrackState { } /// Whether readers can already see content at or past `sequence`. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn shows(&self, sequence: u64) -> bool { self.lookup.range(sequence..).any(|(_, slot)| slot.is_shown()) || self.datagrams.iter().any(|datagram| datagram.sequence >= sequence) } /// The newest live group the cache holds, and the number of frames it has so far. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn newest(&self) -> Option<(u64, u64)> { let (sequence, slot) = self .lookup @@ -1615,6 +1623,7 @@ impl TrackState { /// The largest position the cache holds: the newest live group's last frame (its head, /// while it has none). + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn largest(&self) -> Option { self.newest().map(|(group, frames)| Position { group, @@ -1746,6 +1755,7 @@ pub struct Producer { impl Producer { /// The immutable publisher priority committed when this track was accepted. #[cfg(test)] + #[cfg(feature = "async")] pub(crate) fn publisher_priority(&self) -> u8 { self.info.priority } @@ -1817,12 +1827,14 @@ impl Producer { /// header lands before its first frame. Shown in between, it would leave the group /// before it with no stamped successor, so a joiner at the live edge would be handed /// that older group first. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn receive_group(&self, group: group::Info) -> Result { self.insert(group, true) } /// Show a [`Self::receive_group`] group to readers: its first frame landed, or its /// stream ended. A no-op once another incarnation took its sequence. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn reveal_group(&self, group: &group::Producer) { if let Ok(mut state) = self.modify() { state.reveal(group); @@ -1982,6 +1994,7 @@ impl Producer { } /// End a locally closed session's receive track without declaring a wire boundary. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn close(self) -> Result<()> { let mut state = self.modify()?; state.sealed = true; @@ -1994,6 +2007,7 @@ impl Producer { } /// Abort the groups still receiving from a failed session before ending its track. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn abort_session(self, err: Error) -> Result<()> { let state = self.modify()?; let open: Vec<_> = state @@ -2049,6 +2063,7 @@ impl Producer { /// is a no-op, and a mid-group end may refine a group boundary it falls just before /// (a FETCH_OK names only the boundary). Any other second end is /// [`Error::ProtocolViolation`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn finish_at_pending(&mut self, end: Position) -> Result<()> { let mut state = self.modify()?; match state.end { @@ -2072,6 +2087,7 @@ impl Producer { /// While pending, readers do not end at the boundary: a lower group's stream may /// arrive after a higher one. The session clears it once its tail settles; the last /// producer going ends readers regardless. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn set_tail_pending(&self, pending: bool) { if self.state.read().tail_pending == pending { return; @@ -2098,6 +2114,7 @@ impl Producer { /// Readers get nothing from the cache until [`Self::set_live`]: the upstream /// subscription ended with this copy still held, so what it cached may go stale. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn set_idle(&mut self) { if let Ok(mut state) = self.modify() { state.set_idle(); @@ -2107,6 +2124,7 @@ impl Producer { /// The route answered with its largest position, `None` for nothing yet; see /// [`Self::set_idle`]. Readers resume once the cache shows that position. A no-op /// unless idle. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn set_live(&mut self, largest: Option) { if let Ok(mut state) = self.modify() { state.set_live(largest); @@ -2114,12 +2132,14 @@ impl Producer { } /// Whether readers may take from the cache; see [`Self::set_idle`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn is_live(&self) -> bool { self.state.read().feed == Feed::Live } /// While idle, the newest group cached when the track went idle: a route asked from /// its head sends it again, and its first frame says whether the cache is current. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn idle_newest(&self) -> Option { let state = self.state.read(); state.idle_newest.filter(|_| state.feed == Feed::Idle) @@ -2129,6 +2149,7 @@ impl Producer { /// resolve its start: nothing below `sequence` arrives, exactly as [`Self::start_at`], /// but [`Subscriber::poll_start`] keeps waiting until a later [`Self::start_at`] /// resolves it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn request_start(&mut self, sequence: Option) -> Result<()> { self.modify()?.set_start(sequence, true); Ok(()) @@ -2137,6 +2158,7 @@ impl Producer { /// The declared first sequence of the live feed, once [`Self::start_at`] /// declared one. `None` while nothing was declared. #[cfg(test)] + #[cfg(feature = "async")] pub(crate) fn start_sequence(&self) -> Option { self.state.read().start_sequence } @@ -2192,6 +2214,7 @@ impl Producer { } /// Block until the track is closed or aborted, returning the cause. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { kio::wait(|waiter| self.poll_closed(waiter)).await } @@ -2217,6 +2240,7 @@ impl Producer { } /// Ready once the track holds a group past `sequence`, or is closed. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_past(&self, sequence: u64, waiter: &kio::Waiter) -> Poll<()> { let past = self.state.poll_ref(waiter, |state| match state.max_sequence { Some(latest) if latest > sequence => Poll::Ready(()), @@ -2300,6 +2324,7 @@ impl Producer { /// Yields the most demanding request across all live subscribers, or `None` /// once the last one drops. Used by relays to forward downstream demand /// upstream (e.g. SUBSCRIBE_UPDATE). + #[cfg(feature = "async")] pub async fn subscription_changed(&mut self) -> Result> { kio::wait(|waiter| self.poll_subscription_changed(waiter)).await } @@ -2318,7 +2343,7 @@ impl Producer { snapshot_subscription(&subs, bound) } - /// Poll counterpart to [`subscription_changed`](Self::subscription_changed): the + /// Poll for a changed subscription: the /// aggregate subscription whenever it changes, or `None` once nobody is subscribed. /// Errors once the track is aborted. pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll>> { @@ -2439,11 +2464,12 @@ impl Dynamic { /// /// A relay issues a wire FETCH first; an origin already has the group cached, so /// the fetch resolves without ever reaching here. Errors once the track is aborted. + #[cfg(feature = "async")] pub async fn requested_group(&self) -> Result { kio::wait(|waiter| self.poll_requested_group(waiter)).await } - /// Poll counterpart to [`requested_group`](Self::requested_group). + /// Poll for a group request to serve. pub fn poll_requested_group(&self, waiter: &kio::Waiter) -> Poll> { poll_requested_group(&self.state, &self.fetch, waiter) } @@ -2838,7 +2864,7 @@ impl super::WeakEntry for TrackWeak { /// whether anyone is subscribed (on-demand capture / encoding) without being able /// to publish frames or close the track. It's a weak handle, so it neither keeps /// the track alive nor pins its cached groups; once the owning [`Producer`] -/// goes away, [`used`](Self::used) / [`unused`](Self::unused) report the track's +/// goes away, [`poll_used`](Self::poll_used) / [`poll_unused`](Self::poll_unused) report the track's /// closure. #[derive(Clone)] pub struct Demand { @@ -2853,16 +2879,19 @@ impl Demand { } /// Block until there is at least one active consumer. + #[cfg(feature = "async")] pub async fn used(&self) -> Result<()> { self.state.used().await.map_err(|_| self.abort_reason()) } /// Block until there are no active consumers. + #[cfg(feature = "async")] pub async fn unused(&self) -> Result<()> { self.state.unused().await.map_err(|_| self.abort_reason()) } /// Block until the track is closed or aborted, returning the cause. + #[cfg(feature = "async")] pub async fn closed(&self) -> Error { self.state.closed().await; self.abort_reason() @@ -2886,12 +2915,12 @@ impl Demand { !self.state.is_closed() && self.state.is_used() } - /// Poll-based variant of [`Self::used`]. + /// Poll until a consumer is using this resource. pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll> { self.state.poll_used(waiter).map_err(|_| self.abort_reason()) } - /// Poll-based variant of [`Self::unused`]. + /// Poll until no consumer is using this resource. pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll> { self.state.poll_unused(waiter).map_err(|_| self.abort_reason()) } @@ -2903,7 +2932,7 @@ impl Demand { /// Read the current demand and arm `waiter` for the next transition. /// - /// Level-triggered, unlike [`used`](Self::used) / [`unused`](Self::unused): it + /// Level-triggered, unlike [`poll_used`](Self::poll_used) / [`poll_unused`](Self::poll_unused): it /// answers what the demand is *now* and wakes on whichever way it can move /// next, so a poll-driven caller watching several tracks at once doesn't have /// to track which edge it's waiting for. @@ -3032,6 +3061,7 @@ impl Consumer { /// source that never declares one). Parks while a lite-06+ session still owes its /// SUBSCRIBE_START (see [`Producer::request_start`]); a closed track is ready with /// whatever it last declared, since nothing will resolve it anymore. + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn poll_state_start(state: &kio::Consumer, waiter: &kio::Waiter) -> Poll> { let res = state.poll(waiter, |state| match state.start_pending && state.abort.is_none() { true => Poll::Pending, @@ -3046,6 +3076,7 @@ impl Consumer { /// The newest group, when it is already cached: resolved synchronously, without /// counting as a fetch or a delivery. The IETF publisher snapshots its frame count to /// resolve Largest Object; a group that is not immediately available reads as no edge. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn peek_latest(&self) -> Option { if let Some(serving) = self.serving() { return serving.peek_latest(); @@ -3057,6 +3088,7 @@ impl Consumer { /// The nearest cached group below `sequence`, under the same terms as /// [`Self::peek_group`]. Walks the cache's own order, so gaps in the group numbering /// are crossed and aborted (evicted) entries are skipped. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn peek_before(&self, sequence: u64) -> Option { if let Some(serving) = self.serving() { return serving.peek_before(sequence); @@ -3075,6 +3107,7 @@ impl Consumer { /// fetch, a peek does not refresh the group's cache standing, so it never keeps a /// group alive over one a subscriber actually read; an aborted (evicted) group is a /// miss. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn peek_group(&self, sequence: u64) -> Option { let state = self.state.read(); let group = state.lookup.get(&sequence)?.serving(); @@ -3102,6 +3135,7 @@ impl Consumer { /// [`Self::poll_group`], except a newer group is no verdict: QUIC does not order /// streams, so this one may still land behind it. Pending until it is cached, or the /// feed starts past it or ends before it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_landed(&self, sequence: u64, waiter: &kio::Waiter) -> Poll> { match self.state.poll(waiter, |state| state.landing(sequence)) { Poll::Ready(Ok(group)) => Poll::Ready(group), @@ -3131,6 +3165,7 @@ impl Consumer { /// Whether the cache is fed live, so the newest object it holds is the track's largest. /// Fetch-only, closed, and parked copies are not, even once they learn the end. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn is_live(&self) -> bool { let state = self.state.read(); let Some(routes) = state.routes.clone() else { @@ -3265,6 +3300,7 @@ impl Consumer { } /// The first group the declared end excludes whole, or `None` while the track is open ended. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn final_sequence(&self) -> Option { if let Some(serving) = self.serving() { return serving.final_sequence(); @@ -3454,6 +3490,7 @@ impl group::Request { /// Declare the track's exclusive final sequence, as the publisher answering this /// fetch reported it. A no-op once the track declared an end, or holds a later group. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn finish_track_at(&self, final_sequence: u64) { if let Ok(mut state) = TrackState::modify(&self.state) && state.end.is_none() @@ -4266,6 +4303,7 @@ impl Subscriber { /// be out of sequence due to network reordering or loss. Use [`Self::ordered`] if you /// only want groups whose sequence number is higher than any previously returned. /// See [`Self::poll_recv_group`] for how [`Self::set_groups`] applies. + #[cfg(feature = "async")] pub async fn recv_group(&mut self) -> Result> { kio::wait(|waiter| self.poll_recv_group(waiter)).await } @@ -4302,6 +4340,7 @@ impl Subscriber { /// namespace. To receive both concurrently from one subscriber, poll /// [`Self::poll_recv_group`] and [`Self::poll_recv_datagram`] together in a single `poll` /// closure (sequential `&mut` borrows), rather than awaiting the two `recv` futures at once. + #[cfg(feature = "async")] pub async fn recv_datagram(&mut self) -> Result> { kio::wait(|waiter| self.poll_recv_datagram(waiter)).await } @@ -4342,6 +4381,7 @@ impl Subscriber { /// Poll for where the source's feed starts, raised to this cursor's floor, once /// resolved; see [`Subscriber::poll_start`]. A feed starting below the floor serves the /// floor's group too. `None` when the source declares none. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_start(&mut self, waiter: &kio::Waiter) -> Poll> { let cursor = match &mut self.inner { Inner::Plain(cursor) => cursor, @@ -4354,6 +4394,7 @@ impl Subscriber { /// Poll for the track's cache reflecting its live feed (or the track ending), with the /// largest position it holds; `None` for a track with nothing yet. A front's logical /// track answers for the route serving it. See [`Producer::set_idle`]. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn poll_live(&mut self, waiter: &kio::Waiter) -> Poll> { let cursor = match &mut self.inner { Inner::Plain(cursor) => cursor, @@ -4386,6 +4427,7 @@ impl Subscriber { /// end, not that every group has arrived. A local session close without a declared /// end returns [`Error::Closed`]. Drive [`Self::recv_group`] (or /// [`Ordered::next_group`]) until it yields `None` to observe the track fully drained. + #[cfg(feature = "async")] pub async fn finished(&mut self) -> Result { kio::wait(|waiter| self.poll_finished(waiter)).await } @@ -4409,6 +4451,7 @@ impl Subscriber { /// skipped groups are still delivered and simply not returned. To ask the publisher /// to start there instead, set [`Subscription::start`] via [`Self::update`]. /// See [Local cursor vs wire preference](Self#local-cursor-vs-wire-preference). + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn start_at(&mut self, sequence: u64) { match &mut self.inner { Inner::Plain(cursor) => cursor.min_sequence = sequence, @@ -4542,6 +4585,7 @@ impl Ordered { } /// Return the next group with a higher sequence number than any previously returned. + #[cfg(feature = "async")] pub async fn next_group(&mut self) -> Result> { kio::wait(|waiter| self.poll_next_group(waiter)).await } @@ -4563,6 +4607,7 @@ impl Ordered { /// [`Self::poll_next_group`] and [`Self::poll_recv_datagram`] together in a single /// `poll` closure (sequential `&mut` borrows), rather than awaiting the two `recv` /// futures at once. + #[cfg(feature = "async")] pub async fn recv_datagram(&mut self) -> Result> { kio::wait(|waiter| self.poll_recv_datagram(waiter)).await } @@ -4601,6 +4646,7 @@ impl Ordered { /// /// See [`Subscriber::finished`]: this reports the declared end, not that every group /// has arrived. + #[cfg(feature = "async")] pub async fn finished(&mut self) -> Result { kio::wait(|waiter| self.poll_finished(waiter)).await } @@ -4618,7 +4664,7 @@ impl Ordered { /// A subscriber asked for a track this broadcast doesn't have yet. /// -/// Yielded by [`broadcast::Dynamic::requested_track`](crate::broadcast::Dynamic::requested_track), +/// Yielded by [`poll_requested_track`](crate::broadcast::Dynamic::poll_requested_track), /// or created up front with [`broadcast::Producer::reserve_track`](crate::broadcast::Producer::reserve_track). /// Subscribers block until the request is /// resolved: call [`accept`](Self::accept) to serve it with a [`Producer`], or @@ -4689,6 +4735,7 @@ impl Request { /// (lite-06+), so [`Subscriber::poll_start`] waits for its declaration instead of /// reading the requested floor as the start. Applied atomically with /// [`Self::accept`], before any reader can see the track. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn resolving_start(mut self) -> Self { self.resolving_start = true; self @@ -4744,6 +4791,7 @@ impl Request { /// Reject only while no consumer needs this pending track. Demand and the check /// share one lock, so demand returning after `Demand::poll_unused` wins the race, and the /// close under that lock stops a later consumer attaching to a dead request. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn reject_unused(&self, err: Error) -> bool { match self.state.write_unused() { kio::Unused::Idle(guard) => { @@ -4802,11 +4850,12 @@ impl Request { /// Block until the aggregate [`subscription`](Self::subscription) changes, /// yielding `None` once nobody is waiting. + #[cfg(feature = "async")] pub async fn subscription_changed(&mut self) -> Option { kio::wait(|waiter| self.poll_subscription_changed(waiter)).await } - /// Poll counterpart to [`subscription_changed`](Self::subscription_changed). + /// Poll for a changed subscription. pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll> { let state = self.state.read(); let (subs, bound) = (state.subscriptions.clone(), state.max_age_bound()); @@ -4830,7 +4879,7 @@ use futures::FutureExt; #[allow(missing_docs)] // test-only assertion helpers impl Subscriber { pub fn assert_group(&mut self) -> group::Consumer { - self.recv_group() + kio::wait(|waiter| self.poll_recv_group(waiter)) .now_or_never() .expect("group would have blocked") .expect("would have errored") @@ -4839,23 +4888,34 @@ impl Subscriber { pub fn assert_no_group(&mut self) { assert!( - self.recv_group().now_or_never().is_none(), + kio::wait(|waiter| self.poll_recv_group(waiter)) + .now_or_never() + .is_none(), "recv_group would not have blocked" ); } pub fn assert_not_closed(&mut self) { - assert!(self.finished().now_or_never().is_none(), "should not be closed"); + assert!( + kio::wait(|waiter| self.poll_finished(waiter)).now_or_never().is_none(), + "should not be closed" + ); } pub fn assert_closed(&mut self) { - assert!(self.finished().now_or_never().is_some(), "should be closed"); + assert!( + kio::wait(|waiter| self.poll_finished(waiter)).now_or_never().is_some(), + "should be closed" + ); } // TODO assert specific errors after implementing PartialEq pub fn assert_error(&mut self) { assert!( - self.finished().now_or_never().expect("should not block").is_err(), + kio::wait(|waiter| self.poll_finished(waiter)) + .now_or_never() + .expect("should not block") + .is_err(), "should be error" ); } @@ -4908,6 +4968,7 @@ mod test { } /// Helper: non-blocking datagram receive that must be ready with a datagram. + #[cfg(feature = "async")] fn recv_datagram(dg: &mut Subscriber) -> Datagram { dg.recv_datagram() .now_or_never() @@ -4930,6 +4991,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn append_datagram_roundtrip() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -4944,6 +5006,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_preserves_sequence() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -4960,6 +5023,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_leaves_a_gap() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -4974,6 +5038,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_out_of_order_does_not_rewind() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -4992,6 +5057,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_duplicate_is_best_effort() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -5010,6 +5076,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_stale_does_not_rewind_after_append() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -5029,6 +5096,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn insert_datagram_cloned_producers_share_counter() { let mut producer = track_producer("test", Info::timed()); let mut other = producer.clone(); @@ -5118,6 +5186,7 @@ mod test { /// Datagrams and groups are separate channels that share only a sequence namespace, /// so consuming one must not move the other's cursor. #[test] + #[cfg(feature = "async")] fn recv_datagram_leaves_the_ordered_cursor_alone() { let mut producer = track_producer("test", Info::timed()); let mut datagrams = producer.subscribe(None); @@ -5146,6 +5215,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_normalized_to_track_timescale() { let info = Info::default().with_timescale(Timescale::MICRO); let mut producer = track_producer("test", info); @@ -5176,6 +5246,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_fanout_to_subscribers() { let mut producer = track_producer("test", Info::timed()); // Two independent subscribers, each with its own datagram cursor. @@ -5194,6 +5265,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_replay_starts_within_max_delay() { let info = Info::default().with_timescale(Timescale::MICRO); let mut producer = track_producer("sparse", info); @@ -5222,6 +5294,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_replay_starts_after_evicted_entries_with_zero_delay() { let mut producer = track_producer("test", Info::timed()); for _ in 0..MAX_DATAGRAMS + 1 { @@ -5236,6 +5309,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn untimed_datagram_replay_keeps_the_buffer() { let mut producer = untimed_producer(); for _ in 0..3 { @@ -5250,6 +5324,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_buffer_drops_oldest_at_capacity() { let mut producer = track_producer("test", Info::timed()); let mut slow = producer.subscribe(None); @@ -5267,6 +5342,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn datagram_recv_pends_until_written() { let mut producer = track_producer("test", Info::timed()); let mut dg = producer.subscribe(None); @@ -5312,6 +5388,7 @@ mod test { /// in range, sent between the dropped ones, show the reader is live, so the range is /// what dropped the others. #[test] + #[cfg(feature = "async")] fn the_group_range_bounds_datagrams() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_start(Position::group(5))); @@ -5330,6 +5407,7 @@ mod test { /// A start past the start group's first frame excludes that group's datagram, its only /// frame; a start at frame 0 keeps it. #[test] + #[cfg(feature = "async")] fn a_mid_group_start_drops_the_start_groups_datagram() { let mut producer = track_producer("test", Info::timed()); let mut mid = producer.subscribe(Subscription::default().with_start(Position { group: 5, frame: 1 })); @@ -5348,6 +5426,7 @@ mod test { /// A range update judges the datagrams still buffered when they are read, and one it /// dropped stays dropped once the cap rises again: nothing holds it like a parked group. #[test] + #[cfg(feature = "async")] fn a_range_update_applies_to_buffered_datagrams() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -5374,6 +5453,7 @@ mod test { /// (everything but the QUIC datagram send/recv), catching any field-order mismatch between /// the wire codec and the model. #[test] + #[cfg(feature = "async")] fn datagram_wire_roundtrip_between_tracks() { use crate::coding::Encode; use crate::lite; @@ -5454,6 +5534,7 @@ mod test { /// a subscriber that already drained it must still see the clean end of group. Otherwise a /// track with long groups (a per-minute rollup, say) fails its readers at every boundary. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn aging_out_a_finished_group_keeps_the_clean_end() { let producer = track_producer("test", Info::timed()); let mut group = producer.create_group(group::Info { sequence: 0 }).unwrap(); @@ -5476,6 +5557,7 @@ mod test { /// read restarts the retention clock. A group nobody reads still ages out on /// schedule, so reclamation stays intact. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn active_reader_survives_expiry() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -5513,6 +5595,7 @@ mod test { /// which bounds frames, not elapsed time, so a slow `read_frame` reader has to /// re-stamp on a time bound between refills. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn slow_prefetch_reader_survives_expiry() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -5541,6 +5624,7 @@ mod test { /// delivery just before the group would age out still gets to read it a full /// window later. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn delivery_restarts_the_expiry_clock() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(replay()); @@ -5639,6 +5723,7 @@ mod test { /// Re-offering a parked group (once the cap rises) is a delivery: it restarts /// the expiry clock so the subscriber gets to read what it was just handed. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn parked_reoffer_restarts_the_expiry_clock() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -6113,6 +6198,7 @@ mod test { } /// Every group the subscriber can read right now, in delivery order. + #[cfg(feature = "async")] fn drain(subscriber: &mut Subscriber) -> Vec { let mut sequences = Vec::new(); while let Some(Ok(Some(group))) = subscriber.recv_group().now_or_never() { @@ -6129,6 +6215,7 @@ mod test { /// answer would read as contiguous, and the cached group would reach all the way to the /// answer, fresh to any positive budget. #[test] + #[cfg(feature = "async")] fn an_answer_past_a_reset_group_skips_the_cache() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); @@ -6151,6 +6238,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn real_time_skips_a_backlog_to_the_live_edge() { let mut producer = track_producer("test", Info::timed()); for second in 0..5 { @@ -6169,6 +6257,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn real_time_skips_a_backlog_after_catching_up() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); @@ -6184,6 +6273,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_newer_edge_changes_an_active_catch_up() { let mut producer = track_producer("test", Info::timed()); for second in 0..5 { @@ -6209,6 +6299,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_growing_edge_changes_an_active_catch_up() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); @@ -6237,6 +6328,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_budget_admits_groups_within_it() { let mut producer = track_producer("test", Info::timed()); for second in 0..5 { @@ -6252,6 +6344,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_budget_reaches_back_over_the_cache() { let mut producer = track_producer("test", Info::timed()); for second in 0..5 { @@ -6273,6 +6366,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_named_start_is_a_floor_not_a_request() { let mut producer = track_producer("test", Info::timed()); for second in 0..5 { @@ -6302,6 +6396,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_floor_above_the_live_edge_waits_there() { let mut producer = track_producer("test", Info::timed()); for second in 0..3 { @@ -6324,6 +6419,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_late_lower_group_within_the_budget_is_delivered() { let producer = track_producer("test", Info::timed()); for (sequence, millis) in [(5, 0), (6, 1000), (7, 2000)] { @@ -6348,6 +6444,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn drift_is_measured_in_presentation_time_not_arrival_time() { let mut producer = track_producer("test", Info::timed()); // A relay ingesting a backlog creates every group at once, so arrival time says @@ -6362,6 +6459,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_stamped_successor_expires_an_unstamped_group() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -6375,6 +6473,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_handed_out_group_expires_while_its_first_frame_is_stalled() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -6402,6 +6501,7 @@ mod test { /// A rewound successor can keep a drained group within budget until its abort /// moves the reach to the next cached group, without changing the track itself. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn aborted_stamped_successor_wakes_a_parked_read() { let mut producer = track_producer("test", Info::timed()); let mut head = producer.append_group().unwrap(); @@ -6436,6 +6536,7 @@ mod test { /// a group is bounded by where its successor begins, so the successor itself never /// proves it stale. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_handed_out_group_wakes_when_a_newer_group_gets_its_first_timestamp() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_millis(500))); @@ -6557,6 +6658,7 @@ mod test { /// plus its budget. A first frame below the edge moves nothing, and a new edge short of /// the deadline cannot convict it, so neither wakes it. #[test] + #[cfg(feature = "async")] fn a_parked_read_wakes_only_once_the_edge_reaches_its_deadline() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(10))); @@ -6611,6 +6713,7 @@ mod test { /// An aborted edge hands the edge to a group between it and the successor, and neither /// the abort nor that group's first frame touches the track, so the read must still wake. #[test] + #[cfg(feature = "async")] fn a_parked_read_watches_its_edge_abort() { let mut producer = track_producer("test", crate::track::Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(5))); @@ -6654,6 +6757,7 @@ mod test { /// An aborted successor hands the reach to the next group, which sits below the edge /// and so is watched only because the read re-selects its successor. #[test] + #[cfg(feature = "async")] fn a_parked_read_watches_the_replacement_for_an_aborted_successor() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_millis(500))); @@ -6705,6 +6809,7 @@ mod test { /// past the deadline. No frame write crosses it afterwards, and the abort never touches /// the track, so only the read watching the edge's abort sees the head go stale. #[test] + #[cfg(feature = "async")] fn a_parked_read_watches_its_rewound_edge_abort() { let mut producer = track_producer("test", crate::track::Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(5))); @@ -6743,6 +6848,7 @@ mod test { /// still sees the older edge and parks again. Revealing the group has to wake it once /// more, though the read sits below the group's nearest shown predecessor. #[test] + #[cfg(feature = "async")] fn revealing_a_presented_group_wakes_a_read_it_expires() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_millis(500))); @@ -6786,6 +6892,7 @@ mod test { /// Those groups are withheld until their frames land, so the end is not reached before /// they show: a reader ending at the boundary would never see them. #[test] + #[cfg(feature = "async")] fn a_withheld_group_holds_the_end() { let mut producer = track_producer("test", crate::track::Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(60))); @@ -6812,6 +6919,7 @@ mod test { /// A withheld group whose stream ends with no frame, cleanly or reset, shows when it /// ends, which wakes a reader parked on the end. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_withheld_group_that_ends_empty_releases_the_end() { for abort in [false, true] { let mut producer = track_producer("test", None); @@ -6859,6 +6967,7 @@ mod test { /// at its group's end when that happens, because the FIN and the next group's first /// frame are separate events and the wire does not order them. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn real_time_reads_a_live_stream_without_truncating_it() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -6933,6 +7042,7 @@ mod test { /// Measuring from the group's first frame instead makes the drift 2000ms, so a /// budget shorter than one GOP would drop the tail of every GOP. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_budget_is_measured_from_the_readers_position() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(1))); @@ -6979,6 +7089,7 @@ mod test { /// turn the clean answer into [`Error::Old`] on the next poll, and a caller is /// allowed to probe again past the end of a group. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_ended_group_stays_ended_when_probed_again() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -7006,6 +7117,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_drained_group_finishes_cleanly_after_the_live_edge_advances() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -7021,6 +7133,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_handed_out_partial_frame_expires_while_its_payload_is_stalled() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -7055,6 +7168,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn max_age_bounds_the_budget() { // The publisher only keeps a group around for 500ms, so a subscriber asking to // wait ten seconds for one still gives up at 500ms: the same clamp the aggregate @@ -7071,6 +7185,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn an_explicit_start_gets_no_exemption_from_the_budget() { let mut producer = track_producer("test", Info::timed()); for second in 0..4 { @@ -7096,6 +7211,7 @@ mod test { /// tail is level with the live edge still owes the reader every frame in it, so /// judging it by its first timestamp would discard exactly the group being filled. #[test] + #[cfg(feature = "async")] fn a_long_group_is_not_stale_while_its_tail_reaches_the_edge() { let mut producer = track_producer("test", Info::timed()); @@ -7119,6 +7235,7 @@ mod test { /// where it ends; only group 1's start bounds it. Group 0 is convicted once that bound /// is further behind the edge than the budget allows. #[test] + #[cfg(feature = "async")] fn a_long_group_is_stale_once_its_successor_falls_behind() { let mut producer = track_producer("test", Info::timed()); @@ -7141,6 +7258,7 @@ mod test { /// stamped group proves nothing about where the successor will begin, and /// shrinking the bound is the unsafe direction. #[test] + #[cfg(feature = "async")] fn an_unstamped_immediate_successor_leaves_reach_unbounded() { let mut producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -7161,6 +7279,7 @@ mod test { /// successor begins. Taking the minimum would shrink the bound and discard content that /// is still well inside the budget. #[test] + #[cfg(feature = "async")] fn reach_follows_the_immediate_successor_not_a_later_rewind() { let mut producer = track_producer("test", Info::timed()); @@ -7180,6 +7299,7 @@ mod test { ); } + #[cfg(feature = "async")] fn untimed_producer() -> Producer { track_producer("test", None) } @@ -7203,6 +7323,7 @@ mod test { } /// A group of untimed frames. + #[cfg(feature = "async")] fn append_untimed(producer: &mut Producer) -> u64 { let mut group = producer.append_group().unwrap(); group.write_frame(None, bytes::Bytes::from_static(b"x")).unwrap(); @@ -7213,6 +7334,7 @@ mod test { /// Nothing on an untimed track is ever stale, so a budget can't resolve where a new /// subscriber starts. It takes the latest group instead of replaying the cache. #[test] + #[cfg(feature = "async")] fn an_untimed_track_starts_at_its_latest_group() { let mut producer = untimed_producer(); for _ in 0..5 { @@ -7235,6 +7357,7 @@ mod test { /// An explicit start says how far back to reach, so it wins over the untimed jump to /// the latest group (a resume after failover names the group it lacks). #[test] + #[cfg(feature = "async")] fn an_explicit_start_holds_on_an_untimed_track() { let mut producer = untimed_producer(); for _ in 0..5 { @@ -7248,6 +7371,7 @@ mod test { /// An unfloored untimed subscription with an end starts at the latest group below it, /// as the lite publisher serves a SUBSCRIBE with an end and no start. #[test] + #[cfg(feature = "async")] fn an_untimed_start_stays_under_the_end() { let mut producer = untimed_producer(); for _ in 0..5 { @@ -7295,6 +7419,7 @@ mod test { /// Datagrams are unordered by construction, so the sequence cursor carries them too: /// a track using both channels needs one subscription, not two. #[test] + #[cfg(feature = "async")] fn ordered_carries_datagrams() { let mut producer = track_producer("test", Info::timed()); let mut sub = producer.subscribe(None).ordered(); @@ -7323,6 +7448,7 @@ mod test { } /// Every group the ordered cursor can read right now, in sequence order. + #[cfg(feature = "async")] fn drain_ordered(subscriber: &mut Ordered) -> Vec { let mut sequences = Vec::new(); while let Some(Ok(Some(group))) = subscriber.next_group().now_or_never() { @@ -7335,6 +7461,7 @@ mod test { /// otherwise replay it at 1x and stay behind forever, since nothing it reads ever /// blocks. The budget applies as the cursor reads, exactly as on the arrival cursor. #[test] + #[cfg(feature = "async")] fn next_group_sheds_a_stale_backlog() { let mut producer = track_producer("test", Info::timed()); for second in 0..4 { @@ -7353,6 +7480,7 @@ mod test { /// successor begins), so a budget spanning part of the backlog keeps every group that /// could still present something inside it, and the burst is delivered gap-free. #[test] + #[cfg(feature = "async")] fn next_group_keeps_a_backlog_inside_the_budget() { let mut producer = track_producer("test", Info::timed()); for second in 0..4 { @@ -7374,6 +7502,7 @@ mod test { /// A group whose immediate successor has not presented a frame has no proven reach, /// so nothing says its frames are too old and the ordered cursor keeps it. #[test] + #[cfg(feature = "async")] fn next_group_keeps_a_group_with_no_proven_reach() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); // seq 0 @@ -7387,6 +7516,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn real_time_skips_older_sequences_with_equal_ages() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); @@ -7450,6 +7580,7 @@ mod test { /// A one-shot fetch populates the shared cache but never the live arrival cursor, /// so it cannot make content available to a subscription look stale. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetched_group_is_not_a_live_drift_edge() { let mut producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -7510,6 +7641,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_lower_sequence_is_never_the_live_edge() { let producer = track_producer("test", Info::timed()); // A high timestamp on a lower sequence is not a live edge: backfill served on @@ -7533,6 +7665,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_requested_end_does_not_cap_the_live_edge() { let mut producer = track_producer("test", Info::timed()); for second in 0..4 { @@ -7548,6 +7681,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn a_capped_subscriber_measures_drift_against_its_cap() { let mut producer = track_producer("test", Info::timed()); append_at(&mut producer, 0); @@ -7566,6 +7700,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn subscriber_control_updates_while_read_future_is_pending() { let producer = track_producer("test", Info::timed()); let mut subscriber = producer.subscribe(None); @@ -7771,6 +7906,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn abort_drops_open_groups() { let producer = track_producer("test", Info::timed()); producer.append_group().unwrap(); @@ -7791,6 +7927,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn drop_unfinished_clears_cached_groups() { let producer = track_producer("test", Info::timed()); let writer = producer.clone(); @@ -7839,6 +7976,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn drop_finished_keeps_cached_groups() { let producer = track_producer("test", Info::timed()); producer.append_group().unwrap(); @@ -7857,6 +7995,7 @@ mod test { /// the missing groups will never come: the reader ends cleanly rather than with /// `Dropped`, as it would for a track that never declared its end. #[test] + #[cfg(feature = "async")] fn drop_short_of_the_boundary_ends_cleanly() { let mut producer = track_producer("test", Info::timed()); producer.append_group().unwrap(); @@ -7880,6 +8019,7 @@ mod test { /// The sequence cursor ends the same way. A gap below the boundary is skipped, /// a later cached group is still delivered, and the read finishes cleanly. #[test] + #[cfg(feature = "async")] fn ordered_ends_cleanly_when_sealed_short_of_the_boundary() { let mut producer = track_producer("test", Info::timed()); producer.create_group(group::Info { sequence: 0 }).unwrap(); @@ -8061,6 +8201,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn finish_at_declares_a_future_boundary() { let mut producer = track_producer("test", Info::timed()); producer.create_group(group::Info { sequence: 5 }).unwrap(); @@ -8096,6 +8237,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn readers_end_at_the_boundary_with_missing_lower_groups() { let mut producer = track_producer("test", Info::timed()); let mut arrival = producer.subscribe(None); @@ -8126,6 +8268,7 @@ mod test { /// lower group's stream may arrive after a higher one, until the hole fills or the tail /// settles. The last producer going ends them regardless. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_pending_tail_holds_readers_at_a_hole() { let mut producer = track_producer("test", None); let mut arrival = producer.subscribe(None); @@ -8169,6 +8312,7 @@ mod test { /// A fetched copy of a group the live feed still owes never reaches an arrival-order /// reader, so it does not fill the hole: the reader waits for the live group. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_fetched_group_does_not_fill_a_pending_tail() { let mut producer = track_producer("test", None); let dynamic = producer.dynamic(); @@ -8191,6 +8335,7 @@ mod test { /// An abort ends the wait for a pending tail. Whatever reached the end finished, so /// readers end cleanly: the cache cannot tell a hole from a group it already evicted. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_abort_ends_a_pending_tail_cleanly() { let mut producer = track_producer("test", None); let mut arrival = producer.subscribe(None); @@ -8209,6 +8354,7 @@ mod test { /// reader already past that group stays held at the hole: the end never shows without /// its pending tail. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn an_end_after_the_highest_group_holds_the_reader() { let mut producer = track_producer("test", None); let mut arrival = producer.subscribe(None); @@ -8238,6 +8384,7 @@ mod test { /// An abort before the declared end settled wins over it: the boundary was reached, /// but a group below it was still open, so the track was cut off rather than ended. #[test] + #[cfg(feature = "async")] fn abort_before_the_end_settles_wins() { let mut producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8254,6 +8401,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn local_close_keeps_finished_groups_without_declaring_an_end() { let producer = track_producer("local", Info::timed()); let mut consumer = producer.consume().subscribe(None).await.unwrap(); @@ -8266,6 +8414,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn local_close_cannot_mask_an_abort_before_declared_end_settles() { let mut producer = track_producer("local", Info::timed()); let mut consumer = producer.consume().subscribe(None).await.unwrap(); @@ -8302,6 +8451,7 @@ mod test { /// pulled them yet: it gets them, then the abort. The open group nobody will finish /// is gone, on both cursors. #[test] + #[cfg(feature = "async")] fn abort_keeps_finished_groups_for_a_slow_reader() { let producer = track_producer("test", Info::timed()); let mut arrival = producer.subscribe(None); @@ -8333,6 +8483,7 @@ mod test { /// The last producer dropping without a finish keeps the finished groups the same way. #[test] + #[cfg(feature = "async")] fn dropped_producer_keeps_finished_groups_for_a_slow_reader() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8387,6 +8538,7 @@ mod test { /// An abort after every group below the declared end finished leaves the end standing. #[test] + #[cfg(feature = "async")] fn abort_after_the_end_settles_ends_clean() { let mut producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8407,6 +8559,7 @@ mod test { /// The settled end stands on the ordered cursor too: a reader that starts after the /// abort gets every group below the end, then the clean end, not the abort. #[test] + #[cfg(feature = "async")] fn abort_after_the_end_settles_ends_clean_for_an_ordered_reader() { let mut producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None).ordered(); @@ -8431,6 +8584,7 @@ mod test { /// capped below the end, and with a gap below the cap. Parking is not an option on a /// closed track (the channel would turn it into the abort), so these end clean. #[test] + #[cfg(feature = "async")] fn abort_after_the_end_settles_ends_clean_below_the_cap() { let mut producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None).ordered(); @@ -8482,6 +8636,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn recv_group_finishes_without_waiting_for_gaps() { let producer = track_producer("test", Info::timed()); producer.create_group(group::Info { sequence: 1 }).unwrap(); @@ -8499,6 +8654,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn next_group_skips_late_arrivals() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None).ordered(); @@ -8536,6 +8692,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn next_group_returns_arrivals_in_order() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -8562,6 +8719,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn ordered_and_arrival_cursors_are_independent() { let producer = track_producer("test", Info::timed()); let mut ordered = producer.subscribe(replay()).ordered(); @@ -8586,6 +8744,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn end_at_caps_next_group() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -8618,6 +8777,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn end_at_release_drains_cached_groups() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -8663,6 +8823,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn end_at_lower_than_cursor_parks_consumer() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -8707,6 +8868,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn end_at_toggling_around_late_arrivals() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -8753,6 +8915,7 @@ mod test { /// `next_group`: beyond-cap groups are held, not dropped, and a raised cap /// re-offers them, even after the track finishes. #[test] + #[cfg(feature = "async")] fn end_at_parks_recv_group() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()); @@ -8793,6 +8956,7 @@ mod test { /// A group beyond the cap must not block in-range groups that arrive behind /// it: a relay can ingest a burst micro-reordered (newest first). #[test] + #[cfg(feature = "async")] fn recv_group_serves_arrivals_behind_the_cap() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()); @@ -8822,6 +8986,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn group_ranges_preserve_the_floor_when_the_cap_changes() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8841,6 +9006,7 @@ mod test { /// A raised `start_at` drops parked groups it overtook instead of re-offering /// them once the cap rises. #[test] + #[cfg(feature = "async")] fn start_at_drops_parked_recv_groups() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8866,6 +9032,7 @@ mod test { /// neither delivered once the cap rises nor allowed to hold the stream open /// after the track finishes. This is what bounds parking by the cache policy. #[test] + #[cfg(feature = "async")] fn evicted_parked_recv_groups_are_dropped() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(None); @@ -8898,6 +9065,7 @@ mod test { /// else will poll it (the track already finished), so the entry has to carry a /// waiter or the subscription sleeps forever holding its stream open. #[test] + #[cfg(feature = "async")] fn evicted_parked_group_wakes_the_clean_end() { use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Wake}; @@ -8941,6 +9109,7 @@ mod test { /// An exclusive cap at 0 is the empty range: no group is delivered, even group 0. #[test] + #[cfg(feature = "async")] fn end_at_zero_is_the_empty_range() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()); @@ -8964,6 +9133,7 @@ mod test { /// A requested empty range plus a local empty cap still parks while another /// subscriber keeps aggregate demand unbounded. #[test] + #[cfg(feature = "async")] fn empty_local_cap_holds_while_another_subscriber_requests_everything() { let producer = track_producer("test", Info::timed()); let mut everything = producer.subscribe(replay()); @@ -9000,6 +9170,7 @@ mod test { /// A frame-limited exclusive end includes the last group and stops before that frame. #[test] + #[cfg(feature = "async")] fn end_at_frame_limited_last_group() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -9041,6 +9212,7 @@ mod test { /// An inclusive bound at the last group withholds nothing. #[test] + #[cfg(feature = "async")] fn end_at_maximum_group_is_unbounded() { let producer = track_producer("test", Info::timed()); let mut consumer = producer.subscribe(replay()).ordered(); @@ -9083,6 +9255,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_cache_hit() { let producer = track_producer("test", Info::timed()); @@ -9107,6 +9280,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_miss_signals_dynamic() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9140,6 +9314,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_miss_rejects() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9159,6 +9334,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_miss_drop_rejects() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9178,6 +9354,7 @@ mod test { /// A request stays wanted while any joined fetch waits, and once the last leaves it /// is withdrawn: a later fetch starts a fresh request instead of joining it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_request_unused_once_every_fetch_leaves() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9211,6 +9388,7 @@ mod test { /// A fetch abandoned before any handler took it is withdrawn from the queue too, so /// no handler serves it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_abandoned_while_queued_never_reaches_the_handler() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9225,6 +9403,7 @@ mod test { /// The withdrawal happens as the last caller leaves, not when the handler notices: a /// fetch arriving before the handler drops the abandoned request is not failed with it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_after_the_last_caller_left_is_not_dropped() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9254,6 +9433,7 @@ mod test { /// A handler that accepts after every caller left still caches the group, so a fetch /// that queued a fresh request meanwhile resolves from it and that request is moot. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_accept_after_withdrawal_caches_the_group() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9298,6 +9478,7 @@ mod test { /// A fetch that hits the cache holds the group until polled, so a handler that aborts /// the group once nobody wants it (an abandoned upstream fetch) leaves it alone. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_hit_keeps_the_group_wanted() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9325,6 +9506,7 @@ mod test { /// wider fetch arriving before the handler writes misses instead of being handed a /// reader that would skip the head it asked for. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn accepted_group_starts_at_the_request() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9351,6 +9533,7 @@ mod test { /// Once a group nobody wanted is aborted, a later fetch misses rather than reading /// the abort, and queues a fresh request. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_misses_an_abandoned_group() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9377,6 +9560,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_reject_does_not_poison_retry() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9411,6 +9595,7 @@ mod test { /// someone who asked for the whole group would silently hand back a tail, so it is a /// miss and the fetch goes upstream instead. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_ignores_a_group_that_starts_too_late() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9459,6 +9644,7 @@ mod test { /// in arrival order: subscribers get the fetched copy while it lasts, and the live /// group again once the fetch is gone. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_fills_the_head_of_a_live_group() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9503,6 +9689,7 @@ mod test { /// A refused mid-group end caps none of its group's copies, even one that fit under it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_refused_end_leaves_every_copy_uncapped() { let mut producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9528,6 +9715,7 @@ mod test { } /// A live group with a fetched head: group 0, fed from frame 1, plus a two-frame head. + #[cfg(feature = "async")] async fn live_group_with_head(producer: &Producer) -> (group::Producer, group::Producer) { let dynamic = producer.dynamic(); let consumer = producer.consume(); @@ -9552,6 +9740,7 @@ mod test { /// The live copy ending early (its subscription went away) leaves a fetched head /// that still holds the whole group servable, instead of reclaiming the slot. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn aborted_live_group_keeps_its_fetched_head() { let producer = track_producer("test", Info::timed()); let (live, _head) = live_group_with_head(&producer).await; @@ -9570,6 +9759,7 @@ mod test { /// Reads land on the fetched head, so they keep the slot from expiring even though /// the live copy beside it sits idle. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn reading_the_head_keeps_the_slot_from_expiring() { let producer = track_producer("test", Info::timed()); let (live, _head) = live_group_with_head(&producer).await; @@ -9587,6 +9777,7 @@ mod test { /// Fill group 0 as a live copy plus a fetched head written after it, optionally /// abort the live copy, then write past capacity into a protected latest group, so /// slot 0 is the only evictable content. Returns whether pressure evicted it. + #[cfg(feature = "async")] async fn pressure_evicts_a_headed_slot(abort_live: bool) -> bool { let (producer, _pool) = pooled_producer(10_000); let dynamic = producer.dynamic(); @@ -9618,6 +9809,7 @@ mod test { /// A head left alone in its slot still joins the pool's access average when the /// slot is demoted, so memory pressure can evict it. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_head_only_slot_yields_to_memory_pressure() { assert!(pressure_evicts_a_headed_slot(true).await); } @@ -9625,6 +9817,7 @@ mod test { /// A slot weighs its copies the way the pool's average samples them, so two copies /// with different access stamps can't keep it above the average on their own. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn a_two_copy_slot_yields_to_memory_pressure() { assert!(pressure_evicts_a_headed_slot(false).await); } @@ -9632,6 +9825,7 @@ mod test { /// Joining a queued fetch widens its range, and a caller that arrives once the range /// is already on the wire fails cleanly rather than being handed the narrower group. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_widens_or_fails_cleanly() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9677,6 +9871,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_coalesces_concurrent() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9716,6 +9911,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_coalesced_reject_fails_all() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9745,6 +9941,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_queued_fails_when_handlers_leave() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -9762,6 +9959,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_miss_no_dynamic_not_found() { // A track with no `Dynamic` can't serve old content, so a cache miss // resolves to NotFound instead of blocking forever. @@ -9772,6 +9970,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_past_final_not_found() { let producer = track_producer("test", Info::timed()); producer.append_group().unwrap(); // seq 0 @@ -9839,6 +10038,7 @@ mod test { /// The latest group is never in the eviction order, so it survives any budget. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn latest_group_never_evicted() { // Far too small for even one group: the latest survives anyway. let (mut producer, pool) = pooled_producer(100); @@ -9859,6 +10059,7 @@ mod test { /// recently than the pool-wide average is protected, so the eviction walk skips /// it and evicts a never-read group instead, even one that arrived later. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_refresh_survives_eviction() { let (mut producer, _pool) = pooled_producer(10_000); let consumer = producer.consume(); @@ -9888,6 +10089,7 @@ mod test { /// A consumer holding an evicted group surfaces the eviction, not a hang or a /// truncated clean end. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn eviction_aborts_readers() { let (mut producer, _pool) = pooled_producer(10_000); let mut subscriber = producer.subscribe(None); @@ -9963,6 +10165,7 @@ mod test { /// counter: the counter is owned by the track state, so replacing the info /// can't strand the bytes already-created groups keep charging. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn accept_preserves_write_accounting() { let config = cache::Config::default() .with_capacity(12_000) @@ -10156,6 +10359,7 @@ mod test { /// account exists from the moment the state does, so acceptance replacing the /// `Info` can't leave already-created groups writing for free. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn pre_accept_backfill_settles_late_writes() { let config = cache::Config::default() .with_capacity(2_000) @@ -10224,6 +10428,7 @@ mod test { /// Continuously refreshed entries at the front of the eviction order must not /// starve expiry of entries behind them: the scan cursor rotates. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn refreshed_front_does_not_starve_expiry() { let (producer, _pool) = pooled_producer(1 << 40); let dynamic = producer.dynamic(); @@ -10358,6 +10563,7 @@ mod test { /// A stale arrival entry whose sequence was later re-served by fetched backfill /// must not leak the replacement into arrival-order subscriptions. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn refilled_sequence_stays_out_of_subscriptions() { let (producer, _pool) = pooled_producer(1 << 40); let dynamic = producer.dynamic(); @@ -10394,6 +10600,7 @@ mod test { /// An expired backfill can't hide behind a refreshed one: the eviction-order /// expiry scans a bounded prefix instead of stopping at the first fresh entry. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn expired_backfill_behind_refreshed_reclaimed() { let (producer, _pool) = pooled_producer(1 << 40); let dynamic = producer.dynamic(); @@ -10429,6 +10636,7 @@ mod test { /// A FETCH hit within the same coarse clock tick still protects the group: the /// refresh stamps one tick ahead, so it reads strictly newer than the mean. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn same_tick_fetch_protects() { // No time advances at all: every timestamp lands in the same tick. let (mut producer, _pool) = pooled_producer(10_000); @@ -10451,6 +10659,7 @@ mod test { /// not re-enter the eviction order, or memory pressure could evict the newest /// content. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn refetched_latest_stays_protected() { let (producer, _pool) = pooled_producer(10_000); let dynamic = producer.dynamic(); @@ -10492,6 +10701,7 @@ mod test { /// An evicted group is a cache miss, so a fetch re-fetches it and the accepted /// replacement serves the sequence again (not `Error::Duplicate`). #[moq_net_sim::test] + #[cfg(feature = "async")] async fn eviction_allows_refetch() { let (mut producer, _pool) = pooled_producer(10_000); let dynamic = producer.dynamic(); @@ -10560,6 +10770,7 @@ mod test { /// A fetched (backfill) group is served by sequence but never replayed to /// arrival-order subscribers. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetched_backfill_not_subscribed() { let (producer, _pool) = pooled_producer(1 << 40); let dynamic = producer.dynamic(); @@ -10595,6 +10806,7 @@ mod test { /// Fetched backfill isn't in arrival order, so it ages out through the eviction /// order instead of lingering until the track closes. #[moq_net_sim::test] + #[cfg(feature = "async")] async fn expired_backfill_reclaimed() { let (producer, pool) = pooled_producer(1 << 40); let dynamic = producer.dynamic(); @@ -10626,6 +10838,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn request_and_dynamic_share_weak_track_demand() { let request = Request::new(Arc::new(broadcast::Info::default()), "demand"); let dynamic = request.dynamic(); @@ -10643,6 +10856,7 @@ mod test { } #[test] + #[cfg(feature = "async")] fn fetch_request_demand_counts_every_joined_caller() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); @@ -10731,6 +10945,7 @@ mod test { } #[moq_net_sim::test] + #[cfg(feature = "async")] async fn fetch_aborts_with_track() { let producer = track_producer("test", Info::timed()); let dynamic = producer.dynamic(); diff --git a/rs/moq-net/src/path/mod.rs b/rs/moq-net/src/path/mod.rs index 1a9b402b33d..f59f87eff16 100644 --- a/rs/moq-net/src/path/mod.rs +++ b/rs/moq-net/src/path/mod.rs @@ -138,6 +138,7 @@ impl<'a> Path<'a> { } } + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn from_escaped(s: String) -> PathOwned { if s.is_empty() { Path::empty() diff --git a/rs/moq-net/src/setup.rs b/rs/moq-net/src/setup.rs index a84b5ee0437..5723b9fc48f 100644 --- a/rs/moq-net/src/setup.rs +++ b/rs/moq-net/src/setup.rs @@ -13,12 +13,16 @@ use crate::{ }; // SETUP only carries negotiation parameters. Bound it independently of control messages. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const MAX_SETUP_SIZE: usize = u16::MAX as usize; +#[cfg_attr(not(feature = "async"), allow(dead_code))] const CLIENT_SETUP: u8 = 0x20; +#[cfg_attr(not(feature = "async"), allow(dead_code))] const SERVER_SETUP: u8 = 0x21; /// Draft-17 unified SETUP message type (varint 0x2F00) +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const SETUP_V17: u64 = 0x2F00; /// The moq-transport Setup Options this side offers, each on by default. @@ -65,11 +69,13 @@ impl Token { /// Draft-17+ unified SETUP message, with the same encoding for both client and server. #[derive(Debug, Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Setup { pub parameters: Bytes, } impl Setup { + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn check_version(v: Version) { match v { Version::Ietf(ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16) @@ -103,6 +109,7 @@ impl Decode for Setup { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] enum SetupVersion { Draft14, Draft15Plus, @@ -113,6 +120,7 @@ enum SetupVersion { } impl SetupVersion { + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn from_version(v: Version) -> Self { match v { Version::Ietf(ietf::Version::Draft14) => Self::Draft14, @@ -131,6 +139,7 @@ impl SetupVersion { /// A version-agnostic setup message sent by the client. #[derive(Debug, Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Client { /// The list of supported versions in preferred order. pub versions: coding::Versions, @@ -140,6 +149,7 @@ pub(crate) struct Client { } impl Client { + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn encode_inner(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { match SetupVersion::from_version(v) { SetupVersion::Draft15Plus => { @@ -192,6 +202,7 @@ impl Encode for Client { } /// Read a pre-draft-17 SETUP body: its size, then that many bytes. +#[cfg_attr(not(feature = "async"), allow(dead_code))] fn decode_body<'a>(r: &mut Decoder<'a>, v: Version) -> Result, DecodeError> { let size = match SetupVersion::from_version(v) { SetupVersion::Draft14 | SetupVersion::Draft15Plus => r.u16()? as usize, @@ -208,6 +219,7 @@ fn decode_body<'a>(r: &mut Decoder<'a>, v: Version) -> Result, Decod } /// Reserve the size prefix of a pre-draft-17 SETUP body. +#[cfg_attr(not(feature = "async"), allow(dead_code))] fn prefix_body(w: &mut Encoder<'_>, v: Version) -> Result { match SetupVersion::from_version(v) { SetupVersion::Draft14 | SetupVersion::Draft15Plus => Ok(w.prefix_u16()), @@ -217,6 +229,7 @@ fn prefix_body(w: &mut Encoder<'_>, v: Version) -> Result, prefix: coding::Prefix) -> Result<(), EncodeError> { if w.since(&prefix) > MAX_SETUP_SIZE { w.discard(prefix); @@ -227,6 +240,7 @@ fn fill_body(w: &mut Encoder<'_>, prefix: coding::Prefix) -> Result<(), EncodeEr /// Sent by the server in response to a client setup. #[derive(Debug, Clone)] +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) struct Server { /// The list of supported versions in preferred order. pub version: coding::Version, @@ -236,6 +250,7 @@ pub(crate) struct Server { } impl Server { + #[cfg_attr(not(feature = "async"), allow(dead_code))] fn encode_inner(&self, w: &mut Encoder<'_>, v: Version) -> Result<(), EncodeError> { match SetupVersion::from_version(v) { SetupVersion::Draft15Plus => { diff --git a/rs/moq-net/src/stats.rs b/rs/moq-net/src/stats.rs index bcaf8a86f9e..b6b3da01eee 100644 --- a/rs/moq-net/src/stats.rs +++ b/rs/moq-net/src/stats.rs @@ -16,8 +16,8 @@ //! # Where counting happens //! //! Counting lives in the model layer, not the wire loops. A session tags its -//! origin pair with a [`Session`] context ([`crate::Client::with_stats`] / -//! [`crate::Server::with_stats`], which call `origin::{Consumer, Producer}` +//! origin pair with a [`Session`] context (`crate::Client::with_stats` / +//! `crate::Server::with_stats`, which call `origin::{Consumer, Producer}` //! `with_stats`); every derived handle (broadcast, announce, track, group, frame) //! then attributes its reads (egress = publisher) and writes (ingress = //! subscriber) through that context. So any protocol that drives the model gets @@ -61,7 +61,7 @@ //! the new one. //! * `announces_peak` / `subscriptions_peak` ([`Presence`]): the most //! announcements, or subscriptions, any one session under the auth root held -//! at once, to compare against its [`crate::session::Limits`]. A peak never +//! at once, to compare against its `crate::session::Limits`. A peak never //! goes down; a rollup takes the largest rather than the sum. //! //! Counters are strictly monotonic (only `fetch_add`); a counter going @@ -523,7 +523,7 @@ impl Traffic { /// Connected-session presence for one slice (an auth root on a tier, or any /// sum of such slices): cumulative connects and disconnects. `sessions_started /// - sessions_ended` is the current live session count. The peaks show how close -/// one session came to its [`crate::session::Limits`]. +/// one session came to its `crate::session::Limits`. /// /// Like [`Traffic`], this is also the wire shape of one entry on a published /// sessions, presence, or totals track. Serialize writes both the canonical names and the legacy @@ -925,6 +925,7 @@ impl Registry { /// module exposes this to the stats producer bench. A report drops the entry /// unless a guard still holds it. #[cfg(any(test, feature = "fuzz"))] + #[cfg(feature = "async")] pub(crate) fn bump_publisher_bytes(&self, path: impl AsPath, tier: &Tier, n: u64) { let Some(entry) = self.entry(path) else { return; @@ -1078,8 +1079,8 @@ impl Default for Registry { } } -/// Tier-scoped wrapper around [`Registry`]. What [`crate::Client::with_stats`] and -/// [`crate::Server::with_stats`] accept. Cheap to clone. +/// Tier-scoped wrapper around [`Registry`]. What `crate::Client::with_stats` and +/// `crate::Server::with_stats` accept. Cheap to clone. #[derive(Clone)] pub struct Handle { stats: Registry, @@ -1161,10 +1162,12 @@ pub struct Session { inner: Option>, } -/// Which of a session's [`crate::session::Limits`] a [`Session::hold`] counts against. +/// Which of a session's `crate::session::Limits` a [`Session::hold`] counts against. #[derive(Clone, Copy, Debug)] pub(crate) enum Cap { + #[cfg_attr(not(feature = "async"), allow(dead_code))] Announces, + #[cfg_attr(not(feature = "async"), allow(dead_code))] Subscriptions, } @@ -1251,6 +1254,7 @@ impl Session { } /// Count `live` as what this session holds against `cap`, so a tier change carries it. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn track_held(&self, cap: Cap, live: &Arc) { let Some(inner) = &self.inner else { return }; let mut held = inner.held.lock().unwrap_or_else(PoisonError::into_inner); @@ -1261,6 +1265,7 @@ impl Session { /// Record that this session holds `held` of what `cap` limits, raising that peak on its /// tier and root. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn hold(&self, cap: Cap, held: u64) { let Some(inner) = &self.inner else { return }; let current = inner.current.lock().unwrap_or_else(PoisonError::into_inner); @@ -1651,6 +1656,7 @@ mod tests { /// Each root's sessions row carries the most any one of its sessions held against each /// cap, which a slot released does not lower and a rollup does not sum. #[test] + #[cfg(feature = "async")] fn sessions_report_their_peaks_against_the_caps() { use crate::session::Slots; @@ -1683,6 +1689,7 @@ mod tests { /// A session moving tiers brings the slots it still holds to the new tier's peak, but /// not the old tier's history. #[test] + #[cfg(feature = "async")] fn a_tier_change_carries_the_slots_still_held() { use crate::session::Slots; diff --git a/rs/moq-net/src/time.rs b/rs/moq-net/src/time.rs index 7a713aa7678..d5c878a48e2 100644 --- a/rs/moq-net/src/time.rs +++ b/rs/moq-net/src/time.rs @@ -35,6 +35,7 @@ pub trait Driver { /// /// Tokio on native, `setTimeout` in the browser. Resolves with the driver's /// terminal error, [`Error::Closed`] for a clean finish. +#[cfg(feature = "async")] pub async fn run(mut driver: D) -> Error { let mut timer: Option>> = None; kio::wait(|waiter| { @@ -92,6 +93,7 @@ impl Clock { /// A clock the test executor advances with its simulated time. Outside the /// executor, a synchronous test never waits on a deadline, so nothing advances it. #[cfg(test)] + #[cfg(feature = "async")] pub(crate) fn sim() -> Self { if !moq_net_sim::is_running() { return Self::new(Instant::now()); @@ -105,6 +107,7 @@ impl Clock { clock } + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn new(now: Instant) -> Self { let clock = Self::default(); clock.advance(now); @@ -202,6 +205,7 @@ impl Deadline { /// A duration the clock cannot represent (e.g. [`std::time::Duration::MAX`]) /// leaves the deadline disarmed, so it never fires rather than panicking on /// the overflow. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn after(clock: &Clock, duration: std::time::Duration) -> Self { let mut deadline = Self::new(clock); deadline.set(clock.now().checked_add(duration)); @@ -238,6 +242,7 @@ impl Deadline { } /// The instant this fires at, or `None` while disarmed. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn deadline(&self) -> Option { self.at } diff --git a/rs/moq-net/src/util.rs b/rs/moq-net/src/util.rs index c1eee584891..12e8b819212 100644 --- a/rs/moq-net/src/util.rs +++ b/rs/moq-net/src/util.rs @@ -45,6 +45,7 @@ fn future_task(mut task: MaybeSendBox<'static, ()>) -> MaybeSendTask { /// /// The building block for racing "watchdog" futures whose clean completion should /// not end the race (only their failure should). +#[cfg(feature = "async")] pub(crate) async fn err_only(fut: impl Future>) -> E { match fut.await { Err(err) => err, @@ -84,6 +85,7 @@ impl Tasks { /// /// A task submitted after the set is gone is dropped: the driver has torn down, /// so the task would have been cancelled anyway. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn push(&self, task: impl MaybeBoxedExt<'static, Output = ()>) { let mut state = self.state.lock(); if !state.closed { @@ -174,6 +176,7 @@ impl TaskSet { } /// Queue a future for polling by this set. + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub fn push(&mut self, task: impl MaybeBoxedExt<'static, Output = ()>) { self.active.push(future_task(task.maybe_boxed())); } @@ -222,6 +225,7 @@ impl TaskSet { /// `f` is a poll function rather than a future, so an accept loop polls the /// transport directly and there is no accept future to cancel or rebuild when /// a child finishes. + #[cfg(feature = "async")] pub async fn drive(&mut self, mut f: impl FnMut(&kio::Waiter) -> Poll + Unpin) -> T { kio::wait(|waiter| { if let Poll::Ready(output) = f(waiter) { @@ -239,6 +243,7 @@ impl TaskSet { /// active children finish. The async form of [`Self::poll`], for callers with /// nothing else to poll. #[cfg(test)] + #[cfg(feature = "async")] pub async fn run(mut self) { kio::wait(|waiter| self.poll(waiter)).await } @@ -258,7 +263,7 @@ impl Drop for TaskSet { } } -#[cfg(test)] +#[cfg(all(test, feature = "async"))] mod tests { use std::sync::{ Arc, diff --git a/rs/moq-net/src/version.rs b/rs/moq-net/src/version.rs index 457eee3156a..4dd475fd29f 100644 --- a/rs/moq-net/src/version.rs +++ b/rs/moq-net/src/version.rs @@ -8,6 +8,7 @@ use crate::{coding, ietf, lite}; /// Ordered by preference, with the client's preference taking priority. /// This intentionally includes only SETUP-negotiated versions (Lite02, Lite01, Draft14); /// Lite03 and newer IETF drafts negotiate via dedicated ALPNs instead. +#[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) const NEGOTIATED: [Version; 3] = [ Version::Lite(lite::Version::Lite02), Version::Lite(lite::Version::Lite01), @@ -84,6 +85,7 @@ pub enum Version { } impl Version { + #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn session_code(self, error: &crate::SessionError) -> u32 { match self { Self::Lite(_) => crate::lite::error::to_session_code(error), diff --git a/rs/moq-net/tests/announce_covering_route.rs b/rs/moq-net/tests/announce_covering_route.rs index 98b52791862..faa8bf1104c 100644 --- a/rs/moq-net/tests/announce_covering_route.rs +++ b/rs/moq-net/tests/announce_covering_route.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A peer asking for announcements below a route's prefix hears that route as //! covering the prefix it asked for, the way a local consumer rooted there does. //! diff --git a/rs/moq-net/tests/announce_to_serve.rs b/rs/moq-net/tests/announce_to_serve.rs index 300261fc7bb..ac28e54e59f 100644 --- a/rs/moq-net/tests/announce_to_serve.rs +++ b/rs/moq-net/tests/announce_to_serve.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A broadcast exists for other people only while it is announced, and a consumer //! in the same process sees exactly what a consumer across a session sees. //! diff --git a/rs/moq-net/tests/auth.rs b/rs/moq-net/tests/auth.rs index 5f5c87b27ef..ee2f9e96a9b 100644 --- a/rs/moq-net/tests/auth.rs +++ b/rs/moq-net/tests/auth.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! In-band AUTH over the in-memory mock transport: both sides learn their grant, //! tokens union, and a publication outside the grant fails loud. Every case runs on //! moq-lite-07-wip and on moq-transport with the MoQ Auth extension. diff --git a/rs/moq-net/tests/broadcast_close.rs b/rs/moq-net/tests/broadcast_close.rs index bf0700b0701..cb8a0317530 100644 --- a/rs/moq-net/tests/broadcast_close.rs +++ b/rs/moq-net/tests/broadcast_close.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! `broadcast::Producer::close()` ends a broadcast the same way for a local consumer //! and a remote one: the handle closes, every new track lookup answers //! [`Error::Unroutable`](moq_net::Error::Unroutable), a fresh request for the path diff --git a/rs/moq-net/tests/catalog_acquisition.rs b/rs/moq-net/tests/catalog_acquisition.rs index b0d0c60719a..8a9dfabc10a 100644 --- a/rs/moq-net/tests/catalog_acquisition.rs +++ b/rs/moq-net/tests/catalog_acquisition.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Initial catalog acquisition while the same publisher gains a second session. mod support; diff --git a/rs/moq-net/tests/datagram.rs b/rs/moq-net/tests/datagram.rs index 1a77dd3c232..c3e10c1e10f 100644 --- a/rs/moq-net/tests/datagram.rs +++ b/rs/moq-net/tests/datagram.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Datagram delivery over the in-memory mock transport, on moq-lite and moq-transport. //! //! Covers the whole receive path end to end: publisher encoding, the transport's diff --git a/rs/moq-net/tests/dial_split_horizon.rs b/rs/moq-net/tests/dial_split_horizon.rs index cf8754e6da6..93715edbe57 100644 --- a/rs/moq-net/tests/dial_split_horizon.rs +++ b/rs/moq-net/tests/dial_split_horizon.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A dialed moq-transport session whose peer declares no hop must not be offered //! a route learned from that peer, and a SUBSCRIBE that arrives for that route //! must not be sent back to the peer that announced it. diff --git a/rs/moq-net/tests/drained_claim.rs b/rs/moq-net/tests/drained_claim.rs index 6f09bc1d673..fac272a2f84 100644 --- a/rs/moq-net/tests/drained_claim.rs +++ b/rs/moq-net/tests/drained_claim.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A relay's front for a path ends once unread, end to end over real sessions. //! //! Two workers each claim the `pool` prefix with `origin::Producer::dynamic` and diff --git a/rs/moq-net/tests/empty_group.rs b/rs/moq-net/tests/empty_group.rs index 5abe054854f..cda9fa705df 100644 --- a/rs/moq-net/tests/empty_group.rs +++ b/rs/moq-net/tests/empty_group.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A group finished with no frames reaches the subscriber as an empty group. On drafts 14 //! to 17 its stream is a bare header with no FIRST_OBJECT bit, which must not read as a //! group missing its head. diff --git a/rs/moq-net/tests/extensions.rs b/rs/moq-net/tests/extensions.rs index 5817aa6f359..2245bda4ae6 100644 --- a/rs/moq-net/tests/extensions.rs +++ b/rs/moq-net/tests/extensions.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A side may decline a moq-transport extension with `setup::Extensions`, and then //! connects as a peer that does not speak it. //! diff --git a/rs/moq-net/tests/fetch_cancel.rs b/rs/moq-net/tests/fetch_cancel.rs index 17bbf328130..9713dd94498 100644 --- a/rs/moq-net/tests/fetch_cancel.rs +++ b/rs/moq-net/tests/fetch_cancel.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A FETCH nobody wants anymore is cancelled all the way to the publisher. //! //! Each relay on the path holds a FETCH stream and the group behind it. When the diff --git a/rs/moq-net/tests/fetch_from_feed.rs b/rs/moq-net/tests/fetch_from_feed.rs index d849351f1ed..207c1351ca4 100644 --- a/rs/moq-net/tests/fetch_from_feed.rs +++ b/rs/moq-net/tests/fetch_from_feed.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A relay answers a fetch over a pre-lite-05 upstream, which has no FETCH, from its live //! subscription: a group the feed delivers is served, and one it never will is a miss, //! never a version error that a caller would retry forever. diff --git a/rs/moq-net/tests/fetch_only.rs b/rs/moq-net/tests/fetch_only.rs index e36a1d84582..44aea98130a 100644 --- a/rs/moq-net/tests/fetch_only.rs +++ b/rs/moq-net/tests/fetch_only.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Fetching a group from a moq-transport publisher never subscribes to its track. //! //! The subscriber learns the track from TRACK_STATUS instead, so a finished track is still diff --git a/rs/moq-net/tests/finished_broadcast_mock.rs b/rs/moq-net/tests/finished_broadcast_mock.rs index b8d3a84b0cc..6f45072b9a5 100644 --- a/rs/moq-net/tests/finished_broadcast_mock.rs +++ b/rs/moq-net/tests/finished_broadcast_mock.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Regression: a publisher that finishes its track and then its broadcast, over one //! direct session with no relay, has the subscription reset with //! [`Error::Dropped`](moq_net::Error::Dropped) and the track's content lost. diff --git a/rs/moq-net/tests/finite_relay.rs b/rs/moq-net/tests/finite_relay.rs index 8e2da116a17..1eb555e7176 100644 --- a/rs/moq-net/tests/finite_relay.rs +++ b/rs/moq-net/tests/finite_relay.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A finite track read through a relay: every group below the declared end, then a clean //! end, even when the relay's subscriber reaches the end before the groups finish. //! diff --git a/rs/moq-net/tests/goaway.rs b/rs/moq-net/tests/goaway.rs index c054cf7104d..018f1bfa270 100644 --- a/rs/moq-net/tests/goaway.rs +++ b/rs/moq-net/tests/goaway.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! GOAWAY behavior tests over the in-memory mock transport. //! //! The mock delivers all queued bytes deterministically (no real QUIC or diff --git a/rs/moq-net/tests/group_charge.rs b/rs/moq-net/tests/group_charge.rs index 95be7b2377d..c69f5ca4ce7 100644 --- a/rs/moq-net/tests/group_charge.rs +++ b/rs/moq-net/tests/group_charge.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! What a cached group really costs, measured against what the pool charges for it. //! //! `cache::ENTRY_OVERHEAD` is derived from `size_of`, which keeps it following the diff --git a/rs/moq-net/tests/group_order.rs b/rs/moq-net/tests/group_order.rs index b4f7db31910..0baba6f72fd 100644 --- a/rs/moq-net/tests/group_order.rs +++ b/rs/moq-net/tests/group_order.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A subscription's `Order` decides which pending group a publisher sends first, directly //! and through a relay, and one upstream request serves subscribers of both orders. //! diff --git a/rs/moq-net/tests/head_fetch.rs b/rs/moq-net/tests/head_fetch.rs index e34edfef5a5..6a44bb04038 100644 --- a/rs/moq-net/tests/head_fetch.rs +++ b/rs/moq-net/tests/head_fetch.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A relay that fetches the head of a group it is receiving mid-group keeps delivering //! that group to arrival-order subscribers, along with the frames written into it after. //! diff --git a/rs/moq-net/tests/history_groups.rs b/rs/moq-net/tests/history_groups.rs index 8f19f90cfcd..9bedb5efa54 100644 --- a/rs/moq-net/tests/history_groups.rs +++ b/rs/moq-net/tests/history_groups.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A fresh subscription from group 0 receives a finished older group alongside the open //! newer one, however their streams race into a relay. //! diff --git a/rs/moq-net/tests/idle_cancel_refetch.rs b/rs/moq-net/tests/idle_cancel_refetch.rs index c07914c05c9..f0f17b0ec84 100644 --- a/rs/moq-net/tests/idle_cancel_refetch.rs +++ b/rs/moq-net/tests/idle_cancel_refetch.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A relay whose last reader leaves mid-group cancels the idle upstream subscription. A later //! fetch of that group still gets every frame (#5348, a release-line regression). diff --git a/rs/moq-net/tests/idle_session.rs b/rs/moq-net/tests/idle_session.rs index 26081742c90..04ec56f004c 100644 --- a/rs/moq-net/tests/idle_session.rs +++ b/rs/moq-net/tests/idle_session.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! An idle viewer session costs its server nothing per tick: no driver wakes and //! no transport stats reads while nothing moves. A server holds thousands of these, //! so a fixed-interval timer per session adds up to whole cores. diff --git a/rs/moq-net/tests/late_lower_group.rs b/rs/moq-net/tests/late_lower_group.rs index efa3a4aeef6..98cb74e523a 100644 --- a/rs/moq-net/tests/late_lower_group.rs +++ b/rs/moq-net/tests/late_lower_group.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A subscriber with an explicit floor receives a group created below the first group //! that was served, while it is still inside `max_delay`. //! diff --git a/rs/moq-net/tests/loom.rs b/rs/moq-net/tests/loom.rs index 23312ea5446..dc191dd6917 100644 --- a/rs/moq-net/tests/loom.rs +++ b/rs/moq-net/tests/loom.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Loom model checks for the model layer (broadcast -> track -> group -> frame). //! //! The unit tests drive these handles from one task at a time, so they can only diff --git a/rs/moq-net/tests/mesh_withdraw.rs b/rs/moq-net/tests/mesh_withdraw.rs index 6217d04a4e4..78ba885591f 100644 --- a/rs/moq-net/tests/mesh_withdraw.rs +++ b/rs/moq-net/tests/mesh_withdraw.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! Broadcasts withdrawn from a meshed cluster retract everywhere, once. //! //! Every relay holds a route through each peer that re-advertised a broadcast. diff --git a/rs/moq-net/tests/model.rs b/rs/moq-net/tests/model.rs new file mode 100644 index 00000000000..1f59bc6b983 --- /dev/null +++ b/rs/moq-net/tests/model.rs @@ -0,0 +1,48 @@ +//! The explicit-time model works without async helpers or a runtime. + +use moq_net::{Error, Hop, Timestamp, broadcast, frame, group, kio, origin, track}; +use std::task::Poll; + +fn ready(value: Poll) -> T { + match value { + Poll::Ready(value) => value, + Poll::Pending => panic!("model operation unexpectedly pending"), + } +} + +#[test] +fn origin_front_serves_media_with_only_explicit_polls() { + let now = moq_net::time::Instant::now(); + let waiter = kio::Waiter::noop(); + let (origin, mut driver) = origin::Producer::new(origin::Config::new(Hop::new(1).unwrap())); + driver.poll(now, &waiter).unwrap(); + let broadcast = origin.create_broadcast("room/live").unwrap(); + let track = broadcast + .create_track( + "video", + track::Info::default().with_timescale(moq_net::Timescale::MILLI), + ) + .unwrap(); + let mut group = track.create_group(group::Info { sequence: 7 }).unwrap(); + group.write_frame(Timestamp::ZERO, b"media").unwrap(); + group.finish().unwrap(); + broadcast.announce(origin::Route::default()).unwrap(); + + // Resolving and subscribing pass through the origin's core front tasks. + let requesting = origin.consume().request_broadcast("room/live", None); + assert!(requesting.poll_ok(&waiter).is_pending()); + driver.poll(now, &waiter).unwrap(); + let receiving: broadcast::Consumer = ready(requesting.poll_ok(&waiter)).unwrap(); + let subscribing = receiving.track("video").unwrap().subscribe(None); + driver.poll(now, &waiter).unwrap(); + let mut subscriber = ready(subscribing.poll_ok(&waiter)).unwrap(); + let mut group = ready(subscriber.poll_recv_group(&waiter)).unwrap().unwrap(); + assert_eq!(group.sequence, 7); + let media: frame::Frame = ready(group.poll_read_frame(&waiter)).unwrap().unwrap(); + assert_eq!(media.payload.as_ref(), b"media"); + assert!(ready(group.poll_read_frame(&waiter)).unwrap().is_none()); + + // Driver teardown also works without any executor-owned tasks. + drop(driver); + assert!(matches!(origin.create_broadcast("after-close"), Err(Error::Closed))); +} diff --git a/rs/moq-net/tests/multi_cdn.rs b/rs/moq-net/tests/multi_cdn.rs index 291143613cb..bda5fb8390c 100644 --- a/rs/moq-net/tests/multi_cdn.rs +++ b/rs/moq-net/tests/multi_cdn.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! An endpoint linked to two CDNs uses its preferred one and fails over to the next. //! //! A publisher `P` pushes one broadcast, under one epoch, to CDN relays `A` and `B`. A diff --git a/rs/moq-net/tests/quiet_catalog.rs b/rs/moq-net/tests/quiet_catalog.rs index d0d9a6128df..f8f95ed7c98 100644 --- a/rs/moq-net/tests/quiet_catalog.rs +++ b/rs/moq-net/tests/quiet_catalog.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A catalog publishes one finished group and then stays quiet. Media hides a //! subscription that starts past a group, because the next group arrives; a //! catalog does not produce one, so a reader that never received the snapshot diff --git a/rs/moq-net/tests/rejoin.rs b/rs/moq-net/tests/rejoin.rs index d6f194b388f..d5c5e7f2ead 100644 --- a/rs/moq-net/tests/rejoin.rs +++ b/rs/moq-net/tests/rejoin.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A reader leaving and rejoining a track relayed over the in-memory mock transport. mod support; diff --git a/rs/moq-net/tests/replacement.rs b/rs/moq-net/tests/replacement.rs index 63fe772cfe4..5ac121d0e04 100644 --- a/rs/moq-net/tests/replacement.rs +++ b/rs/moq-net/tests/replacement.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A replacement session, end to end over real sessions: a second session from the //! same peer re-announces every route while the first one is still open, as a GOAWAY //! drain does. diff --git a/rs/moq-net/tests/request_caps.rs b/rs/moq-net/tests/request_caps.rs index 3415481390a..f7e948092ec 100644 --- a/rs/moq-net/tests/request_caps.rs +++ b/rs/moq-net/tests/request_caps.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A server's [`moq_net::session::Limits`] bound what one client can make it hold: a client //! past a cap loses the session with TOO_MANY_REQUESTS, and on moq-transport drafts 14 to 16 //! the MAX_REQUEST_ID window they size is granted back as requests close, so a client that diff --git a/rs/moq-net/tests/restart.rs b/rs/moq-net/tests/restart.rs index 389e4dfb293..1d9d43538aa 100644 --- a/rs/moq-net/tests/restart.rs +++ b/rs/moq-net/tests/restart.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A replaced broadcast restarts announce consumers across relays, end to end over //! real sessions, while the subscriptions already on it stay. //! diff --git a/rs/moq-net/tests/route_change.rs b/rs/moq-net/tests/route_change.rs index d18be569e92..f299e0c9e0a 100644 --- a/rs/moq-net/tests/route_change.rs +++ b/rs/moq-net/tests/route_change.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A subscription survives its route changing, end to end over real sessions. //! //! A publisher `P` is pulled by two relays `A` and `B`, both of which re-advertise diff --git a/rs/moq-net/tests/session_close.rs b/rs/moq-net/tests/session_close.rs index 8329310d503..19b2bcb189a 100644 --- a/rs/moq-net/tests/session_close.rs +++ b/rs/moq-net/tests/session_close.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! `Session::close` delivers what the session queued before closing, within a deadline, //! while `abort` still closes at once. //! diff --git a/rs/moq-net/tests/session_panic.rs b/rs/moq-net/tests/session_panic.rs index 9e2c599051a..8ff80b487bf 100644 --- a/rs/moq-net/tests/session_panic.rs +++ b/rs/moq-net/tests/session_panic.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A panic in one session's driver ends only that session. //! //! A host contains such a panic to the task driving the session (see diff --git a/rs/moq-net/tests/settled_end_survives_abort.rs b/rs/moq-net/tests/settled_end_survives_abort.rs index bf438f41160..91e376ecc7c 100644 --- a/rs/moq-net/tests/settled_end_survives_abort.rs +++ b/rs/moq-net/tests/settled_end_survives_abort.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A track whose declared end has settled holds every group it promised: the end was //! reached and each group below it finished. An abort that lands afterwards, such as //! the session dying, ends the track cleanly but must not throw those groups away: a diff --git a/rs/moq-net/tests/subscribe_tracks.rs b/rs/moq-net/tests/subscribe_tracks.rs index 4bac13268c3..892faba5b9b 100644 --- a/rs/moq-net/tests/subscribe_tracks.rs +++ b/rs/moq-net/tests/subscribe_tracks.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A SUBSCRIBE_TRACKS is refused on its own stream, and the session keeps delivering. //! //! Deterministic: paused time and a single-threaded runtime over the mock transport. diff --git a/rs/moq-net/tests/subscription_end_integrity.rs b/rs/moq-net/tests/subscription_end_integrity.rs index 76dd4f241a2..26ba2a08acc 100644 --- a/rs/moq-net/tests/subscription_end_integrity.rs +++ b/rs/moq-net/tests/subscription_end_integrity.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A subscription ends cleanly only when every group in its range has been accounted //! for; a session closing with a group still in flight ends it with an error. //! diff --git a/rs/moq-net/tests/track_abort_over_session.rs b/rs/moq-net/tests/track_abort_over_session.rs index d30370ea68d..78092394ce1 100644 --- a/rs/moq-net/tests/track_abort_over_session.rs +++ b/rs/moq-net/tests/track_abort_over_session.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A track aborted by its publisher while the broadcast stays announced, read by a //! subscriber on the far side of one session. diff --git a/rs/moq-net/tests/track_info_cancel.rs b/rs/moq-net/tests/track_info_cancel.rs index 1473969cf29..087caff7898 100644 --- a/rs/moq-net/tests/track_info_cancel.rs +++ b/rs/moq-net/tests/track_info_cancel.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A track request nobody wants anymore is cancelled all the way to the publisher. //! //! Until a publisher answers TRACK_INFO, each relay on the path holds a task, a TRACK diff --git a/rs/moq-net/tests/track_stream_demand.rs b/rs/moq-net/tests/track_stream_demand.rs index 6ac6780e54a..aa05ee3ce03 100644 --- a/rs/moq-net/tests/track_stream_demand.rs +++ b/rs/moq-net/tests/track_stream_demand.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A subscription's demand holds across its TRACK and SUBSCRIBE streams. //! //! On moq-lite 05 and later a subscriber learns a track's properties over a TRACK stream diff --git a/rs/moq-net/tests/track_tail.rs b/rs/moq-net/tests/track_tail.rs index 3a06257c0fd..5aa94eb55dd 100644 --- a/rs/moq-net/tests/track_tail.rs +++ b/rs/moq-net/tests/track_tail.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A track's tail: a group stream that reaches the subscriber after the publisher has //! ended the subscription still belongs to the track. //! diff --git a/rs/moq-net/tests/unannounce_release.rs b/rs/moq-net/tests/unannounce_release.rs index a3e0f15269b..4bb181f9a16 100644 --- a/rs/moq-net/tests/unannounce_release.rs +++ b/rs/moq-net/tests/unannounce_release.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A relay reading a broadcast that its publisher unannounced still cancels its upstream //! subscription once its last reader leaves, so the publisher's demand goes unused. diff --git a/rs/moq-net/tests/untimed.rs b/rs/moq-net/tests/untimed.rs index e30cd0b01d9..883ca9a332c 100644 --- a/rs/moq-net/tests/untimed.rs +++ b/rs/moq-net/tests/untimed.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! An untimed track reaches every subscriber untimed, through a relay too, on every wire //! that can say so, and a timed track keeps its timestamps. //! diff --git a/rs/moq-net/tests/wasm.rs b/rs/moq-net/tests/wasm.rs index 01af522bf0c..c53677c3ee9 100644 --- a/rs/moq-net/tests/wasm.rs +++ b/rs/moq-net/tests/wasm.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! wasm32 model-layer tests. //! //! moq-net's model layer (Origin/Broadcast/Track/Group/Frame) is transport- diff --git a/rs/moq-net/tests/wide_sequence.rs b/rs/moq-net/tests/wide_sequence.rs index 6ccfbcbed07..11f6763bb5e 100644 --- a/rs/moq-net/tests/wide_sequence.rs +++ b/rs/moq-net/tests/wide_sequence.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "async")] + //! A group sequence only moq-lite 07 can carry, relayed to a moq-lite 06 reader. mod support; diff --git a/sh/rs/net-features.sh b/sh/rs/net-features.sh new file mode 100644 index 00000000000..8cac14f23bc --- /dev/null +++ b/sh/rs/net-features.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build moq-net alone: a workspace build unifies its default features back in. +# Normal dependencies exclude the simulation executor and bench-only Tokio. +minimal=$(cargo tree --locked -p moq-net --no-default-features -e normal --prefix none) +if grep -qE '^(tokio|web-async) ' <<<"$minimal"; then + echo "moq-net without async must not depend on a native runtime" >&2 + exit 1 +fi + +cargo clippy --locked -p moq-net --no-default-features --all-targets -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc --locked -p moq-net --no-default-features --no-deps +bash sh/rs/nextest.sh cargo nextest run --locked -p moq-net --no-default-features --all-targets + +# A browser leaf chooses its entropy backend. Build a consumer instead of moq-net's +# native-only bench dev-dependencies, with the same dependency versions as the repo. +consumer=$(mktemp -d) +trap 'rm -rf "$consumer"' EXIT +mkdir "$consumer/src" +cat > "$consumer/Cargo.toml" < "$consumer/src/lib.rs" <<'EOF' +pub fn poll(driver: &mut moq_net::origin::Driver, now: moq_net::time::Instant) { + let _ = driver.poll(now, &moq_net::kio::Waiter::noop()); +} +EOF +cp Cargo.lock "$consumer/Cargo.lock" +# The copied lock only needs the temporary leaf's own package entry added. +cargo check --manifest-path "$consumer/Cargo.toml" --target wasm32-unknown-unknown --target-dir "${CARGO_TARGET_DIR:-target}" From 70abeb187f52e8ff7052919ec46c67dc2312169a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 13 Oct 2026 22:34:24 -0700 Subject: [PATCH 3/6] Format the no-async browser consumer check Co-Authored-By: GPT-6 --- sh/rs/net-features.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sh/rs/net-features.sh b/sh/rs/net-features.sh index 8cac14f23bc..dcd71025e4d 100644 --- a/sh/rs/net-features.sh +++ b/sh/rs/net-features.sh @@ -18,7 +18,7 @@ bash sh/rs/nextest.sh cargo nextest run --locked -p moq-net --no-default-feature consumer=$(mktemp -d) trap 'rm -rf "$consumer"' EXIT mkdir "$consumer/src" -cat > "$consumer/Cargo.toml" <"$consumer/Cargo.toml" < "$consumer/src/lib.rs" <<'EOF' +cat >"$consumer/src/lib.rs" <<'EOF' pub fn poll(driver: &mut moq_net::origin::Driver, now: moq_net::time::Instant) { let _ = driver.poll(now, &moq_net::kio::Waiter::noop()); } From 26d40525c2d04269cf93acf2f252a816d5c1e86e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 14 Oct 2026 09:43:31 -0700 Subject: [PATCH 4/6] Resolve routed broadcasts through a poll core `routed_broadcast` was an async-only retry loop, so a consumer without the `async` feature had to reimplement route resolution. It now returns `kio::Pending`, a Task that watches the covering routes and re-asks when they move, like `request_broadcast` returns `Requesting`. PR checks run the no-default moq-net lane whenever moq-net is selected. Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/src/model/mod.rs | 2 +- rs/moq-net/src/model/origin.rs | 110 +++++++++++++++++++++------------ rs/moq-net/tests/model.rs | 29 +++++++++ sh/rs/select.sh | 2 + 4 files changed, 102 insertions(+), 41 deletions(-) diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index b320c4034ae..37faf7e32ef 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -45,7 +45,7 @@ pub use timed::Timed; pub mod origin { pub use super::origin_impl::{ Config, Consumer, Cost, DEFAULT_UPDATE_HOLD, Driver, Dynamic, Preference, Producer, Request, Requesting, Route, - Source, + Routing, Source, }; } diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 806a8ca6adf..d4771532410 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -4665,6 +4665,64 @@ impl kio::Task for Requesting { } } +/// The pollable result of [`Consumer::routed_broadcast`]. +pub struct Routing { + consumer: Consumer, + path: PathOwned, + // The covering routes, watched since the generation the last ask saw. + watch: Option<(Watch, u64)>, + // The ask in flight, dropped once it fails `Unroutable` until the routes move. + request: Option, +} + +impl kio::Task for Routing { + type Output = Result; + + fn poll(&mut self, waiter: &kio::Waiter) -> Poll { + loop { + match (&self.watch, &self.request) { + (_, Some(request)) => match ready!(request.poll_ok(waiter)) { + Ok(broadcast) => return Poll::Ready(Ok(broadcast)), + Err(Error::Unroutable) => self.request = None, + // Teardown parks a pending request with `Dropped`; the contract is + // `Closed` once the origin is gone. + Err(Error::Dropped) if self.consumer.shared.lock().closed => { + return Poll::Ready(Err(Error::Closed)); + } + Err(err) => return Poll::Ready(Err(err)), + }, + (Some((watch, seen)), None) => { + ready!(watch.poll_changed(waiter, *seen)); + self.watch = None; + } + (None, None) => { + // `Unroutable` is a verdict of the routes covering the path as they + // stood when the request was made. Re-asking the same routes would + // spin, so watch them before asking and wait for them to move (a + // route arriving or retracting, an identical standby swapping in, a + // local broadcast announcing at the path), then try again. A change + // between the ask and the wait bumps the watch first, so that retry + // is immediate; the teardown pokes every watch, so a closed origin + // is observed on the next pass. + let mut table = self.consumer.shared.lock(); + if table.closed { + return Poll::Ready(Err(Error::Closed)); + } + let named = self.consumer.root.join(&self.path); + let Some(resolved) = self.consumer.scope.resolve(&named) else { + return Poll::Ready(Err(BoundsExceeded.into())); + }; + let watch = table.watch(&self.consumer.shared, &resolved); + let seen = watch.seen(); + drop(table); + self.watch = Some((watch, seen)); + self.request = Some(self.consumer.request(self.path.as_path(), None).into_inner()); + } + } + } + } +} + /// Derive a read view from a handle. /// /// Lets APIs accept either a producer or a consumer (e.g. @@ -5079,8 +5137,8 @@ impl Consumer { Ok(crate::announce::Follow::new(announced, path.to_owned())) } - /// Block until `path` resolves to a broadcast: [`Self::request_broadcast`], - /// retried whenever the routes covering the path change. + /// Resolve `path` to a broadcast: [`Self::request_broadcast`], retried + /// whenever the routes covering the path change. /// /// A request answers for the routes as they stand, so it can miss an /// announcement that has not arrived yet, lose its covering route to @@ -5091,47 +5149,19 @@ impl Consumer { /// call for resolving a path right after connecting. Returns /// [`Error::Unauthorized`] for a path outside this consumer's scope, /// [`Error::Closed`] once the origin closes, and any other resolution - /// failure as-is. - #[cfg(feature = "async")] - pub async fn routed_broadcast(&self, path: impl AsPath) -> Result { - let path = path.as_path(); + /// failure as-is. Await it, or poll it through [`kio::Task`]. + pub fn routed_broadcast(&self, path: impl AsPath) -> kio::Pending { + let path = path.as_path().to_owned(); // `allowed` keeps narrower permissions intact: if the whole path is not // reachable, no route can ever cover it, so bail rather than loop forever. - if !self.allowed().matches(path.as_str()) { - return Err(Error::Unauthorized); - } - loop { - // `Unroutable` is a verdict of the routes covering the path as they - // stood when the request was made. Re-asking the same routes would - // spin, so watch them before asking and wait for them to move (a - // route arriving or retracting, an identical standby swapping in, a - // local broadcast announcing at the path), then try again. A change - // between the ask and the wait bumps the watch first, so that retry - // is immediate; the teardown pokes every watch, so a closed origin - // is observed on the next pass. - let (watch, seen) = { - let mut table = self.shared.lock(); - if table.closed { - return Err(Error::Closed); - } - let named = self.root.join(&path); - let resolved = self.scope.resolve(&named).ok_or(BoundsExceeded)?; - let watch = table.watch(&self.shared, &resolved); - let seen = watch.seen(); - (watch, seen) - }; - match self.request_broadcast(&path, None).await { - Ok(broadcast) => return Ok(broadcast), - Err(Error::Unroutable) => { - kio::wait(|waiter| watch.poll_changed(waiter, seen)).await; - } - // Teardown parks a pending request with `Dropped`; the contract is - // `Closed` once the origin is gone. - Err(Error::Dropped) if self.shared.lock().closed => return Err(Error::Closed), - Err(err) => return Err(err), - } - } + let request = (!self.allowed().matches(path.as_str())).then(|| Requesting::failed(Error::Unauthorized)); + kio::Pending::new(Routing { + consumer: self.clone(), + path, + watch: None, + request, + }) } /// Returns a consumer rooted at `root` and restricted to matching `patterns`. diff --git a/rs/moq-net/tests/model.rs b/rs/moq-net/tests/model.rs index 1f59bc6b983..5dbcf90053c 100644 --- a/rs/moq-net/tests/model.rs +++ b/rs/moq-net/tests/model.rs @@ -46,3 +46,32 @@ fn origin_front_serves_media_with_only_explicit_polls() { drop(driver); assert!(matches!(origin.create_broadcast("after-close"), Err(Error::Closed))); } + +#[test] +fn routed_broadcast_resolves_with_only_explicit_polls() { + use kio::Task; + + let now = moq_net::time::Instant::now(); + let waiter = kio::Waiter::noop(); + let (origin, mut driver) = origin::Producer::new(origin::Config::new(Hop::new(1).unwrap())); + driver.poll(now, &waiter).unwrap(); + + // Nothing covers the path yet: the first ask fails `Unroutable` and parks on the routes. + let mut routing = origin.consume().routed_broadcast("room/live"); + assert!(routing.poll(&waiter).is_pending()); + let broadcast = origin.create_broadcast("room/live").unwrap(); + assert!(routing.poll(&waiter).is_pending()); + + // Announcing moves the routes, so the next poll asks again and the driver serves it. + broadcast.announce(origin::Route::default()).unwrap(); + assert!(routing.poll(&waiter).is_pending()); + driver.poll(now, &waiter).unwrap(); + let resolved = ready(routing.poll(&waiter)).unwrap(); + assert_eq!(resolved.info().path.as_str(), "room/live"); + + // A closed origin ends a parked resolution. + let mut routing = origin.consume().routed_broadcast("room/other"); + assert!(routing.poll(&waiter).is_pending()); + drop(driver); + assert!(matches!(ready(routing.poll(&waiter)), Err(Error::Closed))); +} diff --git a/sh/rs/select.sh b/sh/rs/select.sh index b3d546ce628..9e528f9a495 100755 --- a/sh/rs/select.sh +++ b/sh/rs/select.sh @@ -121,6 +121,8 @@ case "$action" in # Workspace feature unification hides a broken moq-tokio feature set # whenever any dependent enables a transport. if wants moq-tokio; then just rs tokio-features; fi + # Likewise every dependent turns moq-net's default `async` feature back on. + if wants moq-net; then just rs net-features; fi # moq-wasm's crate root is `#![cfg(target_arch = "wasm32")]`, so only a # wasm32 pass checks it. moq-mux, moq-ffi, and moq-clock ride along in that pass, and # nothing depends on moq-ffi, so each has to reach it on its own. From 57719eb22b60fc4d08f1bfbabec7c961c2b03b7e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 14 Oct 2026 09:53:41 -0700 Subject: [PATCH 5/6] Drop a needless borrow now that routed_broadcast owns its path Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/benches/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-net/benches/session.rs b/rs/moq-net/benches/session.rs index b5429457d4a..0a9f43ef621 100644 --- a/rs/moq-net/benches/session.rs +++ b/rs/moq-net/benches/session.rs @@ -813,7 +813,7 @@ impl ChurnRoom { for _ in 0..shape.sessions { let mut viewer = cluster.join(0).await; viewer.watch(&path(0), &held, None).await; - broadcasts.push(viewer.origin.consume().routed_broadcast(&path(0)).await.unwrap()); + broadcasts.push(viewer.origin.consume().routed_broadcast(path(0)).await.unwrap()); viewers.push(viewer); } Self { From 3968eaf422e88848172346538dc73b904ddddbd3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 14 Oct 2026 12:39:19 -0700 Subject: [PATCH 6/6] Run the net feature lane when its script changes Co-Authored-By: Claude Opus 5.5 --- sh/rs/select.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sh/rs/select.sh b/sh/rs/select.sh index 9e528f9a495..8aa6f45a7ba 100755 --- a/sh/rs/select.sh +++ b/sh/rs/select.sh @@ -88,15 +88,18 @@ wants() { [[ "$packages" == ALL ]] || grep -qwE "$1" <<<"$names" } -# The media feature contract script is no crate, so its own diff selects -# nothing and still has to reach the gate below. +# The feature contract scripts are no crate, so their own diff selects +# nothing and still has to reach the gates below. media_script= if [[ -f "$list" ]] && grep -qx 'sh/rs/media-features.sh' "$list"; then media_script=1; fi +net_script= +if [[ -f "$list" ]] && grep -qx 'sh/rs/net-features.sh' "$list"; then net_script=1; fi case "$packages" in "") - if [[ "$action" == check* && -n "$media_script" ]]; then - just rs media-features + if [[ "$action" == check* && -n "$media_script$net_script" ]]; then + if [[ -n "$media_script" ]]; then just rs media-features; fi + if [[ -n "$net_script" ]]; then just rs net-features; fi else echo "rs: no crates affected; skipping." fi @@ -122,7 +125,7 @@ case "$action" in # whenever any dependent enables a transport. if wants moq-tokio; then just rs tokio-features; fi # Likewise every dependent turns moq-net's default `async` feature back on. - if wants moq-net; then just rs net-features; fi + if [[ -n "$net_script" ]] || wants moq-net; then just rs net-features; fi # moq-wasm's crate root is `#![cfg(target_arch = "wasm32")]`, so only a # wasm32 pass checks it. moq-mux, moq-ffi, and moq-clock ride along in that pass, and # nothing depends on moq-ffi, so each has to reach it on its own.