Repository navigation
feat(net)!: support JavaScript subscription ranges - #6949
Conversation
Quest-Claim: f7f1e9f622015209
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>
# 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>
|
No actionable comments were generated in the recent review. WalkthroughThe 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:
|
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: 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.
There was a problem hiding this comment.
💡 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".
…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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
…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>
|
codex / Codex land fixer: (written by codex / Codex) |
…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>
|
Design change: re-ported onto today's 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 The (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
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.
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>
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
Track.AnchorandTrack.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.Window::startsdoes (moq-dev/moq#6951).Cursor::is_exhausted.Reader::exhausted.withRangecap holding a group back also keeps the read open, since the cap may rise.hooks.parked, the Rusttrack::Subscriber::parked.delay: { min, max }(moq-dev/moq#7597) replaces this PR'smaxDelay.pipelinedoes.Decisions
Choices made while re-porting, for review:
CACHE_EXPIRY, which an earlier round of this PR added. Main bounds it by the publisher'smaxAge, and an unlimited ordered reader waits for its hole (moq-dev/moq#7609, moq-dev/moq#7169), as Rust'sgracedoes.waitsForSubscriberFinnow covers lite-05+ (main's moq-dev/moq#7296). The covered FIN without SUBSCRIBE_END is gated onhasRanges, so it stays lite-07 only.js/netasks 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.a_covered_bounded_subscription_ends_on_lite07does. An ordered read would wait out the missing group 3 under main's owed-hole rule.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
@moq/net):Track.Subscription.groupsis replaced byranges. For example,groups: { start: { included: a }, end: { excluded: b } }becomesranges: [{ start: { absolute: a }, end: { absolute: b } }]. A leftovergroupskey throws aTypeError.@moq/net): arecvGroupornextGroupwhose ranges all end now returnsundefinedonce nothing in them is left, while the track goes on.update({ ranges })reads on from the same subscriber. Both changes are documented injs/net/MIGRATE.md.Track.AnchorandTrack.Rangeare exported.withRangeonly narrows the subscription's ranges.Validation
On 242c940 (the pushed head):
quest gate just ci origin/main: every step passes excepttest rs, which stopped at its first failure. That failure wasmoq-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.sh/rs/nextest.sh cargo nextest run --locked --tests --workspace --no-fail-fast): 11,078 pass, 95 skipped, including the io_uring test.bun testinjs/net: 2,740 pass.bun run checkpasses, and the API report is regenerated.quest check --base origin/main --branch quest/m1/subscribe-ranges/js: passes.just test interop --all.Review
Codex
gpt-6.1-solat medium effort (quest review --base origin/main), in a new session after the design change.js/netnorrs/moq-net): no findings.Online review findings already posted were addressed before this round. CodeRabbit's
goaway.tsgroupskey now usesranges, and its span-order finding is kept by the port (a_resolved_waiting_start_judges_datagrams_by_its_own_end).Follow-ups
fetchGroup, fills and head fills stay inquest/m1/subscribe-ranges/js.md.moq-relayio_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)