Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 23 additions & 2 deletions quest/m0/broadcast-epoch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,13 @@ Decided:
lite-07 claim's answer carries the served broadcast's own epoch, no longer
gates this line (decided 2026-10-08): it is a lite-07 opt-in, so it moved
to m1.
- A catalog `broadcast` reference by name follows the newest epoch, since a
path cannot name one.
- Decided in the 2026-10-10 resume audit: a catalog reference pins its first
resolved sibling for the catalog instance's lifetime in players and
exporters. Optional reference epochs constrain the first resolution on
epoch-aware transports; on epochless transports they remain best effort.
This supersedes following the newest sibling by name. The
[catalog references quest](/quest/m0/broadcast-epoch/catalog-references.md)
owns the schemas, readers, and compatibility documentation.
- Every first-party publisher that can restart mints its own: the apps,
moq-boy, the ingest gateways, moqsink, and the bindings below. Players
(`moq play`, `@moq/watch`, demo/web) follow the announce `Restart`. moq-stats mints one per
Expand Down Expand Up @@ -103,11 +108,27 @@ its `doc/concept/stats.md` sentence from `release`; until then, the
maintainer's pre-release merge of `release` into `main` keeps `main`'s
`rs/moq-stats` and `doc/concept/stats.md`. MoQ Pro's VOD `storage.json` moves from the same seed to its own epoch.

The 2026-10-10 resume audit adds release gates for newly issued requests
through retired handles and for late duplicate delivery after same-epoch
handover. Keep existing reads sticky; stopping new unpinned wire work is a
separate boundary. The catalog-reference work applies the same pinning
policy to players and exporters. Wall-clock expiry, live DROP dispositions,
FETCH budgets, seamless JS handover, and the IETF extension remain m1.

Extend the line's relay/media tests to hold catalog and media from A while B
replaces it, exercise later metadata/subscription/FETCH operations, and prove
that following the replacement starts a fresh catalog with fresh pins. Check
exact and covering routes, matching/different/absent epochs, and legacy
End+Start. The child quests own their focused regressions.

## Required

- [Retired requests](/quest/m0/broadcast-epoch/retired-requests.md) - a held broadcast or track cannot open unpinned wire work against its replacement
- [Resume duplicates](/quest/m0/broadcast-epoch/resume-duplicates.md) - a delayed group is delivered once even beyond the old 1024-entry history
- [JS consume identity](/quest/m0/broadcast-epoch/js-consume-identity.md) - requests carry the serving prefix's epoch and new winners cannot reuse an old cached broadcast
- [JS restart keeps the request](/quest/m0/broadcast-epoch/js-restart-keeps-request.md) - a resolved request remains on its old instance, matching Rust's sticky subscriptions
- [Source pin](/quest/m0/broadcast-epoch/source-pin.md) - `Source` is built from the resolved catalog broadcast and every later request stays on it, so an epochless replacement never splices into the old program
- [Catalog references](/quest/m0/broadcast-epoch/catalog-references.md) - players and exporters pin siblings consistently, with optional reference epochs and documented legacy best effort
- [Publish catalog restart](/quest/m0/broadcast-epoch/publish-catalog-restart.md) - `@moq/publish` never reuses catalog group numbers under one name and epoch after a re-announce
- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and let a publisher announce one
- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the same release retires the per-path stats maps for totals and on-demand prefix tracks (decided 2026-10-05)
74 changes: 74 additions & 0 deletions quest/m0/broadcast-epoch/catalog-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# [L] Catalog references pin their broadcast instances

## Goal

Catalog, media, and data requests use the same pinned broadcast instance
in players and exporters. Each sibling stays on its first resolved
instance for the catalog instance's lifetime. Optional catalog epochs let
a reference name its intended sibling before that first resolution.

## Plan

Decided in the 2026-10-10 resume audit:

