Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
739082c
quest(m1): claim subscribe-ranges/dynamic
kixelated Oct 19, 2026
40d72f0
feat(moq-net): serve each subscriber its own ranges, with time anchors
kixelated Oct 19, 2026
d68f823
chore(moq-net): box the cursor's range filter, regenerate Dart bindings
kixelated Oct 19, 2026
81f8a87
docs: link the MIGRATE entries to #5929
kixelated Oct 19, 2026
8c8c02f
Merge remote-tracking branch 'origin/main' into merge-5929
kixelated Oct 19, 2026
7bea69b
Merge remote-tracking branch 'origin/main' into pr-5929
kixelated Oct 19, 2026
feb8a72
feat(moq-c): give the time anchor its own timestamp_us field
kixelated Oct 19, 2026
5d89b17
Merge branch 'main' of github.com:moq-dev/moq into pr-5929
kixelated Oct 19, 2026
13a2b33
fix(moq-c): refuse a raw track update at the call
kixelated Oct 19, 2026
9dd0ab4
fix(moq-net): resolve time anchors only from frame-0 starts, refuse d…
kixelated Oct 19, 2026
c015690
docs: name the time anchor in the Swift and Dart aliases and the C++ …
kixelated Oct 19, 2026
01cec52
quest(m1): the time anchor moves to exact timestamps with the bindings
kixelated Oct 19, 2026
b6e48df
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
52d2c4f
fix(moq-net): a lowered floor re-offers the cached groups it newly co…
kixelated Oct 19, 2026
e4f31c2
docs(moq-net): state the real bound on a time end; quest the one-grou…
kixelated Oct 19, 2026
d79f671
style(moq-net): name the passed-floor check in has_unread_group
kixelated Oct 19, 2026
4a46ebe
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
9b680ec
docs(moq-net): quest local frame bounds; fix the stale drift cap comment
kixelated Oct 19, 2026
b2cb366
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
bb03fca
fix(moq-net): a lowered floor re-offers only groups readers can see
kixelated Oct 19, 2026
dee0478
docs(drafts): log the lowered Group Start behavior in the lite-07 cha…
kixelated Oct 19, 2026
cee4bc0
fix(moq-net): a failover clamp never widens a range
kixelated Oct 19, 2026
7697958
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
a24bff1
fix(moq-net): an unresolved start never widens to the failover floor
kixelated Oct 19, 2026
d788a30
style(moq-net): format the pending-copy regression test
kixelated Oct 19, 2026
7060b26
fix(moq-net): a replacement copy never reads below where the reader g…
kixelated Oct 19, 2026
bf1105b
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
a4eea28
fix(moq-mux): read the floor through the optional subscription main i…
kixelated Oct 19, 2026
e212240
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 19, 2026
8036328
fix(moq-net): a cursor holds only its range, and widening re-scans th…
kixelated Oct 20, 2026
5f53bdd
fix(moq-net): a copy updated while pending names the ranges it will r…
kixelated Oct 20, 2026
6c77716
docs(moq-net): a finished track ends a capped subscriber
kixelated Oct 20, 2026
6db1f34
test(moq-net): apply the pending copy's update as the reader does
kixelated Oct 20, 2026
27f7080
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
23c4b3c
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
30cb4f1
fix(moq-net): a time end stays open across a group yet to start
kixelated Oct 20, 2026
03611bc
fix(moq-net): a time end settles only once every sequence below it is…
kixelated Oct 20, 2026
d679eaf
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
330ae11
test(moq-c): set refresh in the time anchor test's track info
kixelated Oct 20, 2026
ac9daca
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
9f0ec25
fix(moq-net): measure drift only within the groups a subscriber's ran…
kixelated Oct 20, 2026
07eb25e
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
472a40f
perf(moq-net): keep a single servable run as cheap as a cap
kixelated Oct 20, 2026
a4c9be0
refactor(moq-net)!: drop time anchors from subscription ranges
kixelated Oct 20, 2026
ae8eff0
fix(moq-mux): walk past gaps between a subscriber's ranges; keep the …
kixelated Oct 20, 2026
557ac89
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
1a3cf34
fix(moq-net): name the next group in range for a resumed reader
kixelated Oct 20, 2026
2756260
fix(moq-net): resolve replaced ranges on any copy; republish on Contr…
kixelated Oct 20, 2026
57116be
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
08ae9f7
style(moq-mux): format the merged hole walk
kixelated Oct 20, 2026
84d2256
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
3f8bb34
fix(moq-net): retake a pending drift snapshot; publish window and cap…
kixelated Oct 20, 2026
16ccdd7
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
ab1bce6
fix(moq-net): start a cursor's published window empty
kixelated Oct 20, 2026
b84e94b
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
f49ba80
Merge branch 'main' of github.com:moq-dev/moq into iterate-5929
kixelated Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cpp/moq/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A subscription now reads only the groups its own `ranges` cover.

