From 62157b222d8cb6561876dc07bf921b3f5191e72d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 24 Sep 2026 13:36:22 -0700 Subject: [PATCH 1/3] docs(quest): plan follow-ups from the spawn-quests run Abandon uring-handshake-flush. Add bindings caught up, auth interop grants, auth unauthorized reset, listener close, tokio test ports, and mux cut. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 4 +++- quest/m1/announce-live-bindings.md | 33 +++++++++++++++++++++++++++++ quest/m1/auth/README.md | 4 ++++ quest/m1/auth/interop.md | 22 +++++++++++++++++++ quest/m1/auth/relay-refresh.md | 2 ++ quest/m1/auth/unauthorized.md | 22 +++++++++++++++++++ quest/m1/js-announce-caught-up.md | 4 +++- quest/m1/listener-close.md | 20 ++++++++++++++++++ quest/m1/tokio-test-ports.md | 13 ++++++++++++ quest/m1/uring-handshake-flush.md | 34 ------------------------------ quest/m2/README.md | 1 + quest/m2/mux-cut.md | 20 ++++++++++++++++++ 12 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 quest/m1/announce-live-bindings.md create mode 100644 quest/m1/auth/interop.md create mode 100644 quest/m1/auth/unauthorized.md create mode 100644 quest/m1/listener-close.md create mode 100644 quest/m1/tokio-test-ports.md delete mode 100644 quest/m1/uring-handshake-flush.md create mode 100644 quest/m2/mux-cut.md diff --git a/quest/m1/README.md b/quest/m1/README.md index d753bac7f2..ca70e49326 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -33,9 +33,9 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Ingest source](/quest/m1/net-ingest-source.md) - an announce says whether the broadcast entered on this relay or a peer - [Publisher clocks](/quest/m1/publisher-clock.md) - wire the shared clock through native and browser publisher restarts - [Broadcast route](/quest/m1/js-broadcast-route.md) - JS Announce.Broadcast goes live on any claim matching its path, like Rust routed() -- [io_uring handshake flush](/quest/m1/uring-handshake-flush.md) - a dial resolves only once its last handshake flight is on the wire, never stranding the peer - [CLI inspection](/quest/m1/cli-inspect/README.md) - `moq ls` lists what is live and `moq fetch` reads a group over MoQ, and a guide shows how to inspect a relay - [JS caught up](/quest/m1/js-announce-caught-up.md) - @moq/net's announce consumer says when the initial set has landed, like Rust +- [Bindings caught up](/quest/m1/announce-live-bindings.md) - moq-ffi, libmoq, and every wrapper yield the same flat announce event, `Live` included - [IETF announce count](/quest/m1/ietf-announce-count.md) - an opt-in moq-transport extension carries the replay count, so IETF announce consumers go live without a timer - [Jitter clock](/quest/m1/jitter-flush-clock.md) - moq-mux: catalog jitter measures how far behind the media clock an encoder flushes, fed by encoders only, and never decreases @@ -55,6 +55,8 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Stats retier](/quest/m1/stats-retier.md) - a re-checked tier retags a live session's stats in place instead of waiting for its next connection - [io_uring link facts](/quest/m1/uring-link-facts.md) - the io_uring workers report a session's peer address and SNI to the auth server like the tokio listener does +- [Listener close](/quest/m1/listener-close.md) - closing a listener releases its UDP socket before returning, so a restart can rebind the port +- [Test ports](/quest/m1/tokio-test-ports.md) - moq-tokio tests pre-bind the TCP and UDP sockets they serve on, so a parallel run cannot collide - [Decoded frame ownership](/quest/m1/decoded-frames.md) - retain moq-video Frames across bindings, with native views or CPU conversion as needed - [C++ through moq-ffi](/quest/m1/cpp/README.md) - generated C++ over moq-ffi with futures and expected-style errors, shipped as a tarball, vcpkg, and Conan, and adopted by the OBS plugin - [OBS native codecs](/quest/m1/obs-moq-video/README.md) - remove FFmpeg decoding dependencies, deliver GPU frames, and use native audio/video encoders diff --git a/quest/m1/announce-live-bindings.md b/quest/m1/announce-live-bindings.md new file mode 100644 index 0000000000..fe2464bbab --- /dev/null +++ b/quest/m1/announce-live-bindings.md @@ -0,0 +1,33 @@ +# [M] Bindings see when an announce consumer has caught up + +## Goal + +moq-ffi, libmoq, and every wrapper (`py`, `swift`, `kt`, `go`, `dart`) yield +the same flat announce event Rust does: `Announced`, `Updated`, or `Retracted` +carrying the announce, or `Live` once every route live at subscribe time has +been delivered. A binding app can list what is live and stop, with no timer. + +## Plan + +Mirror the Rust `announce::Event` from +[Caught up](/quest/m1/cli-inspect/caught-up.md) one to one: + +- moq-ffi: `MoqAnnounceConsumer::next` returns `MoqAnnounceEvent`, a uniffi + enum with the four variants, replacing `MoqAnnounceUpdate::active()`. Stop + filtering `Live` in `rs/moq-ffi/src/origin.rs`. +- libmoq: the `on_announce` handle's `moq_announce_update.kind` gains a LIVE + value with no route fields, replacing the active flag. Stop skipping + `Event::Live` in `rs/libmoq/src/origin.rs`. +- Hand-written wrappers and `doc/lib/{py,swift,kt,go,dart,c}` follow, with a + test per binding that an empty origin still yields `Live`. + +Public API: breaking in moq-ffi, libmoq's C ABI, and every wrapper, so it +retargets to `dev` with the Rust break. Wire: none. + +## Required + +- [Caught up](/quest/m1/cli-inspect/caught-up.md) - settles the event shape this mirrors + +## Related + +- [JS caught up](/quest/m1/js-announce-caught-up.md) - the same event in `@moq/net` diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index 9ffe7a6664..a7de50b497 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -92,6 +92,10 @@ existing lite-06 ALPN. - [Lite stream](/quest/m1/auth/lite.md) - both sides of a lite-06 session exchange grants over AUTH streams, exposed as `Session::auth()`, and an out-of-scope announce aborts the session +- [Interop grants](/quest/m1/auth/interop.md) - the interop matrix asserts + each lite-06 cell's grant and that a publish outside it fails loud +- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - a subscription that + loses access resets with a dedicated UNAUTHORIZED stream code - [Relay tokens](/quest/m1/auth/relay-refresh.md) - the relay verifies tokens sent in band, unions their grants, and cancels only work that loses access - [moq-transport](/quest/m1/auth/moq-transport.md) - the same exchange as a diff --git a/quest/m1/auth/interop.md b/quest/m1/auth/interop.md new file mode 100644 index 0000000000..738b999026 --- /dev/null +++ b/quest/m1/auth/interop.md @@ -0,0 +1,22 @@ +# [S] Interop asserts the AUTH grant + +## Goal + +`just test interop --all` fails a lite-06 cell whose client did not receive +the grant its relay token implies, and a new negative round passes only when +a client publishing outside its grant fails loud with `Unauthorized`. Today +the matrix checks media alone, so a malformed AUTH_OK that leaves a session +with no grant passes silently. + +## Plan + +Each interop client (Rust, JS, and every binding that speaks lite-06) prints +the grant it received in one parseable line; `test/interop/interop.sh` +compares it against the token the cell minted. The negative round mints a +token that excludes the published path and expects the publisher's session +to close with `Unauthorized` naming the path, and the subscriber to see +nothing. Cells below lite-06 skip both checks. No public API or wire change. + +## Required + +- [Lite stream](/quest/m1/auth/lite.md) - the AUTH exchange being asserted diff --git a/quest/m1/auth/relay-refresh.md b/quest/m1/auth/relay-refresh.md index dbfb2b92e0..82fc474a97 100644 --- a/quest/m1/auth/relay-refresh.md +++ b/quest/m1/auth/relay-refresh.md @@ -82,3 +82,5 @@ Additive. - [Pattern interest](/quest/m1/path-patterns.md) - AUTH can represent the complete grants relay revalidation returns - [Lite stream](/quest/m1/auth/lite.md) - supplies the AUTH stream and `auth::Request` this consumes +- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - the code this + relay's revocations reset with diff --git a/quest/m1/auth/unauthorized.md b/quest/m1/auth/unauthorized.md new file mode 100644 index 0000000000..05b1425689 --- /dev/null +++ b/quest/m1/auth/unauthorized.md @@ -0,0 +1,22 @@ +# [S] A subscription that loses access resets with UNAUTHORIZED + +## Goal + +When a grant shrinks and a subscription, fetch, or announce loses access, +its stream resets with a dedicated `UNAUTHORIZED` stream code, so the peer +tells revocation apart from a session closing. Today it resets with +`0x3 SESSION_CLOSED` (`StreamError::Session(Unauthorized)`). + +## Plan + +Assign `0x3A UNAUTHORIZED` in the lite draft's stream error table, the next +code in the application block after `TIMESTAMP_MISMATCH`, on lite-06: a +published lite-06 peer already maps an unknown stream code to a generic +error, so the addition is compatible. Add the matching `StreamError` +variant (the enum is non-exhaustive) in Rust and JS, send it from every +revocation path the lite stream added, and cover it in the Rust and JS +tests. Update `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. + +## Required + +- [Lite stream](/quest/m1/auth/lite.md) - the revocation paths that send it diff --git a/quest/m1/js-announce-caught-up.md b/quest/m1/js-announce-caught-up.md index 46785d11e4..c568f1fdbb 100644 --- a/quest/m1/js-announce-caught-up.md +++ b/quest/m1/js-announce-caught-up.md @@ -9,7 +9,9 @@ broadcasts" instead of a spinner that never resolves. ## Plan -Mirror the Rust shape, name, and marker-less fallback. Test the same cases +Mirror the Rust shape, name, and marker-less fallback: one flat event, +`Announced`, `Updated`, `Retracted` (each carrying the announce), or `Live`, +replacing the update's `kind` field. Test the same cases in JS. Public API: the announce consumer's yield type changes, so it lands with the Rust break. Wire: none. diff --git a/quest/m1/listener-close.md b/quest/m1/listener-close.md new file mode 100644 index 0000000000..e765fe2d6d --- /dev/null +++ b/quest/m1/listener-close.md @@ -0,0 +1,20 @@ +# [S] Closing a listener releases its socket before returning + +## Goal + +Closing a moq-tokio server, and moq-ffi's `Listener::close` on top of it, +returns only after the UDP socket is released, so a restart can rebind the +same port at once. Go's `TestReconnectAcrossRelayRestart` +(`go/wrapper/reconnect_test.go`) fails on main today because the rebind +races the old socket. + +## Plan + +`Server::shutdown` (`rs/moq-tokio/src/server.rs`) calls `noq.close()` and +sleeps 100 ms without waiting for the endpoint driver to drop the socket. +Await the driver's exit instead and delete the sleep; moq-ffi's +`Listener::close` already documents a synchronous release, so make it true +rather than change the doc. Check the io_uring and WebSocket listeners for +the same gap. Add a Rust test that closes a server and rebinds its port +immediately; the Go test becomes the cross-language regression. No wire +change; the only API change is that close takes as long as the release. diff --git a/quest/m1/tokio-test-ports.md b/quest/m1/tokio-test-ports.md new file mode 100644 index 0000000000..f449b11478 --- /dev/null +++ b/quest/m1/tokio-test-ports.md @@ -0,0 +1,13 @@ +# [XS] moq-tokio tests pre-bind their ports + +## Goal + +moq-tokio's integration tests never fail on a port another process or +parallel test holds. `websocket_forbidden_does_not_end_a_quic_connect` +(`rs/moq-tokio/tests/broadcast.rs`) fails about 1 in 15 local runs today. + +## Plan + +`test_server()` takes an ephemeral UDP port and then binds TCP on the same +number, which was never reserved on TCP. Bind both sockets up front and hand +them to the server, so nothing is left to collide on. Never retry. No public API or wire change. diff --git a/quest/m1/uring-handshake-flush.md b/quest/m1/uring-handshake-flush.md deleted file mode 100644 index b4e1614a1a..0000000000 --- a/quest/m1/uring-handshake-flush.md +++ /dev/null @@ -1,34 +0,0 @@ -# [M] An io_uring dial resolves once its last handshake flight is sent - -## Goal - -`moq_uring::quic::client::connect` hands back a connection only after the -handshake's final flight has been written to the socket, so a worker that -stops right after the dial resolves cannot strand a peer that is still waiting -for it. A worker that stops before that point fails the dial loudly. No new -public API or wire format. - -## Plan - -Today the dial resolves when the handshake completes locally, while the -client's last flight is still queued behind pacing on the worker (#3865). If -the worker stops in that window nothing is sent, the server times the -connection out, and the client holds a connection that looks established. The -behaviour is documented but easy to hit from a short-lived task. - -Reproduce it first on Linux CI: dial, stop the worker immediately, and show -the server never accepts. Then keep the dial future pending until the driver -reports the handshake flight flushed, and return `quic::Error` when the worker -stops before then. The flush belongs in moq-uring's connect and establish -path; moq-tokio is unaffected. Do not add a timeout or a retry, and do not -drain sends from worker shutdown. - -Linux CI covers the stopped-worker dial (now refused or fully sent, never -stranded), a normal dial still resolving promptly, and a paced flight on a -slow link. Update the moq-uring docs where the window was described. - -Public API: none. Wire: none. - -## Related - -- [uring identity](https://github.com/moq-dev/moq/pull/3865) - where the window was found diff --git a/quest/m2/README.md b/quest/m2/README.md index db6dd0c524..f807d57a9d 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -27,6 +27,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [Audio loss recovery](/quest/m2/audio-loss-recovery.md) - prove a useful Opus recovery policy before exposing another option - [Opus implementation](/quest/m2/audio-opus-backend.md) - compare current codec quality, CPU, and optional build costs - [Latency ledger](/quest/m2/latency-ledger.md) - a session reports where its end-to-end audio delay went, stage by stage +- [One cut](/quest/m2/mux-cut.md) - moq-mux `cut` writes the discontinuity marker as JS does, and `discontinuity()` is gone - [Media Foundation decode](/quest/m2/audio-decode-mediafoundation.md) - Windows decodes HE-AAC, multichannel AAC, and what else the MFTs offer - [Media Foundation encode](/quest/m2/audio-encode-mediafoundation.md) - Windows encodes AAC-LC - [MediaCodec decode](/quest/m2/audio-decode-mediacodec.md) - Android decodes HE-AAC, multichannel AAC, and what else the device offers diff --git a/quest/m2/mux-cut.md b/quest/m2/mux-cut.md new file mode 100644 index 0000000000..2a9d4d038f --- /dev/null +++ b/quest/m2/mux-cut.md @@ -0,0 +1,20 @@ +# [M] One cut in moq-mux + +## Goal + +`moq_mux::container::Producer::cut(end)` closes the open group and writes the +discontinuity marker group, exactly as JS `Container.Legacy.Producer.cut` +does, and `discontinuity()` is gone from the producer and every codec +importer. One call declares a break everywhere, and moq-ffi and libmoq +encoders that already call `cut(None)` mark their pauses for free. + +## Plan + +Fold `discontinuity()` (`rs/moq-mux/src/container/producer.rs`) into `cut`, +delete it from the h264, h265, aac, opus, and legacy importers, and move its +callers (moq-audio's encode producer, moq-boy) to `cut`. Every existing +`cut(None)` caller (moq-ffi, libmoq, moq-gst, moq-cli) now emits a marker at +end of stream too, which subscribers already tolerate. Suggested by #3982. + +Public API: breaking in published moq-mux, so it targets `dev`. Wire: none; +the marker group already exists. From d87e5d6e0dd328aa788be4e177383164404efdac Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 24 Sep 2026 13:59:15 -0700 Subject: [PATCH 2/3] docs(quest): address review on the follow-up quests mux-cut becomes js-discontinuity: Rust cut is the routine group close, so JS renames its break instead. Interop grants cover Rust and JS first, test ports bind independently, and relay refresh narrows instead of aborting. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/auth/bindings.md | 3 +++ quest/m1/auth/interop.md | 6 ++++-- quest/m1/auth/relay-refresh.md | 7 ++++--- quest/m1/tokio-test-ports.md | 9 ++++++--- quest/m2/README.md | 2 +- quest/m2/js-discontinuity.md | 20 ++++++++++++++++++++ quest/m2/mux-cut.md | 20 -------------------- 8 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 quest/m2/js-discontinuity.md delete mode 100644 quest/m2/mux-cut.md diff --git a/quest/m1/README.md b/quest/m1/README.md index ca70e49326..51281a48cf 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -56,7 +56,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Stats retier](/quest/m1/stats-retier.md) - a re-checked tier retags a live session's stats in place instead of waiting for its next connection - [io_uring link facts](/quest/m1/uring-link-facts.md) - the io_uring workers report a session's peer address and SNI to the auth server like the tokio listener does - [Listener close](/quest/m1/listener-close.md) - closing a listener releases its UDP socket before returning, so a restart can rebind the port -- [Test ports](/quest/m1/tokio-test-ports.md) - moq-tokio tests pre-bind the TCP and UDP sockets they serve on, so a parallel run cannot collide +- [Test ports](/quest/m1/tokio-test-ports.md) - moq-tokio tests bind QUIC and WebSocket on independent ephemeral ports, so a parallel run cannot collide - [Decoded frame ownership](/quest/m1/decoded-frames.md) - retain moq-video Frames across bindings, with native views or CPU conversion as needed - [C++ through moq-ffi](/quest/m1/cpp/README.md) - generated C++ over moq-ffi with futures and expected-style errors, shipped as a tarball, vcpkg, and Conan, and adopted by the OBS plugin - [OBS native codecs](/quest/m1/obs-moq-video/README.md) - remove FFmpeg decoding dependencies, deliver GPU frames, and use native audio/video encoders diff --git a/quest/m1/auth/bindings.md b/quest/m1/auth/bindings.md index 596e8437d9..6bd1a36ab2 100644 --- a/quest/m1/auth/bindings.md +++ b/quest/m1/auth/bindings.md @@ -24,6 +24,9 @@ mid-stream can be handed a new one without the plugin reconnecting. async calls use, in `rs/libmoq/src/api.rs` and the session table; regenerate `moq.h`, and update `cpp/obs/src` only if the plugin surfaces a token field, otherwise leave it. +- Interop: the Python, Go, and C interop clients print their grant and join + the assertion [Interop grants](/quest/m1/auth/interop.md) adds for Rust + and JS. - Wrappers: `py/moq-rs/moq/session.py`, `swift/Sources/Moq`, `kt/.../Flows.kt` (a `Flow` over `grant_changed`), `go/wrapper/moq/session.go` (context-cancellable like the rest), and `dart/moq/lib/moq.dart`. Kotlin diff --git a/quest/m1/auth/interop.md b/quest/m1/auth/interop.md index 738b999026..19ed8b8fe7 100644 --- a/quest/m1/auth/interop.md +++ b/quest/m1/auth/interop.md @@ -10,12 +10,14 @@ with no grant passes silently. ## Plan -Each interop client (Rust, JS, and every binding that speaks lite-06) prints +The Rust and JS interop clients print the grant it received in one parseable line; `test/interop/interop.sh` compares it against the token the cell minted. The negative round mints a token that excludes the published path and expects the publisher's session to close with `Unauthorized` naming the path, and the subscriber to see -nothing. Cells below lite-06 skip both checks. No public API or wire change. +nothing. Cells below lite-06 skip both checks, as do binding clients until +[Bindings](/quest/m1/auth/bindings.md) gives them a grant to print; that +quest adds them to the same assertion. No public API or wire change. ## Required diff --git a/quest/m1/auth/relay-refresh.md b/quest/m1/auth/relay-refresh.md index 82fc474a97..3681782584 100644 --- a/quest/m1/auth/relay-refresh.md +++ b/quest/m1/auth/relay-refresh.md @@ -38,16 +38,17 @@ expiry, an expiry that leaves the union intact ends only that token, and - Expiry: the deadline today is the admitted lease's `closed()` inside one `tokio::select!` arm. Select on one lease per token in the set instead, and on any firing recompute the union without it: unchanged means `AUTH_ERROR { Expired }` on that token's stream and the session - continues, shrunk means `session.abort(Unauthorized)` as today. A token + continues, shrunk narrows the origin and resets each stream that lost + access with `UNAUTHORIZED`. A token withdrawn by the client (its stream closed or reset) recomputes the union the same way with nothing written back, since the stream is gone: unchanged - means the session continues, shrunk means abort. Each lease revalidates on + means the session continues, shrunk narrows the same way. Each lease revalidates on its own cadence, and every re-check that changes the grant, a lower `expires` or a different `publish` or `subscribe`, writes the complete replacement grant as an update AUTH_OK on that token's stream and recomputes the union from the latest grant of every token, so the client can present a replacement in time and a narrowed token never keeps its old - scope; a shrunk union aborts as above until Origin scopes resizes. + scope; a shrunk union narrows as above. - Refusals map as connect-time ones do and never expose a status: every refusal `Client::attach` returns for a new token, a `403`, an empty or invalid grant, an unparseable body, or a `5xx`, is diff --git a/quest/m1/tokio-test-ports.md b/quest/m1/tokio-test-ports.md index f449b11478..384654dc0f 100644 --- a/quest/m1/tokio-test-ports.md +++ b/quest/m1/tokio-test-ports.md @@ -1,4 +1,4 @@ -# [XS] moq-tokio tests pre-bind their ports +# [XS] moq-tokio tests bind their ports independently ## Goal @@ -9,5 +9,8 @@ parallel test holds. `websocket_forbidden_does_not_end_a_quic_connect` ## Plan `test_server()` takes an ephemeral UDP port and then binds TCP on the same -number, which was never reserved on TCP. Bind both sockets up front and hand -them to the server, so nothing is left to collide on. Never retry. No public API or wire change. +number, which was never reserved on TCP. Bind the QUIC and WebSocket +listeners each on `:0` and hand the test client the WebSocket port +explicitly, adding a client config override for the fallback URL's port if +none exists (test-only if possible; otherwise note it as a public API +addition). No retry. No wire change. diff --git a/quest/m2/README.md b/quest/m2/README.md index f807d57a9d..028e2e95bf 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -27,7 +27,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [Audio loss recovery](/quest/m2/audio-loss-recovery.md) - prove a useful Opus recovery policy before exposing another option - [Opus implementation](/quest/m2/audio-opus-backend.md) - compare current codec quality, CPU, and optional build costs - [Latency ledger](/quest/m2/latency-ledger.md) - a session reports where its end-to-end audio delay went, stage by stage -- [One cut](/quest/m2/mux-cut.md) - moq-mux `cut` writes the discontinuity marker as JS does, and `discontinuity()` is gone +- [JS discontinuity](/quest/m2/js-discontinuity.md) - JS names its timeline break `discontinuity()` like Rust, so `cut` means the same group close in both - [Media Foundation decode](/quest/m2/audio-decode-mediafoundation.md) - Windows decodes HE-AAC, multichannel AAC, and what else the MFTs offer - [Media Foundation encode](/quest/m2/audio-encode-mediafoundation.md) - Windows encodes AAC-LC - [MediaCodec decode](/quest/m2/audio-decode-mediacodec.md) - Android decodes HE-AAC, multichannel AAC, and what else the device offers diff --git a/quest/m2/js-discontinuity.md b/quest/m2/js-discontinuity.md new file mode 100644 index 0000000000..51669f3cbd --- /dev/null +++ b/quest/m2/js-discontinuity.md @@ -0,0 +1,20 @@ +# [S] JS names the break discontinuity() + +## Goal + +`@moq/hang`'s container producer spells its two operations the way Rust +`moq_mux::container::Producer` does: `cut(end?)` closes the current group, +and `discontinuity()` closes it and writes the empty marker group that tells +subscribers to re-anchor. Today JS's public `cut()` writes the marker, so the +same name means a routine group close in Rust and a timeline break in JS. + +## Plan + +In `js/hang/src/container/legacy.ts`, rename the public `cut(end?)` to +`discontinuity()` (taking the same optional end) and keep the routine close +private until a caller needs it public. Move `js/publish/src/video/encoder.ts` +and any other caller over, and keep data tracks skipping a sequence the way +Rust's `discontinuity()` does if a JS data-track caller appears. Rust is +untouched. + +Public API: breaking in published `@moq/hang`, so it targets `dev`. Wire: none. diff --git a/quest/m2/mux-cut.md b/quest/m2/mux-cut.md deleted file mode 100644 index 2a9d4d038f..0000000000 --- a/quest/m2/mux-cut.md +++ /dev/null @@ -1,20 +0,0 @@ -# [M] One cut in moq-mux - -## Goal - -`moq_mux::container::Producer::cut(end)` closes the open group and writes the -discontinuity marker group, exactly as JS `Container.Legacy.Producer.cut` -does, and `discontinuity()` is gone from the producer and every codec -importer. One call declares a break everywhere, and moq-ffi and libmoq -encoders that already call `cut(None)` mark their pauses for free. - -## Plan - -Fold `discontinuity()` (`rs/moq-mux/src/container/producer.rs`) into `cut`, -delete it from the h264, h265, aac, opus, and legacy importers, and move its -callers (moq-audio's encode producer, moq-boy) to `cut`. Every existing -`cut(None)` caller (moq-ffi, libmoq, moq-gst, moq-cli) now emits a marker at -end of stream too, which subscribers already tolerate. Suggested by #3982. - -Public API: breaking in published moq-mux, so it targets `dev`. Wire: none; -the marker group already exists. From d676b258e24007be986cdf0278d65ae1d007a909 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 24 Sep 2026 14:14:44 -0700 Subject: [PATCH 3/3] docs(quest): align relay-refresh tests, map UNAUTHORIZED in the bindings Co-Authored-By: Claude Opus 5.5 --- quest/m1/auth/relay-refresh.md | 3 ++- quest/m1/auth/unauthorized.md | 4 +++- quest/m1/tokio-test-ports.md | 4 +++- quest/m2/js-discontinuity.md | 5 +++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/quest/m1/auth/relay-refresh.md b/quest/m1/auth/relay-refresh.md index 3681782584..36d14f7c07 100644 --- a/quest/m1/auth/relay-refresh.md +++ b/quest/m1/auth/relay-refresh.md @@ -72,7 +72,8 @@ expiry, an expiry that leaves the union intact ends only that token, and an expired or invalid token is refused and nothing changes; a token with a different root is refused naming it; a token adding a prefix rebuilds the origin scope and the prefix is now solicited; withdrawing the only token - covering a prefix closes the session; an mTLS session refuses a token; a + covering a prefix resets that prefix's streams with `UNAUTHORIZED` and + the session stays connected; an mTLS session refuses a token; a proxy-mode outage on re-check keeps the token. Additive. diff --git a/quest/m1/auth/unauthorized.md b/quest/m1/auth/unauthorized.md index 05b1425689..f5262ba19c 100644 --- a/quest/m1/auth/unauthorized.md +++ b/quest/m1/auth/unauthorized.md @@ -15,7 +15,9 @@ published lite-06 peer already maps an unknown stream code to a generic error, so the addition is compatible. Add the matching `StreamError` variant (the enum is non-exhaustive) in Rust and JS, send it from every revocation path the lite stream added, and cover it in the Rust and JS -tests. Update `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. +tests. Map it to the existing `Unauthorized` protocol kind in +`stream_kind` in `rs/moq-ffi/src/error.rs` and `rs/libmoq/src/error.rs`, +whose wildcard arms would otherwise report `Unknown`, with a test for each. Update `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. ## Required diff --git a/quest/m1/tokio-test-ports.md b/quest/m1/tokio-test-ports.md index 384654dc0f..d62f1a92c6 100644 --- a/quest/m1/tokio-test-ports.md +++ b/quest/m1/tokio-test-ports.md @@ -13,4 +13,6 @@ number, which was never reserved on TCP. Bind the QUIC and WebSocket listeners each on `:0` and hand the test client the WebSocket port explicitly, adding a client config override for the fallback URL's port if none exists (test-only if possible; otherwise note it as a public API -addition). No retry. No wire change. +addition). No retry: this replaces the bounded bind retry +[#4055](https://github.com/moq-dev/moq/pull/4055) adds, if that lands first. +No wire change. diff --git a/quest/m2/js-discontinuity.md b/quest/m2/js-discontinuity.md index 51669f3cbd..bbdd95202e 100644 --- a/quest/m2/js-discontinuity.md +++ b/quest/m2/js-discontinuity.md @@ -10,6 +10,11 @@ same name means a routine group close in Rust and a timeline break in JS. ## Plan +[#4045](https://github.com/moq-dev/moq/pull/4045) deletes +`quest/m1/js-publish-discontinuity.md`, since #3982 already put the marker +in `cut()`; this rename is the only remaining JS work. + + In `js/hang/src/container/legacy.ts`, rename the public `cut(end?)` to `discontinuity()` (taking the same optional end) and keep the routine close private until a caller needs it public. Move `js/publish/src/video/encoder.ts`