Skip to content

feat(net)!: support JavaScript subscription ranges - #6949

Merged
kixelated merged 34 commits into
mainfrom
quest/m1/subscribe-ranges/js
Nov 3, 2026
Merged

kixelated merged 34 commits into
mainfrom
quest/m1/subscribe-ranges/js

Conversation

@kixelated

@kixelated kixelated commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

JavaScript subscriptions used one covering group span instead of Rust's per-reader ranges. Disjoint, relative and time-bounded requests each need their own delivery window, while publishers see the union of all demand.

Approach

  • Add Track.Anchor and Track.Range, a per-reader window (js/net/src/window.ts), aggregate range unions, and lite-07 range encoding and serving. Older wires keep the covering span, and anchors they cannot carry are refused.
  • A lite-07 publisher FINs once its bounded demand is covered. The subscriber keeps that copy and opens a new subscription when an update leaves demand uncovered. That reopen is bounded by the request deadline and given up once nobody wants the track.
  • The JS publisher answers SUBSCRIBE_START with each range's resolved start, as Rust's Window::starts does (moq-dev/moq#6951).
  • A time start waits for real group starts before resolving, in Rust and JS. A relative end stays fixed while that start waits.
  • Bounded reads end, as in Rust (decision below).
    • A plain reader ends once every group in its ranges is handed out, dropped, skipped as stale, or no longer owed by its feed. This mirrors Rust's Cursor::is_exhausted.
    • A reader following route copies is judged on the serving copy, against what it and the copy handed out. A group under the copy's resume point that the reader's local floor or cap keeps out keeps the read open. This mirrors Rust's Reader::exhausted.
    • A withRange cap holding a group back also keeps the read open, since the cap may rise.
    • Wire publishers and route copies park through the package-internal hooks.parked, the Rust track::Subscriber::parked.
  • Re-ported onto main (2026-11-03).
    • Main's delay: { min, max } (moq-dev/moq#7597) replaces this PR's maxDelay.
    • Main's owed-hole waits (moq-dev/moq#7609) now count only holes inside the reader's ranges.
    • Main's clean-close completion (moq-dev/moq#7486) is kept.
    • A lite-07 SUBSCRIBE goes out alongside its TRACK, as on main, unless a time anchor needs the track's timescale. That case waits for TRACK_INFO, as Rust's pipeline does.
    • The Rust waiting time start is ported onto main's window walks (moq-dev/moq#7542) as a placeholder span, the way a lookup span works.
  • Review fixes on the new head:
    • A lite-07 window whose bounded ranges all resolve empty is now answered at once and ends as covered. Before, it never ended.
    • A time anchor's timescale lookup now holds the subscription's own on-demand track request, instead of releasing it before subscribing.

Decisions

  • When does a range-bounded reader end? Decided 2026-11-02 by the maintainer, on moq-dev/moq#7545 and moq-dev/moq#7625. This replaces this PR's 2026-10-31 choice of option 1:
    1. Park in both.
    2. ✅ Complete in both: a bounded read ends once every group in its ranges is accounted for; only wire publishers park.
    3. Leave it: JS copies keep releasing on a covered FIN.
  • How should this PR land after main moved under it? Decided 2026-11-03 by the maintainer:
    1. ✅ Re-port in place.
    2. A fresh branch, closing and deleting this one.
    3. Park it.

Choices made while re-porting, for review:

  • The arrival grace is no longer capped at CACHE_EXPIRY, which an earlier round of this PR added. Main bounds it by the publisher's maxAge, and an unlimited ordered reader waits for its hole (moq-dev/moq#7609, moq-dev/moq#7169), as Rust's grace does.
  • waitsForSubscriberFin now covers lite-05+ (main's moq-dev/moq#7296). The covered FIN without SUBSCRIBE_END is gated on hasRanges, so it stays lite-07 only.
  • Rust's head fill (moq-dev/moq#7625) is not ported, because js/net asks for no fills yet. It is recorded with the quest's remaining Fills work. Until fills exist, a JS bounded read treats a hole its feed no longer owes as given up.
  • The ported covered-copy integration test reads in arrival order, as its Rust twin a_covered_bounded_subscription_ends_on_lite07 does. An ordered read would wait out the missing group 3 under main's owed-hole rule.
  • The model test (resume.model.test.ts) takes Rust's feat(net): bounded reads end through routes, fill missing heads, and learn the moq-transport edge first #7625 changes to the matching Rust model, and the regression case where the local floor holds a read open.

Impact

  • Breaking (@moq/net): Track.Subscription.groups is replaced by ranges. For example, groups: { start: { included: a }, end: { excluded: b } } becomes ranges: [{ start: { absolute: a }, end: { absolute: b } }]. A leftover groups key throws a TypeError.
  • Behavior (@moq/net): a recvGroup or nextGroup whose ranges all end now returns undefined once nothing in them is left, while the track goes on. update({ ranges }) reads on from the same subscriber. Both changes are documented in js/net/MIGRATE.md.
  • Track.Anchor and Track.Range are exported. withRange only narrows the subscription's ranges.
  • Rust: a time start waits for group timestamps instead of guessing the latest group. No Rust public API change.
  • Wire: no format change.

Validation

On 242c940 (the pushed head):

  • quest gate just ci origin/main: every step passes except test rs, which stopped at its first failure. That failure was moq-relay uring::tests::a_group_stopped_before_serving_joins ("io_uring worker 0 failed to start"), a host resource issue in a crate this PR does not touch.
  • Rerunning all Rust tests without fail-fast (sh/rs/nextest.sh cargo nextest run --locked --tests --workspace --no-fail-fast): 11,078 pass, 95 skipped, including the io_uring test.
  • bun test in js/net: 2,740 pass. bun run check passes, and the API report is regenerated.
  • New tests: a bounded read ends past dropped groups, unless a cap or a wire publisher holds it; a subscribed copy's bounded read ends once its feed stops owing a hole; a bounded read through routes ends across a route change, and the local floor holds it open; a lite-07 window that resolves empty is answered and ended; a time range is served from the on-demand track its lookup resolved. The last two fail without their fixes.
  • quest check --base origin/main --branch quest/m1/subscribe-ranges/js: passes.
  • Not rerun after the re-port: just test interop --all.

Review

Codex gpt-6.1-sol at medium effort (quest review --base origin/main), in a new session after the design change.

  • Round 1 on 6d8bd59 (the re-port merge): two findings, both fixed in 4d738d0.
    • Fixed: a lite-07 bounded window that resolves empty never ended.
    • Fixed: a time anchor's timescale lookup released an on-demand track request before subscribing.
  • Round 2 on 242c940 (the fixes plus a conflict-free main merge that touches neither js/net nor rs/moq-net): no findings.

Online review findings already posted were addressed before this round. CodeRabbit's goaway.ts groups key now uses ranges, and its span-order finding is kept by the port (a_resolved_waiting_start_judges_datagrams_by_its_own_end).

Follow-ups

  • The remaining on-demand request surface, fetchGroup, fills and head fills stay in quest/m1/subscribe-ranges/js.md.
  • moq-relay io_uring tests can fail on a loaded host when a worker cannot start. They may be worth skipping or retrying when io_uring setup fails.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 5 commits October 27, 2026 16:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the single Subscription.groups span with ranges of absolute and
relative anchors. Each reader reads only its own ranges; the aggregate is
their union, which lite-07 carries as a list and older wires as the span
covering it. A lite-07 publisher FINs a bounded subscription once it covers
its ranges.

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 and others added 3 commits October 27, 2026 17:43
# Conflicts:
#	js/net/api/index.api.md
#	js/net/api/zod.api.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uest remainder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 28, 2026 01:22
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The JavaScript track API replaces group-bound subscriptions with ranges anchored by absolute groups, relative positions, or timestamps. Track readers resolve ranges into windows that filter groups and datagrams. Lite-07 messages encode range lists, and subscribers can reopen a request when a stream ends before current demand is covered. The Rust window logic also changes how unresolved time starts wait for groups. Tests, interop examples, and documentation are updated.

Merge Risk: Minimal · up to 9fbcb

Range-bounded freshness behaves as intended; no identified issue blocks merging after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: adding JavaScript subscription ranges.
Docstring Coverage Passed Docstring coverage is 90.38% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains the subscription range API changes, per-reader windows, lite-07 behavior, time anchors, bounded reads, compatibility, testing, and deferred work covered …
Full details: Docstring Coverage

Explanation

Docstring coverage is 90.38% which is sufficient. 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 52 functions across 28 files.

Full details: Description check

Explanation

The pull request description directly explains the subscription range API changes, per-reader windows, lite-07 behavior, time anchors, bounded reads, compatibility, testing, and deferred work covered by the changeset.

Finishing Touches
Simplify code
  • Commit to this branch
  • 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

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
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: a82e6aa9-e0ed-4c7e-9c39-05dee49fe98d
Commits

Reviewing files that changed from the base of the PR and between 7c10f33 and 9fbcbde.

Files selected for processing: 5
  • js/net/src/lite/publisher.ts
  • js/net/src/track.test.ts
  • js/net/src/track.ts
  • rs/moq-net/src/model/subscription/window.rs
  • test/interop/clients/js-native/goaway.ts

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

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 ✅ Completed 2026-10-29T19:25:53.437524Z 1b3b7d8 New commits
ℹ️ 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 Author

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: 43b53a4

No additional actionable bugs found in the reviewed range/window, budget, wire adaptation, and bounded-FIN reopening paths. Separating reader-local windows from aggregate demand is the right direction; centralizing normalization and time-end settling avoids duplicating that policy across transports.

The PR description's known limitation remains: readers bounded only by relative/time ends do not independently complete while the track stays live (track.ts #complete still depends on the track end). That follow-up and the stated Rust/JS time-start differences mean this is not yet complete behavioral parity; I am not presenting those already-documented limitations as new findings.

Verification: static inspection of changed code, relevant surrounding paths, and regression tests only. I did not run builds, tests, benchmarks, or interop, and did not independently verify the checks reported in the description.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43b53a4153

ℹ️ 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".

Comment thread js/net/src/track.ts Resolved
Comment thread js/net/src/track.ts Resolved
…iew fixes

A Rust time start resolved at the latest group while no group had started,
so a copy that showed the route's answer (an empty edge group) before the
groups its publisher resolved the time to, or a producer that opened groups
before their first frames, skipped them. It now waits, reading nothing of its
range, as JS does, and reaches back to the oldest group once nothing more can
start.

JS holdings now count as complete only once the producer is gone, as Rust's
sealed or aborted state does, since a group below a reached end may still
start. A window that settles in place bumps a revision so the drop scan looks
again at drops it skipped past the old cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

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: 53cc207

Reviewed the substantive delta from 43b53a4 (base unchanged).

P2 — Preserve the relative end while a time start waits (rs/moq-net/src/model/subscription/window.rs:575–585). Start::Waiting returns before the end is resolved. A later settle() retries the whole span with the new latest group (track.rs:6083), so a relative end drifts while waiting for timestamps. For example, create a time-start/Relative(0)-end window with latest group 9 and no started groups; then advance latest to 10 and start group 2 at the requested time. The end becomes exclusive 11, admitting group 10, instead of the original exclusive 10. This changes the requested bounded snapshot and can delay completion. Capture the relative end at initial resolution (or first live group when initially pending), retain it while only the time start waits, and add a regression where latest advances before the first timestamp.

The direction is sound: waiting for actual starts fixes premature Rust seeking, and revision-based JS drop invalidation is simpler than comparing window runs. The independent pending-start and drop-rescan findings appear addressed, with focused tests. The previously documented JS relative/time-end completion limitation remains; it is not a new finding.

Verification: static inspection of the delta, surrounding resolution/read paths, and tests only; no builds, tests, or interop were run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53cc20773c

ℹ️ 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".

Comment thread js/net/src/track.ts Resolved
kixelated and others added 2 commits October 29, 2026 09:04
Resolves conflicts with #6969 (legacy FIN finishes the track, kept alongside
reopening uncovered demand), #7168 (Window::named also waits on a time start;
its JS fills rule joins the quest's remainder), and #7015's late tail mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g time start keeps its relative end

Codex: an ordered reader closed a group between its ranges once it passed
the one before, so widening the ranges never delivered it. Keep it until
the cursor passes it, as an unsettled window already did.

Review: a range whose time start waited re-resolved its relative end against
whatever group arrived meanwhile. It now counts back from the latest group
it was asked against, in Rust and JS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moq-bot moq-bot Bot removed the quest:merge label Nov 1, 2026
@moq-bot

moq-bot Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

quest land removed this pull request from the queue: its head moved from 7c10f33 to 9fbcbde after it was queued.

@moq-bot

moq-bot Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer: This maintainer pick already used its mechanical fi·x attempt. Run /quest-iterate to diagnose the repeated failure, then pick it again.

(written by codex / Codex)

kixelated and others added 3 commits November 2, 2026 21:51
…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>
A lite-07 bounded window that resolves empty has no group to answer it, so
the publisher sent nothing and the subscriber waited for good. Answer it
with SUBSCRIBE_START at once, and end it as covered.

Waiting on `consumer.info()` for a time anchor's timescale opened an
on-demand track's request that was released before the subscription took
it, which then served a second producer. Subscribe first, asking for
nothing, and set the ranges once its info lands.

Also formats the window walk the merge added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Design change: re-ported onto today's main, and bounded reads now end.

On 2026-11-03 the maintainer chose to re-port this PR in place. Main had rewritten the cursor paths it touched: moq-dev/moq#7486, moq-dev/moq#7542, moq-dev/moq#7597 and moq-dev/moq#7609. The merge (6d8bd59) keeps main's versions and puts the ranges and window back on top.

Bounded reads end instead of parking. This replaces the 2026-10-31 decision to park, which followed Rust at the time. Rust now ends bounded reads (moq-dev/moq#7545, moq-dev/moq#7625), so JS does the same. Each reader checks its own window, as Rust's Cursor::is_exhausted does. A reader following route copies checks its window on the serving copy, as Reader::exhausted does. Wire publishers and route copies keep parking through hooks.parked.

The Delay, arrival grace and owed-hole behaviour now follow main. The local review starts a new session.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Nov 3, 2026

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

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: 242c940

Correct the bounded-reader completion contract (doc/concept/moq-lite.md:301–304). This paragraph says a reader waits after its ranges are covered, but the re-port makes bounded reads end once their demand is accounted for, as the migration guide states. A caller following this guidance can wait indefinitely rather than recognize completion. State that bounded reads end and that updating ranges resumes the subscriber; keep the distinct covered-wire-copy behavior separate.

The explicit completion model and ability to update demand provide a coherent direction. Static GitHub review only; I did not run tests.

Merged via the queue into main with commit 1777a2d Nov 3, 2026
8 of 9 checks passed
@kixelated
kixelated deleted the quest/m1/subscribe-ranges/js branch November 3, 2026 07:09
kixelated added a commit to moq-dev/quest that referenced this pull request Nov 4, 2026
The fixer read "build and test the packages the PR and your fix touch" as
build and test only, and requeued moq-dev/moq#6949 without running moq's
JavaScript type check, which the queue's next build failed on. It now runs
the checks the base names (root instructions, CONTRIBUTING.md, task runner,
or CI workflows), or each touched language's type check and linter when the
base names none, on the head merged with base, including for an unchanged
requeue, the base alone, and a flaky disable branch.

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.

1 participant