feat(net)!: announcements are prefix routes - #3225
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
💡 Codex ReviewThis unrelated change adds 24,037 generated files under Lines 127 to 130 in 14ceaca When a route is retracted or replaced after AGENTS.md reference: AGENTS.md:L196-L200 moq/rs/moq-net/src/model/origin.rs Line 1427 in 14ceaca If a caller passes a AGENTS.md reference: AGENTS.md:L165-L170 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uite The announce Run arm now drains buffered announces before serving route requests, so a route attached in the same poll registers its request-queue waiter before the machine parks. Serving first left a request that raced the attach with no registered waiter and nothing else to re-poll the machine, which deadlocked the internal unix listener deterministically. TrackServe registers its fetch handler before accepting the track request: accept releases the request's built-in fetch gate, so a cache-miss fetch queued during the TRACK_INFO round trip was drained as NotFound in the gap. moq-cli's Publish and moq-bench's publish loop now hold the announcement alongside the producer; both were dropping the guard early, silently unannouncing the broadcast. The goaway cluster tests are reworked to the new failover contract: the drained session ends its subscriptions and the subscriber resubscribes; re-pricing surfaces as metadata updates, never a retraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The announced-broadcast wait in moq-ffi and libmoq resolved routed() and request_broadcast() as two steps, so a covering route that retracted in between (failover churn), or one that covered the path while nothing served it, surfaced as a spurious Unroutable instead of continued waiting. Both now loop, waiting for the path's coverage to change before retrying. The announce cursor dedupe also keys on servability, so a served route replacing an advertise-only one under identical metadata is delivered rather than hidden, which is what wakes such a waiter. Announcement::update silently ignored a Route naming a different prefix, applying the metadata under the old one while the caller believed the new prefix was advertised. The prefix is now pinned at announce time and a mismatch returns NotFound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14ceaca to
37b0230
Compare
|
All three addressed in the force-pushed branch (now also rebased onto the latest
(Written by Claude Fable 5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37b0230e63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (const [path, providers] of remote ?? []) { | ||
| const suffix = Path.stripPrefix(prefix, path); | ||
| if (suffix !== null && fronts[0]) next.set(suffix, fronts[0]); | ||
| if (suffix !== null && providers[0]) next.set(suffix, providers[0]); |
There was a problem hiding this comment.
Clamp covering routes into scoped announcement streams
When the origin contains a broad route such as room, calling announced("room/alice") drops it because Path.stripPrefix("room/alice", "room") returns null. This means the origin-backed Announce.Broadcast used by @moq/watch never receives the empty-suffix active event and waits indefinitely, even though request("room/alice") can resolve through that route. Include routes that cover the requested prefix and present them as an empty suffix, matching the Rust origin and wire behavior.
AGENTS.md reference: AGENTS.md:L194-L200
Useful? React with 👍 / 👎.
| if update.active { | ||
| let node = Node { | ||
| reader: Reader::Resolving(self.origin.request_broadcast(&path)), | ||
| last: None, |
There was a problem hiding this comment.
Re-resolve stats nodes after an invisible route failover
With redundant routes that have the same prefix, hops, and cost, the origin intentionally suppresses an announcement when the winning route entry changes because its visible metadata is identical. If the session serving this newly created reader then ends, advance transitions the node to Reader::Ended; the backup remains announced, so no new active update reaches this branch and the node is permanently omitted from the aggregate. Re-request the broadcast when a reader terminates while its route is still active, rather than relying exclusively on another announcement.
Useful? React with 👍 / 👎.
| if update.active { | ||
| let node = Node { | ||
| reader: Reader::Resolving(self.origin.request_broadcast(&path)), | ||
| last: None, | ||
| }; | ||
| // A replacement (failover) drops the old value until the new | ||
| // subscription catches up. | ||
| self.nodes.insert(absolute, node).is_some_and(|old| old.last.is_some()) |
There was a problem hiding this comment.
Preserve stats readers across metadata-only route updates
A repeated active update for an existing prefix can be only a hops or cost change, but this branch always replaces the live node reader and clears its last cumulative snapshot. During routine route repricing the next merged result therefore temporarily excludes that node, producing a counter regression that downstream interprets as a fresh segment, before a new subscription restores the values. Keep the existing reader for metadata-only active updates instead of treating every active event as a replacement.
Useful? React with 👍 / 👎.
…der in the retire test The tsc outputs under js/common are emitted in place by a root tsc -b and were swept into the branch, tripping biome in CI. The retired-route test also handed the table the producer's only consumer handle, so releasing the materialized front closed the broadcast; a session provider holds its own handle for the session's lifetime and lends out clones, which is what the test now models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
moq/rs/moq-net/src/lite/publisher.rs
Lines 704 to 705 in 4e0f424
When a Lite01/Lite02 announce stream starts with active routes, these suffixes are encoded into ANNOUNCE_INIT but never inserted into self.live. If one of those routes is later retracted, retract() treats it as never sent and emits no Ended, so the peer retains a stale route until the entire announce stream closes. Populate self.live from the final initial set before sending the snapshot. (Written by GPT-5.6 Sol)
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let best = routes | ||
| .iter() | ||
| .filter(|entry| cursor.visible(entry)) | ||
| .filter(|entry| cursor.presented(&entry.prefix.as_path()).contains(presented)) | ||
| .min_by_key(|entry| route_order(&presented.as_path(), entry)); |
There was a problem hiding this comment.
Prefer the most specific route after scope clamping
When a cursor is scoped below a broad route, that route is clamped to the cursor root and can collide with a genuinely narrower route at the same presented prefix. This selection compares only cost and hops, so it may advertise the cheaper broad route's metadata, while best_server() resolves requests through the most-specific original prefix. A scoped relay can therefore forward costs and hops for one upstream but serve the traffic through another; retain original-prefix specificity as the first selection key so announcements match request routing. (Written by GPT-5.6 Sol)
Useful? React with 👍 / 👎.
runtime_workers/runtime_uring (Linux-only), the moq-uring tests and bench (Linux-only), and moq-cli's play/transcode verbs (off-by-default features) never compile on the macOS host, so the announced_broadcast and announce Update call sites in them survived the refactor until CI's Linux build. Waits become routed() and resolution goes through request_broadcast, same as every other migrated site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d99bc3b8b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| routed = consumer.routed(&source_path) => { | ||
| routed.context("origin closed before the source broadcast was announced")?; | ||
| } |
There was a problem hiding this comment.
Retry source resolution across route churn
If the covering route retracts after routed() completes but before the following request_broadcast() snapshots the table, such as during a reconnect or upstream failover, the request returns Unroutable and the transcode command exits even when a replacement route is about to arrive. Keep watching this path's announcement cursor and retry resolution after coverage changes instead of treating this transient gap as terminal.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etric An advertisement is now a Prefix (what it covers) paired with a Route (the hops it took and what using it costs). Prefix is opaque so richer matching (wildcard patterns) can land without a breaking change, and it owns the coverage questions (covers, per-segment matching). announce(prefix, route) returns announce::Producer, the write half of the announce channel: hold it to stay advertised, update(route) to re-price, drop to retract. The old announce::Producer, a consumer factory with a misleading name, is deleted, and update() can no longer name a prefix at all, so metadata landing under a stale prefix is inexpressible rather than checked at runtime. publish_broadcast is removed: announcing before a broadcast has tracks is usually a bug, so the blessed order is create_broadcast, populate, announce. moq-cli's Publish takes the broadcast and attaches the announcement after construction for the same reason. The JS origin matches: insertRoute is now announce(prefix, provider), the announce Event carries prefix rather than path, and the docs bless it as the way to serve a subtree. FFI and every wrapper take announce(prefix, route) with MoqRoute reduced to hops and cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95a7ed3c26
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // First announce, a stale announce being replaced, or a metadata update. | ||
| None | Some(PendingUpdate::Announce(_)) => PendingUpdate::Announce(meta), |
There was a problem hiding this comment.
Preserve retractions after queued metadata updates
When a consumer has already received the active route, a metadata change queues another PendingUpdate::Announce, so this variant no longer necessarily represents an unseen initial announcement. If that route is then retracted before the consumer drains the metadata update, apply_unannounce treats the queued announce as unseen and cancels both events. The consumer never receives active: false and can continue advertising or using a permanently stale route after the upstream session has disappeared.
Useful? React with 👍 / 👎.
…ouncement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carries dev's Hop rename (#3252) through the prefix-route refactor. Conflicts were resolved by keeping this branch's route model and re-applying the rename: Origin -> Hop, OriginList -> Hops, InvalidOrigin -> InvalidHop::Range, with_peer_origin -> with_peer_hop, cluster Peer.origin -> Peer.hop, origin::Dynamic::info() -> hop(), the --origin flag and SETUP parameter terminology, and moq-tokio's server::Config. Also lands the review fixes: - announce cursors track delivered prefixes, so a retraction arriving after an undelivered metadata update still reaches the consumer instead of cancelling with it (regression test included) - sync_cursor filters to the most specific covering prefix like best_server, so a scoped cursor advertises the metadata of the route a request resolves - new origin::Consumer::routed_broadcast composes routed + request_broadcast with the coverage-churn retry every caller needs; moq-ffi, libmoq, moq-cli transcode, moq-rtmp, moq-gst, and moq-relay web now share it instead of hand-rolling (or missing) the loop - moq-stats keeps a node's live reader across metadata-only route updates and re-resolves when a subscription ends, so an invisible identical-route failover no longer drops the node from the aggregate permanently - js announced() clamps a broader covering route to the root suffix, matching request() resolution and the Rust prefix intersection (test included) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An identical standby takes over without any announce update, so a request that queued on the dying route and then failed Unroutable must retry through the already-updated table instead of parking on the announce stream (or, in moq-stats, going permanently Ended). An instant Unroutable still parks or ends: it means nothing serves the path right now, and each immediate retry consumed a real retraction, so neither path can spin. The stats re-arm also drops its dead reader before re-requesting, so its own handle no longer keeps a dying served broadcast resolvable from the cache. Regression test: routed_broadcast_survives_serving_route_retraction, with two back-to-back retractions so the announce stream's initial coverage replay cannot mask a missing retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…timing Requesting::is_queued reports whether the request was handed to a serving route, fixed at request time under the table lock. routed_broadcast and the stats re-arm use it instead of observing whether the first poll was Pending, which misclassified a request whose route retracted between creation and first poll as an instant Unroutable and parked on an announce update that an identical standby never sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0189add8d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // The route was retracted between the lookup and its lock; fall | ||
| // through to the fallback handler. | ||
| drop(serve); | ||
| state = self.shared.lock(); |
There was a problem hiding this comment.
Recheck the route table after a retraction race
When the selected route retracts between best_server() and server.lock(), this branch reacquires the table but proceeds directly to the fallback queue. If another served route already covers the path, such as an identical standby promoted without an announcement update, a direct request_broadcast() therefore returns Unroutable or uses the fallback despite the live replacement. Rerun route selection after reacquiring the table so this lower-layer race is handled before falling back.
AGENTS.md reference: AGENTS.md:L141-L145
Useful? React with 👍 / 👎.
| if !update.active { | ||
| tracing::info!(prefix = %update.route.prefix, "route ended"); | ||
| continue; | ||
| }; | ||
| } | ||
| let broadcast = consumer.request_broadcast(&update.route.prefix).await?; |
There was a problem hiding this comment.
Read the prefix from the announcement update
The new example accesses update.route.prefix, but origin::Route now contains only hops and cost while the prefix is the separate update.prefix field, so this documented subscription loop does not compile. The same invalid access was added to doc/lib/rs/index.md; update both examples to use update.prefix so the Rust documentation matches the new API.
AGENTS.md reference: AGENTS.md:L194-L199
Useful? React with 👍 / 👎.
Two verified corrections from the review pass. Adoption and resume need different hops. The pre-#3225 front kept both: FrontState.publisher was hops.iter().next() (the publisher, for splice decisions) while handover_allowed and the hold keyed on hops.iter().last() (the adjacent carrier), with the rank hash over that last hop. rank.md pointed at the first hop, which every alternate route to one publisher shares by construction, so two distinct carriers would have compared equal and the gate could have permitted the mutual adoption the hold exists to prevent. 2985: two of its three js/net sites are already fixed on dev by keying on the routing front (lite runAnnounce diffs ended-then-active; ietf keeps an offered map that clears a refusal on republish). Only #resolveTrackInfo is still path-and-track keyed. Rescoped to that, and dropped the route-resume coupling, which the remaining cache bug does not need. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#3225 removed the actively-carrying warm discount, its ceiling exemption, and the (cold, hash) adoption rank with its re-parenting delay; the relay forwards accumulated costs only. The draft still specified all of it normatively, which misleads anyone implementing against it. Trim the text to what dev does, keeping the Warm and Cold fields and the selection order, and record the removal in the moq-lite-06 changelog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g tier route-resume: the abort-and-resubscribe behavior was specified by #3225, not lost by accident, so the quest now states the reversal as a decision: routes keep hops/cost, a relay resumes/stitches across routes with the same non-zero first hop, Epoch stays dead, and the implementing PR amends the draft's no-splice paragraph in the same change. Resized to XL since dev has no per-path front spanning routes to hang the identity on. wildcard: specificity-first already decides concrete-versus-wildcard, so the pricing bullet no longer claims there is no such rule. Cost orders within a tier; the seed floor's work is among equal-specificity claims (standby vs running concrete, warm-advertise's exact-path routes), and the shadow a live concrete claim casts over the pool is documented as an accepted consequence bounded by the claiming session. lite-draft-routing: dropped; the work is already open as #3278, so the quest would land completed. References now point at the PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A subscription whose serving route dies now resumes through the next best covering route when both routes name the same non-zero first hop, re-splicing at a group boundary. Different or unknown (0) first hops still end the subscription, so one publisher's subscribers are never spliced onto another's frames and two anonymous peers never pass for one reconnecting. This restores the seamless failover that #3225 removed, keyed on the routing identity that stayed on the wire. request_broadcast now resolves a routed path to a per-(path, exclusion) front: a spliced broadcast whose watcher materializes it from the best covering route through the existing per-route request queues, re-selects when the table changes (a drain reprice migrates before the session dies; a metadata-only reprice changes nothing), and retries a retracted route's failure through the survivor. Fronts are keyed per split-horizon exclusion so a failover can never adopt a route flowing back through one of its own readers. The spliced-track machinery (resume.rs, serve_track) is reused unchanged. The draft's no-splice paragraph becomes the first-hop rule, recorded in the moq-lite-06 changelog. The goaway cluster and route migration tests return to their pre-#3225 seamless expectations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A subscription whose serving route dies now resumes through the next best covering route when both routes name the same non-zero first hop, re-splicing at a group boundary. Different or unknown (0) first hops still end the subscription, so one publisher's subscribers are never spliced onto another's frames and two anonymous peers never pass for one reconnecting. This restores the seamless failover that #3225 removed, keyed on the routing identity that stayed on the wire. request_broadcast now resolves a routed path to a per-(path, exclusion) front: a spliced broadcast whose watcher materializes it from the best covering route through the existing per-route request queues, re-selects when the table changes (a drain reprice migrates before the session dies; a metadata-only reprice changes nothing), and retries a retracted route's failure through the survivor. Fronts are keyed per split-horizon exclusion so a failover can never adopt a route flowing back through one of its own readers. The spliced-track machinery (resume.rs, serve_track) is reused unchanged. The draft's no-splice paragraph becomes the first-hop rule, recorded in the moq-lite-06 changelog. The goaway cluster and route migration tests return to their pre-#3225 seamless expectations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three independent pieces of fallout from `Merge main into dev` (6947217), where a conflict-free textual merge was not a semantic one. Each was independently fatal to `just check`, and the second and third only surfaced once the first stopped failing the build. 1. `rs/moq-tokio/tests/broadcast.rs` did not compile. The merge brought main's `broadcast_moq_transport_20_current_group_join` in verbatim, but on main that test lives in `rs/moq-native/tests/broadcast.rs` and predates both #3225 and the moq-native to moq-tokio split, so it used `Origin::random()`, `create_broadcast(path, Route)`, `moq_native::{Server,Client}Config`, and `Update::broadcast`. Ported onto dev's API, matching the neighbouring tests. The merge had also glued `next_announce`'s doc comment onto the new test's. 2. `cargo shear`: `rs/moq-cli` declared `humantime` with no uses. The usage-rs migration (#3030) replaced clap's `value_parser = humantime::parse_duration` and dropped the dependency in the same commit; the merge took main's side of the manifest. 3. `cargo sort`: `moq-sock` sat above `moq-rtc` and `moq-rtmp` in the root `[workspace.dependencies]`. The assertions the ported test exists for are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
IETF moq-transport's
*_NAMESPACEmessages are specified as prefix routes, but we treatedthem as per-broadcast announcements. This embraces the route model everywhere: an
announcement is now a route, a claim that paths under a prefix can be served, and it is
up to the application to decide that a path is a broadcast and request it.
Core model (
moq-net):origin::Producer::announce(Route) -> Announcement: advertise a prefix explicitly. TheAnnouncementguard keeps it advertised;update()re-prices it (hops/cost); dropping itretracts.
origin::Route { prefix, hops, cost }is the new announced unit.origin::Consumer::announced()yieldsannounce::Update { route, active }, never abroadcast::Consumer. Resolution is explicit:request_broadcast(path)walks localbroadcasts, then the most specific covering route (served on demand by the session that
announced it), then a
dynamic()fallback.create_broadcast(path)no longer takes aRouteand no longer announces. Theconvenience
publish_broadcast(path) -> (broadcast::Producer, Announcement)does both,and is what the FFI's
create_broadcastmaps to (so wrappers keep announcing exact pathsby convention).
Consumer::routed(path)awaits a covering route (replacesannounced_broadcastin the Rust API; the FFI keepsannounced_broadcastasrouted-then-request).
broadcast::Broadcastgoes back to being a simple path:Route,Cost, epoch, and allthe
set_route/route_changed/routes()machinery are gone from it.Cost/MAX_COST/DRAIN_COSTmove toorigin.origin::Producer::dynamic()now works with Rust consumers: announce a short prefix (forexample
.dash) and serve whatever is requested beneath it.Wire: no byte-format change to the lite-06 announce messages. The suffix is
reinterpreted as a route prefix, and the drafts are updated to say so (Epoch, which was
spec-only and never implemented, is removed;
draft-lcurley-moq-broadcastis retired).IETF
PUBLISH_NAMESPACE/SUBSCRIBE_NAMESPACEnow map naturally: each namespace is a route.Applied to every language: Rust workspace, JS (
@moq/netroute table with lazymaterialization), Python, Swift, Kotlin, Go, C (
libmoq), and wasm, plus the docs and theIETF drafts.
Deliberate semantic changes
relied on content identity (epoch / first hop) that per-broadcast announces carried; with
epoch gone the splice is unsound, so a subscription through a dead route ends and the
consumer resubscribes through the best remaining route (matching "refusals are
authoritative; abort so the decoder resubscribes"). Local fronts (two
create_broadcastat one path in the same origin) still splice, newest source wins.
reselect wait). The wire keeps the warm/cold pair and the draft keeps the discount as a
MAY, but the relay forwards accumulated costs only for now. Back to the drawing board
with route-level identity.
wins ties, and identical-meta reannounces are invisible), but a new announce no longer
ends the incumbent's existing subscriptions; those end when their session does.
no producer-side
announce(prefix)yet (it consumes prefix routes; publishing stillannounces exact paths).
API consistency round (post-review)
Prefixnewtype: what a route covers is its own opaque type withcovers()andper-segment matching, so wildcard patterns can land later without a breaking change.
Route { hops, cost }: the prefix is no longer part of the route; it is what theroute covers.
announce(prefix, route),Update { prefix, route, active }, andupdate(route)can no longer express a prefix change at all (the earlier runtimeNotFound check and its test are deleted as unrepresentable).
announce::Produceris the advertisement guard (wasorigin::Announcement),completing the split-handle symmetry with
announce::Consumer/announce::Update. Theold
announce::Producer, a consumer factory with one method, is deleted.publish_broadcastremoved: announcing before a broadcast has tracks is usually abug, so the blessed order is create, populate, announce.
moq-cli'sPublishattachesits announcement after constructing the catalog for the same reason.
insertRouteis nowProducer.announce(prefix, provider)(the fusedannounce+dynamic: serve a subtree from one provider), and the announce
Eventcarriesprefixrather thanpath. FFI + all wrappers takeannounce(prefix, route)withMoqRoutereduced to hops and cost.Wildcards (planned ahead)
Prefixis opaque withnew/covers/as_path, and matching is segment-wiseintersection in one place, so a future pattern type (prefix + suffix, per the moq.pro
wildcard quest) extends
Prefixinternally without touchingRouteor any signature.Cross-Package Sync
rs/moq-netwire/API ->js/net,doc/concept,drafts/draft-lcurley-moq-lite.md: done.rs/moq-ffi->rs/libmoq,py/,swift/,kt/,go/wrapper,doc/lib/*: done.rs/moq-relay->doc/bin/relay/(cluster costs, 1+1): done.moq-literewritten (announce = routes, Epoch removed),moq-clusterupdated(metadata-only updates, no content identity),
moq-broadcastretired.Bugs found while validating
AnnouncePrefix): theRunarm servedroute requests before parking on the announce decode, so a request that arrived
after a route attached in the same pass had no registered waiter and nothing else
re-polled the machine. Deterministic on the internal unix listener (fast in-process
connects), hidden by latency elsewhere. Fixed by draining buffered announces first
and registering on every route's request queue before parking.
ServeLoop::new:track::Request::acceptreleases therequest's built-in fetch handler before the relay's own
dynamic()registered, so acache-miss
fetch_groupissued during the TRACK_INFO round trip drained asNotFound. Fixed by grabbing the dynamic from the request before accepting(regression covered by
broadcast_moq_lite_05_fetch_webtransport).publish_broadcastreturns(Producer, Announcement), and three callers held the producer but let the guard die early,silently unannouncing: moq-cli's
Publish(stdin/capture publishing wasunannounced the moment setup returned), moq-bench's per-connection publish loop,
and several tests.
Publish::new/capturenow take the pair.Testing
cargo nextest run --workspace(all pass) + clippy-D warnings,fmt, rustdoc.
session serves through the handover window, the subscription ends when it closes,
and a resubscribe carries on; route re-pricing surfaces as metadata updates, never a
retraction. The diamond test also stops expecting live-edge catch-up: on lite-05 Max
Age is a staleness tolerance, so a fresh subscription starts at the live edge (dev's
feat(net)!: resolve the subscribe start from max age, with Group Start as an absolute floor #3158 semantics, unchanged here).
just drafts check.just test smoke-full(wire + ffi change).🤖 Generated with Claude Code
(Written by Claude Fable 5)