Repository navigation
feat(net)!: subscriptions take a track Delay { min, max } - #7597
Conversation
Quest-Claim: 696c92bd01246584
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
moq-net's track::Subscription and @moq/net's Track.Subscription carry a Delay with a nullable min in place of a bare max delay, and moq-lite-07 SUBSCRIBE and SUBSCRIBE_UPDATE carry it as Subscriber Min Age. Delay moves out of moq-mux and @moq/hang. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ients Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l mutation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 9451b41d984d75d44868dcf1b8cee8e1fd65938c.
No actionable bugs found in the reviewed changes. The direction is sound: sharing track::Delay removes the competing media/network shapes, while preserving the distinction between an unstated minimum and stated zero. The smallest-stated-min aggregation and version-gated lite-07 encoding keep older peers from falsely lowering the upstream target; reusing the existing demand aggregation is preferable to a parallel latency-signaling mechanism.
Checked the Rust/JS subscription defaults, aggregation, wire encode/decode and update paths, media-consumer propagation, binding adapters, and focused regression tests. The remaining binding API work is explicitly deferred; this review does not certify that follow-up. Static review only: I did not run tests, builds, browser playback, or interop, and have not independently verified the validation reported in the PR.
(Written by OpenAI)
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes
Subscriptions take a track::Delay { min, max }, and the subscriber's target latency (min) now reaches the moq-lite-07 wire. This is the Rust and JS half of quest/m1/subscription-delay; I read the full 207-file diff against main (a546cac).
- Wire (moq-lite-07,
-wip):Subscriber Min Ageadded to SUBSCRIBE and SUBSCRIBE_UPDATE betweenSubscriber Max AgeandRange Count, encoded as milliseconds + 1 with0= not stated. The draft and its changelog are updated; older wires and moq-transport are unchanged. - Rust model (breaking):
moq_net::track::Delay { min: Option<Duration>, max: Option<Duration> };Subscription.max_delaybecomesdelay;moq_mux::container::Delayis gone, with MIGRATE notes in moq-net and moq-mux. - JS model (breaking):
Track.Delay { min?, max? };Track.Subscription.maxDelaybecomesdelay;@moq/hang'sContainer.Delayis gone, with MIGRATE notes in @moq/net and @moq/hang. - Aggregation: a relay forwards the smallest stated
min(unstated when none states one);delay.maxaggregation is unchanged. - Consumers:
container::Consumer::set_delay/newnow carry the whole delay,minincluded, onto the subscription;@moq/watchand the audio/video decoders put their playout delay on media subscriptions asmin. The lite publisher stores but never readsminto move a start.
The wire codec is byte-identical between Rust and JS, pre-lite-07 layouts are unchanged, and the aggregate rule is correct on join/update/leave in both languages.
ℹ️ Stale "min never reaches the subscription" comments and a test name
container::Consumer::set_delay now writes the whole delay, min included, onto the subscription, and the media consumers feed their playout delay through it. Three comments and one test name still assert the opposite, so they now describe behavior this PR removed:
rs/moq-mux/src/container/consumer.rs:3705(doc) and:3707(testthe_min_delay_never_reaches_the_subscription).reach_consumercalls.with_delay(Delay { min: Some(min), .. }), soset_delaydoes updatesubscription.delay.min; the test passes only because it never checks.min.rs/moq-c/src/api.rs:718-721—moq_delay.min_us"stays on the consumer and never reaches the subscription".rs/moq-ffi/src/media.rs:28— the same sentence, left behind when line 24's type reference was updated.
Technical details
# Stale "min never reaches the subscription" claims
## Affected sites
- `rs/moq-mux/src/container/consumer.rs:3705` — doc: "The min delay stays on the consumer: the subscription carries the max delay plus the warmup."
- `rs/moq-mux/src/container/consumer.rs:3707` — test name `the_min_delay_never_reaches_the_subscription`.
- `rs/moq-c/src/api.rs:718-721` — `moq_delay.min_us` "stays on the consumer and never reaches the subscription".
- `rs/moq-ffi/src/media.rs:28` — same sentence for `MoqDelay.min`.
## Required outcome
- Every claim that the playout minimum does not reach the subscription is updated to match the new behavior, or removed where it is no longer meaningful. The mux test should ideally assert the minimum does reach `subscription.delay.min` (or be renamed) so it stops passing for the wrong reason.
## Why it is now false
- `container::Consumer::set_delay` builds `delay.with_max(delay.max + warmup)` (min preserved) and calls `track.update(track.subscription().with_delay(delay))`, so `subscription.delay.min` follows the requested `min` (`rs/moq-mux/src/container/consumer.rs:1139-1142`).
- `moq-audio`/`moq-video` decode consumers call `container::Consumer::new(...).with_delay(options.delay)` (`rs/moq-audio/src/decode/consumer.rs:244`, `rs/moq-video/src/decode/consumer.rs:357`), so the FFI/C `min` reaches the subscription too.deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the subscription' docs SUBSCRIBE_OK carries no min age on any draft, so the lite JS SubscribeOk's minDelay prop was accepted and silently dropped. The media consumers now put the whole delay, min included, on the subscription, so the FFI, C, wrapper, and mux docs that said otherwise are rewritten, and the mux test asserts min. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native video, audio, and C consumers subscribed with a default Delay carrying only the max, so the first request stated a zero min until the container's update corrected it, dragging a relay's aggregate min to zero meanwhile. Subscribe with the delay the container keeps instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the pullfrog review's stale "min never reaches the subscription" note: agreed, fixed in e190b0d. The The local Codex review then found that the native decoders' first SUBSCRIBE still stated a zero min until the container's update. Fixed in 473c054 for moq-video, moq-audio, and moq-c, with regression tests that check the first aggregate. (Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 3c71c501ec23f312d3314c921debe73afbba6d47.
No new actionable bugs found in the update since my previous review, accounting for the merged base changes. The native audio/video/C paths now carry the whole delay on the first subscription instead of briefly advertising a stated-zero minimum; the audio/video first-request tests exercise the live-edge wait before a container could correct it. Sharing the JS dual-decoder delay between subscription and consumer also keeps their warmup budgets aligned.
The direction remains sound: initialize the existing Delay correctly rather than add another correction path. The stale propagation comments/test noted in Pullfrog's review are fixed, with a minimum assertion added; the unused SubscribeOk minimum is removed. No new parallel API or wire mechanism is needed for these fixes.
Static review only. I did not run tests, builds, playback, or interop, and did not independently verify the PR's reported validation.
(Written by OpenAI)
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 296c9b9690098373f2a08f2d15b662e7a33c350b.
No new actionable bugs found since the previous review, accounting for the main-branch merges. rs/moq-cli/src/play/media.rs:282–300 now gives the first video subscription the same full Delay used by its container, including the warmup decoder's minimum, before waiting for the live edge. This closes the analogous CLI initialization gap without a new update mechanism; reusing that already-computed Delay is the simpler, appropriate fix. The merged JS ordered-read call sites and migration notes retain their max-delay semantics under delay.max.
Earlier fixed findings remain fixed. Static review only: I did not run tests, builds, playback, or interop; this delta adds no dedicated CLI first-subscription test.
(Written by OpenAI)
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The commits since the prior pullfrog review (fa98b72) are the CLI first-subscription fix and two merges of origin/main that brought in routes-with-an-instance (#7489) and the JS ordered-read change (#7609). No new issues.
- CLI states the min delay on its first subscription:
rs/moq-cli/src/play/media.rsnow subscribes with the container's wholeDelayinstead ofDelay::default().with_max(...), so the video subscription a cold join makes never advertises a stated-zero minimum before the container's update. The primary decoder passes{ min: playout, max }; the intra-refresh decoder passes both pluswarmup, matchingmoq_video::decode::Consumer. origin/mainmerges: The conflict resolutions keep main's instance/epoch routing and ordered-read logic under this PR'sdelay: { max }spelling, and the JSSubscribeOkstill carries no deadminDelay. The wire codec, aggregation rule, and pre-lite-07 layouts are unchanged.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
quest land watch: the merge queue removed this pull request at 296c9b9 because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |
|
codex / Codex land fixer note:
(written by codex / Codex) |
|
codex / Codex land fixer: Auto-merging rs/moq-mux/MIGRATE.md CONFLICT (content): Merge conflict in rs/moq-mux/MIGRATE.md Auto-merging rs/moq-mux/src/container/fmp4/import.rs Auto-merging rs/moq-mux/src/container/fmp4/import_test.rs Auto-merging rs/moq-mux/src/container/mkv/import_test.rs Auto-merging rs/moq-net/src/fuzz.rs Auto-merging rs/moq-net/src/ietf/publisher.rs CONFLICT (content): Merge conflict in rs/moq-net/src/ietf/publisher.rs Auto-merging rs/moq-net/src/ietf/subscriber.rs Auto-merging rs/moq-net/src/lite/publisher.rs CONFLICT (content): Merge conflict in rs/moq-net/src/lite/publisher.rs Auto-merging rs/moq-net/src/model/subscription.rs CONFLICT (content): Merge conflict in rs/moq-net/src/model/subscription.rs Auto-merging rs/moq-net/src/model/track.rs CONFLICT (content): Merge conflict in rs/moq-net/src/model/track.rs Auto-merging rs/moq-net/src/model/track/demand.rs CONFLICT (content): Merge conflict in rs/moq-net/src/model/track/demand.rs Auto-merging rs/moq-net/src/model/track/miss.rs Auto-merging rs/moq-relay/tests/integration/goaway_cluster.rs Auto-merging rs/moq-relay/tests/integration/upstream_links.rs Automatic merge failed; fix conflicts and then commit the result. (written by codex / Codex) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The commits since the prior pullfrog review (296c9b9) are a merge of origin/main 2738588cfc and a bench-format commit. The merge is a faithful combination of main's subscription live-feed work with this PR's Delay model; no new issues.
- Merged main's subscription live-feed:
Subscriptionnow carries bothdelay: Delayand main'sfeed: bool; the demand aggregate's scalars are(priority, Delay, order, feed), withfeedaggregated as any andmin_delaystill counting only stated minimums. The ietf publisher'sserving_subscriptionkeepsdelay: { min: None, max: None }(moq-transport has no minimum) alongsidefeed: true. - Kept the lite publisher's accepted-update cursor:
TrackRun::updatebuildsSubscription { priority, delay: { min: upd.min_delay, max: serving_max_delay(..) }, order, ..groups }and advancesself.namedonly oncetrack.updatesucceeds, so a refused update cannot move the cursor. - Migrated main's new call sites and reformatted: main's added
with_max_delaycallers in the benches and integration tests moved towith_delay(Delay::default().with_max(..))(a stated-zero minimum, matching the model default), and therangesbench was reformatted with no semantic change.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…nto main's cursors Decided 2026-11-03 (maintainer): re-port in place. Main rewrote the JS cursor paths this PR changed (#7486 clean-close completion, #7597 `Delay { min, max }`, #7609 owed-hole waits, #7542 Rust window walks), so the ranges, window, and lite-07 serving go on top of main's versions. - Bounded reads end, as Rust's do since #7545 and #7625, replacing the 2026-10-31 decision to park: a plain reader judges its window as Rust's `Cursor::is_exhausted` does, and a reader following route copies judges it on the serving copy as Rust's `Reader::exhausted` does. Wire publishers and route copies park through `hooks.parked`. - Main's `delay: { min, max }` replaces this PR's `maxDelay`, with `NO_MAX_DELAY`/`maxDelayOf` as the no-limit value. - The arrival grace is no longer capped at CACHE_EXPIRY: main bounds it by the publisher's max age instead, and an unlimited ordered reader waits for its hole (#7609, #7169). - Owed holes count only inside the reader's ranges. - lite-07: a SUBSCRIBE goes out alongside its TRACK, as on main, unless a time anchor needs the track's timescale, as the Rust `pipeline` does. The covered FIN without SUBSCRIBE_END stays lite-07 only, now that `waitsForSubscriberFin` covers lite-05+. - Rust: the waiting time start is ported onto main's window walks as a placeholder span, as a lookup is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…survey/hang-buffered
Resolve the buffered bench onto main's micro harness (bandwidth, replay,
resume-history), keep both the backlog-start tests and the span test, move
the bench and test subscriptions to `delay: { max }` (#7597), and drop the
hang-buffered item from audit-perf's renumbered list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…benches in any package #7597 moved the delay type to `Track.Delay`. The nightly coverage test read every browser smoke bench from `js/net/bench`, but `buffered` lives in `js/hang/bench`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main replaced Subscription::with_max_delay with a Delay { min, max } in #7597.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Problem
Claims quest/m1/subscription-delay.md. Subscriptions took a bare max delay while the media consumers took
Delay { min, max }(moq-dev/moq#7311), and a subscriber's target latency never reached the wire.Decisions
## Decisions needed: the shared publicDelayhas a nullable minimum,Option<Duration>in Rust and an explicitnullin JS (and FFI, in the bindings follow-up), with explicit absence flags in C and Go so zero-initialized values stay a stated zero. Media consumers read an unstated minimum as zero locally, and subscription metadata keeps it.Delaymoves to moq-net with no re-export,minrides lite-07 SUBSCRIBE and SUBSCRIBE_UPDATE, a relay forwards the smallest statedmin, and older wires decode it as not stated.Subscriber Min Age, mirroringSubscriber Max Age, encoded as milliseconds plus one with zero meaning not stated (the FETCHSubscriber Max Ageconvention).Subscription::with_max_delay(d)becomeswith_delay(Delay), withDelay::with_min/with_maxbuilders.group::Fetch::max_delay,@moq/net'sFetchGroupOptions.maxDelay, and the exporters'with_max_delaystay: none is a subscription, and a fetch has no minimum.container::Consumer::set_delaynow puts the whole delay,minincluded, on the subscription, andConsumer::newinherits both from it. The native video, audio, and C decoders subscribe with that same delay from the first request, so a publisher never sees a stated zerominbefore the container's update.@moq/watchputs its playout delay on its media subscriptions asmin; its max-delay decoder's subscription and consumer share one delay, the playout and max delays each plus the catalogwarmup, as Rust's does.@moq/netrefuses the oldmaxDelaykey at runtime, as it already didmaxAge.moq-audio/moq-videokeep theirdecode::Delayre-export, now pointing atmoq_net::track::Delay.Scope
The quest is [L] and spans every binding, so this PR lands the Rust and JS half and rewrites the quest to the bindings: FFI
MoqSubscription(andMoqDelay.mingoing nullable), moq-c, and the Python, Swift, Kotlin, Go, Dart, and C++ wrappers, with the Dart/Go/C omitted-max fixes. Until then the bindings keepmax_delay, mapped ontodelay.maxwith a stated zeromin. It also drops the quest from lite07-finalize's Required, since the wire half lands here.Impact
moq_net::track::Delay { min: Option<Duration>, max: Option<Duration> };track::Subscription::max_delayisdelay: Delay.moq_mux::container::Delayis gone; usemoq_net::track::Delay, wrappingmininSome. MIGRATE notes in moq-net and moq-mux.Track.Delay { min?: Milli | null; max?: Milli | null };Track.Subscription.maxDelayisdelay.@moq/hang'sContainer.Delayis gone;ConsumerProps.delaytakesTrack.Delay. MIGRATE notes in @moq/net and @moq/hang.-wip) SUBSCRIBE and SUBSCRIBE_UPDATE gainSubscriber Min AgeafterSubscriber Max Age; the draft is updated. Older moq-lite versions and moq-transport are unchanged.Validation
quest gate nix develop --ignore-environment --keep HOME --keep USER --keep PATH --command just ci origin/mainonf593745380(againstorigin/main2738588cfc): passed, every module, 9519/9519 Rust tests.just test interop --allpassed on the pre-merge head9451b41d98(Rust, js-native node and bun, Python; moq-lite-05 through 07 and moq-transport); not rerun after the merge.min_delay_roundtrips_only_on_lite07(Rust codec),relay_forwards_the_smallest_stated_min_delay(a lite-07 relay with lite-07 and lite-06 subscribers), the randomizedcounted_demand_matches_a_full_foldnow draws stated and unstated minimums,the_first_subscription_states_the_min_delayin moq-video and moq-audio (fail withmin: Some(0ns)without the fix), and JS covers the codec, the aggregation (only unstated minimums aggregate to an unstated one), rounding, and refusal of the oldmaxDelaykey.moq-tokio backend::quic_client_close_drains_finished_trackfailed once under host load on the pre-merge head (subscribe failed: Unroutable, before any delay code runs) and then passed 10/10 on the branch and 10/10 onorigin/main.Iteration (2026-11-02)
origin/main(conflicts injs/watch/src/video/decoder.ts,quest/m2/intra-refresh/dual-decoder.md,rs/moq-net/MIGRATE.md), and moved main's newSubscription::with_max_delay/maxDelaycallers ontodelay.minDelayprop from the lite JSSubscribeOk(thread), and rewrote the stale "min never reaches the subscription" docs in moq-c, moq-ffi, the Kotlin/Swift/Go/Dart wrappers, and the mux test, which now assertsdelay.min.Second round, after the merge queue dropped
fa98b72fd7withmerge_conflict:origin/maintwice (moq-dev/moq#7489, routes with an instance and per-instance epoch; then moq-dev/moq#7609, JS ordered reads wait for owed groups). Conflicts injs/e2ee/src/transport.test.ts,js/net/src/lite/subscriber.ts,rs/moq-net/tests/integration/route_change.rs,js/net/MIGRATE.md, andjs/net/src/track.tskeep main's structure (the route-change test'sfollowflow,Epoch.of, the instance import, fix(net): wait for subscription-owed groups in JS ordered reads #7609's MIGRATE entry and "nullreaches back" wording) with this PR'sdelay: { max }/with_delaynames. fix(net): wait for subscription-owed groups in JS ordered reads #7609's new ordered-read tests moved frommaxDelaytodelay: { max }.6a8380f916: the CLI's video subscription (rs/moq-cli/src/play/media.rs) stated a zerominwhile it waited for the live edge; it now subscribes with the container's wholeDelay, as moq-video's reader does.fa98b72fd7) had no findings; no unresolved review threads.Third round, after the merge queue dropped
296c9b9690withmerge_conflict:origin/main2738588cfcin1fdeb62565. Conflicts inrs/moq-net/src/model/subscription.rs,rs/moq-net/src/model/track.rs,rs/moq-net/src/model/track/demand.rs,rs/moq-net/src/ietf/publisher.rs,rs/moq-net/src/lite/publisher.rs,rs/moq-net/MIGRATE.md, andrs/moq-mux/MIGRATE.mdkeep main's live-feed tracking (Subscription::feed, aggregated as any, set by the ietf publisher's serving subscription), the lite publisher's accepted-update cursor (self.namedmoves only onceupdatesucceeds), main's imports (Layered,Runs,Seen), and main's MIGRATE entries (moq-dev/moq#7624, the fMP4 terminal-error note), alongside this PR'sDelay. The aggregate's scalars are(priority, Delay, order, feed).Subscription::with_max_delaycallers ontowith_delay(Delay::default().with_max(..)):rs/moq-net/benches/ranges.rs,rs/moq-net/src/lite/subscriber.rsandrs/moq-net/src/model/track/miss.rstests,rs/moq-net/tests/integration/{fetch_only,route_health}.rs, andrs/moq-tokio/tests/integration/receive_credit.rs; formatted the bench inf593745380.quest/m2/audio-flush-group.mdand itsquest/m2/README.mdlink now saydelay.max.Review
gpt-6.1-sol, medium effort (quest review), session01a19e7e, round 1 (a new session: the merged base changed the review instructions since session01a19de7).f593745380e0bb24682ae613e4ce971df354cd5cagainstorigin/main2738588cfc: no findings.01a19de7: round 2 on296c9b9690, no findings; round 1 on5c92ae8f4a, one finding (the CLI's first video subscription stated a zeromin), fixed in6a8380f916.01a19d7c: round 2 on3c71c501ec, no findings; round 1 on5283982f2c, one finding (the native decoders' first subscription stated a zeromin), fixed in473c05499c. Before that, on9451b41d98: Codexgpt-6-astra, medium, no findings.Follow-ups
moq-tokio backend::quic_client_close_drains_finished_trackcan fail withUnroutableunder heavy load; worth a flake quest if it recurs.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code