- Apply the same pinning policy to players and exporters. This replaces
the earlier rule that a named sibling follows the newest epoch, including
`@moq/watch`'s test that follows a republished rendition independently of
its catalog. A sibling replacement does not silently retarget the old
catalog's reference. A new catalog instance or explicit export stitch
starts fresh pins; same-epoch recovery preserves identity.
- Add optional `epoch` beside every catalog broadcast reference: video,
audio, text, JSON, and binary configs, plus the archive replay reference.
Reuse the canonical UUIDv7 representation and validation from MoQ epochs.
There is no new top-level catalog epoch. Self-references use the already
pinned catalog broadcast; an explicit value must not contradict its
known identity.
- An explicit reference epoch constrains the initial resolution and later
requests wherever the transport can express and verify it. A known
mismatch fails instead of falling back to the current winner. Conflicting
identities for an already-pinned path cannot replace that pin through a
catalog update.
- Omission remains compatible with existing catalogs: pin the first
resolved instance, with no promise about an unresolved sibling's past.
- On epochless transports, explicit catalog epochs are best effort
(maintainer choice). Keep the requested identity in the reference, use
whatever identity the route can verify, and pin the resolved handle.
Unknown identity alone is not a refusal. Document that this cannot prove
the requested epoch, and never promote an unverified catalog value into
transport evidence permitting seamless resume. Older readers can ignore
the additive field and do not gain its guarantee.

Use the shared reference resolution paths rather than adding one rule per
media type. Mirror schemas and behavior in `rs/hang`, `js/hang`, native
readers/exporters and `@moq/watch`; carry the field through catalog producers,
conversions and bindings that expose these configs. A conversion that
cannot preserve the requested identity must report that limitation rather
than silently discard it. Coordinate with catalog rendition IDs, which
changes the neighboring track-name field but is not a prerequisite.

Test all reference-bearing sections in both languages: omitted/matching/
mismatched/malformed epochs, same and sibling paths, a replacement before
first resolution and after pinning, late rendition additions, updates that
conflict with a pin, new catalog instances, and an epochless transport with
an explicit reference. Catalog and media must never independently resolve
the same path across a replacement. Include archive replay references and
concurrent old/new consumers. Use existing unit, media and interop CI lanes;
run `just check`, `just test interop --all`, and `just drafts check`.

Public API: additive optional catalog fields and changed player sibling
following behavior. Wire: additive Hang catalog fields, no transport
framing change. Update the Hang draft, existing catalog/identity docs, and
upgrade notes inline; no separate guide. Explain both the strong epoch-aware
guarantee and the selected legacy best-effort behavior.

## Required

- [Source pin](/quest/m0/broadcast-epoch/source-pin.md) - exporters share their resolved catalog handle
- [JS consume identity](/quest/m0/broadcast-epoch/js-consume-identity.md) - JS resolves the actual serving identity

## Related

- [Retired requests](/quest/m0/broadcast-epoch/retired-requests.md) - held handles cannot emit unpinned requests after retirement
- [Catalog rendition IDs](/quest/m1/catalog-track-id.md) - the adjacent optional wire-track field
- [Catalog track identity](/quest/m1/catalog-tracks.md) - catalog updates cannot redefine a track's content
- [IETF epochs](/quest/m1/ietf-epochs.md) - verified catalog identity over negotiated IETF sessions
1 change: 1 addition & 0 deletions quest/m0/broadcast-epoch/js-consume-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ no framing change, and older versions stay epochless.
## Related

- [JS track handover](/quest/m1/js-group-handover.md) - preserves open tracks across routes of the same explicit identity
- [Retired requests](/quest/m0/broadcast-epoch/retired-requests.md) - separately prevents old callable handles from opening wire work against replacements; this quest owns fresh-consumer selection and cache reuse
38 changes: 38 additions & 0 deletions quest/m0/broadcast-epoch/resume-duplicates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# [M] Resume never delivers a late group twice

## Goal

An unordered reader receives each group once across same-epoch route
changes, even when a duplicate arrives very late. A genuinely unseen late
group remains eligible within the reader's bounds and budget.

## Plan

Reproduced in the 2026-10-10 audit: read group 0 from A, switch to B, read
groups 1 through 1024, then deliver B's delayed copy of group 0. The reader
receives group 0 twice. `model/resume.rs` forgets delivered IDs at
`MAX_DELIVERED = 1024` without proving those groups can no longer arrive.

Decided: this data-correctness fix gates m0. Preserve both duplicate
suppression and unordered delivery; advancing a floor past every late
group would hide the duplication by losing valid data. Bound bookkeeping
using the actual reader/copy lifetime, retained ranges, and terminal
dispositions rather than an arbitrary number of recently delivered IDs.
Choose the representation during implementation; do not retain one entry
per delivered group for the lifetime of an endless broadcast.

Land the failing regression, a genuinely unseen late group beyond the old
window, repeated route flaps, and ordered-reader controls. Cover readers
that keep old groups open and change their group bounds, and prove finished
copies release their bookkeeping. Tests use mocked time and existing CI
lanes. Extend the resume benchmark over readers and retained/reordered
groups so memory and delivery costs are measured.

