From 846c890d641dd933dd120561b4a019cd0fcb8a59 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 06:29:30 -0700 Subject: [PATCH 1/4] chore: claim api route cost quest Co-Authored-By: GPT-5 From e474bb874f1d4574e570e6b7df49dfc3e3ca6aa5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 07:01:45 -0700 Subject: [PATCH 2/4] refactor(net): name route cost construction Co-Authored-By: GPT-5 --- quest/m1/api-route-cost.md | 29 ----------------------------- rs/libmoq/src/api.rs | 8 +++++--- rs/moq-ffi/src/origin.rs | 10 +++++----- rs/moq-ffi/src/test.rs | 6 +++--- rs/moq-net/CHANGELOG.md | 2 +- rs/moq-net/src/model/origin.rs | 25 +++++-------------------- 6 files changed, 19 insertions(+), 61 deletions(-) delete mode 100644 quest/m1/api-route-cost.md diff --git a/quest/m1/api-route-cost.md b/quest/m1/api-route-cost.md deleted file mode 100644 index 823dfdcf74..0000000000 --- a/quest/m1/api-route-cost.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] Route construction names cost parts and hop chains - -## Goal - -`Route::with_hop` and `Cost: From<(u64, u64)>` are gone. The only in-tree -callers that still need both magnitudes and a hop chain (moq-ffi's -`TryFrom`, libmoq's `parse_route`) build a `Hops` then -`Route::with_hops`, and name the two cost halves with a constructor. -Everyone else already uses `with_hops`. - -## Plan - -`Cost` is `#[non_exhaustive]`, so ffi and libmoq cannot write a struct -literal. Add `Cost::from_warm_cold(warm, cold)` for a discounted warm -alongside its undiscounted cold, the meaning the tuple `From` has today. -Keep `Cost::new` / `From` for an undiscounted route. - -In `rs/moq-ffi/src/origin.rs` and `rs/libmoq/src/api.rs`, collect hops with -`Hops::push` (same `InvalidHop` as `with_hop`) and -`Route::default().with_cost(Cost::from_warm_cold(warm, cold)).with_hops(hops)`. -Then delete `Route::with_hop` and `impl From<(u64, u64)> for Cost`. Wrappers -do not construct `moq_net::Route` themselves; only those two parsers do. - -Public API: breaking on moq-net, so on dev. Wire: none. The C/UniFFI route -structs are unchanged. - -## Related - -- [libmoq units](/quest/m1/api-libmoq-units.md) - other C ABI breaks on the same crates diff --git a/rs/libmoq/src/api.rs b/rs/libmoq/src/api.rs index c599959ba9..b277117a9e 100644 --- a/rs/libmoq/src/api.rs +++ b/rs/libmoq/src/api.rs @@ -658,7 +658,7 @@ unsafe fn parse_route(route: *const moq_route) -> Result 0 { if route.hops.is_null() { return Err(Error::InvalidPointer); @@ -670,10 +670,12 @@ unsafe fn parse_route(route: *const moq_route) -> Result for moq_net::origin::Route { fn try_from(route: MoqRoute) -> Result { let cold = route.cold.unwrap_or(route.cost); - let mut out = moq_net::origin::Route::default().with_cost((route.cost, cold)); + let mut hops = moq_net::Hops::new(); for id in route.hops { let origin = if id == 0 { moq_net::Hop::UNKNOWN } else { moq_net::Hop::new(id).map_err(|e| MoqError::InvalidRoute(e.to_string()))? }; - out = out - .with_hop(origin) - .map_err(|e| MoqError::InvalidRoute(e.to_string()))?; + hops.push(origin).map_err(|e| MoqError::InvalidRoute(e.to_string()))?; } - Ok(out) + Ok(moq_net::origin::Route::default() + .with_cost(moq_net::origin::Cost::from_warm_cold(route.cost, cold)) + .with_hops(hops)) } } diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 155b374e66..0d655adba6 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -265,7 +265,7 @@ fn origin_config_set_cache_capacity() { #[test] fn route_cold_cost_conversions_are_lossless() { // An explicit cold half survives the round trip in both directions. - let route = moq_net::origin::Route::default().with_cost((0u64, 9u64)); + let route = moq_net::origin::Route::default().with_cost(moq_net::origin::Cost::from_warm_cold(0, 9)); let ffi = MoqRoute::from(route.clone()); assert_eq!(ffi.cost, 0); assert_eq!(ffi.cold, Some(9)); @@ -281,7 +281,7 @@ fn route_cold_cost_conversions_are_lossless() { anonymous: false, }) .unwrap(); - assert_eq!(seeded.cost, moq_net::origin::Cost::from((5u64, 5u64))); + assert_eq!(seeded.cost, moq_net::origin::Cost::from_warm_cold(5, 5)); let anonymous = MoqRoute::from( moq_net::origin::Route::default().with_hops(moq_net::Hops::try_from(vec![moq_net::Hop::UNKNOWN]).unwrap()), @@ -326,7 +326,7 @@ async fn announced_route_keeps_cold_cost_on_reannounce() { // the conversion rather than waiting for a second update.) broadcast.announce(route.clone()).unwrap(); let back = moq_net::origin::Route::try_from(route.clone()).unwrap(); - assert_eq!(back.cost, moq_net::origin::Cost::from((0u64, 9u64))); + assert_eq!(back.cost, moq_net::origin::Cost::from_warm_cold(0, 9)); assert_eq!(MoqRoute::from(back), route); broadcast.finish().unwrap(); diff --git a/rs/moq-net/CHANGELOG.md b/rs/moq-net/CHANGELOG.md index 480910b304..d623973297 100644 --- a/rs/moq-net/CHANGELOG.md +++ b/rs/moq-net/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- [**breaking**] Dead exports removed: `Hops::replace_first`, `origin::Dynamic::{hop, root}`, `DRAIN_COST` / `MAX_COST` (use `Cost::{DRAIN, MAX}`), `broadcast::Producer::remove_track`, `track::Producer::start_sequence`, `Subscriber::with_groups`, `Ordered::with_groups`, `group::Consumer::with_frames`, `cache::Pool::same_pool`, `Timestamp::new_const`, `Error::to_code`. `Route::with_hop` and `Cost: From<(u64, u64)>` stay; libmoq and moq-ffi still call them. +- [**breaking**] Dead exports removed: `Hops::replace_first`, `origin::Dynamic::{hop, root}`, `DRAIN_COST` / `MAX_COST` (use `Cost::{DRAIN, MAX}`), `broadcast::Producer::remove_track`, `track::Producer::start_sequence`, `Subscriber::with_groups`, `Ordered::with_groups`, `group::Consumer::with_frames`, `cache::Pool::same_pool`, `Timestamp::new_const`, `Error::to_code`, `Route::with_hop` (build `Hops` and use `with_hops`), and `Cost: From<(u64, u64)>` (use `Cost::from_warm_cold`). - [**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. diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index a6e2ff551e..e9a73806d2 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -400,6 +400,11 @@ impl Cost { Self { warm: cost, cold: cost } } + /// A discounted warm cost alongside the same route's undiscounted cold cost. + pub const fn from_warm_cold(warm: u64, cold: u64) -> Self { + Self { warm, cold } + } + /// The highest cost either half can take, and where accumulation saturates. /// /// A draining session stamps this on its routes so every other candidate outranks @@ -445,17 +450,6 @@ impl From for Cost { } } -impl From<(u64, u64)> for Cost { - /// Both magnitudes explicitly: `(warm, cold)`. - /// - /// Unlike [`new`](Self::new), which prices the route undiscounted, this keeps a - /// discounted `warm` alongside its undiscounted `cold`, which is what an - /// application re-announcing an observed route means. - fn from((warm, cold): (u64, u64)) -> Self { - Self { warm, cold } - } -} - /// The path a route took through the mesh and what using it costs. /// /// The metadata half of an advertisement: [`Producer::dynamic`] pairs it with @@ -499,15 +493,6 @@ impl Default for Route { } impl Route { - /// Append a hop to the chain, oldest first. - /// - /// Fails with [`crate::InvalidHop`] for a hop the wire would reject: one past the - /// chain's length cap, or one already in it, which is a loop. - pub fn with_hop(mut self, hop: Hop) -> Result { - self.hops.push(hop)?; - Ok(self) - } - /// Replace the hop chain. pub fn with_hops(mut self, hops: Hops) -> Self { self.hops = hops; From 51f739ac4a41b5d6451fa16e56a733aa9a147d41 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 14:07:18 -0700 Subject: [PATCH 3/4] fix(net): expose route cost fields Co-Authored-By: GPT-5 --- quest/m1/README.md | 1 - quest/m1/api-review-gate.md | 1 - rs/libmoq/src/api.rs | 2 +- rs/moq-ffi/src/origin.rs | 2 +- rs/moq-ffi/src/test.rs | 6 +++--- rs/moq-net/CHANGELOG.md | 2 +- rs/moq-net/src/model/origin.rs | 6 ------ 7 files changed, 6 insertions(+), 14 deletions(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index 42797c9fa9..7017717037 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -31,7 +31,6 @@ the transport line in m2 assumes a single stack. - [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 -- [Route cost](/quest/m1/api-route-cost.md) - `Route::with_hop` and `Cost: From<(u64, u64)>` go; ffi and libmoq build `Hops` and `Cost::from_warm_cold` - [Rendition ownership](/quest/m1/api-mux-rendition.md) - one handle publishes a media track and reports its estimate, instead of five - [Gateway types](/quest/m1/api-gateways.md) - no `anyhow` in a gateway `Error`, `PathOwned` prefixes, `Duration` segments, `moq_rtc::Server::new(config)`, an SRT reject with a reason - [libmoq units](/quest/m1/api-libmoq-units.md) - `moq_client_config` is all microseconds, the header declares every enum and error code, NULL callbacks are refused diff --git a/quest/m1/api-review-gate.md b/quest/m1/api-review-gate.md index 83b5859af8..6d440b4796 100644 --- a/quest/m1/api-review-gate.md +++ b/quest/m1/api-review-gate.md @@ -18,7 +18,6 @@ quest is deleted too. No code. The list: [Announce event](/quest/m1/api-net-announce.md), [Origin scoping](/quest/m1/api-net-origin.md), -[Route cost](/quest/m1/api-route-cost.md), [Rendition ownership](/quest/m1/api-mux-rendition.md), [Gateway types](/quest/m1/api-gateways.md), [libmoq units](/quest/m1/api-libmoq-units.md). diff --git a/rs/libmoq/src/api.rs b/rs/libmoq/src/api.rs index b277117a9e..96d9ac329e 100644 --- a/rs/libmoq/src/api.rs +++ b/rs/libmoq/src/api.rs @@ -674,7 +674,7 @@ unsafe fn parse_route(route: *const moq_route) -> Result for moq_net::origin::Route { hops.push(origin).map_err(|e| MoqError::InvalidRoute(e.to_string()))?; } Ok(moq_net::origin::Route::default() - .with_cost(moq_net::origin::Cost::from_warm_cold(route.cost, cold)) + .with_cost(moq_net::origin::Cost { warm: route.cost, cold }) .with_hops(hops)) } } diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 0d655adba6..84c96debfa 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -265,7 +265,7 @@ fn origin_config_set_cache_capacity() { #[test] fn route_cold_cost_conversions_are_lossless() { // An explicit cold half survives the round trip in both directions. - let route = moq_net::origin::Route::default().with_cost(moq_net::origin::Cost::from_warm_cold(0, 9)); + let route = moq_net::origin::Route::default().with_cost(moq_net::origin::Cost { warm: 0, cold: 9 }); let ffi = MoqRoute::from(route.clone()); assert_eq!(ffi.cost, 0); assert_eq!(ffi.cold, Some(9)); @@ -281,7 +281,7 @@ fn route_cold_cost_conversions_are_lossless() { anonymous: false, }) .unwrap(); - assert_eq!(seeded.cost, moq_net::origin::Cost::from_warm_cold(5, 5)); + assert_eq!(seeded.cost, moq_net::origin::Cost { warm: 5, cold: 5 }); let anonymous = MoqRoute::from( moq_net::origin::Route::default().with_hops(moq_net::Hops::try_from(vec![moq_net::Hop::UNKNOWN]).unwrap()), @@ -326,7 +326,7 @@ async fn announced_route_keeps_cold_cost_on_reannounce() { // the conversion rather than waiting for a second update.) broadcast.announce(route.clone()).unwrap(); let back = moq_net::origin::Route::try_from(route.clone()).unwrap(); - assert_eq!(back.cost, moq_net::origin::Cost::from_warm_cold(0, 9)); + assert_eq!(back.cost, moq_net::origin::Cost { warm: 0, cold: 9 }); assert_eq!(MoqRoute::from(back), route); broadcast.finish().unwrap(); diff --git a/rs/moq-net/CHANGELOG.md b/rs/moq-net/CHANGELOG.md index d623973297..48d63fbf7c 100644 --- a/rs/moq-net/CHANGELOG.md +++ b/rs/moq-net/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- [**breaking**] Dead exports removed: `Hops::replace_first`, `origin::Dynamic::{hop, root}`, `DRAIN_COST` / `MAX_COST` (use `Cost::{DRAIN, MAX}`), `broadcast::Producer::remove_track`, `track::Producer::start_sequence`, `Subscriber::with_groups`, `Ordered::with_groups`, `group::Consumer::with_frames`, `cache::Pool::same_pool`, `Timestamp::new_const`, `Error::to_code`, `Route::with_hop` (build `Hops` and use `with_hops`), and `Cost: From<(u64, u64)>` (use `Cost::from_warm_cold`). +- [**breaking**] Dead exports removed: `Hops::replace_first`, `origin::Dynamic::{hop, root}`, `DRAIN_COST` / `MAX_COST` (use `Cost::{DRAIN, MAX}`), `broadcast::Producer::remove_track`, `track::Producer::start_sequence`, `Subscriber::with_groups`, `Ordered::with_groups`, `group::Consumer::with_frames`, `cache::Pool::same_pool`, `Timestamp::new_const`, `Error::to_code`, `Route::with_hop` (build `Hops` and use `with_hops`), and `Cost: From<(u64, u64)>` (use `Cost { warm, cold }`). - [**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. diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index e9a73806d2..bbbbc5dee3 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -372,7 +372,6 @@ const MAX_COST: u64 = (1 << 62) - 1; /// path as if nothing were cached, so it stays meaningful once discounts have /// flattened `warm`. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] pub struct Cost { /// The cost of pulling content via this route as the mesh stands today, /// accumulated per link. Lower wins. @@ -400,11 +399,6 @@ impl Cost { Self { warm: cost, cold: cost } } - /// A discounted warm cost alongside the same route's undiscounted cold cost. - pub const fn from_warm_cold(warm: u64, cold: u64) -> Self { - Self { warm, cold } - } - /// The highest cost either half can take, and where accumulation saturates. /// /// A draining session stamps this on its routes so every other candidate outranks From dc6165bf45344cca71629f1db21be3fbd87024fb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 20 Sep 2026 15:30:40 -0700 Subject: [PATCH 4/4] fix(hls): pass catalog config in test Co-Authored-By: GPT-5 --- rs/moq-hls/src/export/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index bee8b6bb59..489aa013f4 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -642,7 +642,7 @@ mod tests { let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let reserved = catalog.reserve(); let mut registration = reserved.video("video0").unwrap();