Repository navigation
feat(js): join moq-transport tracks at the current group - #7089
Conversation
A draft-18 publisher delivers only objects after Largest, so a JS viewer
joining late lost a group written before it subscribed (the hang catalog)
and dropped its first media group as headless.
A subscription now says where it joins a track already under way, with
Track.Subscription.join:
- "current" (the default): the group in progress from object 0, as the
Rust subscriber joins. SUBSCRIBE asks for Largest Object, and after a
SUBSCRIBE_OK with Largest {G, k} a Relative Joining FETCH (offset 0)
brings the head. A relay that refuses or fails it gets one standalone
FETCH for {G, 0} up to {G, k + 1}: some deployed relays cannot resolve
a join for a lone subscriber but serve the range. Tracks written rarely,
such as the catalog and JSON snapshots, need this.
- "next": Next Group Start and no FETCH, so media starts at the live edge
rather than up to a group behind it. A group in progress that a lenient
publisher sends anyway is skipped. @moq/watch joins media this way.
Any "current" subscriber makes the aggregate "current". moq-lite serves
the current group either way.
For the current group, the fetch stream creates group G and writes the
head as it arrives, so readers see it at once; the live tail from k + 1
takes the same group over, waiting for the head if it lands first. If
both FETCHes fail, or the head is empty or unstitchable, only G's head is
lost (moq-dev#6407). A whole group before the head wins the sequence; after it,
both are refused, as in Rust.
It sends its FETCHes with the codec from moq-dev#6945 and adds a fetch-object
decoder. The ietf order tests leave fetch streams out of the group stream
ranking they check, now that the JS publisher answers the join too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for this. The maintainer decided the scope on #7088 (planned in #7124,
Please hold off pushing here meanwhile, so we don't race. (Written by Claude Opus 5.5) |
# Conflicts: # doc/concept/standard.md # js/net/src/ietf/object.ts
Every moq-transport subscription now joins at the current group, as the Rust subscriber does, per the scope decided on moq-dev#7088: - Drop Track.Join, Subscription.join, and @moq/watch's MEDIA_JOIN. - Drop the standalone-FETCH fallback and the 10 s request deadline: the joining FETCH lives as long as the subscription, and a refusal costs only the group's head. - On draft 22, subscribe at Next Object with a StartGroup=1 fill. The fill names the SUBSCRIBE's request, may overtake SUBSCRIBE_OK, and counts toward PUBLISH_DONE's Stream Count; Join stitches it like the draft-18 head. - Finish quest/m1/ietf-js-join.md and update the notes it made stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I pushed the trim as e41e967, on top of your commit and a merge of
The maintainer's reasons are on moq-dev/moq#7088. In short:
(Written by Claude Opus 5.5) |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 9 minutes. View limit details
No actionable comments were generated in the recent review.
|
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. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: e41e96731f3ada1ab3236865145fd2f9a5409193.
Two actionable lifecycle issues:
-
P2 — Do not finish a fetched prefix on SUBSCRIPTION_ENDED (
js/net/src/ietf/subscriber.ts:1039–1043). Draft 18 treats this status as drainable, but it does not establish that the current group ended. With a fetched head and no live tail,join.end()finishes that prefix before the status check throws. The subsequent track abort deliberately preserves finished groups, so readers see a truncated group as complete. Only calljoin.end()for TRACK_ENDED; leave a subscription-only end to cancel/abort the unclaimed head. Add a fetched-prefix/no-tail SUBSCRIPTION_ENDED test. -
P2 — Observe joining-request failure after FETCH_OK (
js/net/src/ietf/subscriber.ts:1180–1182). If the publisher sends FETCH_OK and then resets that request before opening its fetch stream,RequestStreamobserves the reset, but this wait observes onlyjoin.settledand subscription cancellation. The join remains pending and holdsTailactive; even a subsequent PUBLISH_DONE cannot settle, so the subscription's cancellation signal never fires. Propagate the request failure intojoin.refuse()while waiting for the head (without mistaking a normal control-stream completion for missing data), and test reset-after-FETCH_OK with no fetch stream and a parked tail/PUBLISH_DONE.
Direction: always joining the current group and centralizing head/tail ownership in Join is preferable to adding a public join knob or fallback retry; the scripted ordering and draft-specific stream-count tests are valuable. Verification was static, including an independent lifecycle review and codec/context inspection; I did not run tests or reproduce the reported interop/CI results.
|
Maintainer decision: on draft 22, the JS subscriber joins with Rust's form (a Next Object SUBSCRIBE plus a relative(1) fill), as implemented. (Written by Claude Opus 5.5) |
…FETCH Only TRACK_ENDED finishes a head nothing claimed: SUBSCRIPTION_ENDED leaves the group open upstream, so the head is cancelled instead. The joining FETCH keeps reading its request after FETCH_OK, so a reset before the fetch stream releases the parked tail and PUBLISH_DONE's settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both findings from the review of e41e967 are fixed in 5b24448, each with a test that fails without it:
(Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 5b24448
Both prior P2 findings are fixed:
SUBSCRIPTION_ENDEDnow throws beforejoin.end(), so cleanup aborts an unclaimed fetched prefix instead of publishing it as complete.- After FETCH_OK, reading the request observes a reset and reaches
join.refuse(), releasing the pending head/Tail hold. A normal control-stream FIN still waits for the fetch data rather than declaring it missing.
The added regressions cover the prefix-only subscription end and reset-before-fetch-stream with a parked tail/PUBLISH_DONE. No new actionable bugs found in the delta from e41e967 (base unchanged).
Direction: these targeted lifecycle corrections preserve the centralized Join state rather than adding a timeout or fallback resolution. Verification: static inspection of the changed paths, RequestStream, Join/Tail ownership and tests, with independent confirmation. I did not run tests or interop.
|
Landing 5b24448 with the maintainer's approval.
(Written by Claude Opus 5.5) |
# Conflicts: # quest/m1/ietf-fill-timeout.md # quest/m1/ietf-js-join.md # quest/m1/interop-coverage/js-ietf-fetch.md
|
Not landed yet. After I queued 5b24448,
(Written by Claude Opus 5.5) |
|
Landing ec6d84a on the maintainer's decision (2026-10-29): land this head as is, without waiting for a fresh review.
(Written by Claude Opus 5.5) |
|
Measured this join against an IETF relay serving a moqxr publisher (H.264, 1 s groups) in Chrome, on drafts 22 and 18 (with #7187 for the direct subscribe):
(Written by Claude Opus 5.5) |
… tail open with End of Group On a fetch stream, a later object's Object ID Delta is added to the prior ID, so 1 is the next object and 0 repeats it. A live tail whose first object is End of Group past the head's last object now claims the head and finishes it. 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 8e183c9275a90008405ae30694f485345c38cd3c.
No new actionable bugs found in the substantive update, excluding incoming main changes. The independently reported FETCH delta issue and marker-only tail issue are fixed: explicit delta 1 is contiguous, delta 0 is refused, and an offset End of Group claims the matching head before completing it. The added regressions exercise those cases. Both lifecycle fixes from my previous review remain intact.
Direction: these narrow codec/hand-off corrections preserve the centralized Join ownership model without adding retry or timeout machinery. Verification: static inspection of the delta, current codecs, Join and subscription lifecycle paths, with an independent pass. Tests and live interop were not run.
|
Landing 9c793ba.
(Written by Claude Opus 5.5) |
Only TRACK_ENDED makes an unclaimed head its whole group. Any other end, such as draft-18 SUBSCRIPTION_ENDED, leaves the group open upstream, so the Rust subscriber now aborts the head instead of finishing it, as the JS subscriber does since #7089. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
A spec-strict moq-transport relay (such as moqx) delivers only the objects after Largest and serves earlier ones through a joining FETCH (draft 18) or a fill (draft 22). The JS subscriber asked for neither, so a late viewer never got a group written once before it subscribed (the hang catalog, so
@moq/watchstayed on "loading") and dropped its first media group as headless. moq-dev/moq#6945 added the publisher half; this is the subscriber half.Closes #7088.
Originally written by @michaelstuurman; trimmed on this branch to the scope the maintainer decided on moq-dev/moq#7088 (quest
quest/m1/ietf-js-join.md, claimed in moq-dev/moq#7152). His commit and the review history stay here.Approach
Every subscription joins at the current group, mirroring the Rust subscriber (
subscribe_join,recv_fill,claim_fill). There is no join knob.{G, k}, a Relative Joining FETCH (offset 0) goes out as its own request, outside PUBLISH_DONE's Stream Count. It has no deadline of its own: it lives as long as the subscription. A refusal or failure costs only G's head (moq-dev/moq#6407 semantics). There is no standalone-FETCH fallback.relative(1)), the draft's current-group join (section 3.5). The fill's fetch stream names the SUBSCRIBE's request ID. It can overtake SUBSCRIBE_OK, so it waits for it, and it counts toward PUBLISH_DONE's Stream Count.Join(inietf/join.ts) owns group G. The fetch stream creates the group and writes the head as it arrives, so readers see it at once and a catalog written once needs no tail. The live tail fromk + 1takes the same producer over, and waits for the head if it lands first. On draft 18 the head holds PUBLISH_DONE's settle while a parked tail lets go of it; on draft 22 the counted fill stream holds it.FetchFrame, usingEndOfRangefrom moq-dev/moq#7051. Fetch streams now go to the subscriber instead of being rejected.Trimmed from the original
Track.Join,Subscription.join, and@moq/watch'sMEDIA_JOIN = "next". Rejected on moq-dev/moq#7088: lite and Rust have no such anchor, a "next" join costs a GOP of startup, and how far behind live to play is the player's choice throughmaxDelay.Impact
{0, 0}. After a SUBSCRIBE_OK with Largest, a Relative Joining FETCH follows.{LOCATION_FILTER: relative(1)}, instead of arelative(1)LOCATION_FILTER.doc/concept/standard.mdsays how a JS subscription joins.test/interop/README.mdand the quests that said JS sends no joining FETCH or fill are updated. JS subscribers FETCH is narrowed to the standalone FETCH that remains.Choices made without asking
relative(1)carries the matching fill" as Rust's form: a Next Object subscription plus a fill whose filter isrelative(1). The other reading keepsrelative(1)as the subscription's own filter and adds a fill on top. I didn't take it, because a strict publisher only has to honor the current-group join (Next Object plus the fill), and Rust sends exactly that.Fill::endstill finishes it on any clean end; that is a follow-up.Follow-ups
Fill::endinrs/moq-net/src/ietf/subscriber.rs); it should cancel it as JS now does.Testing
just cipasses, including the merge ofmain. That merge needed a hand resolution inobject.ts, where the fetch decoder now reusesEndOfRange, and indoc/concept/standard.md.just test interop --tail --versions moq-transport-18,moq-transport-22passes. The late-join lane passes for Rust, Node, and Bun subscribers on both drafts. Its subscription now starts after Largest, so the relay no longer replays the group, and the JS subscriber gets the head only through its joining FETCH or fill.ietf/join.test.ts, against a scripted publisher:(Written by Claude Opus 5.5)
🤖 Generated with Claude Code