Public API: duplicate suppression is corrected; no signature changes.
Wire: none. Update stale resume comments inline. JS handover must preserve
the corrected contract instead of copying the fixed-size history.

## Related

- [Resume reorder](/quest/m1/resume-reorder.md) - an unseen reordered group is not mistaken for a missing one
- [JS track handover](/quest/m1/js-group-handover.md) - the same delivery contract in JS
65 changes: 65 additions & 0 deletions quest/m0/broadcast-epoch/retired-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# [L] Retired handles never request a replacement's content

## Goal

A Rust or JS broadcast or track handle keeps its resolved instance for
TRACK, SUBSCRIBE, and FETCH. Once that instance retires, a new wire request
cannot resolve the replacement by path. Existing reads and cached content
from the old instance remain usable within their normal lifetime and budgets.

## Plan

Reproduced in the 2026-10-10 resume audit: resolve catalog and media from A,
publish B at the same path, wait for the replacement announcement, then
FETCH a missing group through A's held media track. Rust returns B's bytes
on lite-05/06, epochless lite-07, and IETF drafts 14-22. Explicit-epoch
lite-07 refuses the stale request. JS also accepts a new TRACK_INFO through
a held epochless broadcast after End+Start on lite-06/07.

Decided by the maintainer in that audit:

- This correctness fix gates m0. Supporting epochless peers on updated
relays remains required; neither new paths nor the IETF epoch extension
is a prerequisite.
- Preserve instance validity through every handle and deferred operation,
including a track handed out before retirement, coalesced requests, and
a request whose setup races withdrawal. A fresh request may proceed only
when it still names the held instance. An explicit wire epoch can prove
that; an epochless path after retirement cannot.
- Keep already-open reads sticky and allow old cached bytes. Do not close
every old subscription to prevent new requests. A rejected operation must
settle its waiter and release its demand, without migrating it to B.
- Same explicit epoch permits route replacement and recovery. Absent
epochs never prove continuity. Before an epochless peer observes a
replacement, its wire cannot identify the old instance; do not claim
this fixes arbitrary legacy caches or that unavoidable ambiguity.

Fix validity at the request/source boundary shared by these operations,
rather than patching individual exporters. `Source` handle pinning and the
JS consume cache fix select the right initial handle but do not prevent a
surviving handle from opening new path-based wire work. Coordinate with
those active quests without duplicating their cache-selection changes.
IETF standalone FETCH resolves a path, whereas joining FETCH reads the
saved subscription cache; preserve that distinction and check both.

Add mocked-time regressions with A still alive after B is announced: held
catalog plus held media FETCH, late track lookup, re-subscription, pending
TRACK/FETCH setup, concurrent old/new consumers, exact and covering routes,
and source withdrawal without replacement. Test matching, different, and
absent epochs. Keep the explicit lite-07 epoch control and the failing
lite-05/06, epochless lite-07, and IETF14-22 matrix in CI; cover supported
legacy request forms and refusals too. Run `just check` and
`just test interop --all`. Benchmark any routing or invalidation fanout over
both routes and held consumers.

Public API: corrected request lifetime and refusal behavior; preserve
readable old subscriptions. Wire: none; published framing is unchanged.
Update existing identity and request-lifetime documentation inline.

## Related

- [Source pin](/quest/m0/broadcast-epoch/source-pin.md) - the exporter holds the exact catalog broadcast
- [JS consume identity](/quest/m0/broadcast-epoch/js-consume-identity.md) - fresh consumers select the serving announcement's identity
- [JS restart keeps the request](/quest/m0/broadcast-epoch/js-restart-keeps-request.md) - an existing resolved request stays sticky
- [IETF epochs](/quest/m1/ietf-epochs.md) - explicit wire identity removes the epochless ambiguity
- [Pipelined first FETCH](/quest/m1/pipeline-requests/fetch.md) - staged requests must retain the same instance validity
5 changes: 5 additions & 0 deletions quest/m0/broadcast-epoch/source-pin.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@ of resolving the current route. Document it on `Source`, fix stale
comments inline, and add a line for the `Source::new` break to the
Unreleased section of `doc/setup/upgrade.md`; no new doc page (decided
2026-10-10). Wire: none.

## Related

