Skip to content

feat(js): join moq-transport tracks at the current group - #7089

Merged
kixelated merged 8 commits into
moq-dev:mainfrom
michaelstuurman:feat/js-joining-fetch
Oct 29, 2026
Merged

kixelated merged 8 commits into
moq-dev:mainfrom
michaelstuurman:feat/js-joining-fetch

Conversation

@michaelstuurman

@michaelstuurman michaelstuurman commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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/watch stayed 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.

  • Draft 18: SUBSCRIBE with Largest Object. After a SUBSCRIBE_OK with Largest {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.
  • Draft 22: SUBSCRIBE at Next Object with a StartGroup=1 fill (FILL_PARAMETERS carrying 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.
  • Stitching: Join (in ietf/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 from k + 1 takes 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.
  • Duplicates, as in Rust: a whole group from object 0 that arrives before the head wins, and the head is discarded. One that arrives after the head is refused, along with the head.
  • Codecs: the FETCH and FETCH_OK use the codec from moq-dev/moq#6945. A new fetch-object decoder mirrors FetchFrame, using EndOfRange from 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's MEDIA_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 through maxDelay.
  • The standalone-FETCH fallback. A relay that refuses the joining FETCH for a lone subscriber has its own bug (openmoq/moqx#883).
  • The 10 s per-request deadline. Rust has none, and FILL_TIMEOUT is a separate bound on the publisher side.

Impact

  • Public API: none.
  • Wire, draft 18: SUBSCRIBE carries LOCATION_FILTER Largest Object (0x2) instead of Absolute Start {0, 0}. After a SUBSCRIBE_OK with Largest, a Relative Joining FETCH follows.
  • Wire, draft 22: SUBSCRIBE carries Next Object (0x5) plus FILL_PARAMETERS {LOCATION_FILTER: relative(1)}, instead of a relative(1) LOCATION_FILTER.
  • Between JS peers, the head now comes through the joining FETCH or fill. The JS publisher used to replay the group on the subscription.
  • Docs: doc/concept/standard.md says how a JS subscription joins. test/interop/README.md and 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

  • The draft-22 spelling. I read the plan's "relative(1) carries the matching fill" as Rust's form: a Next Object subscription plus a fill whose filter is relative(1). The other reading keeps relative(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.
  • The claim PR. The quest's claim PR, moq-dev/moq#7152, carries no work and only links here. The quest file's deletion lands in this PR. Close The JS moq-transport subscriber joins at the current group #7152 once this merges.
  • No request deadline anywhere on the joining FETCH. Opening its bidi stream has no deadline either. It is bounded only by the subscription.
  • SUBSCRIPTION_ENDED cancels an unclaimed head. Only TRACK_ENDED finishes a head nothing claimed: SUBSCRIPTION_ENDED leaves the group open upstream, so finishing the head would pass a prefix off as the whole group. Rust's Fill::end still finishes it on any clean end; that is a follow-up.

Follow-ups

  • Rust's subscriber finishes an unclaimed fill head on SUBSCRIPTION_ENDED too (Fill::end in rs/moq-net/src/ietf/subscriber.rs); it should cancel it as JS now does.

Testing

  • just ci passes, including the merge of main. That merge needed a hand resolution in object.ts, where the fetch decoder now reuses EndOfRange, and in doc/concept/standard.md.
  • just test interop --tail --versions moq-transport-18,moq-transport-22 passes. 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:
    • Draft 18: head before tail and tail before head; a catalog written once; an empty tail; a refused or empty joining FETCH costs only the head, and no retry follows; a tail parked on a refused FETCH; a FETCH reset after FETCH_OK releases its parked tail and the end; SUBSCRIPTION_ENDED cancels an unclaimed head; no deadline after 60 s; the duplicate-group strictness; an unstitchable head.
    • Draft 22: the SUBSCRIBE spelling, with no FETCH sent; the fill and its tail stitch; a fill that overtakes SUBSCRIBE_OK; the fill counting toward the Stream Count.
  • End to end against the JS publisher on both drafts: a catalog written once and a media group in progress arrive from object 0. The JS publisher splits the draft-22 fill from the subscription at Largest, as a strict relay does.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

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>
@kixelated

Copy link
Copy Markdown
Collaborator

Thanks for this. The maintainer decided the scope on #7088 (planned in #7124, quest/m1/ietf-js-join.md). We'll take this PR over and trim it on this branch, keeping your commit and authorship:

  • keep the draft-18 Largest Object + Relative Joining FETCH and the head/tail stitching, with the duplicate-group strictness as you have it;
  • drop Track.Join / Subscription.join and the @moq/watch MEDIA_JOIN change: everything joins at the current group, as in Rust;
  • drop the standalone-FETCH fallback and the 10 s per-request deadline, matching Rust;
  • add the draft-22 fill for relative(1).

Please hold off pushing here meanwhile, so we don't race.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 28, 2026 15:08
# 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>
@kixelated kixelated changed the title feat(js): join draft-18 tracks at the current or next group feat(js): join moq-transport tracks at the current group Oct 28, 2026
@kixelated

Copy link
Copy Markdown
Collaborator

I pushed the trim as e41e967, on top of your commit and a merge of main. Your commit and authorship are unchanged.

  • Kept: the draft-18 Largest Object SUBSCRIBE and the Relative Joining FETCH, Join's head and tail stitching, the duplicate-group strictness, and the fetch-object decoder. The decoder now reuses EndOfRange from moq-dev/moq#7051, which conflicted on merge.
  • Dropped:
    • Track.Join, Subscription.join, and @moq/watch's MEDIA_JOIN. Every subscription joins at the current group.
    • The standalone-FETCH fallback.
    • The 10 s request deadline. The joining FETCH lives as long as the subscription, as in Rust.
  • Added: the draft-22 fill. The SUBSCRIBE goes out at Next Object with a StartGroup=1 fill, and Join stitches the fill's fetch stream like the draft-18 head. That stream can overtake SUBSCRIBE_OK, so it waits for it, and it counts toward PUBLISH_DONE's Stream Count.

The maintainer's reasons are on moq-dev/moq#7088. In short:

  • Lite and Rust have no "next group" anchor, and how far behind live to play is the player's job through maxDelay.
  • The moqx refusal is a moqx bug: openmoq/moqx#883.

just ci and the interop --tail lanes pass on drafts 18 and 22, including the late join with Node and Bun subscribers. The PR body has the details. Thanks for the groundwork: the stitching carried over almost untouched.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 28, 2026 22:48
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5d827434-3c18-4c69-b514-85e6b14d6142
Commits

Reviewing files that changed from the base of the PR and between ec6d84a and 9c793ba.

Files selected for processing: 15
  • doc/concept/standard.md
  • js/net/src/ietf/connection.ts
  • js/net/src/ietf/filter.test.ts
  • js/net/src/ietf/join.test.ts
  • js/net/src/ietf/join.ts
  • js/net/src/ietf/object.ts
  • js/net/src/ietf/subscribe.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/order.test.ts
  • js/net/src/tail.ts
  • quest/m1/README.md
  • quest/m1/ietf-js-join.md
  • quest/m1/interop-coverage/README.md
  • quest/m1/interop-coverage/js-ietf-fetch.md
  • test/interop/README.md

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 71965942-afdd-4ea4-b73a-6574619c042b
Commits

Reviewing files that changed from the base of the PR and between edce00d and ec6d84a.

Files selected for processing: 15
  • doc/concept/standard.md
  • js/net/src/ietf/connection.ts
  • js/net/src/ietf/filter.test.ts
  • js/net/src/ietf/join.test.ts
  • js/net/src/ietf/join.ts
  • js/net/src/ietf/object.ts
  • js/net/src/ietf/subscribe.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/order.test.ts
  • js/net/src/tail.ts
  • quest/m1/README.md
  • quest/m1/ietf-js-join.md
  • quest/m1/interop-coverage/README.md
  • quest/m1/interop-coverage/js-ietf-fetch.md
  • test/interop/README.md
Files with no reviewable changes: 2
  • quest/m1/README.md
  • quest/m1/ietf-js-join.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

JavaScript IETF subscribers now join an in-progress group by combining a fetched head with its live tail. Draft 18 uses a separate joining FETCH. Draft 22 requests a fill through SUBSCRIBE. The change adds join-state and fetched-object handling, subscription cleanup, and tests for joining behavior. Related interop and quest descriptions are updated. The history-fetching documentation also describes Rust reader handling of TRACK_STATUS.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Minimal · previous review ec6d8

This change lets JS subscribers receive the in-progress group, including catalogs and media group heads, when they join late over draft 18 and draft 22. The public API is unchanged, and the reported CI and interop runs pass for both drafts. No merge-blocking risk was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: JS moq-transport subscribers now join tracks at the current group.
Description check Passed The description directly explains the current-group joining behavior, draft-specific implementation, stitching, testing, and scope decisions.
Linked Issues check Passed Active direct targets: #7088 (coding work) and #7152 (quest claim). #7152 has no coding requirements. Its only task is the quest file deletion, which this PR contains. For #7088, the current scope is …
Out of Scope Changes check Passed Each changed area supports the join objectives or the trimmed scope:
- js/net/src/ietf/join.ts, subscriber.ts, subscribe.ts, filter.ts: the join, its wire filters, and the fill. These implement #7088 …
Full details: Linked Issues check

Explanation

Active direct targets: #7088 (coding work) and #7152 (quest claim). #7152 has no coding requirements. Its only task is the quest file deletion, which this PR contains. For #7088, the current scope is the maintainer's trimmed scope from the PR objectives, not the original proposal. Requirements and evidence:

  1. Draft 18, Largest Object join [#7088]: subscribe.ts returns { kind: "nextObject" }, and filter.ts encodes it as TAG_LARGEST_OBJECT (0x2) on draft 18. The join is therefore a Largest Object SUBSCRIBE. The subscriber.ts summary says a separate joining FETCH starts after SUBSCRIBE_OK. The FETCH send path was not read in full (see Note).
  2. Draft 22 fill [#7088]: subscribe.ts adds fill: { filter: relative(1), rangeFilters: false } for draft 22, with nextObject as the subscription filter (0x5). filter.ts encodes this as the typed Location Filter. This matches the maintainer-confirmed spelling.
  3. Head and tail stitching [#7088]: join.ts Join writes the head into one group as it arrives. claim() waits for a pending or writing head. It drops the head when the tail start differs from the head's next object. A tail that arrives first waits for the head.
  4. Failure cost [#7088]: refuse(), finish() on an empty stream, and fail() affect only group G's head. Later groups stay whole.
  5. Duplicates [#7088]: whole() lets a whole group win before the head. It refuses the whole group and aborts the head when the head already exists, as in Rust.
  6. Lifecycle [#7088, author fixes]: end() finishes a head only when the track ended. cancel() aborts an unclaimed head on SUBSCRIPTION_ENDED.
  7. Fetch-object decoding and routing [#7088]: the subscriber summary and connection.ts summary say fetch streams reach Subscriber.handleFetch instead of being rejected. object.ts adds decodeFetchObject.

Requirements excluded by the maintainer's trimmed scope are not assessed: Track.Join, Subscription.join, MEDIA_JOIN, the standalone-FETCH fallback, the 10 s deadline, and the open questions in the original body. Those open questions are answered by the maintainer comments in the PR objectives.

Note: the author-reported just ci and interop results, and the join.test.ts cases, were not independently re-run or read. The PASS relies on the code evidence above and the change summaries.

Full details: Out of Scope Changes check

Explanation

Each changed area supports the join objectives or the trimmed scope:

  • js/net/src/ietf/join.ts, subscriber.ts, subscribe.ts, filter.ts: the join, its wire filters, and the fill. These implement #7088 directly.
  • js/net/src/ietf/object.ts: fetch-object decoder (decodeFetchObject) for the joining FETCH and fill streams.
  • js/net/src/ietf/connection.ts: routes incoming FetchHeader streams to the subscriber. Needed for the head.
  • js/net/src/tail.ts (hold, suspend, touch): keeps the subscription end open for a pending head. Used by join.ts.
  • Tests (join.test.ts, filter.test.ts, order.test.ts): verify the join, the wire encodings, and the stream order. They are supporting tests.
  • doc/concept/standard.md and test/interop/README.md: describe the new join behavior. Documentation that matches the implementation.
  • quest/m1/README.md, quest/m1/ietf-js-join.md (deleted), quest/m1/interop-coverage/*: remove the completed quest and narrow the FETCH quest to the standalone FETCH that remains. These follow from #7152 and the trimmed scope.

No changed code adds a public API, a Track.Join option, MEDIA_JOIN, a standalone-FETCH fallback, or a request deadline. The PR explicitly excludes those.

Note: the subscriber.ts diff could not be read (changed blobs unavailable), and the full file read was truncated. The classification of subscriber.ts changes relies on its change summary.

Full details: Docstring Coverage

Explanation

Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 11 functions across 9 files. (4 skipped: 4 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
✨ Simplify code
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-28T22:48:33.444450Z e41e967 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 call join.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, RequestStream observes the reset, but this wait observes only join.settled and subscription cancellation. The join remains pending and holds Tail active; even a subsequent PUBLISH_DONE cannot settle, so the subscription's cancellation signal never fires. Propagate the request failure into join.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.

@kixelated

Copy link
Copy Markdown
Collaborator

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>
@kixelated

Copy link
Copy Markdown
Collaborator

Both findings from the review of e41e967 are fixed in 5b24448, each with a test that fails without it:

  • SUBSCRIPTION_ENDED: only TRACK_ENDED finishes a head nothing claimed now. Any other clean end throws first, so the cancel on the way out aborts the head instead of passing a prefix off as the whole group. Rust's Fill::end has the same gap; it is listed under Follow-ups.
  • Reset after FETCH_OK: the joining FETCH keeps reading its request while it waits for the head. A reset refuses the join, which releases the parked tail and PUBLISH_DONE's settle. A FIN keeps waiting, and any other message is a protocol violation, as in Rust's poll_end.

just ci passes.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 5b24448

Both prior P2 findings are fixed:

  • SUBSCRIPTION_ENDED now throws before join.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.

@kixelated

Copy link
Copy Markdown
Collaborator

Landing 5b24448 with the maintainer's approval.

  • Changes since the takeover: the trim to the current-group join with the draft-22 fill (e41e967), then the two lifecycle fixes from review (5b24448): SUBSCRIPTION_ENDED cancels an unclaimed head, and a joining FETCH reset after FETCH_OK refuses the join.
  • Decisions: the draft-22 join uses Rust's form (Next Object plus a relative(1) fill), as the maintainer confirmed above. The quest file quest/m1/ietf-js-join.md is deleted here; the claim PR moq-dev/moq#7152 closes once this merges.
  • Checks: just ci passes locally and Check passed on this head; the automated review of this head found nothing new; quest check passes against main.
  • Follow-up: Rust's Fill::end still finishes an unclaimed head on SUBSCRIPTION_ENDED.

(Written by Claude Opus 5.5)

@kixelated
kixelated enabled auto-merge October 29, 2026 04:53
# Conflicts:
#	quest/m1/ietf-fill-timeout.md
#	quest/m1/ietf-js-join.md
#	quest/m1/interop-coverage/js-ietf-fetch.md
@kixelated

Copy link
Copy Markdown
Collaborator

Not landed yet. After I queued 5b24448, main moved and conflicted, so I merged it in ec6d84a. That merge hand-resolved quest files only:

  • dropped quest/m1/ietf-fill-timeout.md, which main deleted;
  • kept this PR's deletion of quest/m1/ietf-js-join.md;
  • combined both sides of quest/m1/interop-coverage/js-ietf-fetch.md.

just ci and Check pass on ec6d84a. A hand-resolved merge needs a review of the new head before landing, and none came within an hour. I turned auto-merge off, and the merge is waiting on the maintainer.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator

Landing ec6d84a on the maintainer's decision (2026-10-29): land this head as is, without waiting for a fresh review.

  • Why no new review: the last automated review covered 5b24448 and was clean. The only change since is the main merge ec6d84a, whose hand-resolved conflicts touched only quest files (quest/m1/ietf-fill-timeout.md, quest/m1/ietf-js-join.md, quest/m1/interop-coverage/js-ietf-fetch.md). No code was hand-resolved.
  • Changes since the takeover: the trim to the current-group join with the draft-22 fill (e41e967), then the two lifecycle fixes from review (5b24448).
  • Decisions: the draft-22 join uses Rust's form (Next Object plus a relative(1) fill), as confirmed above.
  • Checks: CI passes on ec6d84a, the PR merges cleanly into main, and quest check passes against origin/main.
  • Follow-up: the claim PR moq-dev/moq#7152 closes once this merges.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 29, 2026
Comment thread js/net/src/ietf/subscriber.ts Resolved
Comment thread js/net/src/ietf/object.ts Resolved
@zshenker

Copy link
Copy Markdown
Contributor

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):

  • The relay's head is contiguous and spells Object ID Delta 1 on every object after the first, so without the !== 1 fix from the review above, every join fell back to the next group ("a gap in the head cannot be stitched"). With it applied locally: 0 stitch failures and 0 headless drops across 8 runs.
  • First frame: draft 22 at 471-506 ms (catalog 193-206 ms), draft 18 at 556-644 ms, against 1.1-2.1 s when joining at the next group. The catalog arrives on join too.
  • One observation: "skipping covered group: N -> N" appears once or twice per run, the same sequence on both sides, so the consumer seems to hold the stitched group twice. Playback is unaffected, since the skip fires only after the first copy is presented, but it may be worth a look.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 29, 2026 08:25
… 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 kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kixelated

Copy link
Copy Markdown
Collaborator

Landing 9c793ba.

  • Since the last landing attempt: fixed both findings from @zshenker's review in 8e183c9. A head's later Object ID Delta now counts from the prior ID, so delta 1 is contiguous and a repeated ID (delta 0) drops the head. A live tail whose first object is End of Group past the head now claims the head and finishes the group. Three regressions cover these cases. The automated review of 8e183c9 found nothing new.
  • Base merge: 9c793ba merges main again. The only conflict was the quest/m1/README.md link list, which the quest merge driver resolved by keeping this PR's removal of ietf-js-join.md. Nothing was resolved by hand.
  • Checks: just ci origin/main passes locally on 9c793ba, and quest check passes against origin/main.
  • Follow-ups: Rust's Fill::end still finishes an unclaimed head on SUBSCRIPTION_ENDED. Separately, zshenker saw "skipping covered group: N -> N" once or twice per run, which suggests the consumer holds the stitched group twice.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 29, 2026
Merged via the queue into moq-dev:main with commit 9de090b Oct 29, 2026
3 checks passed
kixelated added a commit that referenced this pull request Oct 30, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@moq/net: a late draft-18 JS subscriber never receives a catalog written once

3 participants