### Track demand reports the wanted group (#5930)

`TrackDemand::is_used`, `used`, and `unused` are gone.
Expand Down
4 changes: 4 additions & 0 deletions dart/moq/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A subscription now reads only the groups its own `ranges` cover.

### Track demand reports the wanted group (#5930)

`TrackDemand.isUsed()`, `used()`, and `unused()` are gone.
Expand Down
10 changes: 6 additions & 4 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,12 @@ On moq-lite 05 and later, a publisher explicitly reports groups that will not ar
This lets a finished subscription settle without waiting for missing groups.
Datagram delivery stays best effort.

Ranges are a request, not a filter. A subscriber reads from where its lowest
range starts, and may also get groups another subscriber of the same track
asked for. A relay merges its subscribers' ranges into one upstream request,
which today's wire versions carry as the single span covering them.
Ranges are both a request and a filter. A subscriber reads only the groups its
own ranges cover, resolved again whenever it replaces them, even when another
subscriber of the same track asked for more. Widening them, including lowering
the start, also serves the cached groups they newly cover, never one already
served. A relay merges its subscribers' ranges into one upstream request, which
today's wire versions carry as the single span covering them.

Order only decides what goes first, never what is skipped, so catching up oldest
first needs a max delay that covers the backlog. When a relay serves subscribers of
Expand Down
2 changes: 2 additions & 0 deletions drafts/draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,7 @@ MUST be 0 when `Group End` is 0, since an unbounded subscription has no end grou
A subscriber can modify a subscription with a SUBSCRIBE_UPDATE message.
A subscriber MAY send multiple SUBSCRIBE_UPDATE messages to update the subscription.
The start and end group can be changed in either direction (growing or shrinking).
Lowering `Group Start` covers the groups below the old start that the publisher still holds, not only those that arrive later; a group already sent is not sent again.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

