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..2b04faa45f 100644 --- a/quest/m0/broadcast-epoch/source-pin.md +++ b/quest/m0/broadcast-epoch/source-pin.md @@ -1,13 +1,13 @@ -# [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 -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. +`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. ## Plan @@ -24,25 +24,42 @@ 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 (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 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. - 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. -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`, 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.