From c373a3545165644e6b6e6a355934d5b086cf0e09 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 10 Oct 2026 13:14:54 -0700 Subject: [PATCH 1/3] quest(m0): build Source from the resolved catalog broadcast Co-Authored-By: Claude Opus 5.5 --- quest/m0/broadcast-epoch/README.md | 2 +- quest/m0/broadcast-epoch/source-pin.md | 31 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/quest/m0/broadcast-epoch/README.md b/quest/m0/broadcast-epoch/README.md index cce1026afe..9398e937e9 100644 --- a/quest/m0/broadcast-epoch/README.md +++ b/quest/m0/broadcast-epoch/README.md @@ -107,7 +107,7 @@ maintainer's pre-release merge of `release` into `main` keeps `main`'s - [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) - an export's later requests stay on the broadcast it resolved, so an epochless replacement never splices into the old program +- [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 - [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) diff --git a/quest/m0/broadcast-epoch/source-pin.md b/quest/m0/broadcast-epoch/source-pin.md index 02ac13c458..ea7e1a1dfb 100644 --- a/quest/m0/broadcast-epoch/source-pin.md +++ b/quest/m0/broadcast-epoch/source-pin.md @@ -1,10 +1,10 @@ -# [S] An export's later requests stay on the broadcast it resolved +# [M] An export's later requests stay on the broadcast it resolved ## Goal -`moq_mux::Source` keeps the first `broadcast::Consumer` it resolves for each -path, its own and every cross-broadcast reference, and serves every later -request for that path from that handle. A late rendition subscribe or SI +`moq_mux::Source` is built from the catalog broadcast its caller resolved, +keeps the first `broadcast::Consumer` it resolves for each cross-broadcast +reference, and serves every later request for a path from that handle. A late rendition subscribe or SI repoint never lands on a replacement, epoch or not. On a replaced instance it fails loudly instead of splicing the new publisher's data into the old program. @@ -24,12 +24,21 @@ replacement won in the meantime, it serves that request even without happens next (`Replaced`, or a stitch). - Decided: pin every resolved path, not only the source's own, so a replaced sibling (`broadcast: ./source`) cannot splice in either. +- Decided (2026-10-10, amending the lazy own-path cache): `Source` is built + from the resolved catalog broadcast, which seeds its own-path handle. Most + callers (moq-cli, moq-ffi, moq-c, moq-hls, the fmp4, flv, and codec + exporters) subscribe the catalog outside `Source`, so a lazy first resolve + could still land on a replacement. Construction closes that for every + exporter. moq-hls's own catalog hold (`export/upstream.rs`) should then be + redundant; delete it if so. Callers that let the export resolve today (the + TS export from `moq publish`) resolve first, or the export seeds the + handle; pick whichever reads simpler. - The pin lifecycle follows the export's instance. A stitch builds a new `Source` whose own-path entry is seeded with the broadcast `follow` was given (sibling entries start empty), so its first track request can't resolve a newer replacement. A same-epoch return replaces the own-path handle with the returned broadcast, since the old one's routes are gone. -- The guarantee starts at a path's first resolution. A sibling the catalog +- A sibling's guarantee starts at its first resolution. A sibling the catalog first references after a replacement resolves whatever serves it then. The export never held another instance of that path, so that isn't a splice. @@ -41,8 +50,10 @@ replacement won in the meantime, it serves that request even without it followed. A same-epoch return reads the returned handle. Today's epoch-pin test (`source.rs`) moves to the handle. -Public API: no signature change, but a behavior change. A `Source` reused -across a route replacement now returns its pinned handle for a path it -already resolved (`broadcast`, `catalog`, and track requests), or fails on -the replaced instance, instead of resolving the current route. Document it -on `Source`. Wire: none. +Public API: breaking. `Source::new` takes the resolved catalog broadcast, +and every caller in the repo moves with it (follow the Cross-Package Sync +table for moq-ffi and moq-c). A `Source` reused across a route replacement +returns its pinned handle for a path it already holds (`broadcast`, +`catalog`, and track requests), or fails on the replaced instance, instead +of resolving the current route. Document it on `Source`, and fix stale +comments inline; no new doc page (decided 2026-10-10). Wire: none. From c91417bac9e4770815ccad2eebea6d159b134ecd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 10 Oct 2026 13:23:28 -0700 Subject: [PATCH 2/3] quest(m0): tighten source-pin caller list, TS seeding, and tests Co-Authored-By: Claude Opus 5.5 --- quest/m0/broadcast-epoch/source-pin.md | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/quest/m0/broadcast-epoch/source-pin.md b/quest/m0/broadcast-epoch/source-pin.md index ea7e1a1dfb..1856d1404c 100644 --- a/quest/m0/broadcast-epoch/source-pin.md +++ b/quest/m0/broadcast-epoch/source-pin.md @@ -4,10 +4,10 @@ `moq_mux::Source` is built from the catalog broadcast its caller resolved, keeps the first `broadcast::Consumer` it resolves for each cross-broadcast -reference, and serves every later request for a path from that handle. A late rendition subscribe or SI -repoint never lands on a replacement, epoch or not. On a replaced instance -it fails loudly instead of splicing the new publisher's data into the old -program. +reference, and serves every later request for a path from that handle. A +late rendition subscribe or SI repoint never lands on a replacement, epoch +or not. On a replaced instance it fails loudly instead of splicing the new +publisher's data into the old program. ## Plan @@ -26,13 +26,15 @@ replacement won in the meantime, it serves that request even without sibling (`broadcast: ./source`) cannot splice in either. - Decided (2026-10-10, amending the lazy own-path cache): `Source` is built from the resolved catalog broadcast, which seeds its own-path handle. Most - callers (moq-cli, moq-ffi, moq-c, moq-hls, the fmp4, flv, and codec - exporters) subscribe the catalog outside `Source`, so a lazy first resolve - could still land on a replacement. Construction closes that for every - exporter. moq-hls's own catalog hold (`export/upstream.rs`) should then be - redundant; delete it if so. Callers that let the export resolve today (the - TS export from `moq publish`) resolve first, or the export seeds the - handle; pick whichever reads simpler. + callers (e.g. moq-cli, moq-ffi, moq-c, moq-hls, moq-rtc, moq-rtmp, and the + fmp4, flv, and codec exporters) subscribe the catalog outside `Source`, so + a lazy first resolve could still land on a replacement. Construction closes + that for every exporter. moq-hls's own catalog hold (`export/upstream.rs`) + should then be redundant, once `Source` returns the seeded handle for every + self-reference; delete it if so. Callers that let the export resolve today + (the TS export from `moq publish`) resolve first, or the export seeds the + handle from the exact broadcast it resolved, never a second resolve; pick + whichever reads simpler. - The pin lifecycle follows the export's instance. A stitch builds a new `Source` whose own-path entry is seeded with the broadcast `follow` was given (sibling entries start empty), so its first track request can't @@ -45,7 +47,9 @@ replacement won in the meantime, it serves that request even without - Tests, with mocked time, on an epochless route: an export resolves `live`, a replacement wins `live`, and then a rendition added to the old catalog (and an SI repoint) fails rather than reading the replacement. The same - for a sibling reference already resolved. A stitch whose replacement is + for a sibling reference already resolved. A `Source` built from `live` + after a replacement already won still reads the broadcast it was given on + its first track request. A stitch whose replacement is itself replaced before the first track request still reads the broadcast it followed. A same-epoch return reads the returned handle. Today's epoch-pin test (`source.rs`) moves to the handle. From d7064c8508221bcb4d43f3cacf55f044a5cef8ea Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 10 Oct 2026 13:43:23 -0700 Subject: [PATCH 3/3] quest(m0): source-pin adds an upgrade-guide line for Source::new Co-Authored-By: Claude Opus 5.5 --- quest/m0/broadcast-epoch/source-pin.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quest/m0/broadcast-epoch/source-pin.md b/quest/m0/broadcast-epoch/source-pin.md index 1856d1404c..2b04faa45f 100644 --- a/quest/m0/broadcast-epoch/source-pin.md +++ b/quest/m0/broadcast-epoch/source-pin.md @@ -59,5 +59,7 @@ and every caller in the repo moves with it (follow the Cross-Package Sync table for moq-ffi and moq-c). A `Source` reused across a route replacement returns its pinned handle for a path it already holds (`broadcast`, `catalog`, and track requests), or fails on the replaced instance, instead -of resolving the current route. Document it on `Source`, and fix stale -comments inline; no new doc page (decided 2026-10-10). Wire: none. +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.