~~~
SUBSCRIBE_UPDATE Message {
Expand Down Expand Up @@ -1618,6 +1619,7 @@ The `Message Length` describes the payload size on the wire.
- SUBSCRIBE_OK `Group` names where delivery starts when the subscription resolves, which need not be the first group sent. It is not a new floor. A subscriber does not wait for a group between the requested floor and it, but a publisher still delivers one that arrives later within Subscriber Max Age.
- Corrected the SUBSCRIBE note that offset `Group Start` by 1. Only `Group End` and `Frame End` are offset so 0 can mean absent. `Group Start` is an absolute sequence, and 0 is group 0.
- A subscription's range bounds datagrams like groups, and FETCH never returns a datagram.
- A SUBSCRIBE_UPDATE that lowers `Group Start` covers the groups below the old start that the publisher still holds, not only those that arrive later. A group already sent is not sent again.
- A publisher SHOULD order a datagram among the subscription's streams as if it were its Group's stream, ahead of that stream when both exist. It SHOULD drop a datagram rather than block when the transport has no room for it, where it previously sent one only if the congestion controller could transmit it immediately.
- Assigned 0x3A NOT_FETCHABLE in the stream error table: a FETCH for a group delivered only as a datagram.
- The subscriber FINs its Subscribe Stream after settling its tail; graceful session close waits for that FIN or reset.
Expand Down
4 changes: 4 additions & 0 deletions go/wrapper/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A subscription now reads only the groups its own `Ranges` cover.

### Track demand reports the wanted group (#5930)

`TrackDemand.IsUsed`, `Used`, and `Unused` are gone.
Expand Down
4 changes: 4 additions & 0 deletions kt/moq/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A subscription now reads only the groups its own `ranges` cover.

### Track demand reports the wanted group (#5930)

`TrackDemand.isUsed()`, `used()`, and `unused()` are gone.
Expand Down
4 changes: 4 additions & 0 deletions py/moq-rs/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A subscription now reads only the groups its own `ranges` cover.

### Track demand reports the wanted group (#5930)

`TrackDemand.is_used()`, `used()`, and `unused()` are gone.
Expand Down
8 changes: 6 additions & 2 deletions quest/m1/subscribe-ranges/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ relay bounds a sparse FETCH only if the model can request ranges:
has no time filter. It is part of lite-07 and must land before
[lite-07 finalizes](/quest/m1/lite07-finalize.md), which this line gates. This amends the 09-29
premise that ranges are group-only because a group's timestamp isn't always
known: timed tracks know it, and untimed ones refuse.
known: timed tracks know it, and untimed ones refuse. Split out of #5929
(2026-10-19) into [time anchors](/quest/m1/subscribe-ranges/time-anchors.md),
which lands in its own PR.
- Ranges are frame-precise (`Position`), not whole groups. The IETF joining
FETCH for a mid-group SUBSCRIBE's uncached prefix stays, because today's
bridge relies on it (maintainer, 09-29).
Expand All @@ -85,10 +87,12 @@ every sequence delivered or dropped.

## Required

- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - `Dynamic` fills misses by range, a subscriber reads only its own ranges, and a range end may be a time
- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - `Dynamic` fills misses by range, and a subscriber reads only its own ranges
- [Lite-07 ranges](/quest/m1/subscribe-ranges/lite.md) - the lite-07 wire, Rust publisher and subscriber, and the draft
- [moq-transport ranges](/quest/m1/subscribe-ranges/ietf.md) - range FETCH upstream per missing run, and non-blocking FETCH served downstream
- [JS ranges](/quest/m1/subscribe-ranges/js.md) - `@moq/net` model and lite-07 parity
- [Frame bounds](/quest/m1/subscribe-ranges/frame-bounds.md) - a local subscriber reads only the frames its ranges cover
- [Time anchors](/quest/m1/subscribe-ranges/time-anchors.md) - a range start or end may name a presentation time

## Related

Expand Down
36 changes: 10 additions & 26 deletions quest/m1/subscribe-ranges/dynamic.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,16 @@
A publisher's `track::Dynamic` receives range requests for misses instead of
one request per group, and a track serves a subscription's ranges from one
cursor, in its order, without duplicating a group, naming every sequence in
range it won't deliver. A range end may be a `Time(pts)`, resolved by
whoever holds the track. `requested_group` and `fetch_group` are replaced, not
range it won't deliver. `requested_group` and `fetch_group` are replaced, not
kept beside it.

## Plan

`track::Subscription` already carries aggregated `ranges`, but only as a wire
preference: locally a subscriber's ranges set where its reads start, and it
still reads every later group the aggregate brings in. Serving a range locally
means filtering a subscriber's reads to its own ranges, and a miss below the
cache becomes a range request on `Dynamic` rather than a group request.
A subscriber already reads only its own ranges, re-resolved when they are
replaced. What remains is the miss: one below the cache becomes a range request
on `Dynamic` rather than a group request. Time anchors land separately; see
[time anchors](/quest/m1/subscribe-ranges/time-anchors.md).

- Add a `Time(pts)` anchor beside `Absolute` and `Relative` (see the
[line](/quest/m1/subscribe-ranges/README.md)), carried through moq-ffi and
every binding like the other anchors. A track resolves it from its cached
groups' timestamps and passes an unresolved one to its `track::Dynamic`
handler; an untimed track refuses it. Test a time start and end resolved
against the cache, an unresolved one passed through to a handler, and an
untimed track refusing one.
- Replace `requested_group` and `fetch_group` rather than keep both. Update
every caller (relay, moq-archive, hls, the ladder) in the same PR,
including the archive consumer's time resolution from its key listing if it
Expand All @@ -34,7 +25,9 @@ cache becomes a range request on `Dynamic` rather than a group request.
`moq_relay::fetch_group` (added in the 2026-10-05 audit).
- Every sequence in range either arrives or is named as dropped, so a reader
never waits on a miss. Share the accounting with
SUBSCRIBE_DROP rather than adding a second one.
SUBSCRIBE_DROP rather than adding a second one:
the cursor's drop scan already names only sequences inside the reader's own
ranges.
- Decided in the 2026-10-10 audit: `max_delay` caps every range, including
historical FETCH replacements, and publisher `max_age` always clamps it.
A FETCH with no reader delay uses publisher max-age alone, not the live
Expand All @@ -43,14 +36,5 @@ cache becomes a range request on `Dynamic` rather than a group request.
budget or lets one expired caller cancel another still-valid reader.
Reuse FETCH's optional per-reader budget and publisher clamp rather than
restoring a historical exemption.
- Move every relative-start resolution into the model, which counts back
from produced groups only. Today two consumers re-resolve relative starts
on their own. moq-c's `apply_raw_subscription` (`rs/moq-c/src/consume.rs`)
counts back from `Ordered::latest()`, the shared group and datagram
sequence, so a datagram past the newest group skews the start. It also
re-resolves on every update, because the model doesn't. moq-mux's
`container::Consumer::new` treats a relative start as an unfloored join
and adopts the first arrival, which under newest-first delivery can skip
the older groups the range asked for.
- Benchmark serving and filling over range count and span as separate axes,
beside `track_subscription_ranges`, which covers aggregation.
- Benchmark filling over range count and span as separate axes, beside
`track_range_serving`, which covers serving.
37 changes: 37 additions & 0 deletions quest/m1/subscribe-ranges/frame-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# [M] A local subscriber reads only its frame bounds

## Goal

A `track::Subscriber` whose ranges start or end partway through a group reads only the
frames they cover: a start at group 4 frame 5 hands out group 4 from frame 5, and an
end mid-group stops there, whatever another subscriber widened the aggregate to. Today
the model filters whole groups and hands out every cached frame, so local readers
(moq-ffi, moq-mux, in-process apps) see frames they never asked for.

## Plan

Found by Codex on #5929 and reproduced on `main` (824179a3a) as well as on that PR: a
subscription starting at frame 5 of group 0 is handed the group from frame 0. #5929's
`Window` collapses each range to group sequences, but the leak predates it: `main`'s
cursor never positioned a delivered group either. Only the datagram path checks the
start frame (`mid_group` in `Cursor::poll_recv_datagram`).

The wire is not affected: the lite publisher positions each served group per
subscription (`position_group` in `rs/moq-net/src/lite/publisher.rs`, and the same
shape in `js/net/src/lite/publisher.ts`).

- Keep each range's absolute frame bounds in the `Window` beside its group runs.
- Position every group the cursor hands out (`group::Consumer::start_at`/`end_at`), on
the arrival, ordered, and re-offer paths, using the bounds of the range that covers it.
A group whose head is gone and that was asked for from frame 0 stays a miss, as
`position_group` does today.
- Then let the lite publisher rely on the model's positioning instead of its own, if
that is simpler.
- Check `js/net`'s `track` subscriber for the same shape and mirror the fix.

Regression test: a start at frame 5 and an end at frame 3 of a cached group, each read
through `recv_group` and `ordered()`.

## Related

- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - the per-subscriber ranges this extends to frames
56 changes: 56 additions & 0 deletions quest/m1/subscribe-ranges/time-anchors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# [L] Time anchors

## Goal

A range start or end may name a presentation time: `Time(pts)` in the track's
timescale. A start resolves to the group containing pts, an end to just after that
group, and an untimed track refuses either. It lands in its own PR, before
[lite-07 finalizes](/quest/m1/lite07-finalize.md).

## Plan

Split out of #5929 (maintainer, 2026-10-19), which ships absolute and relative
anchors only. Start from the `quest/m1/subscribe-ranges/time-end` branch, which
holds that PR's time anchors through every binding: `Anchor::Time`, the
`Holdings` resolution in `Window`, the end settlement (`Holdings::after`,
`TrackState::accounted`), and the failover handling. Merge main into it and drop
what #5929 changed since.

Decide first whether time starts ship alone. A start resolves once from the
cache, but keeping it without time ends needs a start-only anchor type in every
binding, which is why #5929 dropped both.

Open findings from #5929's review, each needing a regression test:

- **Per-read rescan behind a peer-controlled gap** (Codex 4320985614). Settling an
unresolved end walks back from the live edge on every unsettled read. A peer that
leaves an early sequence missing and keeps sending later groups makes each read
revisit a growing prefix until the gap's grace runs out. Keep an incremental
settle boundary, so each read scans only what arrived since.
- **Failover replacing an unsettled end** (Codex 4320985627). A copy capped at a
route switch reports an unsettled time end as unresolved, so the cap replaces
the `Time` anchor for good, and a group starting before the time that arrives
later on the old copy is delivered. Keep the time filter on the reader, separate
from the copy cap.
- **One speculative group past an end.** An unresolved end covers every sequence
through `u64::MAX`, so each group handed out before its first frame can leak past
it at the live edge. Cap it at the newest held group known to start at or before
pts, plus the first unstarted group above it, and recompute as groups start.
Raising the cap must wake readers: register on the speculative group's start
(`group::Producer::poll_started`) from both `Cursor::poll_recv_group` and
`poll_next_group`. A held group that never shows a start (a copy joined
mid-group) must not pin the cap. Datagrams are not in `lookup`, so on a
datagram-only timed track an end never resolves: resolve datagram starts too,
or leave datagram reads uncapped. Reproduced on #5929: range
`time(3000)..time(3500)` with three groups opened before any frame delivers
all three.

Also pass a time anchor the cache can't resolve to the `track::Dynamic` handler
in its range request, once [dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md)
land, and update `quest/m1/binding-timestamps.md` for the anchor's
`timestamp_us` fields.

## Related

- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - range requests a `Dynamic` handler resolves
- [Archive line](/quest/m1/archive/README.md) - DVR seek, which needs time anchors
4 changes: 4 additions & 0 deletions rs/moq-c/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Ranges filter reads (#5929)

A raw track consumer now reads only the groups its `ranges` cover, rather than every group from the lowest start to the last absolute end.

### Demand reports the wanted group (#5930)

`moq_track_info` gains `refresh` and `refresh_present`, which changes its ABI; a zeroed field leaves refresh off.
Expand Down
Loading