From 403affc19fb90501bea71f9ba33bff9820631fc4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 06:14:42 -0700 Subject: [PATCH 1/3] chore: claim api net origin quest Co-Authored-By: GPT-5 From 32e246ecac0a14347049efc5a5f7cee7d4b8b230 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 07:13:17 -0700 Subject: [PATCH 2/3] feat(net)!: simplify origin scoping Co-Authored-By: GPT-5 --- doc/concept/moq-lite.md | 2 +- doc/lib/rs/index.md | 9 +- doc/lib/rs/moq-net.md | 1 + doc/lib/rs/moq-room.md | 4 +- quest/m1/README.md | 1 - quest/m1/api-net-origin.md | 53 ---- quest/m1/api-review-gate.md | 1 - rs/hang/examples/subscribe.rs | 2 +- rs/hang/examples/video.rs | 2 +- rs/libmoq/src/origin.rs | 2 +- rs/moq-bench/src/connection.rs | 12 +- rs/moq-boy/src/main.rs | 6 +- rs/moq-cli/src/complete.rs | 10 +- rs/moq-cli/src/hls.rs | 2 +- rs/moq-cli/src/main.rs | 5 +- rs/moq-cli/src/play/source.rs | 2 +- rs/moq-cli/src/publish.rs | 4 +- rs/moq-cli/src/rtc.rs | 6 +- rs/moq-cli/src/transcode.rs | 4 +- rs/moq-ffi/src/origin.rs | 13 +- rs/moq-gst/src/sink/session.rs | 5 +- rs/moq-gst/src/source/imp.rs | 2 +- rs/moq-hls/src/export/mod.rs | 2 +- rs/moq-hls/src/export/rendition.rs | 2 +- rs/moq-hls/src/export/upstream.rs | 2 +- rs/moq-hls/src/server/mod.rs | 2 +- rs/moq-hls/src/server/routes.rs | 6 +- rs/moq-mux/src/container/source.rs | 2 +- rs/moq-mux/src/container/ts/export.rs | 2 +- rs/moq-mux/src/source.rs | 10 +- rs/moq-net/CHANGELOG.md | 5 +- rs/moq-net/benches/track.rs | 4 +- rs/moq-net/src/ietf/publisher.rs | 4 +- rs/moq-net/src/ietf/session.rs | 7 +- rs/moq-net/src/ietf/subscriber.rs | 10 +- rs/moq-net/src/lite/publisher.rs | 16 +- rs/moq-net/src/lite/session.rs | 6 +- rs/moq-net/src/lite/subscriber.rs | 2 +- rs/moq-net/src/model/broadcast.rs | 25 +- rs/moq-net/src/model/mod.rs | 2 +- rs/moq-net/src/model/origin.rs | 345 ++++++++++++----------- rs/moq-net/src/model/track.rs | 31 +- rs/moq-net/tests/group_charge.rs | 8 +- rs/moq-net/tests/loom.rs | 8 +- rs/moq-relay/src/cluster.rs | 28 +- rs/moq-relay/src/internal.rs | 8 +- rs/moq-relay/src/nodes.rs | 29 +- rs/moq-relay/tests/auth_lifetime.rs | 14 +- rs/moq-relay/tests/cluster_unknown.rs | 7 +- rs/moq-relay/tests/drills.rs | 20 +- rs/moq-relay/tests/embed.rs | 4 +- rs/moq-relay/tests/goaway_cluster.rs | 17 +- rs/moq-relay/tests/runtime_uring.rs | 16 +- rs/moq-relay/tests/runtime_workers.rs | 6 +- rs/moq-relay/tests/session_revalidate.rs | 4 +- rs/moq-relay/tests/smoke.rs | 34 +-- rs/moq-room/README.md | 4 +- rs/moq-room/src/room.rs | 8 +- rs/moq-rtc/src/egress.rs | 2 +- rs/moq-rtc/src/lib.rs | 6 +- rs/moq-rtc/src/server/mod.rs | 2 +- rs/moq-rtmp/src/dial.rs | 9 +- rs/moq-rtmp/src/server.rs | 13 +- rs/moq-srt/src/dial.rs | 2 +- rs/moq-srt/src/server.rs | 2 +- rs/moq-srt/src/ts.rs | 5 +- rs/moq-stats/src/aggregate.rs | 4 +- rs/moq-stats/src/consume.rs | 2 +- rs/moq-stats/src/produce.rs | 8 +- rs/moq-tokio/CHANGELOG.md | 3 +- rs/moq-tokio/examples/chat.rs | 2 +- rs/moq-tokio/examples/clock.rs | 4 +- rs/moq-tokio/src/client.rs | 8 +- rs/moq-tokio/src/origin.rs | 15 +- rs/moq-tokio/src/server.rs | 4 +- rs/moq-tokio/tests/alpn.rs | 4 +- rs/moq-tokio/tests/backend.rs | 11 +- rs/moq-tokio/tests/broadcast.rs | 128 ++++----- rs/moq-tokio/tests/reconnect.rs | 4 +- rs/moq-transcode/examples/transcode.rs | 4 +- rs/moq-transcode/src/lib.rs | 2 +- rs/moq-uring/benches/session_lite.rs | 4 +- rs/moq-uring/tests/session.rs | 10 +- rs/moq-uring/tests/web.rs | 4 +- rs/moq-video/src/decode/consumer.rs | 2 +- rs/moq-wasm/src/lib.rs | 2 +- 86 files changed, 535 insertions(+), 579 deletions(-) delete mode 100644 quest/m1/api-net-origin.md diff --git a/doc/concept/moq-lite.md b/doc/concept/moq-lite.md index 9c65dd0a8b..86b1bd1aa5 100644 --- a/doc/concept/moq-lite.md +++ b/doc/concept/moq-lite.md @@ -79,7 +79,7 @@ Patterns never travel as announcements. `origin.dynamic(prefix, route)` advertises a prefix: the call claims that `prefix` and every path beneath it *can* be served, not that any exist. A route is a capability, not an inventory: a subscriber must not treat a prefix as a concrete broadcast name. Use -`create_broadcast(path)` and `announce(route)` when the path is known; use +`publish(path, route)` when the path is known; use `dynamic` when the set of paths is not, and refuse the requests you will not serve. A pattern lives in two places: the token, which scopes what a session may publish and subscribe to, and a local filter a consumer applies to the diff --git a/doc/lib/rs/index.md b/doc/lib/rs/index.md index 130766bb29..0f692aad16 100644 --- a/doc/lib/rs/index.md +++ b/doc/lib/rs/index.md @@ -40,12 +40,12 @@ The reference implementation. Every crate is on ```rust // The Origin is the local hub: the session fills it with remote broadcasts // and serves your local broadcasts out of it. -let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); +let origin = moq_tokio::origin::spawn(); let client = moq_tokio::connect::Config::default().init(Default::default())?; let url = url::Url::parse("https://cdn.moq.dev/anon")?; // Reconnects on its own; `closed()` resolves when it gives up. -let session = client.with_subscriber(origin.clone()).with_publisher(&origin).connect(url); +let session = client.with_origin(origin.clone()).connect(url); // Subscribe: wait for a route, resolve the broadcast at its path, read the catalog. let consumer = origin.consume(); @@ -62,10 +62,9 @@ while let Some(update) = announced.next().await { ``` ```rust -// Publish: create a broadcast on the origin, fill it, then announce its path. -let mut broadcast = origin.create_broadcast("my-stream.hang")?; +// Publish: create and announce a broadcast on the origin, then fill it. +let mut broadcast = origin.publish("my-stream.hang", Default::default())?; // moq-mux (from a container) or moq-video / moq-audio (from a device) fill it. -broadcast.announce(Default::default())?; // The route retracts on `unannounce()` or when the broadcast ends. To serve a whole // subtree on demand instead, `origin.dynamic("room", Default::default())?` yields // each requested path for the application to accept or reject. diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index 6712767a9c..a743c842a5 100644 --- a/doc/lib/rs/moq-net.md +++ b/doc/lib/rs/moq-net.md @@ -73,6 +73,7 @@ assert_eq!( Three operations, on an origin: +- `origin.publish(path, route)` creates and advertises a broadcast in one call. - `origin.create_broadcast(path)` returns a producer. The broadcast is reachable by exact path immediately and invisible to discovery until advertised. diff --git a/doc/lib/rs/moq-room.md b/doc/lib/rs/moq-room.md index e232bd39a3..8ccdbf9125 100644 --- a/doc/lib/rs/moq-room.md +++ b/doc/lib/rs/moq-room.md @@ -23,11 +23,11 @@ cargo add moq-room ``` ```rust -use moq_net::{Hop, Path}; +use moq_net::Path; use moq_room::{Kind, Room, claims}; let token = key.sign(&claims("meet/demo", "alice")?, None)?; -let origin = moq_tokio::origin::spawn(Hop::random()); +let origin = moq_tokio::origin::spawn(); let mut room = Room::new(&origin.consume(), Some(Path::new("alice").to_owned())); while let Some(event) = room.next().await { if event.kind == Kind::Camera { diff --git a/quest/m1/README.md b/quest/m1/README.md index 9e0c8fa7b5..0b183926c8 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -28,7 +28,6 @@ the transport line in m2 assumes a single stack. - [One QUIC backend](/quest/m1/quic-one-backend.md) - quinn and quiche are deleted; noq (and iroh on it) is the only QUIC stack, with the qmux fallbacks untouched - [Announce event](/quest/m1/api-net-announce.md) - publishers announce prefixes on every wire, consumers scoped by a pattern read the covered path already trimmed, with no `as_prefix().expect()` at 89 call sites -- [Origin scoping](/quest/m1/api-net-origin.md) - `scope(root, patterns)` is one fallible call, a fresh origin has a random hop, and the handles stop derefing to `Hop` - [Bindings announce match](/quest/m1/api-origin-scopes.md) - every binding takes a pattern scope and reports the announce match with its captures - [PathPrefixes](/quest/m1/api-path-prefixes.md) - the unused moq_net::PathPrefixes type is deleted before the release - [Rendition ownership](/quest/m1/api-mux-rendition.md) - one handle publishes a media track and reports its estimate, instead of five diff --git a/quest/m1/api-net-origin.md b/quest/m1/api-net-origin.md deleted file mode 100644 index a79690122b..0000000000 --- a/quest/m1/api-net-origin.md +++ /dev/null @@ -1,53 +0,0 @@ -# [S] An origin is scoped in one call and defaults to a real hop - -## Goal - -`origin::Producer` and `origin::Consumer` read like the rest of moq-net: -a scoped handle comes from one fallible call that says why it failed, a -fresh origin has a hop of its own, and the handle is not a pointer to its -hop. - -## Plan - -- `origin::Config::default()` mints `Hop::random()`. 195 call sites here - and 48 in moq.pro spell `Config::new(Hop::random())` or - `spawn(Hop::random())` today, and none of them wants loop detection off. - `moq_tokio::origin::spawn()` then takes no argument for the common case. - Decide the `TEMPORARY` 53-bit cap in `origin.rs` at the same time; JS - already reads a full u62. -- `broadcast::Info` stops embedding an `origin::Config`. Nothing reads a - broadcast's `origin.id`; only the cache pool is used, so `Info { pool, - path }` with `create_broadcast` handing the origin's pool down. Otherwise - a random-hop `Default` makes every standalone broadcast (every - `Info::new()` in moq-json) mint an identity nobody reads and a pool of its - own. -- `with_root(prefix)?.scope(&patterns)` becomes - `scope(root, &Patterns) -> Result` returning `Unauthorized` - for an empty intersection or a root nothing lies under, on both Producer - and Consumer. PR #3746 makes `scope` intersect any union and keeps both - calls returning `Option`; the two-step chain is at every auth site - (`rs/moq-relay/src/cluster.rs`, `rs/moq-cli/src/auth.rs`, - `rs/moq-ffi/src/origin.rs`) and `None` carries no reason. -- `routed_broadcast` and `request_broadcast` agree on the out-of-scope - error: `Unauthorized`, matching `create_broadcast`. `request_broadcast` - says `Unroutable` today and an unparseable path surfaces as `Closed`. -- Drop `impl Deref` on both handles; add `hop()` and rename - `Config.id` to `Config.hop`, the spelling `Request::peer_hop` and - `Dynamic::hop` already use. -- `origin::Pending` becomes `origin::Requesting` beside - `track::{Subscribing, Querying, Fetching}`; moq-stats writes - `kio::Pending` today. -- `origin::Producer::publish(path, route) -> Result` - folds `create_broadcast` plus `announce`; moq.pro wrote that helper in - three crates and one comment flags the window between the two calls. - The connect side stops reading `with_subscriber(producer)` next to - `with_publisher(&producer)`. - -Public API: breaking on moq-net and moq-tokio, so on dev. Wire: none. -Consumers: everything that spawns an origin; `just check` across the -workspace plus moq.pro's next pin. - -## Related - -- [PathPrefixes](/quest/m1/api-path-prefixes.md) - the scope adapter PR #3746 retires -- [Origin narrowing](/quest/m2/origin-narrowing.md) - a live handle narrowing after this settles the static shape diff --git a/quest/m1/api-review-gate.md b/quest/m1/api-review-gate.md index 8c7e5b754d..2b0e904e63 100644 --- a/quest/m1/api-review-gate.md +++ b/quest/m1/api-review-gate.md @@ -17,7 +17,6 @@ file is deleted on completion) or deletes the quest with a note in quest is deleted too. No code. The list: [Announce event](/quest/m1/api-net-announce.md), -[Origin scoping](/quest/m1/api-net-origin.md), [Rendition ownership](/quest/m1/api-mux-rendition.md), [Gateway types](/quest/m1/api-gateways.md). diff --git a/rs/hang/examples/subscribe.rs b/rs/hang/examples/subscribe.rs index 17a7fe63be..aa469f2ac1 100644 --- a/rs/hang/examples/subscribe.rs +++ b/rs/hang/examples/subscribe.rs @@ -10,7 +10,7 @@ async fn main() -> anyhow::Result<()> { moq_tokio::Log::new(tracing::Level::DEBUG).init()?; // Create an origin that the session can publish incoming broadcasts to. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); // Run the subscription and the session in parallel. diff --git a/rs/hang/examples/video.rs b/rs/hang/examples/video.rs index e6a0c83cc8..85fbe218d1 100644 --- a/rs/hang/examples/video.rs +++ b/rs/hang/examples/video.rs @@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> { moq_tokio::Log::new(tracing::Level::DEBUG).init()?; // Create an origin that we can publish to and the session can consume from. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // Run the broadcast production and the session in parallel. // This is a simple example of how you can concurrently run multiple tasks. diff --git a/rs/libmoq/src/origin.rs b/rs/libmoq/src/origin.rs index bb558262b4..6fc8a2d1f9 100644 --- a/rs/libmoq/src/origin.rs +++ b/rs/libmoq/src/origin.rs @@ -51,7 +51,7 @@ impl Origin { pub fn create(&mut self) -> Result { // Every FFI entry point runs inside `RUNTIME.enter()`, so the driver // lands on the dedicated libmoq runtime. - self.active.insert(moq_tokio::origin::spawn(moq_net::Hop::random())) + self.active.insert(moq_tokio::origin::spawn()) } pub fn get(&self, id: Id) -> Result<&moq_net::origin::Producer, Error> { diff --git a/rs/moq-bench/src/connection.rs b/rs/moq-bench/src/connection.rs index 356a18952a..452a14cfee 100644 --- a/rs/moq-bench/src/connection.rs +++ b/rs/moq-bench/src/connection.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use moq_tokio::Status; -use moq_tokio::moq_net::{self, Hop, bytes::Bytes}; +use moq_tokio::moq_net::{self, bytes::Bytes}; use moq_tokio::moq_net::{broadcast, group, track}; use rand::RngExt; use serde::{Deserialize, Serialize}; @@ -96,9 +96,9 @@ pub async fn run(ctx: Connection) { let url = config.client.url.clone().expect("url required"); // Publish side: an origin we fill with our broadcasts and hand to the session. - let publish = moq_tokio::origin::spawn(Hop::random()); + let publish = moq_tokio::origin::spawn(); // Consume side: the session fills this with peer announcements. - let consume = moq_tokio::origin::spawn(Hop::random()); + let consume = moq_tokio::origin::spawn(); let namespace = format!("{}/{run_id:08x}", config.name()); let discovery = if config.publishes() { @@ -286,7 +286,7 @@ async fn produce( fn discover(consume: &moq_net::origin::Producer, name: &str) -> moq_net::origin::Consumer { consume .consume() - .with_root(name) + .scope(name, &moq_net::Patterns::from(moq_net::Pattern::all())) .expect("origin must permit the bench namespace") } @@ -737,7 +737,7 @@ mod tests { tokio::time::pause(); let stats = Arc::new(Stats::default()); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); // The relay-internal broadcast: announced, but with no bench data track. let _internal = origin.create_broadcast(".stats/node/host").unwrap(); @@ -776,7 +776,7 @@ mod tests { #[tokio::test] async fn named_subscription_waits_for_the_exact_broadcast() { let stats = Arc::new(Stats::default()); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let consume = origin.consume(); let task = tokio::spawn(subscribe_named(consume, "bench/run/chat".into(), stats.clone())); diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index 9ee6a32033..e57c22a3f8 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -225,7 +225,7 @@ async fn run(config: &Config) -> Result<()> { let client = config.client.clone().init(config.quic.clone())?; // Publish origin: the game session broadcast. - let publish_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let publish_origin = moq_tokio::origin::spawn(); let default_game_prefix = format!("{}/game", config.prefix); let default_viewer_prefix = format!("{}/viewer", config.prefix); let game_prefix = config.prefix_game.as_deref().unwrap_or(&default_game_prefix); @@ -243,9 +243,9 @@ async fn run(config: &Config) -> Result<()> { // Consume origin: viewer broadcasts under the viewer prefix. // JS publishes viewer feedback at "{viewer_prefix}/{name}/{viewerId}" let viewer_path = format!("{viewer_prefix}/{name}"); - let consume_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let consume_origin = moq_tokio::origin::spawn(); let viewer_consumer = consume_origin - .with_root(&viewer_path) + .scope(&viewer_path, &moq_net::Patterns::from(moq_net::Pattern::all())) .expect("viewer prefix should be valid") .consume(); diff --git a/rs/moq-cli/src/complete.rs b/rs/moq-cli/src/complete.rs index e926f13606..e63f6ac422 100644 --- a/rs/moq-cli/src/complete.rs +++ b/rs/moq-cli/src/complete.rs @@ -596,7 +596,7 @@ async fn catalog( /// what tells a relay two sessions carry the same content. async fn dial(side: &MoqSide, deadline: Instant) -> Option<(moq_net::origin::Producer, moq_tokio::Connection)> { let url = side.client.url.clone()?; - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // Building the client reads the TLS material off disk synchronously, so it goes on // the blocking pool and under the deadline like everything else: a `--connect-tls-root` @@ -775,7 +775,7 @@ mod tests { /// because the shell exports a relay for the publishing it usually does. #[tokio::test] async fn the_environment_cannot_ask_for_a_moq_side() { - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let _alpha = origin.create_broadcast("alpha").expect("alpha"); _alpha.announce(Default::default()).expect("alpha"); let connect = relay(&origin); @@ -867,7 +867,7 @@ mod tests { #[tokio::test] async fn a_relay_on_the_line_answers_broadcast() { let _env = EnvGuard::clear(&["MOQ_CONNECT"]); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let _alpha = origin.create_broadcast("alpha").expect("alpha"); _alpha.announce(Default::default()).expect("alpha"); let _nested = origin.create_broadcast("room/beta").expect("beta"); @@ -892,7 +892,7 @@ mod tests { let _env = EnvGuard::clear(&["MOQ_CONNECT"]); use hang::catalog::{AudioCodec, AudioConfig, H264, VideoConfig}; - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // Two broadcasts with different renditions, so a completer reading the wrong // one fails loudly instead of matching by luck. @@ -939,7 +939,7 @@ mod tests { #[tokio::test] async fn the_catalog_format_on_the_line_is_honored() { let _env = EnvGuard::clear(&["MOQ_CONNECT"]); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("room").expect("broadcast"); broadcast.announce(Default::default()).expect("broadcast"); diff --git a/rs/moq-cli/src/hls.rs b/rs/moq-cli/src/hls.rs index 5d23f79a56..d8f5b37145 100644 --- a/rs/moq-cli/src/hls.rs +++ b/rs/moq-cli/src/hls.rs @@ -77,7 +77,7 @@ pub async fn export(origin: moq_net::origin::Consumer, args: ExportArgs, name: S moq_net::Pattern::subtree(&name).with_context(|| format!("invalid broadcast name `{name}`"))?, ); let scoped = origin - .scope(&scope) + .scope("", &scope) .with_context(|| format!("failed to scope origin to broadcast `{name}`"))?; let mut config = moq_hls::export::Config::default(); diff --git a/rs/moq-cli/src/main.rs b/rs/moq-cli/src/main.rs index 05650d8ef4..18d38444d3 100644 --- a/rs/moq-cli/src/main.rs +++ b/rs/moq-cli/src/main.rs @@ -225,14 +225,13 @@ async fn serve_client( // What the grant allows, as origin handles rooted where the session dialed. let token = lease.token(); - let rooted = origin.with_root(&token.root); let publish = directions .publish - .then(|| rooted.as_ref().and_then(|o| o.scope(&token.subscribe))) + .then(|| origin.scope(&token.root, &token.subscribe).ok()) .flatten(); let subscribe = directions .consume - .then(|| rooted.as_ref().and_then(|o| o.scope(&token.publish))) + .then(|| origin.scope(&token.root, &token.publish).ok()) .flatten(); if publish.is_none() && subscribe.is_none() { request.reject(moq_tokio::server::Reject::Forbidden).await.ok(); diff --git a/rs/moq-cli/src/play/source.rs b/rs/moq-cli/src/play/source.rs index 838192fe33..5696665246 100644 --- a/rs/moq-cli/src/play/source.rs +++ b/rs/moq-cli/src/play/source.rs @@ -32,7 +32,7 @@ mod tests { async fn subscribe_waits_for_the_announcement() { tokio::time::pause(); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); // Direct resolution has no route before the announcement. diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index 8af61728be..b8d71f9d78 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -558,7 +558,7 @@ mod tests { async fn manufacture_input() -> Vec { // Create the broadcast on a throwaway origin so the exporter can resolve it by path. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut broadcast = origin.create_broadcast("cli").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; @@ -668,7 +668,7 @@ mod tests { // Publish side: `Publish::new(Ts)` builds a `ts::Import`, so the verbatim // streams land in the broadcast instead of being dropped by the media-only path. // The broadcast is created on a throwaway origin so the exporter can resolve it by path. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("cli").unwrap(); settle().await; let mut publish = Publish::new(broadcast, &PublishFormat::Ts, Default::default()).unwrap(); diff --git a/rs/moq-cli/src/rtc.rs b/rs/moq-cli/src/rtc.rs index cb65fe37b6..99a31db27a 100644 --- a/rs/moq-cli/src/rtc.rs +++ b/rs/moq-cli/src/rtc.rs @@ -71,11 +71,11 @@ pub async fn listen_export(origin: moq_net::origin::Consumer, name: String, list moq_net::Pattern::subtree(&name).with_context(|| format!("invalid broadcast name `{name}`"))?, ); let subscriber = origin - .scope(&scope) + .scope("", &scope) .with_context(|| format!("failed to scope origin to broadcast `{name}`"))?; // A WHEP server only reads; it still needs a publisher handle for the shared // glue, so hand it an unused, empty Origin producer. - let publisher = moq_tokio::origin::spawn(moq_net::Hop::random()); + let publisher = moq_tokio::origin::spawn(); let server = moq_rtc::Server::new(server_config(&listen), publisher, subscriber); serve(server.subscribe_router(), "WHEP", listen).await } @@ -86,7 +86,7 @@ fn scope_producer(origin: &moq_net::origin::Producer, name: &str) -> anyhow::Res moq_net::Pattern::subtree(name).with_context(|| format!("invalid broadcast name `{name}`"))?, ); origin - .scope(&scope) + .scope("", &scope) .with_context(|| format!("failed to scope origin to broadcast `{name}`")) } diff --git a/rs/moq-cli/src/transcode.rs b/rs/moq-cli/src/transcode.rs index e53aaa3008..8266ac5216 100644 --- a/rs/moq-cli/src/transcode.rs +++ b/rs/moq-cli/src/transcode.rs @@ -128,11 +128,11 @@ pub async fn run(moq: MoqSide, args: Args, net: Net) -> anyhow::Result<()> { .url .clone() .context("`transcode` requires a relay: pass --connect ")?; - let publish = moq_tokio::origin::spawn(moq_net::Hop::random()); + let publish = moq_tokio::origin::spawn(); // A session drop closes the source broadcast and ends the run: the outage is // surfaced rather than transcoded over. The reconnect loop covers the dial; // restarting after a mid-run drop is the caller's call. - let remote = moq_tokio::origin::spawn(moq_net::Hop::random()); + let remote = moq_tokio::origin::spawn(); let session = net .client(moq.client.clone())? .with_publisher(&publish) diff --git a/rs/moq-ffi/src/origin.rs b/rs/moq-ffi/src/origin.rs index 7bb7372fb6..142f11db2e 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -191,7 +191,7 @@ impl MoqOriginProducer { } fn from_config(config: MoqOriginConfig) -> Self { - let mut origin = moq_net::origin::Config::new(moq_net::Hop::random()); + let mut origin = moq_net::origin::Config::default(); if let Some(capacity) = config.cache_capacity_bytes { let cache = moq_net::cache::Config::default() .with_capacity(capacity) @@ -230,14 +230,14 @@ pub(crate) fn resolve_pair( ) -> (moq_net::origin::Producer, moq_net::origin::Producer) { if publish.is_none() && consume.is_none() { // Clones of a Producer share the underlying origin, so this is one origin, not two. - let shared = spawn(moq_net::Hop::random().into()); + let shared = spawn(moq_net::origin::Config::default()); return (shared.clone(), shared); } let resolve = |origin: Option<&Arc>| { origin .map(|o| o.inner().clone()) - .unwrap_or_else(|| spawn(moq_net::Hop::random().into())) + .unwrap_or_else(|| spawn(moq_net::origin::Config::default())) }; (resolve(publish), resolve(consume)) } @@ -302,7 +302,9 @@ impl MoqOriginConsumer { /// Subscribe to routes under a requested prefix; updates return covered prefixes relative to it. pub fn announced(&self, prefix: String) -> Result, MoqError> { let _guard = crate::ffi::enter(); - let origin = self.inner.with_root(prefix).ok_or(MoqError::Unauthorized)?; + let origin = self + .inner + .scope(prefix, &moq_net::Patterns::from(moq_net::Pattern::all()))?; Ok(Arc::new(MoqAnnounceConsumer { task: Task::new(Announced { inner: origin.announced(), @@ -320,7 +322,8 @@ impl MoqOriginConsumer { // Probe the permission eagerly so an unreachable path fails here, rather than // surfacing later as a `Closed` the caller can't tell from the origin ending. - self.inner.with_root(&path).ok_or(MoqError::Unauthorized)?; + self.inner + .scope(&path, &moq_net::Patterns::from(moq_net::Pattern::all()))?; Ok(Arc::new(MoqAnnouncedBroadcast { task: Task::new(AnnouncedBroadcast { diff --git a/rs/moq-gst/src/sink/session.rs b/rs/moq-gst/src/sink/session.rs index 534a92dfb0..9e00dc8f76 100644 --- a/rs/moq-gst/src/sink/session.rs +++ b/rs/moq-gst/src/sink/session.rs @@ -279,9 +279,8 @@ impl Session { // Producer setup may touch tokio time (group eviction), so run it inside the runtime context. let _rt = RUNTIME.enter(); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); - let mut broadcast = origin.create_broadcast(&settings.broadcast)?; - broadcast.announce(moq_net::origin::Route::default())?; + let origin = moq_tokio::origin::spawn(); + let mut broadcast = origin.publish(&settings.broadcast, moq_net::origin::Route::default())?; let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default())?; let status = Arc::new(Status::default()); diff --git a/rs/moq-gst/src/source/imp.rs b/rs/moq-gst/src/source/imp.rs index 9ed67cb0d9..517e67051f 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -354,7 +354,7 @@ async fn run_session( let mut config = moq_tokio::connect::Config::default(); config.tls.insecure = Some(settings.tls_disable_verify); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let origin_consumer = origin.consume(); let client = config.init(Default::default())?.with_subscriber(origin); diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index 489aa013f4..2996bc3d4b 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -360,7 +360,7 @@ async fn watch( mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-hls/src/export/rendition.rs b/rs/moq-hls/src/export/rendition.rs index 0288e4c71d..506376cb2c 100644 --- a/rs/moq-hls/src/export/rendition.rs +++ b/rs/moq-hls/src/export/rendition.rs @@ -757,7 +757,7 @@ mod tests { } fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); std::mem::forget(driver); producer } diff --git a/rs/moq-hls/src/export/upstream.rs b/rs/moq-hls/src/export/upstream.rs index 63ca786921..eea5b4cf4a 100644 --- a/rs/moq-hls/src/export/upstream.rs +++ b/rs/moq-hls/src/export/upstream.rs @@ -39,7 +39,7 @@ mod tests { #[tokio::test] async fn self_references_keep_the_catalog_broadcast_after_replacement() { - let (origin, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (origin, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); let driver = tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); let old = origin.create_broadcast("a/live").unwrap(); let source = moq_mux::Source::new(origin.consume(), "a/live"); diff --git a/rs/moq-hls/src/server/mod.rs b/rs/moq-hls/src/server/mod.rs index 3065102ee0..a0c4997dec 100644 --- a/rs/moq-hls/src/server/mod.rs +++ b/rs/moq-hls/src/server/mod.rs @@ -144,7 +144,7 @@ async fn evict_closed(inner: Arc, name: String, broadcaster: Arc moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-hls/src/server/routes.rs b/rs/moq-hls/src/server/routes.rs index b1389d1dc7..275a63646c 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -361,8 +361,8 @@ mod tests { use std::net::TcpListener; let lite: moq_net::Version = "moq-lite-05".parse().expect("lite version"); - let pub_origin = moq_tokio::origin::spawn(config); - let sub_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let pub_origin = moq_tokio::origin::spawn_config(config.into()); + let sub_origin = moq_tokio::origin::spawn(); for _ in 0..20 { let probe = TcpListener::bind("127.0.0.1:0").expect("bind probe"); @@ -481,7 +481,7 @@ mod tests { async fn a_session_crossed_cache_miss_answers_404() { let pool = moq_net::cache::Pool::new(moq_net::cache::Config::default().with_capacity(1)); let pair = lite_pair_pub({ - let mut config = moq_net::origin::Config::new(moq_net::Hop::random()); + let mut config = moq_net::origin::Config::default(); config.pool = pool; config }) diff --git a/rs/moq-mux/src/container/source.rs b/rs/moq-mux/src/container/source.rs index acfe8e5116..9e340fe62c 100644 --- a/rs/moq-mux/src/container/source.rs +++ b/rs/moq-mux/src/container/source.rs @@ -50,7 +50,7 @@ impl VideoTransform { enum SourceState { /// Waiting for the target broadcast (the catalog broadcast, or a cross-broadcast /// reference) to resolve; the track (by name) is subscribed once it does. - Requesting(kio::Pending, String), + Requesting(kio::Pending, String), /// Waiting for the subscription to resolve (blocks on the publisher's SUBSCRIBE_OK). Subscribing(kio::Pending), /// The resolved consumer, reading frames. Boxed because it's much larger than diff --git a/rs/moq-mux/src/container/ts/export.rs b/rs/moq-mux/src/container/ts/export.rs index e9d89aaae0..b0a763f8c8 100644 --- a/rs/moq-mux/src/container/ts/export.rs +++ b/rs/moq-mux/src/container/ts/export.rs @@ -269,7 +269,7 @@ struct SiTrack { enum SiState { /// Waiting for the catalog broadcast to resolve; the track (by name) is /// subscribed once it does. - Requesting(kio::Pending, String), + Requesting(kio::Pending, String), /// Waiting for the subscription to resolve. Subscribing(kio::Pending), /// The resolved subscription, reading snapshot groups. diff --git a/rs/moq-mux/src/source.rs b/rs/moq-mux/src/source.rs index 6eb61e5d10..460da9ce29 100644 --- a/rs/moq-mux/src/source.rs +++ b/rs/moq-mux/src/source.rs @@ -60,7 +60,7 @@ impl Source { } /// Begin resolving the catalog broadcast (the one at this source's path). - pub(crate) fn request_catalog(&self) -> kio::Pending { + pub(crate) fn request_catalog(&self) -> kio::Pending { self.origin.request_broadcast(&self.path) } @@ -100,7 +100,7 @@ impl Source { pub(crate) fn request( &self, rel: Option<&moq_net::PathRelative<'_>>, - ) -> crate::Result> { + ) -> crate::Result> { Ok(self.origin.request_broadcast(&self.target(rel)?)) } @@ -113,7 +113,7 @@ impl Source { pub(crate) fn try_request( &self, rel: Option<&moq_net::PathRelative<'_>>, - ) -> Option> { + ) -> Option> { Some(self.origin.request_broadcast(&self.resolve_reference(rel)?)) } @@ -222,7 +222,7 @@ enum Bound { /// A broadcast the caller already holds. Ready(moq_net::broadcast::Consumer), /// A request issued when the binding was made. - Requested(moq_net::origin::Pending), + Requested(moq_net::origin::Requesting), } impl Binding { @@ -286,7 +286,7 @@ impl BroadcastConfig for hang::catalog::BinaryConfig { /// Test helper: build an origin producer, spawning its driver on the ambient runtime. #[cfg(test)] pub(crate) fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-net/CHANGELOG.md b/rs/moq-net/CHANGELOG.md index 48d63fbf7c..d333980fdc 100644 --- a/rs/moq-net/CHANGELOG.md +++ b/rs/moq-net/CHANGELOG.md @@ -22,7 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [**breaking**] `create_track`, `reserve_track`, `unique_track`, `finish`, `create_group`, and `append_group` take `&self`. `track::Consumer::info()` is `query()`. `track::Demand` gains `is_used` / `poll_used` / `poll_unused`. `track::Producer::poll_unused` returns `Poll>`. `bandwidth::Producer::closed()` returns the cause. - [**breaking**] `stats::Presence` and `stats::Traffic` name both edges of each cumulative pair `*_started` / `*_ended` (`sessions_started` / `sessions_ended`, `announces_started` / `announces_ended`, `broadcasts_*`, `subscriptions_*`). Serialize still writes the previous `announced` / `*_closed` names beside the new ones; deserialize accepts either spelling, with the canonical name winning. - `origin::Info` is `origin::Config` with public fields and no `with_*` builders. `Producer::info()` is `config()`. -- `origin::Requesting` is `origin::Pending`, the consumer-side wait for a request to resolve. +- [**breaking**] `origin::Config::default()` mints a random hop, `Config::id` is `hop`, and origin handles expose `hop()` instead of dereferencing to `Hop`. +- [**breaking**] `origin::Producer::scope(root, patterns)` and `origin::Consumer::scope(root, patterns)` replace the separate `with_root` / `scope` calls and return `Result` with `Unauthorized` for an empty grant. +- [**breaking**] `origin::Pending` is `origin::Requesting`, the consumer-side wait for a request to resolve. +- `origin::Producer::publish(path, route)` creates and advertises a broadcast together. - `track::Producer::write_datagram(Datagram)` is now `insert_datagram(sequence, timestamp, payload)`, matching TypeScript `insertDatagram`. The supplied sequence is preserved; `append_datagram` remains the next-sequence convenience. - `Timescale` no longer implements `From>`. Use `Timescale::new` or `TryFrom` so values above the QUIC varint range are refused at construction. - Register moq-lite stream codes NOT_FOUND 0x33, OLD 0x34, and EVICTED 0x35 so a cache miss round-trips as the named variant instead of an opaque reserved-range placeholder. diff --git a/rs/moq-net/benches/track.rs b/rs/moq-net/benches/track.rs index 29c1292172..5592698996 100644 --- a/rs/moq-net/benches/track.rs +++ b/rs/moq-net/benches/track.rs @@ -48,7 +48,7 @@ impl Fanout { let config = cache::Config::default() .with_capacity(CACHE_CAPACITY) .with_expiry(cache::DEFAULT_EXPIRY); - info.origin.pool = cache::Pool::new(config); + info.pool = cache::Pool::new(config); let broadcast = broadcast::Producer::new(info); let track = broadcast.create_track("bench", None).unwrap(); let mut subscribers: Vec<_> = (0..subscribers).map(|_| track.subscribe(None)).collect(); @@ -150,7 +150,7 @@ fn parallel_write(pool: &cache::Pool, writers: usize, iterations: u64) -> Durati let iterations = iterations / writers as u64 + u64::from((writer as u64) < iterations % writers as u64); scope.spawn(move || { let mut info = broadcast::Info::default(); - info.origin.pool = pool; + info.pool = pool; let broadcast = broadcast::Producer::new(info); let track = broadcast.create_track("bench", None).unwrap(); let payload = Bytes::from_static(&[0; PAYLOAD]); diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 96e59fa932..1031cbe6b5 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -291,7 +291,7 @@ where Self { runtime, session, - self_origin: *origin, + self_origin: origin.hop(), origin, control, peer_hop, @@ -1684,7 +1684,7 @@ where let scope = crate::Pattern::subtree(prefix.as_str()) .map(crate::Patterns::from) .unwrap_or_default(); - let origin = self.origin.scope(&scope).unwrap_or_else(|| self.origin.empty()); + let origin = self.origin.scope("", &scope).unwrap_or_else(|_| self.origin.empty()); // Send OK response match self.version { diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index eb784069cf..4b8521bdb4 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -442,8 +442,8 @@ pub struct PeerSetup { /// has no content to route, so a throwaway id is all it can offer. fn self_origin(publish: Option<&origin::Consumer>, subscribe: Option<&origin::Producer>) -> Hop { publish - .map(|origin| **origin) - .or_else(|| subscribe.map(|origin| **origin)) + .map(|origin| origin.hop()) + .or_else(|| subscribe.map(|origin| origin.hop())) .unwrap_or_else(Hop::random) } @@ -984,8 +984,7 @@ mod tests { .map(|prefix| crate::Pattern::subtree(prefix).unwrap()) .collect(); let scoped = origin - .with_root("rootns") - .and_then(|rooted| rooted.scope(&scope)) + .scope("rootns", &scope) .expect("scope the origin to two prefixes"); let gate = kio::Producer::new(true); diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index b8985e7c17..998e83cd75 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -2873,10 +2873,7 @@ mod tests { async fn a_rooted_subscriber_asks_for_its_scope_not_its_root() { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let scope = crate::Patterns::from(crate::Pattern::subtree("cam").unwrap()); - let scoped = origin - .with_root("rootns") - .and_then(|rooted| rooted.scope(&scope)) - .expect("scope the origin"); + let scoped = origin.scope("rootns", &scope).expect("scope the origin"); let gate = kio::Producer::new(true); let session = crate::lite::test_transport::SinkSession::gated_bi(gate.consume()); @@ -2946,10 +2943,7 @@ mod tests { let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); let consumer = origin.consume(); let scope = crate::Patterns::from(crate::Pattern::subtree("cam").unwrap()); - let scoped = origin - .with_root("rootns") - .and_then(|rooted| rooted.scope(&scope)) - .expect("scope the origin"); + let scoped = origin.scope("rootns", &scope).expect("scope the origin"); let session = crate::lite::test_transport::ScriptedSession::new(namespace_response(VERSION, "x.hang").await); let (tasks, _task_set) = crate::util::TaskSet::new(); diff --git a/rs/moq-net/src/lite/publisher.rs b/rs/moq-net/src/lite/publisher.rs index 61a597efaa..6ae0051c4d 100644 --- a/rs/moq-net/src/lite/publisher.rs +++ b/rs/moq-net/src/lite/publisher.rs @@ -143,7 +143,7 @@ impl Publisher AnnounceServe { let origin = self .shared .origin - .scope(&scope) - .unwrap_or_else(|| self.shared.origin.empty()); + .scope("", &scope) + .unwrap_or_else(|_| self.shared.origin.empty()); // Register the split-horizon peer on the announce cursor too. The origin // model uses this exposure to park a reflected copy before it can replace // the source we are currently advertising to that peer. @@ -858,7 +858,7 @@ enum TrackInfoState { /// Resolving the broadcast (may wait on a dynamic handler). Request { msg: lite::Track<'static>, - requesting: origin::Pending, + requesting: origin::Requesting, }, /// Waiting for the track's info. Query { @@ -987,7 +987,7 @@ enum SubscribeState { /// Resolving the broadcast (may wait on a dynamic handler). Request { msg: lite::Subscribe<'static>, - requesting: origin::Pending, + requesting: origin::Requesting, }, /// Waiting for the model subscription to be confirmed. Confirm { @@ -1213,7 +1213,7 @@ enum FetchState { /// Resolving the broadcast (may wait on a dynamic handler). Request { msg: lite::Fetch<'static>, - requesting: origin::Pending, + requesting: origin::Requesting, }, /// Waiting for the fetched group. Fetch { @@ -1754,7 +1754,7 @@ mod announce_test { }; let task = tokio::spawn(async move { let mut announced = consumer.announced(); - let self_origin = *consumer; + let self_origin = consumer.hop(); TestPublisher::run_announce(&mut stream, &consumer, &mut announced, "", self_origin, VERSION).await }); settle().await; @@ -1858,7 +1858,7 @@ mod announce_test { }; let task = tokio::spawn(async move { let mut announced = consumer.announced(); - let self_origin = *consumer; + let self_origin = consumer.hop(); TestPublisher::run_announce(&mut stream, &consumer, &mut announced, "", self_origin, VERSION).await }); settle().await; diff --git a/rs/moq-net/src/lite/session.rs b/rs/moq-net/src/lite/session.rs index 260800ee52..43f5639733 100644 --- a/rs/moq-net/src/lite/session.rs +++ b/rs/moq-net/src/lite/session.rs @@ -128,9 +128,9 @@ where // (they share it in practice). if our_setup.hop.is_none() { our_setup.hop = publish - .as_deref() - .or(subscribe.as_deref()) - .copied() + .as_ref() + .map(|origin| origin.hop()) + .or_else(|| subscribe.as_ref().map(|origin| origin.hop())) .filter(|hop| hop.id() != 0); } diff --git a/rs/moq-net/src/lite/subscriber.rs b/rs/moq-net/src/lite/subscriber.rs index 59a962b3c1..74fe865480 100644 --- a/rs/moq-net/src/lite/subscriber.rs +++ b/rs/moq-net/src/lite/subscriber.rs @@ -92,7 +92,7 @@ impl Subscriber { // origin we publish into so it matches the relay identity across // every session sharing that origin, required for cross-session // loop detection. - let self_origin = *config.origin; + let self_origin = config.origin.hop(); Self { session: config.session, origin: config.origin, diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index 75b94fa83a..bf863efaa4 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -6,7 +6,7 @@ //! [`Producer::finish`] or when the last producer drops. //! //! [Info] is the broadcast's static metadata, fixed for its lifetime. -use crate::{stats, track}; +use crate::{cache, stats, track}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, @@ -23,15 +23,14 @@ use super::{Requests, WeakCache}; /// /// Create via [`Info::produce`] to obtain both [`Producer`] and [`Consumer`] pair. /// This is the broadcast's static identity, fixed for its lifetime. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] #[non_exhaustive] pub struct Info { - /// The origin this broadcast belongs to (its identity, and the cache pool its - /// tracks and groups inherit). A track reaches its pool by walking up this link, - /// so the pool has a single home on the origin rather than being copied per - /// broadcast. Defaults to an unknown origin with an unbounded pool (a standalone - /// broadcast with no relay origin). - pub origin: super::origin::Config, + /// The cache pool this broadcast's tracks and groups inherit. + pub pool: cache::Pool, + + /// Ceiling on each track's media-timestamp retention window. + pub cache_duration: std::time::Duration, /// The path this broadcast is named by, which relative references in a catalog it /// serves (hang's `broadcast` field) resolve against. @@ -49,6 +48,16 @@ pub struct Info { pub path: crate::PathOwned, } +impl Default for Info { + fn default() -> Self { + Self { + pool: cache::Pool::new(cache::Config::default().with_expiry(cache::DEFAULT_EXPIRY)), + cache_duration: std::time::Duration::MAX, + path: crate::PathOwned::default(), + } + } +} + impl Info { /// Create a new broadcast with default metadata. pub fn new() -> Self { diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index e44e4d6f77..783a852748 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -35,7 +35,7 @@ pub use time::*; /// Publishing broadcasts, announcing routes, and consuming both through an origin. pub mod origin { - pub use super::origin_impl::{Config, Consumer, Cost, Driver, Dynamic, Pending, Producer, Request, Route, Run}; + pub use super::origin_impl::{Config, Consumer, Cost, Driver, Dynamic, Producer, Request, Requesting, Route, Run}; } /// Subscribing to route (un)announcements from an origin. diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index bbbbc5dee3..9245db3ba3 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -60,15 +60,9 @@ impl Hop { /// Generate a fresh hop with a random non-zero id. Use this for any relay that /// does not need a stable identity across restarts. - /// - /// TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS - /// clients decode `AnnounceInterest.exclude_hop` as a u53 (number) and - /// throw on anything > 2^53-1. To keep those clients alive against - /// fresh relays, we cap the random id at 53 bits. Restore to 62 bits - /// once the JS u62 fix has propagated to deployed bundles. pub fn random() -> Self { let mut rng = rand::rng(); - let id = rng.random_range(1..(1u64 << 53)); + let id = rng.random_range(1..(1u64 << 62)); Self { id } } @@ -88,19 +82,15 @@ impl Hop { /// An origin's identity plus the cache pool its broadcasts inherit. /// -/// Doubles as the construction config for an [origin `Producer`](Producer) and as the -/// parent handle every broadcast carries ([`broadcast::Info::origin`]): the origin owns -/// the [`cache::Pool`] every group in the tree charges into, so a relay configures one -/// bounded pool here and every broadcast, track, and group beneath it reaches that single -/// budget by walking up the ownership chain. Defaults to no byte target and the -/// cache's standard idle expiry. Cheap to clone (a `Copy` id plus an `Arc`-handle -/// bump), so it's stored by value rather than behind another `Arc`. +/// Construction config for an [origin `Producer`](Producer). The origin passes its +/// [`cache::Pool`] to every broadcast it creates, so every track and group beneath it +/// shares one budget. Defaults to no byte target and the cache's standard idle expiry. #[derive(Clone, Debug)] #[non_exhaustive] pub struct Config { /// The origin's wire identity, appended to broadcast hop chains for loop /// detection and shortest-path routing. - pub id: Hop, + pub hop: Hop, /// The cache pool broadcasts under this origin charge their groups into. It flows /// down the ownership chain (origin -> broadcast -> track -> group): a track opens @@ -132,12 +122,11 @@ pub struct Config { } impl Default for Config { - /// An unknown origin (id `0`, no loop detection) with no byte target and the - /// default idle expiry. This is what a standalone broadcast inherits. + /// A fresh random hop with no byte target and the default idle expiry. fn default() -> Self { let pool = cache::Pool::new(cache::Config::default().with_expiry(cache::DEFAULT_EXPIRY)); Self { - id: Hop::UNKNOWN, + hop: Hop::random(), pool, cache_duration: Duration::MAX, default_max_age: track::DEFAULT_MAX_AGE, @@ -147,15 +136,15 @@ impl Default for Config { impl Config { /// Config for the given origin id with no byte target and the default idle expiry. - pub fn new(id: Hop) -> Self { - Self { id, ..Self::default() } + pub fn new(hop: Hop) -> Self { + Self { hop, ..Self::default() } } } impl From for Config { /// Config for the given origin id with the defaults of [`Config::new`]. - fn from(id: Hop) -> Self { - Self::new(id) + fn from(hop: Hop) -> Self { + Self::new(hop) } } @@ -1060,7 +1049,7 @@ pub struct AnnounceUpdate { pub struct Producer { // Identity for this origin. Appended to route hops when re-announcing so // downstream relays can detect loops and prefer the shortest path. - info: Hop, + hop: Hop, // The tree and the absolute patterns this handle may publish under. scope: OriginScope, @@ -1098,14 +1087,6 @@ pub struct Producer { timers: TimersSlot, } -impl std::ops::Deref for Producer { - type Target = Hop; - - fn deref(&self) -> &Self::Target { - &self.info - } -} - impl Producer { /// Build a producer from a [`Config`] (identity + cache pool) with no scoped /// prefix and no pre-existing broadcasts, paired with the [`Driver`] that runs @@ -1122,7 +1103,7 @@ impl Producer { let timers = TimersSlot::default(); let pool = config.pool.clone(); let producer = Self { - info: config.id, + hop: config.hop, scope: scope.clone(), root: PathOwned::default(), shared: shared.clone(), @@ -1155,17 +1136,21 @@ impl Producer { self } - /// This origin's [`Config`] (identity + cache pool), the parent handle a broadcast - /// created under this origin carries (see [`broadcast::Info::origin`]). + /// This origin's construction config. pub fn config(&self) -> Config { Config { - id: self.info, + hop: self.hop, pool: self.pool.clone(), cache_duration: self.cache_duration, default_max_age: self.default_max_age, } } + /// This origin's hop identity. + pub fn hop(&self) -> Hop { + self.hop + } + // The retention window for a track whose publisher advertises none (see // [`Config::default_max_age`]). Cheaper than `config()`, which clones the pool. pub(crate) fn default_max_age(&self) -> Duration { @@ -1176,12 +1161,12 @@ 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. - pub(crate) fn empty(info: Hop) -> Self { + 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. let (tasks, _) = TaskSet::new(); Self { - info, + hop, scope: OriginScope::empty(), root: PathOwned::default(), shared: kio::Shared::default(), @@ -1221,8 +1206,8 @@ impl Producer { /// than a clean end. /// /// Fails with [`Error::Unauthorized`] if `path` is outside the prefixes this - /// producer may publish under (after [`scope`](Self::scope) / - /// [`with_root`](Self::with_root)), [`Error::BoundsExceeded`] if the full + /// producer may publish under (after [`scope`](Self::scope)), + /// [`Error::BoundsExceeded`] if the full /// rooted path exceeds [`Path::MAX_PARTS`], or [`Error::Closed`] once the /// origin's [`Driver`] has been dropped. pub fn create_broadcast(&self, path: impl AsPath) -> Result { @@ -1259,7 +1244,7 @@ impl Producer { // The broadcast advertises its own exact path, already checked against the scope. let announcer = Announcer { announcing: Announcing { - hop: self.info, + hop: self.hop, shared: self.shared.clone(), requested: full.clone(), prefixes: vec![full.clone()], @@ -1271,7 +1256,8 @@ impl Producer { }; let source = broadcast::Info { - origin: self.config(), + pool: self.pool.clone(), + cache_duration: self.cache_duration, path: full.clone(), } .produce() @@ -1313,8 +1299,15 @@ impl Producer { Ok(source) } + /// Create and advertise a broadcast in one call. + pub fn publish(&self, path: impl AsPath, route: Route) -> Result { + let broadcast = self.create_broadcast(path)?; + broadcast.announce(route)?; + Ok(broadcast) + } + /// Mint a standalone source broadcast for a served-route request: it carries - /// this origin's identity (cache pool included) and ingress attribution, but + /// this origin's cache policy and ingress attribution, but /// is *not* inserted into the broadcast tree. Sessions answer /// [`Dynamic`] requests with one of these; the requester already holds /// the request's result channel, so the tree never needs to resolve it. @@ -1323,7 +1316,8 @@ impl Producer { let full = self.root.join(&path).to_owned(); let ingress = self.stats.ingress(&full); broadcast::Info { - origin: self.config(), + pool: self.pool.clone(), + cache_duration: self.cache_duration, path: full, } .produce() @@ -1375,14 +1369,20 @@ impl Producer { }) } - /// Returns a new Producer restricted to publishing paths matching `patterns`, - /// relative to this producer's root. - pub fn scope(&self, patterns: &Patterns) -> Option { - let rooted = patterns.rooted(self.root.as_str()).ok()?; - Some(Producer { - info: self.info, - scope: self.scope.narrow(&rooted)?, - root: self.root.clone(), + /// Returns a producer rooted at `root` and restricted to matching `patterns`. + /// + /// `root` is relative to this producer's root, and `patterns` are relative to + /// the new root. Returns [`Error::Unauthorized`] when the requested scope has + /// no overlap with this producer's scope, or [`Error::BoundsExceeded`] when + /// rooting the patterns would exceed the path limit. + pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result { + let root = self.root.join(root).to_owned(); + let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?; + let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?; + Ok(Producer { + hop: self.hop, + scope, + root, shared: self.shared.clone(), pool: self.pool.clone(), cache_duration: self.cache_duration, @@ -1403,30 +1403,6 @@ impl Producer { Consumer::from_producer(self, stats::Session::default()) } - /// Returns a new Producer that automatically strips out the provided prefix. - /// - /// The scope is unchanged and merely renamed from the new root. Returns None - /// when nothing in scope lies under it. - pub fn with_root(&self, prefix: impl AsPath) -> Option { - let root = self.root.join(prefix).to_owned(); - if self.scope.relative(&root).is_empty() { - return None; - } - - Some(Self { - info: self.info, - root, - scope: self.scope.clone(), - shared: self.shared.clone(), - pool: self.pool.clone(), - cache_duration: self.cache_duration, - default_max_age: self.default_max_age, - stats: self.stats.clone(), - tasks: self.tasks.clone(), - timers: self.timers.clone(), - }) - } - /// Returns the root that is automatically stripped from all paths. pub fn root(&self) -> &Path<'_> { &self.root @@ -1478,7 +1454,7 @@ impl Announcing { return Err(Error::Unauthorized); } Ok(Self { - hop: producer.info, + hop: producer.hop, shared: producer.shared.clone(), requested: requested.clone(), prefixes: vec![requested], @@ -2108,7 +2084,8 @@ fn attach_source( // First source: create the broadcast and publish it into the tree. let broadcast = broadcast::Producer::new_spliced(broadcast::Info { - origin: ctx.origin.clone(), + pool: ctx.origin.pool.clone(), + cache_duration: ctx.origin.cache_duration, path: ctx.full.clone(), }); let state = kio::Producer::new(FrontState { @@ -3302,7 +3279,7 @@ impl Drop for Request { /// immediately when the broadcast was already announced, or once an [`Dynamic`] /// handler serves the request. Resolves to an error if the request is rejected or every /// handler drops before serving it. -pub struct Pending { +pub struct Requesting { inner: RequestState, // The path the requester asked for, relative to its cursor's root. Stamped on the // resolved broadcast (see [`broadcast::Info::path`]) because a handler is free to @@ -3326,7 +3303,7 @@ enum RequestState { Pending(kio::Consumer), } -impl Pending { +impl Requesting { fn ready(broadcast: broadcast::Consumer) -> Self { Self::new(RequestState::Ready(broadcast)) } @@ -3404,7 +3381,7 @@ impl Pending { } } -impl kio::Pollable for Pending { +impl kio::Pollable for Requesting { type Output = Result; fn poll(&self, waiter: &kio::Waiter) -> Poll { @@ -3478,7 +3455,7 @@ impl Consume for track::Consumer { #[derive(Clone)] pub struct Consumer { // Identity of the origin this consumer was derived from. - info: Hop, + hop: Hop, scope: OriginScope, // A prefix that is automatically stripped from all paths. @@ -3499,9 +3476,10 @@ pub struct Consumer { // default) filters nothing. exclude: Option, - // The origin config remote fronts inherit (identity, cache pool, retention), - // mirroring what `create_broadcast` gives a local front. - origin: Config, + // The cache policy remote fronts inherit, mirroring what + // `create_broadcast` gives a local front. + pool: cache::Pool, + cache_duration: Duration, // Non-owning submission handle to the origin's [`Driver`], for the front // watcher a routed `request_broadcast` spawns. Non-owning so a lingering @@ -3513,29 +3491,27 @@ pub struct Consumer { timers: TimersSlot, } -impl std::ops::Deref for Consumer { - type Target = Hop; - - fn deref(&self) -> &Self::Target { - &self.info - } -} - impl Consumer { fn from_producer(producer: &Producer, stats: stats::Session) -> Self { Self { - info: producer.info, + hop: producer.hop, scope: producer.scope.clone(), root: producer.root.clone(), shared: producer.shared.clone(), stats, exclude: None, - origin: producer.config(), + pool: producer.pool.clone(), + cache_duration: producer.cache_duration, tasks: producer.tasks.downgrade(), timers: producer.timers.clone(), } } + /// This origin's hop identity. + pub fn hop(&self) -> Hop { + self.hop + } + /// A clone that never serves the given peer its own data: routes whose hop /// chain contains `peer`, or whose announcing session is `peer`, are invisible /// and never resolved from, matching what the announce loop advertises to them. @@ -3630,7 +3606,7 @@ impl Consumer { // A max-depth path cannot be spelled as `path/**` (`**` would be a 33rd // segment), so watch the existing stream and match covering claims instead. let consumer = match Pattern::subtree(path.as_str()) { - Ok(subtree) => self.scope(&Patterns::from(subtree))?, + Ok(subtree) => self.scope("", &Patterns::from(subtree)).ok()?, Err(InvalidPattern::TooManySegments) => self.clone(), Err(_) => return None, }; @@ -3707,12 +3683,19 @@ impl Consumer { } } - /// Returns a new Consumer restricted to broadcasts matching `patterns`, - /// relative to this consumer's root. - pub fn scope(&self, patterns: &Patterns) -> Option { - let rooted = patterns.rooted(self.root.as_str()).ok()?; - Some(Consumer { - scope: self.scope.narrow(&rooted)?, + /// Returns a consumer rooted at `root` and restricted to matching `patterns`. + /// + /// `root` is relative to this consumer's root, and `patterns` are relative to + /// the new root. Returns [`Error::Unauthorized`] when the requested scope has + /// no overlap with this consumer's scope, or [`Error::BoundsExceeded`] when + /// rooting the patterns would exceed the path limit. + pub fn scope(&self, root: impl AsPath, patterns: &Patterns) -> Result { + let root = self.root.join(root).to_owned(); + let rooted = patterns.rooted(root.as_str()).map_err(|_| BoundsExceeded)?; + let scope = self.scope.narrow(&rooted).ok_or(Error::Unauthorized)?; + Ok(Consumer { + scope, + root, ..self.clone() }) } @@ -3739,7 +3722,7 @@ impl Consumer { /// A route claims capability, not inventory: resolving a covered path /// succeeds optimistically, and a path that names nothing surfaces as /// [`Error::NotFound`] on its tracks instead. - pub fn request_broadcast(&self, path: impl AsPath) -> kio::Pending { + pub fn request_broadcast(&self, path: impl AsPath) -> kio::Pending { let path = path.as_path(); // Key requests by absolute path so scoped/rooted consumers and handlers @@ -3754,20 +3737,20 @@ impl Consumer { // A local broadcast at the exact path wins. if let Some(broadcast) = self.resolve(&path) { - let resolved = Pending::ready(broadcast).with_path(requested).with_stats(scope); + let resolved = Requesting::ready(broadcast).with_path(requested).with_stats(scope); return kio::Pending::new(resolved); } // Routes only cover paths within this consumer's scope. if !self.scope.permits(&absolute) { - return kio::Pending::new(Pending::failed(Error::Unroutable)); + return kio::Pending::new(Requesting::failed(Error::Unauthorized)); } let mut state = self.shared.lock(); // The origin's driver dropped: nothing will ever serve this. if state.closed { - return kio::Pending::new(Pending::failed(Error::Closed)); + return kio::Pending::new(Requesting::failed(Error::Closed)); } // Join the live front for this path and exclusion, if any: its watcher @@ -3777,7 +3760,7 @@ impl Consumer { // for as long as its session does. let key = (absolute.clone(), self.exclude); if let Some(front) = state.fronts.get(&key) { - let pending = Pending::queued(front.request.consume()) + let pending = Requesting::queued(front.request.consume()) .with_path(requested) .with_stats(scope) .with_generation(state.generation); @@ -3789,7 +3772,7 @@ impl Consumer { .best_route(&absolute.as_path(), self.exclude, None, &HashSet::new()) .is_none() { - return kio::Pending::new(Pending::failed(Error::Unroutable)); + return kio::Pending::new(Requesting::failed(Error::Unroutable)); } // A route covers the path: mint the front and hand its watcher the @@ -3797,7 +3780,8 @@ impl Consumer { // route, resolves the channel, and re-splices the front through // routes sharing its first hop for as long as one serves. let broadcast = broadcast::Producer::new_spliced(broadcast::Info { - origin: self.origin.clone(), + pool: self.pool.clone(), + cache_duration: self.cache_duration, path: absolute.clone(), }); let front_state = kio::Producer::new(FrontState { @@ -3828,25 +3812,13 @@ impl Consumer { timers: self.timers.clone(), })); kio::Pending::new( - Pending::queued(consumer) + Requesting::queued(consumer) .with_path(requested) .with_stats(scope) .with_generation(generation), ) } - /// Returns a new Consumer that automatically strips out the provided prefix. - /// - /// The scope is unchanged and merely renamed from the new root. Returns None - /// when nothing in scope lies under it. - pub fn with_root(&self, prefix: impl AsPath) -> Option { - let root = self.root.join(prefix).to_owned(); - if self.scope.relative(&root).is_empty() { - return None; - } - Some(Self { root, ..self.clone() }) - } - /// Returns the prefix that is automatically stripped from all paths. pub fn root(&self) -> &Path<'_> { &self.root @@ -4125,6 +4097,15 @@ mod tests { .collect() } + #[test] + fn default_config_mints_a_real_hop() { + let config = Config::default(); + assert_ne!(config.hop, Hop::UNKNOWN); + let (producer, _driver) = Producer::new(config.clone()); + assert_eq!(producer.hop(), config.hop); + assert_eq!(producer.consume().hop(), config.hop); + } + /// Yield to the driver until `check` passes, bounded so a bug fails instead /// of hanging. async fn settle(mut check: impl FnMut() -> bool) { @@ -4233,6 +4214,14 @@ mod tests { announced.assert_next_ended("room/alice"); } + #[tokio::test] + async fn publish_creates_and_announces_together() { + let producer = origin(1).produce(); + let mut announced = producer.consume().announced(); + let _broadcast = producer.publish("room/alice", Route::default()).unwrap(); + announced.assert_next_active("room/alice"); + } + #[tokio::test] async fn standalone_broadcast_cannot_announce() { let broadcast = broadcast::Info::new().produce(); @@ -4263,9 +4252,14 @@ mod tests { let producer = origin(1).produce(); let bare = producer.node_count(); - let scoped = producer.consume().scope(&scopes(&["room/a", "room/b"])).unwrap(); + let scoped = producer.consume().scope("", &scopes(&["room/a", "room/b"])).unwrap(); assert_eq!(producer.node_count(), bare, "scoping should not create nodes"); - assert!(producer.consume().with_root("room/c").is_some()); + assert!( + producer + .consume() + .scope("room/c", &Patterns::from(Pattern::all())) + .is_ok() + ); assert_eq!(producer.node_count(), bare, "rooting should not create nodes"); drop(scoped); @@ -4281,8 +4275,8 @@ mod tests { #[tokio::test] async fn scoped_handles_survive_a_prune() { let producer = origin(1).produce(); - let scoped = producer.scope(&scopes(&["channel"])).unwrap(); - let consumer = producer.consume().scope(&scopes(&["channel"])).unwrap(); + let scoped = producer.scope("", &scopes(&["channel"])).unwrap(); + let consumer = producer.consume().scope("", &scopes(&["channel"])).unwrap(); // Create the scoped subtree and prune it straight back out. let first = scoped.create_broadcast("channel/chat").unwrap(); @@ -4300,7 +4294,7 @@ mod tests { #[tokio::test] async fn announce_keeps_its_prefix_under_a_producer_scope() { let producer = origin(1).produce(); - let scoped = producer.scope(&scopes(&["room"])).unwrap(); + let scoped = producer.scope("", &scopes(&["room"])).unwrap(); // Prefix advertisements stay prefixes. The scope filters requests locally. let _a = scoped.announce("", Route::default()).unwrap(); @@ -4319,7 +4313,7 @@ mod tests { let producer = origin(1).produce(); let _a = producer.announce("", Route::default()).unwrap(); - let consumer = producer.consume().scope(&scopes(&["room"])).unwrap(); + let consumer = producer.consume().scope("", &scopes(&["room"])).unwrap(); let mut announced = consumer.announced(); announced.assert_next_active(""); } @@ -4329,7 +4323,10 @@ mod tests { let producer = origin(1).produce(); let _a = producer.announce("room/alice", Route::default()).unwrap(); - let consumer = producer.consume().with_root("room").unwrap(); + let consumer = producer + .consume() + .scope("room", &Patterns::from(Pattern::all())) + .unwrap(); let mut announced = consumer.announced(); announced.assert_next_active("alice"); } @@ -4541,7 +4538,10 @@ mod tests { let _broad = producer.announce("room", Route::default().with_cost(1)).unwrap(); let _narrow = producer.announce("room/alice", Route::default().with_cost(9)).unwrap(); - let consumer = producer.consume().with_root("room/alice").unwrap(); + let consumer = producer + .consume() + .scope("room/alice", &Patterns::from(Pattern::all())) + .unwrap(); let mut announced = consumer.announced(); let route = announced.assert_next_active(""); assert_eq!(route.cost, Cost::new(9)); @@ -4555,22 +4555,22 @@ mod tests { let exact = producer.announce("room/alice", Route::default()).unwrap(); let consumer = producer .consume() - .scope(&Patterns::from("room/*".parse::().unwrap())) + .scope("", &Patterns::from("room/*".parse::().unwrap())) .unwrap() - .with_root("room/alice") + .scope("room/alice", &Patterns::from(Pattern::all())) .unwrap(); let mut announced = consumer.announced(); let first = announced.next().now_or_never().expect("next").expect("announce"); assert_eq!(first.path.as_str(), ""); assert_eq!(first.kind, AnnounceKind::Announced); - assert_eq!(first.captures, Some(vec!["alice".parse::().unwrap()])); + assert_eq!(first.captures, Some(Vec::new())); drop(exact); let retracted = announced.next().now_or_never().expect("next").expect("retract"); assert_eq!(retracted.path.as_str(), ""); assert_eq!(retracted.kind, AnnounceKind::Retracted); - assert_eq!(retracted.captures, Some(vec!["alice".parse::().unwrap()])); + assert_eq!(retracted.captures, Some(Vec::new())); let replacement = announced.next().now_or_never().expect("next").expect("announce"); assert_eq!(replacement.path.as_str(), ""); assert_eq!(replacement.kind, AnnounceKind::Announced); @@ -4659,7 +4659,7 @@ mod tests { #[test] fn dynamic_may_cover_a_scope_but_disjoint_prefixes_are_refused() { let producer = origin(1).produce(); - let scoped = producer.scope(&scopes(&["room"])).unwrap(); + let scoped = producer.scope("", &scopes(&["room"])).unwrap(); let _broad = scoped .dynamic("", Route::default()) .expect("an overlapping prefix is accepted"); @@ -4677,12 +4677,20 @@ mod tests { async fn dynamic_route_keeps_its_producer_scope() { let producer = origin(1).produce(); let scope = Patterns::from("*/chat".parse::().unwrap()); - let scoped = producer.scope(&scope).unwrap(); + let scoped = producer.scope("", &scope).unwrap(); let dynamic = scoped.dynamic("", Route::default()).unwrap(); - let mut matching = producer.consume().scope(&scopes(&["room/chat"])).unwrap().announced(); + let mut matching = producer + .consume() + .scope("", &scopes(&["room/chat"])) + .unwrap() + .announced(); matching.assert_next_active(""); - let mut outside = producer.consume().scope(&scopes(&["room/video"])).unwrap().announced(); + let mut outside = producer + .consume() + .scope("", &scopes(&["room/video"])) + .unwrap() + .announced(); outside.assert_next_wait(); let refused = producer @@ -5078,14 +5086,14 @@ mod tests { /// A path outside the consumer's scope never reaches a live dynamic handler. /// - /// `scope` is a read filter, so an out-of-scope path looks like "nothing here", - /// and that is exactly what would otherwise send a request to the handler. A - /// `Request` carries only a path, so the handler cannot tell who asked. + /// `scope` is authoritative, so an out-of-scope path is unauthorized before + /// routing can send a request to the handler. A `Request` carries only a path, + /// so the handler cannot tell who asked. #[tokio::test] async fn out_of_scope_request_never_reaches_the_dynamic_handler() { let producer = origin(1).produce(); let dynamic = producer.dynamic("", Route::default()).unwrap(); - let scoped = producer.consume().scope(&scopes(&["tenant-a"])).unwrap(); + let scoped = producer.consume().scope("", &scopes(&["tenant-a"])).unwrap(); // `tenant-a-other` shares a character prefix but not a segment, so this // also pins that the check is segment-aware rather than textual. @@ -5094,7 +5102,7 @@ mod tests { .request_broadcast(path) .now_or_never() .expect("an out-of-scope request must be refused synchronously, not queued"); - assert!(matches!(refused, Err(Error::Unroutable))); + assert!(matches!(refused, Err(Error::Unauthorized))); assert!( dynamic.requested_broadcast().now_or_never().is_none(), "the dynamic handler was asked to create a broadcast the requester may not read" @@ -5800,7 +5808,7 @@ mod tests { let producer = origin(1).produce(); let _a = producer.announce("", Route::default()).unwrap(); - let consumer = producer.consume().scope(&scopes(&["alpha", "beta"])).unwrap(); + let consumer = producer.consume().scope("", &scopes(&["alpha", "beta"])).unwrap(); let mut announced = consumer.announced(); announced.assert_next_active(""); announced.assert_next_wait(); @@ -5811,31 +5819,35 @@ mod tests { let producer = origin(1).produce(); // The root grant is `**`, the old empty prefix. - let root = producer.scope(&Patterns::from(Pattern::all())).unwrap(); + let root = producer.scope("", &Patterns::from(Pattern::all())).unwrap(); assert_eq!(root.allowed(), Patterns::from(Pattern::all())); // `foo/**` keeps the old `foo` prefix meaning. - let scoped = producer.scope(&scopes(&["room"])).unwrap(); + let scoped = producer.scope("", &scopes(&["room"])).unwrap(); assert_eq!(scoped.allowed(), scopes(&["room"])); // Multiple prefixes round-trip, with overlap collapsed. - let multi = producer.scope(&scopes(&["room", "room/chat", "anon"])).unwrap(); + let multi = producer.scope("", &scopes(&["room", "room/chat", "anon"])).unwrap(); assert_eq!(multi.allowed(), scopes(&["room", "anon"])); // The consumer side reports the same way. - let consumer = producer.consume().scope(&scopes(&["room"])).unwrap(); + let consumer = producer.consume().scope("", &scopes(&["room"])).unwrap(); assert_eq!(consumer.allowed(), scopes(&["room"])); for text in ["room", "", "*room", "room/*", "*", "**/room", "room/**/chat", "*.hang"] { let union = Patterns::from(text.parse::().unwrap()); - assert_eq!(producer.scope(&union).expect(text).allowed(), union, "{text}"); - assert_eq!(producer.consume().scope(&union).expect(text).allowed(), union, "{text}"); + assert_eq!(producer.scope("", &union).expect(text).allowed(), union, "{text}"); + assert_eq!( + producer.consume().scope("", &union).expect(text).allowed(), + union, + "{text}" + ); } let mixed: Patterns = ["room/**".parse().unwrap(), "other".parse().unwrap()] .into_iter() .collect(); - assert_eq!(producer.scope(&mixed).unwrap().allowed(), mixed); + assert_eq!(producer.scope("", &mixed).unwrap().allowed(), mixed); } #[test] @@ -5843,8 +5855,11 @@ mod tests { let producer = origin(1).produce(); // An empty union grants nothing: scoping is refused, like a disjoint prefix. - assert!(producer.scope(&Patterns::new()).is_none()); - assert!(producer.consume().scope(&Patterns::new()).is_none()); + assert!(matches!(producer.scope("", &Patterns::new()), Err(Error::Unauthorized))); + assert!(matches!( + producer.consume().scope("", &Patterns::new()), + Err(Error::Unauthorized) + )); } #[test] @@ -5852,15 +5867,18 @@ mod tests { let producer = origin(1).produce(); // Narrowing twice intersects; the grant stays in the new vocabulary. - let scoped = producer.scope(&scopes(&["room"])).unwrap(); - let nested = scoped.scope(&scopes(&["room/chat"])).unwrap(); + let scoped = producer.scope("", &scopes(&["room"])).unwrap(); + let nested = scoped.scope("", &scopes(&["room/chat"])).unwrap(); assert_eq!(nested.allowed(), scopes(&["room/chat"])); // A disjoint nesting is refused, not widened. - assert!(scoped.scope(&scopes(&["other"])).is_none()); + assert!(matches!( + scoped.scope("", &scopes(&["other"])), + Err(Error::Unauthorized) + )); // A literal root rebases the grant without changing its meaning. - let rooted = nested.with_root("room/chat").unwrap(); + let rooted = nested.scope("room/chat", &Patterns::from(Pattern::all())).unwrap(); assert_eq!(rooted.allowed(), scopes(&[""])); // Publishing through the nested view lands where the root says. @@ -5873,19 +5891,22 @@ mod tests { fn scope_intersects_and_rebases_arbitrary_grants() { let producer = origin(1).produce(); let rooms = producer - .scope(&Patterns::from("room/*".parse::().unwrap())) + .scope("", &Patterns::from("room/*".parse::().unwrap())) .unwrap(); let chats = rooms - .scope(&Patterns::from("*/chat".parse::().unwrap())) + .scope("", &Patterns::from("*/chat".parse::().unwrap())) .unwrap(); assert_eq!(chats.allowed(), Patterns::from("room/chat".parse::().unwrap())); let exact = producer - .scope(&Patterns::from("room/alice".parse::().unwrap())) + .scope("", &Patterns::from("room/alice".parse::().unwrap())) .unwrap(); - let rooted = exact.with_root("room").unwrap(); + let rooted = exact.scope("room", &Patterns::from(Pattern::all())).unwrap(); assert_eq!(rooted.allowed(), Patterns::from("alice".parse::().unwrap())); - assert!(exact.with_root("room/bob").is_none()); + assert!(matches!( + exact.scope("room/bob", &Patterns::from(Pattern::all())), + Err(Error::Unauthorized) + )); let broadcast = exact.create_broadcast("room/alice").unwrap(); assert!(matches!( @@ -5901,7 +5922,7 @@ mod tests { let producer = origin(1).produce(); let consumer = producer .consume() - .scope(&Patterns::from("room/*/chat".parse::().unwrap())) + .scope("", &Patterns::from("room/*/chat".parse::().unwrap())) .unwrap(); let mut announced = consumer.announced(); diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index dc18941c30..e366609f6a 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -303,7 +303,7 @@ pub(crate) struct FetchOutcome { impl TrackState { fn normalize_info(broadcast: &broadcast::Info, mut info: Info) -> Info { - info.max_age = info.max_age.min(broadcast.origin.cache_duration); + info.max_age = info.max_age.min(broadcast.cache_duration); info } @@ -781,7 +781,7 @@ impl TrackState { broadcast: broadcast.clone(), ..Default::default() }); - let cache = cache::Track::new(broadcast.origin.pool.clone(), state.downgrade()); + let cache = cache::Track::new(broadcast.pool.clone(), state.downgrade()); state.write().ok().expect("a new track is open").cache = cache; state } @@ -4811,13 +4811,9 @@ mod test { /// Mint a track under an origin caching into `pool`. fn track_producer_pooled(name: impl Into>, pool: cache::Pool) -> Producer { - let origin = crate::origin::Config { - pool, - ..crate::origin::Config::default() - }; Producer::new( Arc::new(broadcast::Info { - origin, + pool, ..Default::default() }), name, @@ -5022,13 +5018,9 @@ mod test { /// Mint a track under an origin whose retention ceiling is `cap`, so the /// track's own window is clamped down to it on bind. fn track_producer_capped(name: impl Into>, info: Info, cap: Duration) -> Producer { - let origin = crate::origin::Config { - cache_duration: cap, - ..crate::origin::Config::default() - }; Producer::new( Arc::new(broadcast::Info { - origin, + cache_duration: cap, ..Default::default() }), name, @@ -7321,10 +7313,7 @@ mod test { .with_expiry(cache::DEFAULT_EXPIRY); let pool = cache::Pool::new(config); let broadcast = broadcast::Info { - origin: crate::origin::Config { - pool: pool.clone(), - ..crate::origin::Config::default() - }, + pool: pool.clone(), ..Default::default() }; let producer = Producer::new(Arc::new(broadcast), "test", None); @@ -7499,10 +7488,7 @@ mod test { .with_expiry(cache::DEFAULT_EXPIRY); let pool = cache::Pool::new(config); let broadcast = broadcast::Info { - origin: crate::origin::Config { - pool: pool.clone(), - ..crate::origin::Config::default() - }, + pool: pool.clone(), ..Default::default() }; let request = Request::new(Arc::new(broadcast), "test"); @@ -7690,10 +7676,7 @@ mod test { .with_expiry(cache::DEFAULT_EXPIRY); let pool = cache::Pool::new(config); let broadcast = broadcast::Info { - origin: crate::origin::Config { - pool: pool.clone(), - ..crate::origin::Config::default() - }, + pool: pool.clone(), ..Default::default() }; let request = Request::new(Arc::new(broadcast), "test"); diff --git a/rs/moq-net/tests/group_charge.rs b/rs/moq-net/tests/group_charge.rs index 103a64c783..9bfbd7bd3c 100644 --- a/rs/moq-net/tests/group_charge.rs +++ b/rs/moq-net/tests/group_charge.rs @@ -15,7 +15,7 @@ use std::alloc::{GlobalAlloc, Layout, System}; use std::cell::Cell; -use moq_net::{Timestamp, broadcast, cache, origin}; +use moq_net::{Timestamp, broadcast, cache}; thread_local! { /// Bytes *this* thread has allocated and not yet freed. Per-thread and not a @@ -60,11 +60,7 @@ fn measure() -> (usize, u64) { // Unbounded: nothing may be evicted underneath the measurement. let pool = cache::Pool::unbounded(); let mut info = broadcast::Info::new(); - info.origin = { - let mut origin = origin::Config::default(); - origin.pool = pool.clone(); - origin - }; + info.pool = pool.clone(); let broadcast = info.produce(); let track = broadcast.create_track("chat", None).unwrap(); diff --git a/rs/moq-net/tests/loom.rs b/rs/moq-net/tests/loom.rs index 694fe08484..e55c21160e 100644 --- a/rs/moq-net/tests/loom.rs +++ b/rs/moq-net/tests/loom.rs @@ -22,7 +22,7 @@ use bytes::Bytes; use loom::{future::block_on, thread}; -use moq_net::{Error, Timestamp, broadcast, cache, origin}; +use moq_net::{Error, Timestamp, broadcast, cache}; /// A frame written on the publisher thread must reach a subscriber parked on /// `next_frame`, however the write interleaves with the reader's parking. @@ -182,11 +182,7 @@ fn concurrent_tracks_drain_a_shared_pool() { .with_expiry(cache::DEFAULT_EXPIRY); let pool = cache::Pool::new(config); let mut info = broadcast::Info::new(); - info.origin = { - let mut origin = origin::Config::default(); - origin.pool = pool.clone(); - origin - }; + info.pool = pool.clone(); let mut broadcast = info.produce(); let handles: Vec<_> = ["video", "audio"] diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index 6504c543c7..ed54bd3977 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -1106,9 +1106,9 @@ impl Cluster { origin_config.pool = cache.pool; origin_config.cache_duration = cache.duration; } - let origin = moq_tokio::origin::spawn(origin_config); + let origin = moq_tokio::origin::spawn_config(origin_config); let nodes = crate::nodes::Nodes::new(origin.clone()); - tracing::info!(hop_id = %origin.id(), configured = config.id.is_some(), "cluster initialized"); + tracing::info!(hop_id = %origin.hop(), configured = config.id.is_some(), "cluster initialized"); Ok(Cluster { config, client: None, @@ -1199,12 +1199,12 @@ impl Cluster { /// Passed by reference to [`moq_net::Server::with_publisher`] (or the /// equivalent per-request setter), which derives the read handle. pub fn subscriber(&self, token: &auth::Token) -> Option { - self.origin.with_root(&token.root)?.scope(&token.subscribe) + self.origin.scope(&token.root, &token.subscribe).ok() } /// Returns an [`origin::Producer`] scoped to this session's publish permissions. pub fn publisher(&self, token: &auth::Token) -> Option { - self.origin.with_root(&token.root)?.scope(&token.publish) + self.origin.scope(&token.root, &token.publish).ok() } /// Resolve whether gossip is on and which URL this relay advertises, from @@ -1561,7 +1561,11 @@ impl Cluster { /// unannounce-then-announce within sub-milliseconds, which clears the /// pending-cleanup timestamp long before the sweep fires. async fn run_discovery(self, self_url: String, token: String, dialed: DialMap) { - let Some(consumer) = self.origin.consume().with_root(MESH_PREFIX) else { + let Ok(consumer) = self + .origin + .consume() + .scope(MESH_PREFIX, &moq_net::Patterns::from(moq_net::Pattern::all())) + else { tracing::warn!("could not scope cluster origin to {MESH_PREFIX}; discovery disabled"); return; }; @@ -1916,8 +1920,7 @@ impl Cluster { // Cluster dials use their configured stats tier. Cluster peers carry no auth // root, so presence is keyed under the empty root within the cluster tier. let mut client = client - .with_publisher(&self.origin) - .with_subscriber(self.origin.clone()) + .with_origin(self.origin.clone()) .with_stats(self.stats.tier(self.cluster_tier()).session("")); if let Some(cost) = cost { client = client.with_cost(cost); @@ -1982,8 +1985,7 @@ impl Cluster { let addrs = moq_tokio::Addrs::collect(target.addrs()).context("peer advertised no reachable address")?; let mut client = self .lan_client(target.fingerprint.as_deref())? - .with_publisher(&self.origin) - .with_subscriber(self.origin.clone()); + .with_origin(self.origin.clone()); if let Some(cost) = target.cost { client = client.with_cost(cost); } @@ -2885,7 +2887,7 @@ mod tests { ..Default::default() }) .expect("valid id"); - assert_eq!(cluster.origin.id(), 42); + assert_eq!(cluster.origin.hop().id(), 42); } /// Cache settings land on the one origin serving, node discovery, and stats @@ -2908,7 +2910,7 @@ mod tests { .expect("cluster"); let origin = cluster.origin.clone(); - assert_eq!(origin.id(), 42); + assert_eq!(origin.hop().id(), 42); assert_eq!(origin.config().cache_duration, duration); assert_eq!(origin.config().pool.expiry(), Some(duration)); @@ -2920,7 +2922,7 @@ mod tests { .build(origin.clone()); let cluster = cluster.with_stats(stats); - assert_eq!(cluster.origin.id(), origin.id()); + assert_eq!(cluster.origin.hop().id(), origin.hop().id()); assert_eq!(cluster.origin.config().cache_duration, duration); assert_eq!(cluster.origin.config().pool.expiry(), Some(duration)); @@ -2939,7 +2941,7 @@ mod tests { let path = Path::new(MESH_PREFIX).join("https://peer.example/"); let mut announced = consumer .clone() - .with_root(MESH_PREFIX) + .scope(MESH_PREFIX, &moq_net::Patterns::from(moq_net::Pattern::all())) .expect("mesh prefix") .announced(); let registration = origin.create_broadcast(&path).expect("node advertise"); diff --git a/rs/moq-relay/src/internal.rs b/rs/moq-relay/src/internal.rs index 1982bf4a80..de9d988cd6 100644 --- a/rs/moq-relay/src/internal.rs +++ b/rs/moq-relay/src/internal.rs @@ -860,7 +860,7 @@ mod tests { #[tokio::test] async fn nodes_endpoint_uses_the_attached_cluster_registry() { - let origin = moq_tokio::origin::spawn(moq_net::Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(moq_net::Hop::new(100).unwrap())); let nodes = crate::nodes::Nodes::new(origin); let _connection = nodes.connect_outbound(0, "https://relay-b.example/"); let state = InternalState { @@ -880,15 +880,15 @@ mod tests { /// value, summed across broadcasts. #[tokio::test(start_paused = true)] async fn metrics_render_exposition() { + use moq_net::Timestamp; use moq_net::stats::{Registry, Tier}; - use moq_net::{Hop, Timestamp}; let stats = Registry::new(Default::default()); // Default-tier egress: an untagged local publisher writes, a tagged egress // consumer reads it out, so publisher `bytes` advance on the default tier. let default_ctx = stats.tier(Tier::default()).session("acme"); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let egress = pub_origin.consume().with_stats(default_ctx.clone()); let mut announced = egress.announced(); let pub_source = pub_origin.create_broadcast("demo/x").unwrap(); @@ -898,7 +898,7 @@ mod tests { // Named-tier ingress: a tagged ingress producer writes, so subscriber // `bytes` advance on the regional tier. let regional_ctx = stats.tier(Tier::new("region/sjc")).session("peer"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()).with_stats(regional_ctx.clone()); + let sub_origin = moq_tokio::origin::spawn().with_stats(regional_ctx.clone()); let sub_source = sub_origin.create_broadcast("demo/x").unwrap(); sub_source.announce(Default::default()).unwrap(); let sub_track = sub_source.create_track("audio", None).unwrap(); diff --git a/rs/moq-relay/src/nodes.rs b/rs/moq-relay/src/nodes.rs index 849cf6ea6a..23acd51222 100644 --- a/rs/moq-relay/src/nodes.rs +++ b/rs/moq-relay/src/nodes.rs @@ -153,9 +153,13 @@ impl Nodes { /// The node advertisements currently visible under [`MESH_PREFIX`]. fn announced(&self) -> Announced { - match self.origin.consume().with_root(MESH_PREFIX) { - Some(consumer) => self.scan_announced(&mut consumer.announced()), - None => Announced::default(), + match self + .origin + .consume() + .scope(MESH_PREFIX, &moq_net::Patterns::from(moq_net::Pattern::all())) + { + Ok(consumer) => self.scan_announced(&mut consumer.announced()), + Err(_) => Announced::default(), } } @@ -175,7 +179,7 @@ impl Nodes { let route = update.route; let hop_ids = route.hops.iter().map(|origin| origin.id()).collect::>(); // An advertisement with no hops never crossed a link, so it is our own. - let origin_id = hop_ids.first().copied().unwrap_or_else(|| self.origin.id()); + let origin_id = hop_ids.first().copied().unwrap_or_else(|| self.origin.hop().id()); scanned .origins @@ -298,7 +302,7 @@ mod tests { async fn snapshot_combines_announcements_and_live_connections() { const REMOTE_ID: u64 = 9_007_199_254_740_993; - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin.clone()); let _remote = announced_node(&origin, "https://relay-b.example/", &[REMOTE_ID], 7).await; let _outbound = nodes.connect_outbound(0, "https://relay-b.example/"); @@ -334,7 +338,7 @@ mod tests { #[tokio::test] async fn snapshot_omits_unresolved_inbound_connections() { - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin); let _inbound = nodes.connect_inbound(0, Hop::new(200).unwrap()); @@ -343,7 +347,7 @@ mod tests { #[tokio::test] async fn snapshot_stops_reporting_closed_outbound_connections() { - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin); let connection = nodes.connect_outbound(0, "https://relay-b.example/"); assert_eq!(nodes.snapshot().nodes.len(), 1); @@ -354,7 +358,7 @@ mod tests { #[tokio::test] async fn outbound_node_omits_credentials_from_url() { - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin); let _connection = nodes.connect_outbound(0, "https://relay-b.example/?jwt=secret"); @@ -367,12 +371,15 @@ mod tests { /// churned mid-request. #[tokio::test(start_paused = true)] async fn scan_skips_an_unannounce_queued_ahead_of_another_node() { - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin.clone()); let first = announced_node(&origin, "https://relay-a.example/", &[200], 1).await; let _second = announced_node(&origin, "https://relay-b.example/", &[300], 1).await; - let consumer = origin.consume().with_root(MESH_PREFIX).expect("mesh prefix is in root"); + let consumer = origin + .consume() + .scope(MESH_PREFIX, &moq_net::Patterns::from(moq_net::Pattern::all())) + .expect("mesh prefix is in root"); let mut announced = consumer.announced(); // Take relay-a's replayed announce, then retire it so the cursor queues a @@ -393,7 +400,7 @@ mod tests { #[tokio::test] async fn duplicate_origin_ids_do_not_resolve_inbound_connections() { - let origin = moq_tokio::origin::spawn(Hop::new(100).unwrap()); + let origin = moq_tokio::origin::spawn_config(moq_net::origin::Config::new(Hop::new(100).unwrap())); let nodes = Nodes::new(origin.clone()); let _first = announced_node(&origin, "https://relay-b.example/", &[200], 1).await; let _second = announced_node(&origin, "https://relay-c.example/", &[200], 1).await; diff --git a/rs/moq-relay/tests/auth_lifetime.rs b/rs/moq-relay/tests/auth_lifetime.rs index 6c58dddbcb..9bf5bfd9e4 100644 --- a/rs/moq-relay/tests/auth_lifetime.rs +++ b/rs/moq-relay/tests/auth_lifetime.rs @@ -20,7 +20,7 @@ use axum::routing::post; use axum::{Json, Router}; use moq_auth::{Event, Grant, Pattern, Patterns, Request}; use moq_relay::{Config, Connection, Relay, auth, cluster, web}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); @@ -216,7 +216,7 @@ fn room_url(scheme: &str, port: u16) -> url::Url { /// Connect a publisher and a subscriber to `url` and prove one frame /// round-trips. Returns both sessions so the caller can watch them close. async fn connect_and_round_trip(url: &url::Url) -> (moq_tokio::Connection, moq_tokio::Connection) { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -238,7 +238,7 @@ async fn connect_and_round_trip(url: &url::Url) -> (moq_tokio::Connection, moq_t .expect("publisher connect timeout") .expect("publisher connect failed"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let sub_session = tokio::time::timeout( @@ -291,7 +291,7 @@ async fn assert_refused(url: &url::Url) { } async fn assert_refused_with(client: moq_tokio::Client, url: &url::Url) { - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let result = tokio::time::timeout( TIMEOUT, client @@ -476,7 +476,7 @@ async fn http_routes_hold_a_lease() { let (port, relay) = spawn_ws_relay(build_auth(script.spawn().await)).await; // A publisher whose group stays open, so a fetch of it keeps streaming. - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce"); let track = broadcast.create_track("video", None).expect("create track"); @@ -498,7 +498,7 @@ async fn http_routes_hold_a_lease() { // Wait until the announcement reaches the relay before asking over HTTP: // the /announced handler only reports what has arrived so far. - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let mut announcements = sub_origin.consume().announced(); let _sub_session = tokio::time::timeout( TIMEOUT, @@ -727,7 +727,7 @@ async fn a_certificate_admits_only_what_the_server_grants() { .await; let (addr, relay) = spawn_quic_relay(build_auth(narrow), Some(root.clone())).await; let url: url::Url = format!("moql://127.0.0.1:{}/room", addr.port()).parse().unwrap(); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let session = tokio::time::timeout( TIMEOUT, mtls_client() diff --git a/rs/moq-relay/tests/cluster_unknown.rs b/rs/moq-relay/tests/cluster_unknown.rs index 4ec3eded44..a46aab8303 100644 --- a/rs/moq-relay/tests/cluster_unknown.rs +++ b/rs/moq-relay/tests/cluster_unknown.rs @@ -4,7 +4,6 @@ use std::{net::TcpListener, time::Duration}; -use moq_net::Hop; use moq_relay::{Config, Relay}; use url::Url; @@ -82,7 +81,7 @@ impl Drop for Publisher { async fn publish_version(port: u16, version: &str) -> Publisher { let url: Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast(PATH).expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -132,7 +131,7 @@ async fn publish_unknown(port: u16) -> Publisher { /// broke rather than just "no frame". async fn read_first_frame(port: u16) -> Result, String> { let url: Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); let session = tokio::time::timeout(TIMEOUT, client(None).with_subscriber(origin).connect(url).established()) .await @@ -182,7 +181,7 @@ async fn read_first_frame(port: u16) -> Result, String> { async fn watch_announces(port: u16, window: Duration) -> Vec<(String, bool)> { let url: Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut announced = origin.consume().announced(); let _session = tokio::time::timeout(TIMEOUT, client(None).with_subscriber(origin).connect(url).established()) .await diff --git a/rs/moq-relay/tests/drills.rs b/rs/moq-relay/tests/drills.rs index 2d80630351..95e9473002 100644 --- a/rs/moq-relay/tests/drills.rs +++ b/rs/moq-relay/tests/drills.rs @@ -20,7 +20,7 @@ use std::time::Duration; use moq_relay::{Config, Relay}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; /// Ceiling for anything a drill waits on. Every wait is bounded, so a broken /// handoff fails as a timeout with a message instead of hanging the suite. @@ -263,7 +263,7 @@ async fn cancel_under_backpressure_releases_the_reader() { let relay = RelayHost::start(None).await; let url = relay.url(); - let publisher = moq_tokio::origin::spawn(Hop::random()); + let publisher = moq_tokio::origin::spawn(); let broadcast = publisher.create_broadcast("live").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce broadcast"); let mut track = broadcast.create_track(TRACK, None).expect("create track"); @@ -279,7 +279,7 @@ async fn cancel_under_backpressure_releases_the_reader() { .expect("publisher connect timed out") .expect("publisher connect failed"); - let subscriber = moq_tokio::origin::spawn(Hop::random()); + let subscriber = moq_tokio::origin::spawn(); let subscribed = subscriber.consume(); let subscribe_session = tokio::time::timeout( TIMEOUT, @@ -348,7 +348,7 @@ async fn cancel_under_backpressure_releases_the_reader() { // ...and the relay survived it: a fresh subscriber still gets served, off the // upstream subscription the cancel left in place. - let rejoin = moq_tokio::origin::spawn(Hop::random()); + let rejoin = moq_tokio::origin::spawn(); let rejoined = rejoin.consume(); let rejoin_session = tokio::time::timeout( TIMEOUT, @@ -383,13 +383,13 @@ async fn relay_killed_mid_group_aborts_then_resumes() { let port = relay.port; let url = relay.url(); - let publisher = moq_tokio::origin::spawn(Hop::random()); + let publisher = moq_tokio::origin::spawn(); let broadcast = publisher.create_broadcast("live").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce broadcast"); let mut track = broadcast.create_track(TRACK, None).expect("create track"); let mut publish_loop = client(&url).publish(publisher.consume()).expect("no connect url"); - let subscriber = moq_tokio::origin::spawn(Hop::random()); + let subscriber = moq_tokio::origin::spawn(); let subscribed = subscriber.consume(); let mut subscribe_loop = client(&url).consume(subscriber).expect("no connect url"); @@ -484,7 +484,7 @@ async fn interrupted_publisher_republishes_new_content() { let relay = RelayHost::start(None).await; let url = relay.url(); - let subscriber = moq_tokio::origin::spawn(Hop::random()); + let subscriber = moq_tokio::origin::spawn(); let subscribed = subscriber.consume(); let subscribe_session = tokio::time::timeout( TIMEOUT, @@ -499,7 +499,7 @@ async fn interrupted_publisher_republishes_new_content() { .expect("subscriber connect failed"); let mut announced = subscribed.announced(); - let first = moq_tokio::origin::spawn(Hop::random()); + let first = moq_tokio::origin::spawn(); let broadcast = first.create_broadcast("live").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce broadcast"); let mut track = broadcast.create_track(TRACK, None).expect("create track"); @@ -538,7 +538,7 @@ async fn interrupted_publisher_republishes_new_content() { println!("fault activated: the interrupted publisher's broadcast was withdrawn"); // Restore: the same name, a new publisher, different content. - let second = moq_tokio::origin::spawn(Hop::random()); + let second = moq_tokio::origin::spawn(); let broadcast = second.create_broadcast("live").expect("re-create broadcast"); broadcast.announce(Default::default()).expect("announce broadcast"); let mut track = broadcast.create_track(TRACK, None).expect("re-create track"); @@ -595,7 +595,7 @@ async fn no_publisher_never_delivers() { let relay = RelayHost::start(None).await; let url = relay.url(); - let subscriber = moq_tokio::origin::spawn(Hop::random()); + let subscriber = moq_tokio::origin::spawn(); let subscribed = subscriber.consume(); let session = tokio::time::timeout( TIMEOUT, diff --git a/rs/moq-relay/tests/embed.rs b/rs/moq-relay/tests/embed.rs index 814c07a34c..6b9940e4cb 100644 --- a/rs/moq-relay/tests/embed.rs +++ b/rs/moq-relay/tests/embed.rs @@ -15,7 +15,7 @@ use std::net::{SocketAddr, TcpListener}; use std::time::Duration; use moq_relay::{Config, Relay}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); @@ -147,7 +147,7 @@ async fn embed_and_stop(mut config: Config) { group.finish().expect("finish group"); let url: url::Url = format!("https://{quic}/").parse().expect("parse url"); - let subscriber_origin = moq_tokio::origin::spawn(Hop::random()); + let subscriber_origin = moq_tokio::origin::spawn(); let consumer = subscriber_origin.consume(); let mut announced = consumer.announced(); let subscriber = tokio::time::timeout( diff --git a/rs/moq-relay/tests/goaway_cluster.rs b/rs/moq-relay/tests/goaway_cluster.rs index d06be60583..e01adf2468 100644 --- a/rs/moq-relay/tests/goaway_cluster.rs +++ b/rs/moq-relay/tests/goaway_cluster.rs @@ -10,7 +10,6 @@ use std::collections::BTreeSet; use std::net::TcpListener; use std::time::Duration; -use moq_net::Hop; use moq_relay::{ Connection, auth, cluster::{self, Peer}, @@ -98,7 +97,7 @@ fn drain_session_with_zero_timeout_closes_at_once() { async fn drain_session_with_zero_timeout_closes_at_once_inner() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let (port, mut accepted, _handle) = spawn_upstream(origin); wait_listening(port).await; @@ -148,7 +147,7 @@ fn spawn_upstream( let mut server = server.listen().await.expect("listen"); while let Some(request) = server.accept().await { // Serve the shared origin bidirectionally, like a relay peer would. - let scratch = moq_tokio::origin::spawn(Hop::random()); + let scratch = moq_tokio::origin::spawn(); let session = match request.with_publisher(&origin).with_subscriber(scratch).ok().await { Ok(session) => session, Err(err) => { @@ -187,7 +186,7 @@ async fn cluster_migrates_on_upstream_goaway_inner() { tokio::time::timeout(TEST_TIMEOUT, async { // ── the shared "live" broadcast both siblings can serve ───────── - let upstream_origin = moq_tokio::origin::spawn(Hop::random()); + let upstream_origin = moq_tokio::origin::spawn(); let broadcast = upstream_origin.create_broadcast("cam").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -384,7 +383,7 @@ async fn cluster_diamond_goaway_seamless_failover_inner() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); // ── TOP: origin server serving the same broadcast to both mids ────── - let top_origin = moq_tokio::origin::spawn(Hop::random()); + let top_origin = moq_tokio::origin::spawn(); let broadcast = top_origin.create_broadcast("diamond").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -402,7 +401,7 @@ async fn cluster_diamond_goaway_seamless_failover_inner() { .expect("TOP accept channel closed"); // ── MID-A: mini-relay consuming TOP, serving BOTTOM, drains later ─── - let mid_a_origin = moq_tokio::origin::spawn(Hop::random()); + let mid_a_origin = moq_tokio::origin::spawn(); let mut client_config = moq_tokio::connect::Config::default(); client_config.tls.insecure = Some(true); // Short handover so the test observes the old session close quickly. @@ -434,7 +433,7 @@ async fn cluster_diamond_goaway_seamless_failover_inner() { .expect("MID-A accept channel closed"); // ── SUBSCRIBER: connects to BOTTOM ─────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let mut sub_client_config = moq_tokio::connect::Config::default(); sub_client_config.tls.insecure = Some(true); let sub_client = sub_client_config @@ -656,7 +655,7 @@ async fn collect_group(sub: &mut moq_net::track::Subscriber, seen: &mut BTreeSet async fn cluster_reconnects_on_empty_uri_goaway_inner() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - let upstream_origin = moq_tokio::origin::spawn(Hop::random()); + let upstream_origin = moq_tokio::origin::spawn(); let broadcast = upstream_origin.create_broadcast("cam").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -775,7 +774,7 @@ fn goaway_handover_is_enforced_while_the_replacement_dial_hangs() { async fn goaway_handover_is_enforced_while_the_replacement_dial_hangs_inner() { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - let upstream_origin = moq_tokio::origin::spawn(Hop::random()); + let upstream_origin = moq_tokio::origin::spawn(); let (port, mut accepted, _handle) = spawn_upstream(upstream_origin); wait_listening(port).await; diff --git a/rs/moq-relay/tests/runtime_uring.rs b/rs/moq-relay/tests/runtime_uring.rs index 354085df15..4306dd1fc3 100644 --- a/rs/moq-relay/tests/runtime_uring.rs +++ b/rs/moq-relay/tests/runtime_uring.rs @@ -11,7 +11,7 @@ use std::net::{SocketAddr, UdpSocket}; use std::time::Duration; use moq_relay::{Config, Relay}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); const WORKERS: u16 = 2; @@ -133,7 +133,7 @@ async fn uring_workers_serve_webtransport_and_raw_quic() { // ...and a raw-QUIC subscriber (moql, the native path). let raw_url: url::Url = format!("moql://127.0.0.1:{port}/uring").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -148,7 +148,7 @@ async fn uring_workers_serve_webtransport_and_raw_quic() { // Several subscribers of each flavor, spread over the steered workers. let mut subscribers = Vec::new(); for url in [&wt_url, &raw_url, &wt_url, &raw_url] { - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); let announced = consumer.announced(); let connection = connect(client().with_subscriber(origin), url.clone()).await; @@ -296,7 +296,7 @@ async fn an_mtls_client_authenticates_without_a_token() { // unauthorized session establishes and is then closed, so merely // connecting proves nothing. let url: url::Url = format!("moql://127.0.0.1:{port}/mtls").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -307,7 +307,7 @@ async fn an_mtls_client_authenticates_without_a_token() { group.finish().expect("finish group"); let publisher = connect(client().with_publisher(&origin), url.clone()).await; - let subscriber_origin = moq_tokio::origin::spawn(Hop::random()); + let subscriber_origin = moq_tokio::origin::spawn(); let consumer = subscriber_origin.consume(); let mut announced = consumer.announced(); let subscriber = connect(client().with_subscriber(subscriber_origin), url).await; @@ -376,8 +376,8 @@ async fn uring_workers_write_qlog_traces() { // A real session, so a trace covers a handshake and application data // rather than a connection that only ever exchanged Initials. let url: url::Url = format!("moql://127.0.0.1:{port}/qlog").parse().expect("parse url"); - let origin = moq_tokio::origin::spawn(Hop::random()); - let broadcast = origin.create_broadcast("test").expect("create broadcast"); + let origin = moq_tokio::origin::spawn(); + let mut broadcast = origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); let mut group = track.append_group().expect("append group"); @@ -387,7 +387,7 @@ async fn uring_workers_write_qlog_traces() { group.finish().expect("finish group"); let publisher = connect(client().with_publisher(&origin), url.clone()).await; - let subscriber_origin = moq_tokio::origin::spawn(Hop::random()); + let subscriber_origin = moq_tokio::origin::spawn(); let consumer = subscriber_origin.consume(); let mut announced = consumer.announced(); let subscriber = connect(client().with_subscriber(subscriber_origin), url).await; diff --git a/rs/moq-relay/tests/runtime_workers.rs b/rs/moq-relay/tests/runtime_workers.rs index 3ef2bf35c8..ed6a30d2a2 100644 --- a/rs/moq-relay/tests/runtime_workers.rs +++ b/rs/moq-relay/tests/runtime_workers.rs @@ -10,7 +10,7 @@ use std::net::{SocketAddr, UdpSocket}; use std::time::Duration; use moq_relay::{Config, Relay}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); const WORKERS: u16 = 4; @@ -96,7 +96,7 @@ async fn workers_serve_quic_and_share_one_origin() { let url: url::Url = format!("https://127.0.0.1:{port}/workers").parse().expect("parse url"); // ── publisher ─────────────────────────────────────────────────── - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -111,7 +111,7 @@ async fn workers_serve_quic_and_share_one_origin() { // ── subscribers ───────────────────────────────────────────────── let mut subscribers = Vec::new(); for _ in 0..WORKERS { - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); let announced = consumer.announced(); let connection = connect(client().with_subscriber(origin), url.clone()).await; diff --git a/rs/moq-relay/tests/session_revalidate.rs b/rs/moq-relay/tests/session_revalidate.rs index 52964e808e..5d777ae0ba 100644 --- a/rs/moq-relay/tests/session_revalidate.rs +++ b/rs/moq-relay/tests/session_revalidate.rs @@ -17,7 +17,7 @@ use axum::{Json, Router}; use moq_auth::{Event, Grant, Pattern, Patterns, Request}; use moq_relay::session::{Filter, List, Nudged}; use moq_relay::{Connection, auth, cluster, internal, web}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); @@ -265,7 +265,7 @@ async fn connect(url: url::Url, bind: &str) -> moq_tokio::Connection { tokio::time::timeout( TIMEOUT, client_at(bind) - .with_subscriber(moq_tokio::origin::spawn(Hop::random())) + .with_subscriber(moq_tokio::origin::spawn()) .with_reconnect(false) .connect(url) .established(), diff --git a/rs/moq-relay/tests/smoke.rs b/rs/moq-relay/tests/smoke.rs index 912ff4e283..61e4d34456 100644 --- a/rs/moq-relay/tests/smoke.rs +++ b/rs/moq-relay/tests/smoke.rs @@ -10,7 +10,7 @@ use std::{net::TcpListener, time::Duration}; use moq_relay::{Config, Connection, Relay, auth, cluster, web}; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; const TIMEOUT: Duration = Duration::from_secs(10); @@ -168,7 +168,7 @@ async fn relay_websocket_round_trip_uses_newest_version() { let expected_version = newest_lite_version(); // ── publisher ─────────────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -190,7 +190,7 @@ async fn relay_websocket_round_trip_uses_newest_version() { ); // ── subscriber ────────────────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -366,7 +366,7 @@ async fn relay_websocket_root_path_upgrades() { let url: url::Url = format!("ws://127.0.0.1:{port}").parse().expect("parse url"); // ── publisher ─────────────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -385,7 +385,7 @@ async fn relay_websocket_root_path_upgrades() { .expect("publisher connect failed (root-path WS upgrade)"); // ── subscriber ────────────────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let (_client, sub_connection) = @@ -438,7 +438,7 @@ async fn two_publish_only_clients_coexist() { let url: url::Url = format!("ws://127.0.0.1:{port}/smoke").parse().expect("parse url"); // ── two publish-only publishers, each serving a distinct broadcast ── - let pub_a = moq_tokio::origin::spawn(Hop::random()); + let pub_a = moq_tokio::origin::spawn(); let broadcast_a = pub_a.create_broadcast("alpha").expect("create broadcast a"); broadcast_a.announce(Default::default()).expect("create broadcast a"); let track_a = broadcast_a.create_track("video", None).expect("create track a"); @@ -448,7 +448,7 @@ async fn two_publish_only_clients_coexist() { .write_frame(moq_net::Timestamp::ZERO, b"a".as_ref()) .expect("write frame a"); - let pub_b = moq_tokio::origin::spawn(Hop::random()); + let pub_b = moq_tokio::origin::spawn(); let broadcast_b = pub_b.create_broadcast("beta").expect("create broadcast b"); broadcast_b.announce(Default::default()).expect("create broadcast b"); let track_b = broadcast_b.create_track("video", None).expect("create track b"); @@ -474,7 +474,7 @@ async fn two_publish_only_clients_coexist() { .expect("publisher b connect failed"); // ── one subscriber should see broadcasts from both publish-only clients ── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let (_client, sub_connection) = @@ -592,7 +592,7 @@ async fn internal_tcp_round_trip() { let expected_version = newest_lite_version(); // ── publisher ─────────────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -616,7 +616,7 @@ async fn internal_tcp_round_trip() { ); // ── subscriber ────────────────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let (_client, sub_connection) = @@ -708,7 +708,7 @@ async fn internal_unix_round_trip() { let expected_version = newest_lite_version(); // ── publisher ─────────────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -732,7 +732,7 @@ async fn internal_unix_round_trip() { ); // ── subscriber ────────────────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let (_client, sub_connection) = @@ -800,7 +800,7 @@ fn path_versions() -> Vec { /// whether the request path reached the server (it scopes the publisher's grant /// to that root). async fn path_round_trip(version: moq_net::Version, pub_url: url::Url, sub_url: url::Url, broadcast: &str) -> String { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let bc = pub_origin.create_broadcast(broadcast).expect("create broadcast"); bc.announce(Default::default()).expect("create broadcast"); let track = bc.create_track("video", None).expect("create track"); @@ -816,7 +816,7 @@ async fn path_round_trip(version: moq_net::Version, pub_url: url::Url, sub_url: .expect("publisher connect timeout") .expect("publisher connect failed"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); let sub_client = client_version(Some(version)).with_subscriber(sub_origin); @@ -982,7 +982,7 @@ async fn subscribe_only_public_rejects_publisher_role() { let (port, handle) = spawn_subscribe_only_relay().await; let url: url::Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); // The lite-05 client resolves `connect()` optimistically, so it may return Ok // before the relay's verdict lands. Either the connect fails outright, or the @@ -1014,7 +1014,7 @@ async fn subscribe_only_public_accepts_subscriber_role() { let (port, handle) = spawn_subscribe_only_relay().await; let url: url::Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let (_client, connection) = tokio::time::timeout(TIMEOUT, connect_once(client().with_subscriber(sub_origin), url)) .await .expect("subscriber connect timeout") @@ -1068,7 +1068,7 @@ async fn publish_only_public_rejects_subscriber_role() { let (port, handle) = spawn_publish_only_relay().await; let url: url::Url = format!("tcp://127.0.0.1:{port}").parse().expect("parse url"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); // Like the publisher case, `connect()` may resolve optimistically; either it fails // outright, or the session the relay hands back closes shortly after. diff --git a/rs/moq-room/README.md b/rs/moq-room/README.md index 231f27ccf2..98718b40ce 100644 --- a/rs/moq-room/README.md +++ b/rs/moq-room/README.md @@ -7,13 +7,13 @@ Participants are discovered from the announce stream. Identity is the path befor This is the native counterpart of [`@moq/room`](https://www.npmjs.com/package/@moq/room). hang.live and [iroh-live](https://github.com/n0-computer/iroh-live) (`iroh-rooms` is being redesigned onto the announce bus) can depend on it for roster, path convention, token claims, and the `chat` track. Gossip, tickets, and 1:1 Call stay in iroh-live. Capture/encode stay in `moq-video` / `moq-audio`. Native `Local`/`Remote` media plumbing stays with those crates too; this crate is media-free. ```rust -use moq_net::{Hop, Path}; +use moq_net::Path; use moq_room::{Kind, Room, chat, claims}; let token = key.sign(&claims("meet/demo", "alice")?, None)?; // Dial the relay at meet/demo?jwt=... -let origin = moq_tokio::origin::spawn(Hop::random()); +let origin = moq_tokio::origin::spawn(); let mut room = Room::new(&origin.consume(), Some(Path::new("alice").to_owned())); while let Some(event) = room.next().await { if event.kind == Kind::Camera { diff --git a/rs/moq-room/src/room.rs b/rs/moq-room/src/room.rs index 4cca72925c..8e08e1e2ee 100644 --- a/rs/moq-room/src/room.rs +++ b/rs/moq-room/src/room.rs @@ -4,7 +4,7 @@ use std::task::{Poll, ready}; use moq_net::{ PathOwned, announce, broadcast, - origin::{self, Pending}, + origin::{self, Requesting}, }; use crate::path::{Kind, parse}; @@ -38,7 +38,7 @@ struct Inflight { identity: PathOwned, kind: Kind, path: PathOwned, - request: Pending, + request: Requesting, } /// Runs the announce loop and yields remote participant broadcasts. @@ -151,10 +151,10 @@ impl Room { #[cfg(test)] mod tests { use super::*; - use moq_net::{Hop, Path, origin::Route}; + use moq_net::{Path, origin::Route}; fn origin() -> origin::Producer { - moq_tokio::origin::spawn(Hop::random()) + moq_tokio::origin::spawn() } fn publish(origin: &origin::Producer, path: &str) -> broadcast::Producer { diff --git a/rs/moq-rtc/src/egress.rs b/rs/moq-rtc/src/egress.rs index 07676d14ce..dd3313614b 100644 --- a/rs/moq-rtc/src/egress.rs +++ b/rs/moq-rtc/src/egress.rs @@ -300,7 +300,7 @@ pub fn dispatch(rtc: &mut str0m::Rtc, request: WriteRequest, wallclock: Instant) mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index 89eae87b68..f8789c494a 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -85,7 +85,7 @@ mod tests { #[tokio::test] async fn whip_and_whep_round_trip_opus() { - let source_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let source_origin = moq_tokio::origin::spawn(); let source_consumer = source_origin.consume(); let mut announcements = source_consumer.announced(); let mut source = source_origin @@ -113,7 +113,7 @@ mod tests { assert!(announcement.kind.is_active(), "source was unannounced"); drop(announcements); - let server_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let server_origin = moq_tokio::origin::spawn(); let server = Server::new( server::Config::default(), server_origin.clone(), @@ -135,7 +135,7 @@ mod tests { .expect("WHIP negotiation timed out") .expect("WHIP negotiation failed"); - let output_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let output_origin = moq_tokio::origin::spawn(); let output = output_origin .create_broadcast("output") .expect("create output broadcast"); diff --git a/rs/moq-rtc/src/server/mod.rs b/rs/moq-rtc/src/server/mod.rs index 88e549f2cd..055a15448b 100644 --- a/rs/moq-rtc/src/server/mod.rs +++ b/rs/moq-rtc/src/server/mod.rs @@ -300,7 +300,7 @@ pub(crate) async fn delete(State(server): State, Path(path): Path moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-rtmp/src/dial.rs b/rs/moq-rtmp/src/dial.rs index f1b78f4e27..4e987e63e1 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -496,11 +496,8 @@ struct Publisher { impl Publisher { fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> anyhow::Result { let mut broadcast = origin - .create_broadcast(path) + .publish(path, moq_net::origin::Route::default()) .map_err(|err| anyhow::anyhow!("broadcast '{path}' could not be published: {err}"))?; - broadcast - .announce(moq_net::origin::Route::default()) - .map_err(|err| anyhow::anyhow!("broadcast '{path}' could not be announced: {err}"))?; let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); @@ -564,7 +561,7 @@ mod tests { let mut vframe = vec![0x17, 0x01, 0x00, 0x00, 0x00]; vframe.extend_from_slice(&[0, 0, 0, 5, 0x65, 0x88, 0x84, 0x21, 0x00]); - let server_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let server_origin = moq_tokio::origin::spawn(); let mut broadcast = server_origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); @@ -587,7 +584,7 @@ mod tests { }); // Client: dial, connect(`live`), play(`cam0`), republish into our own origin. - let client_origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let client_origin = moq_tokio::origin::spawn(); let announced = client_origin.consume(); let pull_origin = client_origin.clone(); let pull = tokio::spawn(async move { diff --git a/rs/moq-rtmp/src/server.rs b/rs/moq-rtmp/src/server.rs index 758069507e..398c0b36f1 100644 --- a/rs/moq-rtmp/src/server.rs +++ b/rs/moq-rtmp/src/server.rs @@ -1296,8 +1296,7 @@ impl Publisher { /// Open a broadcast at `path` and prime the importer with the FLV file /// header, so subsequent tags decode against an initialized demuxer. fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> anyhow::Result { - let mut broadcast = origin.create_broadcast(path)?; - broadcast.announce(moq_net::origin::Route::default())?; + let mut broadcast = origin.publish(path, moq_net::origin::Route::default())?; let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); let mut importer = FlvImport::new(broadcast, catalog.reserve()); @@ -1632,7 +1631,7 @@ mod tests { vseq.extend_from_slice(&[0x01, 0x42, 0xc0, 0x1f, 0xff, 0xe1, 0x00, 0x04, 0x67, 0x42, 0xc0, 0x1f]); vseq.extend_from_slice(&[0x01, 0x00, 0x04, 0x68, 0xce, 0x3c, 0x80]); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut publisher = Publisher::new( &origin, "live/cam0", @@ -1669,7 +1668,7 @@ mod tests { vframe.extend_from_slice(&[0, 0, 0, 5, 0x65, 0x88, 0x84, 0x21, 0x00]); // Publish the broadcast at `live/cam0` by feeding synthetic FLV to the importer. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); @@ -1717,7 +1716,7 @@ mod tests { async fn play_enhanced_codec_rejects_legacy_client() { const VP9_KEYFRAME_320X240: &[u8] = &[0x82, 0x49, 0x83, 0x42, 0x20, 0x13, 0xf0, 0x0e, 0xf0, 0x00]; - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); @@ -1839,7 +1838,7 @@ mod tests { let nalu = |b: u8| vec![0, 0, 0, 0, 0, 5, 0x65, b, 0x84, 0x21, 0x00]; let frames = multitrack_body(CODED_FRAMES, &[(0, nalu(0x88)), (1, nalu(0x99))]); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); @@ -1900,7 +1899,7 @@ mod tests { let mut server = Server::bind("127.0.0.1:0".parse().unwrap()).await.unwrap(); let addr = server.local_addr().unwrap(); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let consumer = origin.consume(); let stream = TcpStream::connect(addr).await.unwrap(); diff --git a/rs/moq-srt/src/dial.rs b/rs/moq-srt/src/dial.rs index 4f49072a91..9759f21c9e 100644 --- a/rs/moq-srt/src/dial.rs +++ b/rs/moq-srt/src/dial.rs @@ -146,7 +146,7 @@ impl Mode { mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-srt/src/server.rs b/rs/moq-srt/src/server.rs index 3077b316eb..83e34b8f59 100644 --- a/rs/moq-srt/src/server.rs +++ b/rs/moq-srt/src/server.rs @@ -818,7 +818,7 @@ mod tests { // Ten minutes in, the span from the controlled-rewind evidence on #2833. const OFFSET: u64 = 600_000_000; - let (origin, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (origin, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); let mut broadcast = origin.create_broadcast("rewind").unwrap(); broadcast.announce(moq_net::origin::Route::default()).unwrap(); diff --git a/rs/moq-srt/src/ts.rs b/rs/moq-srt/src/ts.rs index 9c62053bed..9646e423e8 100644 --- a/rs/moq-srt/src/ts.rs +++ b/rs/moq-srt/src/ts.rs @@ -40,8 +40,7 @@ impl Publisher { /// (`with_max_age`) and the connection allocator passthrough tracks claim on /// (`with_bandwidth`). pub fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> Result { - let mut broadcast = origin.create_broadcast(path)?; - broadcast.announce(moq_net::origin::Route::default())?; + let mut broadcast = origin.publish(path, moq_net::origin::Route::default())?; let config = config.with_catalog(moq_mux::catalog::hang::Catalog::::default()); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; let handle = broadcast.clone(); @@ -144,7 +143,7 @@ mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-stats/src/aggregate.rs b/rs/moq-stats/src/aggregate.rs index 847b97b2f9..bcd6009f34 100644 --- a/rs/moq-stats/src/aggregate.rs +++ b/rs/moq-stats/src/aggregate.rs @@ -206,7 +206,7 @@ enum Reader { /// retracting (the table has already changed), while an unqueued /// `Unroutable` means nothing serves the path at all. Resolving { - pending: Pending, + pending: Pending, queued: bool, }, /// Awaiting the subscription handshake. @@ -478,7 +478,7 @@ fn resolve(origin: &origin::Consumer, path: &PathOwned) -> Reader< mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-stats/src/consume.rs b/rs/moq-stats/src/consume.rs index cedf67e1f8..6703ad582c 100644 --- a/rs/moq-stats/src/consume.rs +++ b/rs/moq-stats/src/consume.rs @@ -106,7 +106,7 @@ impl SessionsConsumer { mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-stats/src/produce.rs b/rs/moq-stats/src/produce.rs index b651ebd479..02c1a46ac7 100644 --- a/rs/moq-stats/src/produce.rs +++ b/rs/moq-stats/src/produce.rs @@ -636,17 +636,13 @@ struct GroupPublisher { impl GroupPublisher { fn create(origin: &origin::Producer, prefix: &Path, group: &Path, node: Option<&str>) -> Option { let advertised = advertised_path(prefix, group, node); - let broadcast = match origin.create_broadcast(&advertised) { + let broadcast = match origin.publish(&advertised, origin::Route::default()) { Ok(broadcast) => broadcast, Err(err) => { tracing::warn!(advertised = %advertised, ?err, "stats: origin rejected stats broadcast"); return None; } }; - if let Err(err) = broadcast.announce(origin::Route::default()) { - tracing::warn!(advertised = %advertised, ?err, "stats: origin rejected stats announce"); - return None; - } tracing::debug!(advertised = %advertised, "stats: publishing broadcast"); let mut traffic = TrackFamily::new(); @@ -885,7 +881,7 @@ fn advertised_path(prefix: &Path, group: &Path, node: Option<&str>) -> PathOwned mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-tokio/CHANGELOG.md b/rs/moq-tokio/CHANGELOG.md index a7ce3c78da..c59a4a11bf 100644 --- a/rs/moq-tokio/CHANGELOG.md +++ b/rs/moq-tokio/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [**breaking**] `connection::Monitor::presence` reports `sessions_started` / `sessions_ended`. -- `origin::spawn` takes an `origin::Config` (formerly `origin::Info`) or a bare `Hop`. +- [**breaking**] `origin::spawn()` uses `origin::Config::default()`; explicit configurations use `origin::spawn_config(config)`. ### Added - `Request::remote_addr`, `local_addr`, `server_name`, and `alpn` report what the transport saw; `PeerIdentity::name` and `issuer` read the leaf certificate. +- `Client::with_origin` and `Server::with_origin` publish and subscribe through one shared origin. ### Added diff --git a/rs/moq-tokio/examples/chat.rs b/rs/moq-tokio/examples/chat.rs index 4b3295566b..2e78dc5881 100644 --- a/rs/moq-tokio/examples/chat.rs +++ b/rs/moq-tokio/examples/chat.rs @@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> { moq_tokio::Log::new(tracing::Level::DEBUG).init()?; // Create an origin that we can publish to and the session can consume from. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // Run the broadcast production and the session in parallel. // This is a simple example of how you can concurrently run multiple tasks. diff --git a/rs/moq-tokio/examples/clock.rs b/rs/moq-tokio/examples/clock.rs index 5db575b1ed..a2e5979f1b 100644 --- a/rs/moq-tokio/examples/clock.rs +++ b/rs/moq-tokio/examples/clock.rs @@ -60,7 +60,7 @@ async fn main() -> anyhow::Result<()> { let track = config.track; - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); match config.role { Command::Publish => { @@ -101,7 +101,7 @@ async fn main() -> anyhow::Result<()> { let scope = moq_net::Patterns::from(moq_net::Pattern::subtree(path.as_str()).context("invalid broadcast name")?); let consumer = origin - .scope(&scope) + .scope("", &scope) .context("not allowed to consume broadcast")? .consume(); let mut announced = consumer.announced(); diff --git a/rs/moq-tokio/src/client.rs b/rs/moq-tokio/src/client.rs index f60e110ae3..237aab2032 100644 --- a/rs/moq-tokio/src/client.rs +++ b/rs/moq-tokio/src/client.rs @@ -226,6 +226,12 @@ impl Client { self } + /// Publish and subscribe through one shared origin. + pub fn with_origin(mut self, origin: moq_net::origin::Producer) -> Self { + self.moq = self.moq.with_origin(origin); + self + } + /// Subscribe to the peer's broadcasts, ingesting them into the given origin. pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self { self.moq = self.moq.with_subscriber(subscribe); @@ -724,7 +730,7 @@ mod tests { let server = listen.init(Default::default()).unwrap(); let mut server = server.listen().await.unwrap(); let peer = server.local_addr().unwrap(); - let origin = crate::origin::spawn(moq_net::Hop::random()); + let origin = crate::origin::spawn(); let server_origin = origin.clone(); let accepted = tokio::spawn(async move { let request = server.accept().await.unwrap(); diff --git a/rs/moq-tokio/src/origin.rs b/rs/moq-tokio/src/origin.rs index 0320543b35..b305b89a6a 100644 --- a/rs/moq-tokio/src/origin.rs +++ b/rs/moq-tokio/src/origin.rs @@ -10,10 +10,13 @@ /// /// Panics if called outside a tokio runtime. /// -/// Accepts a [`Config`](moq_net::origin::Config) or a bare -/// [`Hop`](moq_net::Hop) id (which uses the default config). -pub fn spawn(config: impl Into) -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(config.into()); +pub fn spawn() -> moq_net::origin::Producer { + spawn_config(moq_net::origin::Config::default()) +} + +/// Build and spawn an origin producer with an explicit configuration. +pub fn spawn_config(config: moq_net::origin::Config) -> moq_net::origin::Producer { + let (producer, driver) = moq_net::origin::Producer::new(config); tokio::spawn(driver.run(crate::runtime::Runtime::<()>::new())); producer } @@ -26,7 +29,7 @@ mod tests { /// reaches a consumer, and dropping the announcement retracts it. #[tokio::test] async fn spawn_drives_the_origin() { - let origin = spawn(moq_net::origin::Config::new(moq_net::Hop::random())); + let origin = spawn(); let mut announced = origin.consume().announced(); let broadcast = origin.create_broadcast("cam").expect("create broadcast"); @@ -46,6 +49,6 @@ mod tests { #[test] #[should_panic(expected = "no reactor running")] fn spawn_outside_runtime_panics() { - let _ = spawn(moq_net::origin::Config::new(moq_net::Hop::random())); + let _ = spawn(); } } diff --git a/rs/moq-tokio/src/server.rs b/rs/moq-tokio/src/server.rs index 555d4b97dc..508798e9c3 100644 --- a/rs/moq-tokio/src/server.rs +++ b/rs/moq-tokio/src/server.rs @@ -1683,7 +1683,7 @@ mod tests { let path = PathBuf::from(format!("/tmp/moq-tokio-publish-{}.sock", std::process::id())); let _ = std::fs::remove_file(&path); - let origin = crate::origin::spawn(moq_net::Hop::random()); + let origin = crate::origin::spawn(); let broadcast = origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -1720,7 +1720,7 @@ mod tests { const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); let url: Url = format!("unix://{}", path.display()).parse().expect("parse url"); - let subscriber = crate::origin::spawn(moq_net::Hop::random()); + let subscriber = crate::origin::spawn(); let consumer = subscriber.consume(); let mut announced = consumer.announced(); let client = crate::connect::Config::default() diff --git a/rs/moq-tokio/tests/alpn.rs b/rs/moq-tokio/tests/alpn.rs index 2dc34c0a52..3ce3ab0b00 100644 --- a/rs/moq-tokio/tests/alpn.rs +++ b/rs/moq-tokio/tests/alpn.rs @@ -24,7 +24,7 @@ async fn connect_with_version(version: &str) { let addr = server.local_addr().expect("failed to get local addr"); // Provide a dummy origin so the MoQ handshake has something to negotiate. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // ── client ────────────────────────────────────────────────────── let mut client_config = moq_tokio::connect::Config::default(); @@ -74,7 +74,7 @@ async fn connect_with_webtransport(version: Option<&str>) { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("failed to get local addr"); - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); // ── client ────────────────────────────────────────────────────── let mut client_config = moq_tokio::connect::Config::default(); diff --git a/rs/moq-tokio/tests/backend.rs b/rs/moq-tokio/tests/backend.rs index b3456b0008..82eb78fc5a 100644 --- a/rs/moq-tokio/tests/backend.rs +++ b/rs/moq-tokio/tests/backend.rs @@ -4,7 +4,6 @@ //! Each test is gated with `#[cfg(feature = "...")]` so it only compiles when the //! corresponding backend is enabled. Running `cargo test --all-features` exercises all. -use moq_tokio::moq_net::Hop; use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(10); @@ -118,7 +117,7 @@ async fn connect_test(config: ConnectTest<'_>) { } = config; // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -140,7 +139,7 @@ async fn connect_test(config: ConnectTest<'_>) { let addr = server.local_addr().expect("failed to get local addr"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -475,7 +474,7 @@ struct MtlsPaths { async fn mtls_test(scheme: &str, backend: moq_tokio::QuicBackend, reject: bool) { let (_dir, paths) = generate_mtls_certs(); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let mut server_config = moq_tokio::listen::Config::default(); server_config.bind = Some("127.0.0.1:0".parse().unwrap()); @@ -709,7 +708,7 @@ async fn iroh_connect() { use moq_tokio::iroh::Config as IrohConfig; // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -749,7 +748,7 @@ async fn iroh_connect() { let mut server = server.listen().await.expect("failed to listen"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); diff --git a/rs/moq-tokio/tests/broadcast.rs b/rs/moq-tokio/tests/broadcast.rs index 1f4480972a..729b612160 100644 --- a/rs/moq-tokio/tests/broadcast.rs +++ b/rs/moq-tokio/tests/broadcast.rs @@ -23,7 +23,7 @@ async fn broadcast_test(scheme: &str, client_version: Option<&str>, server_versi let server_version: Option = server_version.map(|v| v.parse().expect("invalid server version")); // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -49,7 +49,7 @@ async fn broadcast_test(scheme: &str, client_version: Option<&str>, server_versi let addr = server.local_addr().expect("failed to get local addr"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -132,7 +132,7 @@ async fn broadcast_test(scheme: &str, client_version: Option<&str>, server_versi async fn lite05_timestamp_roundtrip(scheme: &str) { use moq_tokio::moq_net::{Timescale, Timestamp}; - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -172,7 +172,7 @@ async fn lite05_timestamp_roundtrip(scheme: &str) { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("failed to get local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -257,7 +257,7 @@ async fn broadcast_moq_lite_05_timestamps_webtransport() { async fn lite05_fetch_roundtrip(scheme: &str) { use moq_tokio::moq_net::{Timescale, Timestamp}; - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -295,7 +295,7 @@ async fn lite05_fetch_roundtrip(scheme: &str) { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("failed to get local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -391,7 +391,7 @@ async fn lite05_fetch_during_subscribe(scheme: &str) { } } - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -425,7 +425,7 @@ async fn lite05_fetch_during_subscribe(scheme: &str) { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("failed to get local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -517,7 +517,7 @@ async fn broadcast_moq_lite_05_fetch_during_subscribe_webtransport() { async fn broadcast_moq_lite_05_default_timescale() { use moq_tokio::moq_net::Timescale; - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -536,7 +536,7 @@ async fn broadcast_moq_lite_05_default_timescale() { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -614,7 +614,7 @@ async fn broadcast_moq_lite_05_default_timescale() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_moq_transport_20_current_group_join() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("announce"); let track = broadcast.create_track("video", None).expect("create track"); @@ -636,7 +636,7 @@ async fn broadcast_moq_transport_20_current_group_join() { let mut server = server.listen().await.expect("listen"); let addr = server.local_addr().expect("local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -724,7 +724,7 @@ async fn next_announce(announcements: &mut moq_net::announce::Consumer) -> moq_n #[tracing_test::traced_test] #[tokio::test] async fn broadcast_moq_lite_06_announce_lifecycle() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); // Announced before the client connects, so it rides the initial set. let first = pub_origin.create_broadcast("first").expect("create broadcast"); @@ -738,7 +738,7 @@ async fn broadcast_moq_lite_06_announce_lifecycle() { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -851,7 +851,7 @@ async fn broadcast_route_migration() { let publisher = Hop::new(0x42).unwrap(); // ── publisher A: the preferred route (cheaper) ────────────────── - let origin_a = moq_tokio::origin::spawn(Hop::random()); + let origin_a = moq_tokio::origin::spawn(); let mut hops_a = moq_net::Hops::new(); hops_a.push(publisher).unwrap(); let broadcast_a = origin_a.create_broadcast("test").expect("create broadcast"); @@ -870,7 +870,7 @@ async fn broadcast_route_migration() { } // ── publisher B: the standby, carrying an extra hop so A wins ─── - let origin_b = moq_tokio::origin::spawn(Hop::random()); + let origin_b = moq_tokio::origin::spawn(); let mut hops_b = moq_net::Hops::new(); hops_b.push(publisher).unwrap(); hops_b.push(Hop::new(0x1234).unwrap()).unwrap(); @@ -926,7 +926,7 @@ async fn broadcast_route_migration() { }); // ── one subscriber origin fed by both sessions ─────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -1005,7 +1005,7 @@ async fn route_reannounce_test(version: Option<&str>) { let version: Option = version.map(|v| v.parse().expect("invalid version")); // ── publisher (server) ────────────────────────────────────────── - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); // The original publisher: the first hop of every advertised chain. Keeping // it stable across the update is what makes the restart an in-place route // change rather than a broadcast replacement. @@ -1046,7 +1046,7 @@ async fn route_reannounce_test(version: Option<&str>) { }); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -1328,7 +1328,7 @@ async fn max_age_test(version: &str) -> Duration { let version: moq_net::Version = version.parse().expect("invalid version"); // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let info = moq_net::track::Info::default().with_max_age(MAX_AGE_PUBLISHED); @@ -1355,9 +1355,9 @@ async fn max_age_test(version: &str) -> Duration { // The origin the session writes remote broadcasts into decides the window for // tracks whose protocol can't carry the publisher's. let sub_origin = { - let mut config = moq_net::origin::Config::new(Hop::random()); + let mut config = moq_net::origin::Config::default(); config.default_max_age = MAX_AGE_DEFAULT; - moq_tokio::origin::spawn(config) + moq_tokio::origin::spawn_config(config) }; let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -1624,10 +1624,8 @@ async fn broadcast_webtransport_negotiate_client_all_server_transport_19() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_websocket() { - use moq_tokio::moq_net::Hop; - // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -1657,7 +1655,7 @@ async fn broadcast_websocket() { let mut server = server.listen().await.expect("failed to listen"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -1743,10 +1741,8 @@ async fn broadcast_websocket() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_websocket_fallback() { - use moq_tokio::moq_net::Hop; - // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -1776,7 +1772,7 @@ async fn broadcast_websocket_fallback() { let mut server = server.listen().await.expect("failed to listen"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -1874,7 +1870,7 @@ const NEWEST_LITE: &str = "moq-lite-05"; #[tracing_test::traced_test] #[tokio::test] async fn broadcast_websocket_uses_newest_version() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -1901,7 +1897,7 @@ async fn broadcast_websocket_uses_newest_version() { let server = config.init().expect("failed to init server"); let mut server = server.listen().await.expect("failed to listen"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let mut client_config = moq_tokio::connect::Config::default(); client_config.tls.insecure = Some(true); client_config.websocket.delay = Duration::ZERO; @@ -1946,7 +1942,7 @@ async fn broadcast_websocket_uses_newest_version() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_race_quic_wins() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -1976,7 +1972,7 @@ async fn broadcast_race_quic_wins() { let server = config.init().expect("failed to init server"); let mut server = server.listen().await.expect("failed to listen"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let mut client_config = moq_tokio::connect::Config::default(); client_config.tls.insecure = Some(true); // Zero head start: QUIC has to win on its own merit, not by penalising WS. @@ -2032,7 +2028,7 @@ async fn broadcast_race_quic_wins() { async fn quic_driver_task_inherits_connection_span() { use tracing::Instrument; - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -2052,7 +2048,7 @@ async fn quic_driver_task_inherits_connection_span() { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("failed to get local addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -2161,7 +2157,7 @@ async fn quic_driver_task_inherits_connection_span() { /// consumer. #[tokio::test] async fn resubscribe_keeps_flowing_moq_lite_03() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -2180,7 +2176,7 @@ async fn resubscribe_keeps_flowing_moq_lite_03() { let mut server = server.listen().await.expect("failed to listen"); let addr = server.local_addr().expect("server addr"); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -2296,7 +2292,7 @@ fn active_viewers(registry: &moq_net::stats::Registry) -> u64 { /// viewer nobody is watching (chained through relays, one phantom per hop). #[tokio::test] async fn idle_subscription_releases_the_viewer_count() { - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); broadcast.announce(Default::default()).expect("create broadcast"); let track = broadcast.create_track("video", None).expect("create track"); @@ -2318,7 +2314,7 @@ async fn idle_subscription_releases_the_viewer_count() { let registry = moq_net::stats::Registry::new(moq_net::stats::Config::new()); let stats = registry.tier(moq_net::stats::Tier::default()).session(""); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -2492,7 +2488,7 @@ async fn websocket_forbidden_does_not_end_a_quic_connect() { Ok::<_, anyhow::Error>(()) }); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let server_handle = tokio::spawn(async move { let request = server.accept().await.expect("no incoming connection"); let session = request.with_publisher(&pub_origin).ok().await?; @@ -2582,7 +2578,7 @@ async fn one_shot_connect_surfaces_the_session_close() { // close each session as soon as it lands. let accepts = Arc::new(AtomicUsize::new(0)); let server_accepts = accepts.clone(); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let server_handle = tokio::spawn(async move { while let Some(request) = server.accept().await { server_accepts.fetch_add(1, Ordering::SeqCst); @@ -2634,7 +2630,7 @@ async fn a_dead_session_unannounces_while_the_reconnect_retries() { let (mut server, addr) = test_server().await; let url: url::Url = format!("https://localhost:{}", addr.port()).parse().unwrap(); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let _broadcast = pub_origin.create_broadcast("live").expect("create broadcast"); _broadcast.announce(Default::default()).expect("create broadcast"); @@ -2651,7 +2647,7 @@ async fn a_dead_session_unannounces_while_the_reconnect_retries() { Ok::<_, anyhow::Error>(()) }); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -2693,10 +2689,8 @@ async fn a_dead_session_unannounces_while_the_reconnect_retries() { #[tracing_test::traced_test] #[tokio::test] async fn announce_interest_unauthorized_keeps_session_alive() { - use moq_tokio::moq_net::Hop; - // ── publisher (server): only allowed to announce under "allowed" ── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin .create_broadcast("allowed/test") .expect("failed to create broadcast"); @@ -2712,16 +2706,20 @@ async fn announce_interest_unauthorized_keeps_session_alive() { let publish = pub_origin .consume() - .scope(&moq_net::Patterns::from(moq_net::Pattern::subtree("allowed").unwrap())) + .scope( + "", + &moq_net::Patterns::from(moq_net::Pattern::subtree("allowed").unwrap()), + ) .expect("failed to scope publish origin"); let (mut server, addr) = test_server().await; // ── subscriber (client): interested in both "allowed" and "denied" ── // "denied" is disjoint from the publisher's scope, so its announce stream is FINed. - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let consume = sub_origin .scope( + "", &["allowed", "denied"] .into_iter() .map(|prefix| moq_net::Pattern::subtree(prefix).unwrap()) @@ -2780,7 +2778,7 @@ async fn wildcard_scope_test(version: &str, server_scope: &str) { let scope = |text: &str| moq_net::Patterns::from(text.parse::().unwrap()); // ── publisher (server): rooms with a chat and an audio track each ── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let mut broadcasts = Vec::new(); for path in [ "room/alice/chat", @@ -2801,7 +2799,7 @@ async fn wildcard_scope_test(version: &str, server_scope: &str) { // The server's own grant excludes bob: the client never learns of his chat. let publish = pub_origin .consume() - .scope(&scope(server_scope)) + .scope("", &scope(server_scope)) .expect("scope publish origin"); let mut server_config = moq_tokio::listen::Config::default(); @@ -2813,8 +2811,10 @@ async fn wildcard_scope_test(version: &str, server_scope: &str) { let addr = server.local_addr().expect("local addr"); // ── subscriber (client): every room's chat ── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); - let consume = sub_origin.scope(&scope("room/*/chat")).expect("scope consume origin"); + let sub_origin = moq_tokio::origin::spawn(); + let consume = sub_origin + .scope("", &scope("room/*/chat")) + .expect("scope consume origin"); let sub_consumer = consume.consume(); let mut announcements = sub_consumer.announced(); @@ -2919,12 +2919,11 @@ async fn broadcast_wildcard_server_scope_lite_05() { #[tracing_test::traced_test] #[tokio::test] async fn publish_only_client_to_subscribe_only_server() { - use moq_tokio::moq_net::Hop; - // ── subscriber (server): interested in both "allowed" and "denied" ── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let consume = sub_origin .scope( + "", &["allowed", "denied"] .into_iter() .map(|prefix| moq_net::Pattern::subtree(prefix).unwrap()) @@ -2989,7 +2988,7 @@ async fn publish_only_client_to_subscribe_only_server() { }); // ── publisher (client): only allowed to serve under "allowed" ── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin .create_broadcast("allowed/test") .expect("failed to create broadcast"); @@ -3005,7 +3004,10 @@ async fn publish_only_client_to_subscribe_only_server() { let publish = pub_origin .consume() - .scope(&moq_net::Patterns::from(moq_net::Pattern::subtree("allowed").unwrap())) + .scope( + "", + &moq_net::Patterns::from(moq_net::Pattern::subtree("allowed").unwrap()), + ) .expect("failed to scope publish origin"); let (_client, connection) = tokio::time::timeout(TIMEOUT, connect_once(test_client().with_publisher(publish), url)) @@ -3065,7 +3067,7 @@ async fn goaway_test(scheme: &str, version: &str, expect_wire_timeout: bool) { let version: moq_net::Version = version.parse().expect("invalid version"); // ── publisher (server) ────────────────────────────────────────── - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); broadcast .announce(Default::default()) @@ -3088,7 +3090,7 @@ async fn goaway_test(scheme: &str, version: &str, expect_wire_timeout: bool) { let addr = server.local_addr().expect("failed to get local addr"); // ── subscriber (client) ───────────────────────────────────────── - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let sub_consumer = sub_origin.consume(); let mut announcements = sub_consumer.announced(); @@ -3219,7 +3221,7 @@ async fn goaway_moq_transport_19_quic() { async fn goaway_timeout_force_close_moq_transport_19_quic() { let version: moq_net::Version = "moq-transport-19".parse().unwrap(); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let mut server_config = moq_tokio::listen::Config::default(); server_config.bind = Some("[::]:0".parse().unwrap()); @@ -3248,7 +3250,7 @@ async fn goaway_timeout_force_close_moq_transport_19_quic() { Ok::<_, anyhow::Error>(()) }); - let sub_origin = moq_tokio::origin::spawn(Hop::random()); + let sub_origin = moq_tokio::origin::spawn(); let (_client, connection) = tokio::time::timeout(TIMEOUT, connect_once(client.with_subscriber(sub_origin), url)) .await .expect("client connect timed out") @@ -3306,7 +3308,7 @@ async fn zero_initial_backoff_still_gives_up_on_a_flapping_peer() { let (mut server, addr) = test_server().await; let url: url::Url = format!("https://localhost:{}", addr.port()).parse().unwrap(); - let pub_origin = moq_tokio::origin::spawn(Hop::random()); + let pub_origin = moq_tokio::origin::spawn(); let server_handle = tokio::spawn(async move { // Accept and immediately sever, over and over. while let Some(request) = server.accept().await { diff --git a/rs/moq-tokio/tests/reconnect.rs b/rs/moq-tokio/tests/reconnect.rs index 8358f75cb4..4eef51b3eb 100644 --- a/rs/moq-tokio/tests/reconnect.rs +++ b/rs/moq-tokio/tests/reconnect.rs @@ -9,7 +9,7 @@ use std::net::TcpListener; use std::time::Duration; -use moq_tokio::moq_net::{self, Hop}; +use moq_tokio::moq_net; /// A client whose reconnect loop escalates fast enough to assert on inside a test. fn client(backoff: moq_tokio::Backoff) -> moq_tokio::Client { @@ -129,7 +129,7 @@ async fn spawn_server() -> ( let (accepted, sessions) = tokio::sync::mpsc::unbounded_channel(); let handle = tokio::spawn(async move { while let Some(request) = server.accept().await { - let origin = moq_tokio::origin::spawn(Hop::random()); + let origin = moq_tokio::origin::spawn(); match request.with_publisher(&origin).ok().await { Ok(session) => { let _ = accepted.send(session); diff --git a/rs/moq-transcode/examples/transcode.rs b/rs/moq-transcode/examples/transcode.rs index f798df7680..d5431d42e1 100644 --- a/rs/moq-transcode/examples/transcode.rs +++ b/rs/moq-transcode/examples/transcode.rs @@ -41,8 +41,8 @@ async fn main() -> anyhow::Result<()> { // Publish the derivative through one origin and consume the source through // another, over a single auto-reconnecting session. - let publish = moq_tokio::origin::spawn(moq_net::Hop::random()); - let remote = moq_tokio::origin::spawn(moq_net::Hop::random()); + let publish = moq_tokio::origin::spawn(); + let remote = moq_tokio::origin::spawn(); let client = moq_tokio::connect::Config::default().init(Default::default())?; let session = client diff --git a/rs/moq-transcode/src/lib.rs b/rs/moq-transcode/src/lib.rs index 9cd7172ee9..7ddf3e739d 100644 --- a/rs/moq-transcode/src/lib.rs +++ b/rs/moq-transcode/src/lib.rs @@ -765,7 +765,7 @@ mod tests { // The passthrough reference (`..`) resolves against the output broadcast's path, so // the output must be minted through an origin: a standalone producer has no path, and // `..` from it would escape, failing the catalog read below. - let origin = moq_tokio::origin::spawn(moq_net::Hop::random()); + let origin = moq_tokio::origin::spawn(); let output = origin.create_broadcast("room/transcode").unwrap(); let consumer = output.consume(); let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config)); diff --git a/rs/moq-uring/benches/session_lite.rs b/rs/moq-uring/benches/session_lite.rs index eb8601d625..c0f418e40a 100644 --- a/rs/moq-uring/benches/session_lite.rs +++ b/rs/moq-uring/benches/session_lite.rs @@ -135,8 +135,8 @@ mod linux { let mut worker = Worker::new(Config::default()).expect("worker"); let handle = worker.handle(); - let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); - let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); + let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::default()); + let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::default()); let origins = std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_time() diff --git a/rs/moq-uring/tests/session.rs b/rs/moq-uring/tests/session.rs index 2dd1af2af8..fd6cdfd075 100644 --- a/rs/moq-uring/tests/session.rs +++ b/rs/moq-uring/tests/session.rs @@ -44,8 +44,8 @@ fn lite_session_over_the_worker() { let handle = worker.handle(); // The model and its origins, driven on a tokio thread (see module docs). - let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); - let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); + let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::default()); + let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::default()); let origins = std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_time() @@ -161,9 +161,9 @@ fn two_lite_sessions_share_the_server_socket() { let Some(mut worker) = worker() else { return }; let handle = worker.handle(); - let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); - let (sub_a, sub_a_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); - let (sub_b, sub_b_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); + let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::default()); + let (sub_a, sub_a_driver) = origin::Producer::new(origin::Config::default()); + let (sub_b, sub_b_driver) = origin::Producer::new(origin::Config::default()); let origins = std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_time() diff --git a/rs/moq-uring/tests/web.rs b/rs/moq-uring/tests/web.rs index b8448c3e83..6f3d746f30 100644 --- a/rs/moq-uring/tests/web.rs +++ b/rs/moq-uring/tests/web.rs @@ -210,7 +210,7 @@ fn lite_session_over_webtransport() { let handle = worker.handle(); let certs = support::certs().expect("certificates"); - let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); + let (pub_origin, pub_driver) = origin::Producer::new(origin::Config::default()); let origins = std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_time() @@ -234,7 +234,7 @@ fn lite_session_over_webtransport() { let client = quinn_client(format!("https://{addr}/"), |session| { Box::pin(async move { assert_eq!(session.protocol(), Some(PROTO), "negotiated subprotocol"); - let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::new(moq_net::Hop::random())); + let (sub_origin, sub_driver) = origin::Producer::new(origin::Config::default()); let driver = tokio::spawn(sub_driver.run(moq_tokio::runtime::Runtime::<()>::new())); let moq = moq_net::Client::new() diff --git a/rs/moq-video/src/decode/consumer.rs b/rs/moq-video/src/decode/consumer.rs index 31b36c5e54..b4cdbe384c 100644 --- a/rs/moq-video/src/decode/consumer.rs +++ b/rs/moq-video/src/decode/consumer.rs @@ -143,7 +143,7 @@ mod tests { /// Build an origin producer, spawning its driver on the ambient runtime. fn produce_origin() -> moq_net::origin::Producer { - let (producer, driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); if tokio::runtime::Handle::try_current().is_ok() { tokio::spawn(driver.run(moq_tokio::runtime::Runtime::<()>::new())); } else { diff --git a/rs/moq-wasm/src/lib.rs b/rs/moq-wasm/src/lib.rs index f659e38eda..2c1539be8a 100644 --- a/rs/moq-wasm/src/lib.rs +++ b/rs/moq-wasm/src/lib.rs @@ -74,7 +74,7 @@ impl Session { async fn handshake(transport: transport::Session) -> Result { // Wire a subscribe origin so the session has somewhere to insert the // broadcasts the remote announces; keep a consumer to read them. - let (origin, origin_driver) = moq_net::origin::Producer::new(moq_net::Hop::random().into()); + let (origin, origin_driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); web_async::spawn(origin_driver.run(runtime::Runtime)); let consumer = origin.consume(); let client = moq_net::Client::new().with_subscriber(origin); From 15f6a22b6e6ad8541fbf0f2e29b0fb6ad2cc5e12 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 15:45:44 -0700 Subject: [PATCH 3/3] fix(net): close origin release regressions Co-authored-by: Luke Curley --- rs/moq-net/CHANGELOG.md | 8 ++++---- rs/moq-tokio/CHANGELOG.md | 3 --- rs/moq-tokio/tests/broadcast.rs | 8 ++++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/rs/moq-net/CHANGELOG.md b/rs/moq-net/CHANGELOG.md index d333980fdc..6313c1a968 100644 --- a/rs/moq-net/CHANGELOG.md +++ b/rs/moq-net/CHANGELOG.md @@ -21,13 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [**breaking**] `track::SubscriberControl` is `track::Control`, `track::GroupRequest` is `group::Request`, `ConnectionStats` is `session::Stats` with `estimated_send_rate` / `estimated_recv_rate` as `Option`, and the paused handshake `Request` is `server::Handshake`. - [**breaking**] `create_track`, `reserve_track`, `unique_track`, `finish`, `create_group`, and `append_group` take `&self`. `track::Consumer::info()` is `query()`. `track::Demand` gains `is_used` / `poll_used` / `poll_unused`. `track::Producer::poll_unused` returns `Poll>`. `bandwidth::Producer::closed()` returns the cause. - [**breaking**] `stats::Presence` and `stats::Traffic` name both edges of each cumulative pair `*_started` / `*_ended` (`sessions_started` / `sessions_ended`, `announces_started` / `announces_ended`, `broadcasts_*`, `subscriptions_*`). Serialize still writes the previous `announced` / `*_closed` names beside the new ones; deserialize accepts either spelling, with the canonical name winning. -- `origin::Info` is `origin::Config` with public fields and no `with_*` builders. `Producer::info()` is `config()`. -- [**breaking**] `origin::Config::default()` mints a random hop, `Config::id` is `hop`, and origin handles expose `hop()` instead of dereferencing to `Hop`. +- [**breaking**] `origin::Info` is `origin::Config` with public fields and no `with_*` builders. `Producer::info()` is `config()`. +- [**breaking**] `origin::Config::default()` mints a random hop, `Config::id` is `hop`, and origin handles expose `hop()` instead of dereferencing to `Hop`. Random hops now use the full 62-bit wire range; current `@moq/net` clients decode them as `bigint`, while legacy `@moq/lite` clients limited to `Number.MAX_SAFE_INTEGER` can reject larger values and must upgrade. - [**breaking**] `origin::Producer::scope(root, patterns)` and `origin::Consumer::scope(root, patterns)` replace the separate `with_root` / `scope` calls and return `Result` with `Unauthorized` for an empty grant. - [**breaking**] `origin::Pending` is `origin::Requesting`, the consumer-side wait for a request to resolve. - `origin::Producer::publish(path, route)` creates and advertises a broadcast together. -- `track::Producer::write_datagram(Datagram)` is now `insert_datagram(sequence, timestamp, payload)`, matching TypeScript `insertDatagram`. The supplied sequence is preserved; `append_datagram` remains the next-sequence convenience. -- `Timescale` no longer implements `From>`. Use `Timescale::new` or `TryFrom` so values above the QUIC varint range are refused at construction. +- [**breaking**] `track::Producer::write_datagram(Datagram)` is now `insert_datagram(sequence, timestamp, payload)`, matching TypeScript `insertDatagram`. The supplied sequence is preserved; `append_datagram` remains the next-sequence convenience. +- [**breaking**] `Timescale` no longer implements `From>`. Use `Timescale::new` or `TryFrom` so values above the QUIC varint range are refused at construction. - Register moq-lite stream codes NOT_FOUND 0x33, OLD 0x34, and EVICTED 0x35 so a cache miss round-trips as the named variant instead of an opaque reserved-range placeholder. - Every received protocol code is `Error::Session` / `Error::Stream`, preserving its registry and numeric value, including cancellation and internal-error codes. diff --git a/rs/moq-tokio/CHANGELOG.md b/rs/moq-tokio/CHANGELOG.md index c59a4a11bf..b4521542d8 100644 --- a/rs/moq-tokio/CHANGELOG.md +++ b/rs/moq-tokio/CHANGELOG.md @@ -16,9 +16,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Request::remote_addr`, `local_addr`, `server_name`, and `alpn` report what the transport saw; `PeerIdentity::name` and `issuer` read the leaf certificate. - `Client::with_origin` and `Server::with_origin` publish and subscribe through one shared origin. - -### Added - - *(tokio)* [**breaking**] partition LAN discovery by a DNS-SD application (`mdns::App`) ### Fixed diff --git a/rs/moq-tokio/tests/broadcast.rs b/rs/moq-tokio/tests/broadcast.rs index 729b612160..bf606c52fe 100644 --- a/rs/moq-tokio/tests/broadcast.rs +++ b/rs/moq-tokio/tests/broadcast.rs @@ -2871,10 +2871,14 @@ async fn wildcard_scope_test(version: &str, server_scope: &str) { .expect("read_frame failed") .expect("group closed"); assert_eq!(frame.payload.as_ref(), b"room/alice/chat"); - for path in ["room/alice/audio", "room/bob/chat", "lobby/alice/chat"] { + // The server grant excludes bob's chat, so that otherwise-authorized request is + // unroutable. Audio and lobby never pass the client's own scope and are unauthorized. + let refused = sub_consumer.request_broadcast("room/bob/chat").await.err(); + assert!(matches!(refused, Some(moq_net::Error::Unroutable)), "{refused:?}"); + for path in ["room/alice/audio", "lobby/alice/chat"] { let refused = sub_consumer.request_broadcast(path).await.err(); assert!( - matches!(refused, Some(moq_net::Error::Unroutable)), + matches!(refused, Some(moq_net::Error::Unauthorized)), "{path}: {refused:?}" ); }