- [Retired requests](/quest/m0/broadcast-epoch/retired-requests.md) - held track FETCH and JS wire operations also need instance validity after retirement; handle pinning alone does not fix them
- [Catalog references](/quest/m0/broadcast-epoch/catalog-references.md) - players adopt the same sibling policy, and optional catalog epochs constrain initial resolution where supported
2 changes: 2 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ bench all require its crate.
## Required

- [One max_age meaning](/quest/m1/cache-max-age.md) - a superseded group goes stale on wall clock since its successor arrived or on media time, whichever is first, in Rust and js/net; fixes the untimed failover stall
- [FETCH max-delay](/quest/m1/fetch-max-delay.md) - historical readers obey publisher max-age and an optional tighter reader budget, including while holding a group
- [Upstream position regression](/quest/m1/largest-regression.md) - a relay copy that sees upstream's largest group go backwards on moq-transport or epochless lite-07 fails loud instead of serving the old instance's cache
- [Untimed decisions](/quest/m1/untimed-decisions.md) - the maintainer decides whether moq-archive keeps refusing untimed tracks, and whether a malformed FETCH object ends its track
- [Untimed by default in Rust](/quest/m1/rust-untimed-default.md) - an undeclared Rust timescale means untimed, and shared-clock publishers declare milliseconds
Expand Down Expand Up @@ -84,6 +85,7 @@ bench all require its crate.
- [JSON window timestamps](/quest/m1/json-window-timestamps.md) - Rust and JS window consumers return each event's frame timestamp like the snapshot and stream consumers
- [IETF object gaps](/quest/m1/ietf-object-gaps.md) - a gapped object ID is refused loudly like a subgroup in Rust and JS, and an overflowing one closes the session in Rust
- [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface
- [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement
- [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request
- [Churn with held subscriptions](/quest/m1/session-churn-held.md) - opening and closing a request costs the same with 1 or 1,024 held subscriptions
- [Catalog track identity](/quest/m1/catalog-tracks.md) - a track's codec and description never change for its name; resolution changes in band below ceilings fixed at creation, and anything else mints a new rendition or epoch
Expand Down
9 changes: 7 additions & 2 deletions quest/m1/cache-max-age.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ Facts, and work carried over:
until the track ends; the replaced copy's lease stays alive too. The wall
term convicts it. Settle what "nothing pending" means against the recovery
fetch (`a_pending_recovery_fetch_does_not_disable_expiry`), so a fetch
about to fill the group isn't cut short. A FETCH reader has no budget and
stays as it is.
about to fill the group is still subject to its reader's deadline. Do not
make a pending fetch disable expiry. Decided in the 2026-10-10 audit:
FETCH also obeys publisher max-age and an optional reader max-delay;
omission uses the publisher's limit alone. This supersedes the old FETCH
exemption. [FETCH max-delay](/quest/m1/fetch-max-delay.md) owns that API,
request propagation and per-reader enforcement, building on this rule.
- Start resolution: the untimed model starts an untimed track at the latest
group (`TrackState::untimed_start`). Replace that special case with the
normal rule, replaying the cached groups that aren't stale. Until then a
Expand Down Expand Up @@ -126,3 +130,4 @@ encoding change; Max Age semantics in the lite draft change.
- [JS track handover](/quest/m1/js-group-handover.md) - mirrors the failover rule in JS
- [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - relay memory past the expiry window, in the same cache
- [Generated @moq/net](/quest/m1/rs2ts/README.md) - retires js/net's hand-written model
- [FETCH max-delay](/quest/m1/fetch-max-delay.md) - applies the shared rule to historical requests and reconciles native IETF retention clocks
1 change: 1 addition & 0 deletions quest/m1/catalog-track-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ Rejected: an ID plus a required `track`.

- [Media stats](/quest/m1/stats/README.md) - keys stats and feedback by this
ID
- [Catalog references](/quest/m0/broadcast-epoch/catalog-references.md) - optional epoch beside broadcast across every reference-bearing section, with shared instance pinning
1 change: 1 addition & 0 deletions quest/m1/catalog-tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ the hang catalog's field semantics, documented in its draft.
- [Catalog rendition IDs](/quest/m1/catalog-track-id.md) - lets a catalog list a new track name for a changed rendition
- [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - a restart is a new epoch rather than a changed track
- [Archive](/quest/m1/archive/README.md) - storage and replay consume the identity contract
- [Catalog references](/quest/m0/broadcast-epoch/catalog-references.md) - an update cannot retarget an already-pinned sibling instance
Loading
Loading