From c1651c33b2bba7a77b819c0167b5ba33ecd2dd9d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 10:34:37 -0700 Subject: [PATCH 1/3] quest: plan moq-net API audit follow-ups (checkpoint) Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/README.md | 4 +++ quest/m1/api-audit/ietf-private.md | 42 +++++++++++++++++++++++ quest/m1/api-audit/lifecycle.md | 54 ++++++++++++++++++++++++++++++ quest/m1/api-audit/net-exports.md | 19 ++++++++++- quest/m1/api-audit/net-names.md | 50 +++++++++++++++++++++++++++ quest/m1/api-audit/relay-config.md | 5 +-- quest/m1/api-audit/requests.md | 31 +++++++++++++++++ quest/m1/public-model/deferred.md | 6 +++- 8 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 quest/m1/api-audit/ietf-private.md create mode 100644 quest/m1/api-audit/lifecycle.md create mode 100644 quest/m1/api-audit/net-names.md create mode 100644 quest/m1/api-audit/requests.md diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 250bb6bc3fa..7e75d1ec6c1 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -90,6 +90,10 @@ Rejected in planning: - [Transport config](/quest/m1/api-audit/transport-config.md) - moq-tokio and moq-quic configs have one meaning for unset, enums for modes, no order-dependent setters, and no leftover features - [Relay config](/quest/m1/api-audit/relay-config.md) - moq-relay and its embedders load config through one path that refuses old spellings, under names that do not collide - [Auth and clock names](/quest/m1/api-audit/auth-clock-names.md) - refusal and reason types have distinct names, and the Rust and JS clocks mirror each other +- [moq-transport stays private](/quest/m1/api-audit/ietf-private.md) - no public moq-net item exists only for moq-transport, and a client names its request target once as a URL +- [One lifecycle vocabulary](/quest/m1/api-audit/lifecycle.md) - finish, abort, closed, and close mean one thing on every handle, and Rust and JS observers fire alike +- [moq-net names say one thing](/quest/m1/api-audit/net-names.md) - demand says `wanted`, colliding stats and transport names are renamed, and Rust and JS info lookups mirror +- [Request handlers are named requests](/quest/m1/api-audit/requests.md) - `dynamic()` and `Dynamic` become `requests()` and `Requests` at every layer ## Related diff --git a/quest/m1/api-audit/ietf-private.md b/quest/m1/api-audit/ietf-private.md new file mode 100644 index 00000000000..29c7e1ecc30 --- /dev/null +++ b/quest/m1/api-audit/ietf-private.md @@ -0,0 +1,42 @@ +# [M] moq-transport stays private + +## Goal + +No public moq-net or `@moq/net` item exists only for moq-transport, except +the recorded exceptions below. A client names its request target once, as a +URL, and each version sends the part it carries. + +## Plan + +Decided 2026-10-23 (API audit follow-up, maintainer): + +- `track::Request::stage` and `track::Staged` (`rs/moq-net/src/model/track.rs`), + used only by the moq-transport subscriber, become `pub(crate)`. JS + `Track.Staged` and `Request.stage` (`js/net/src/track.ts`), used only by + `ietf/subscriber.ts`, become `@internal`. +- `Client::with_path` and `Client::with_authority` become one + `Client::with_url(url::Url)`. moq-net gains the `url` dependency and owns + what moq-tokio's `setup_path` and `setup_authority` decide today + (`rs/moq-tokio/src/client.rs`): the scheme says what rides SETUP (path for + moq-lite, path and authority for moq-transport, nothing where the URI + travels in the transport). moq-tokio, moq-uring, moq-wasm, and moq-ffi call + it instead. +- `transport::Binding { Uri, InBand }` becomes `transport::Address { Uri, Setup }` + with `fn address()`: "binding" means a language binding in this repository. + +Recorded exceptions, so the next audit skips them: + +- moq-transport-only error variants (`Error::{InvalidJoiningRequestId, + InvalidRange, Remote, UnsupportedExtension}`, the `DecodeError` auth-token + variants) and the session error codes the lite draft registers with + moq-transport's meaning stay public. Errors are fine; only dead ones go + ([net-exports](/quest/m1/api-audit/net-exports.md)). +- `setup::Extensions::{auth, solicit, epoch}` stay as interop knobs, and so + does the relay's `[listen|connect.extensions]` TOML. Document each field + as affecting moq-transport only. +- `setup::Token` and `server::Handshake::token` stay: + [token in band](/quest/m1/auth/token-in-band.md) gives lite-07 a SETUP + token, which makes them shared and drops the moq-transport wording. + +Public API: moq-net and `@moq/net` breaking (`with_url`, `Address`, `Staged` +private), and every transport crate and binding that sets a path. Wire: none. diff --git a/quest/m1/api-audit/lifecycle.md b/quest/m1/api-audit/lifecycle.md new file mode 100644 index 00000000000..4a416ec06c6 --- /dev/null +++ b/quest/m1/api-audit/lifecycle.md @@ -0,0 +1,54 @@ +# [L] One lifecycle vocabulary + +## Goal + +Every model handle in moq-net and `@moq/net` (broadcast, track, group, their +demand, `Finish`, listings) uses each lifecycle word with one meaning, and +Rust and JS agree on when each observer fires. + +## Plan + +Decided 2026-10-23 (API audit follow-up, maintainer), from a matrix of every +handle's writer verbs and observers: + +- **`finish()` / `finish_at(end)`** declare a clean end. Not terminal: lower + writes stay possible. JS gains `Track.Producer.finish()`. +- **`abort(err)`** ends with an error and is terminal. An abort after the + outcome settled (a finished group, a track whose end was reached) only + releases cache: readers keep the clean outcome. Tracks behave this way + today; groups change to match and record no `cut` unless the abort is + eviction policy. +- **`closed()`** on producers, `Finish`, and demand handles resolves once the + outcome is final: a settled finish, an abort, or the last writer dropping. + Rust returns `Result<(), Error>` (`Ok` for clean), JS `Error | null`. It + never fires because readers left; `unused` reports that. Today Rust + `closed()` ignores finish at every layer and reads `Dropped` after a clean + close. +- **`close()`** is JS-only and releases that handle, like a Rust drop. It + never changes state other handles share. Today a JS group, announce, or + broadcast consumer's `close()` ends it for every reader, and the last + broadcast consumer's close ends the producer; check what the JS wire layer + relies on before changing it. `broadcast::Producer::close` keeps its name: + a broadcast has no abort, and it means "drop every clone now". + +Changes: + +- Rust `track::Producer::close()` folds into `abort(Error::SessionClosed)`. + Readers that drained a closed copy to a clean end now get `SessionClosed`: + update `resume.rs`'s clean-end mapping and the sessions that call it; + failover is unchanged since `route_failed` already lists both errors. +- Rust `group::Producer::closed`/`poll_closed` fire on finish; sessions that + need abort-only get a private poll. +- JS `Group.Producer.close(abort?)` and `Track.Producer.close(abort?)` split + into `finish()` and `abort(err)`. +- JS `Finish.closed` also fires when a newer epoch supersedes it, as Rust does. +- JS `Lister.ended` becomes `closed`. JS `Demand.unused` rejects on close as + Rust's does. +- Rust `track::Producer::end()` becomes `finished()` (the declared end, a + snapshot). JS `Subscriber.end()` and `Ordered.end()` go: readers await + `finished()`. + +Behavior changes get tests on mock time in both languages. + +Public API: moq-net and `@moq/net` breaking (observer semantics, JS producer +verbs, renames). Wire: none. diff --git a/quest/m1/api-audit/net-exports.md b/quest/m1/api-audit/net-exports.md index 0e69ac57b7b..7400c8e3d3f 100644 --- a/quest/m1/api-audit/net-exports.md +++ b/quest/m1/api-audit/net-exports.md @@ -1,4 +1,4 @@ -# [S] moq-net exports with a consumer +# [M] moq-net exports with a consumer ## Goal @@ -52,4 +52,21 @@ JS (`js/net/src/`): - Stale docs: `{@link update}` and `{@link accept}` in `origin.ts`, `dynamic` serving "tracks", undocumented `Timed`, `SessionCode`, `StreamCode`. +Added 2026-10-23 (API audit follow-up, maintainer): + +- `Epoch::time` and FFI `moq_epoch_time_ms` with its wrappers: only binding + tests read them. `moq_mint_epoch` stays. +- `announce::Consumer::is_synced` (tests only) goes test-only, and + `announce::Consumer::synced` goes; `poll_synced` stays for `Follow`. +- `varint::MAX_QUIC`: `pub(crate)`. +- `track::Producer::drop_groups` and JS `Track.Producer.dropGroups`: internal. + Dropping a range publicly belongs on `track::Fill` + ([dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md)). +- JS `Group.Demand` and `ConnectProps`/`AcceptProps.extensions`: `@internal`. +- Dead code: `Error::TooManyParameters` is never constructed; delete it. + +Kept, so the next audit skips them: `transport::memory` stays public and +ungated, since an in-process transport is real API that other crates' tests +use. + Public API: removals and renames in moq-net and `@moq/net`. Wire: none. diff --git a/quest/m1/api-audit/net-names.md b/quest/m1/api-audit/net-names.md new file mode 100644 index 00000000000..3026fb3c97d --- /dev/null +++ b/quest/m1/api-audit/net-names.md @@ -0,0 +1,50 @@ +# [L] moq-net names say one thing + +## Goal + +moq-net and `@moq/net` names don't collide with other items, don't describe +the wrong shape, and mirror each other. Each rename reaches the bindings and +docs that expose it. + +## Plan + +Decided 2026-10-23 (API audit follow-up, maintainer): + +- **Demand says `wanted`.** `group::Demand` and `broadcast::Demand` drop + `is_used`/`used`/`unused`/`poll_used`/`poll_unused` for `track::Demand`'s + `wanted()` snapshot plus `wanted_changed()` (and its poll). JS mirrors it + (`Broadcast.Demand`'s `used` getter and `unused()` promise). +- **Allocator.** `Session::bandwidth()` returns the send allocator beside + `send_bandwidth()`/`recv_bandwidth()` estimates: rename it `allocator()`, and + `Client::with_bandwidth` `with_allocator`. +- **Received copies.** `track::Request::request_start(floor)` is a builder + that requests nothing: `with_floor(floor)`. `track::Producer::received()` + reads as a predicate: `set_received()`. +- **Stats and transport collisions.** `stats::Control` (vs `track::Control`) + becomes `stats::Message`; `stats::Bytes` (vs `bytes::Bytes`) becomes + `stats::Totals`; the trait `transport::Stats` (vs `session::Stats` and the + `stats` module) becomes `transport::Metrics`. +- **Wire owns bytes.** `transport::Metrics` drops `bytes_sent` and + `bytes_received`: `transport::Wire` already counts them, and + `session::Stats` reads them from it. `StatsUnavailable` goes; the trait + method gets a default. +- **Credential.** `auth::Auth` (JS `Auth.Auth`) is one presented credential: + `auth::Credential` in Rust, JS, and the FFI, including moq-tokio's + `connection::auth::Auth`. Update [issuer](/quest/m1/auth/issuer.md) and + the [auth epic](/quest/m1/auth/README.md), which use the old name. +- **Info.** `track::Consumer::query() -> Pending` becomes + `info() -> Pending`, mirroring JS `Track.Consumer.info()`. + `Pending` stays generic over the task so poll callers and `track()` keep + working; awaiting it still yields `Result`. +- **Reservation.** `bandwidth::Reservation::consumer()` becomes `consume()`; + JS `Reservation.grant` (vs `Auth.Grant`) becomes `rate`, as + [binding trim](/quest/m1/api-audit/binding-trim.md) names it. +- **JS groups.** `Track.Producer.writeGroup` becomes `createGroup`, Rust's name. + +Kept, so the next audit skips them: `largest()` stays a snapshot on +`track::Producer` and a stream on readers, and `subscription()` stays the +aggregate on producers and requests: the receiver type tells them apart. +`server::Activity` and `stats::Refusal` stay in moq-net. + +Public API: moq-net, `@moq/net`, moq-tokio, moq-ffi and its wrappers renamed. +Wire: none. diff --git a/quest/m1/api-audit/relay-config.md b/quest/m1/api-audit/relay-config.md index 08e0072eb52..d03dd09daef 100644 --- a/quest/m1/api-audit/relay-config.md +++ b/quest/m1/api-audit/relay-config.md @@ -26,8 +26,9 @@ Decided 2026-10-22 (API audit): `*.tls.root`: `[cluster.api] url`, `tls.root`. - `auth.verifications`/`Auth::with_verifications(usize)` is used only by a bench and panics on 0: a private constant. -- `[listen|connect.extensions]` is TOML-only and unset everywhere: drop the - config field and `Web::with_extensions`, keep the Rust API. +- `[listen|connect.extensions]` stays (maintainer, 2026-10-23, reversing the + drop): it is the operator's interop knob for moq-transport extensions + ([moq-transport stays private](/quest/m1/api-audit/ietf-private.md)). - `uring::Workers::spawn` then `serve()` is order-dependent (`serve()` alone no-ops): typestate `spawn(self) -> Spawned`. - `cluster::with_client_tls` repeats TLS `with_connect` holds: delete. diff --git a/quest/m1/api-audit/requests.md b/quest/m1/api-audit/requests.md new file mode 100644 index 00000000000..fc79617e167 --- /dev/null +++ b/quest/m1/api-audit/requests.md @@ -0,0 +1,31 @@ +# [L] Request handlers are named requests + +## Goal + +The handle that answers incoming requests is named for its role at every +layer: `origin`, `broadcast`, and `track` producers have `requests()` +returning `Requests`, which yields `Request`s, in Rust, JS, and the bindings. + +## Plan + +Decided 2026-10-23 (API audit follow-up, maintainer): `dynamic()` names the +mechanism; `auth::Handle::requests() -> auth::Requests` is the precedent. + +- Rename `{origin,broadcast,track}::Producer::dynamic()` (and + `track::Request::dynamic()`) to `requests()`, and each `Dynamic` type to + `Requests`. JS `dynamic()`/`Dynamic` mirror it. moq-ffi and its wrappers + follow; the frozen hand-written moq-c ABI stays as it is. +- The hidden peer-route hatches (`dynamic_route`, `update_route`) follow the + rename only if [routes](/quest/m1/public-model/routes.md) has not made + them private by then. +- Quests that say `Dynamic` (subscribe ranges, public model, archive) switch + to the new name in this PR. + +About 240 Rust, 30 binding, and 10 JS call sites; land it in one sweep. + +Public API: moq-net, `@moq/net`, moq-ffi and wrappers renamed. Wire: none. + +## Related + +- [Claim request type](/quest/m1/public-model/claim-request-type.md) - reshapes the origin claim this renames +- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - adds `requested_fill` to the track handle this renames diff --git a/quest/m1/public-model/deferred.md b/quest/m1/public-model/deferred.md index 6287e4dc746..9dd1977a3ff 100644 --- a/quest/m1/public-model/deferred.md +++ b/quest/m1/public-model/deferred.md @@ -42,7 +42,11 @@ the prerequisites will have deleted or renamed most of these. shape Rust settled in [moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111): `finish_at` and `close` on a `received()` copy, and `start_at` after - `track::Request::request_start`. + `track::Request::request_start`. Mirror the names + [lifecycle](/quest/m1/api-audit/lifecycle.md) and + [net names](/quest/m1/api-audit/net-names.md) settle: `close` becomes + `abort`, `received()` becomes `set_received()`, and `request_start` + becomes `with_floor`. Tests: as in the other children, mock time in each language, with the built-in sessions moved onto the public calls. From d2a9ef47053a0f59180c442b99dfebf5b282a409 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 11:18:09 -0700 Subject: [PATCH 2/3] quest: plan moq-net API audit follow-ups and retire the public-model goal Adds api-audit children for moq-transport-only surface, one lifecycle vocabulary, net renames, and dynamic() -> requests(). Folds public-model into api-audit with its goal dropped: session-only items stay private, serve/answer/claim-request-type are reshaped, routes and deferred are deleted, and the model-markers lint that enforced the old goal goes. Co-Authored-By: Claude Opus 5.5 --- .github/justfile | 1 - quest/m1/README.md | 11 +- quest/m1/api-audit/README.md | 11 +- quest/m1/api-audit/announce-owner.md | 3 +- quest/m1/api-audit/answer.md | 27 ++ quest/m1/api-audit/claim-request-type.md | 33 ++ quest/m1/api-audit/ietf-private.md | 11 +- quest/m1/api-audit/js-prepared.md | 2 +- quest/m1/api-audit/lifecycle.md | 12 +- quest/m1/api-audit/model-private.md | 36 +++ quest/m1/api-audit/net-exports.md | 24 +- quest/m1/api-audit/net-names.md | 14 +- quest/m1/api-audit/requests.md | 10 +- quest/m1/api-audit/serve.md | 31 ++ quest/m1/audit-lints.md | 3 - quest/m1/ietf-extension/README.md | 9 +- quest/m1/public-model/README.md | 114 ------- quest/m1/public-model/answer.md | 142 --------- quest/m1/public-model/claim-request-type.md | 41 --- quest/m1/public-model/deferred.md | 58 ---- quest/m1/public-model/routes.md | 88 ------ quest/m1/public-model/serve.md | 156 ---------- quest/m1/track-live.md | 7 +- quest/m2/audit-dead-code.md | 2 +- quest/m2/release/api-review.md | 2 +- rs/justfile | 1 - sh/js/workspace.py | 1 - sh/rs/model-markers.py | 326 -------------------- sh/rs/model-markers.test.py | 188 ----------- 29 files changed, 192 insertions(+), 1172 deletions(-) create mode 100644 quest/m1/api-audit/answer.md create mode 100644 quest/m1/api-audit/claim-request-type.md create mode 100644 quest/m1/api-audit/model-private.md create mode 100644 quest/m1/api-audit/serve.md delete mode 100644 quest/m1/public-model/README.md delete mode 100644 quest/m1/public-model/answer.md delete mode 100644 quest/m1/public-model/claim-request-type.md delete mode 100644 quest/m1/public-model/deferred.md delete mode 100644 quest/m1/public-model/routes.md delete mode 100644 quest/m1/public-model/serve.md delete mode 100644 sh/rs/model-markers.py delete mode 100644 sh/rs/model-markers.test.py diff --git a/.github/justfile b/.github/justfile index 0bfbf22fba3..5b30219337e 100644 --- a/.github/justfile +++ b/.github/justfile @@ -51,7 +51,6 @@ test: python3 sh/js/workspace.test.py python3 sh/js/audit.test.py python3 sh/rs/test-lints.test.py - python3 sh/rs/model-markers.test.py python3 sh/gh/nightly.test.py python3 sh/gh/pages-clean.test.py python3 sh/gh/bench-report.test.py diff --git a/quest/m1/README.md b/quest/m1/README.md index 24fdc76a867..7d297e1a170 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -17,7 +17,7 @@ here instead of moving out; the cut is what ships them. - Preserve epoch correctness, routing recovery, outbound federation, and announcement takeover on both lite and IETF. Catalogs, long-lived readers, and late joiners obey the same identity contract. -- Keep IETF passthrough, the public model audit, the broad clock conversion, +- Keep IETF passthrough, the API audit, the broad clock conversion, archive replay/DVR correctness, and the TS fidelity expansion in m1. Keep their correctness, interoperability, memory, and performance evidence with them. @@ -42,10 +42,10 @@ acceptance with the downstream operator. Keep one owner per shared code area and order prerequisites before consumers (re-ranked in the 2026-10-20 audit: auth before cluster routing, the C line -before the binding breaks, and every lite-07 and public model prerequisite -before those quests). -The moq-transport passthrough fields remain crate-private, the existing -explicit exception to the public model audit. +before the binding breaks, and every lite-07 prerequisite before those +quests). +The moq-transport passthrough fields remain crate-private: no public moq-net +item exists only for moq-transport (maintainer, 2026-10-23). Added 2026-10-16: refresh, a breaking `Demand` change, so it lands before the cut. @@ -96,7 +96,6 @@ tracks, so per-group stats stop costing cluster announces. - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [Moxygen minimal interop](/quest/m1/smoke-moxygen.md) - waiting on Moxygen to send a registered filter type, then the smoke lane stops hiding its failure - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause -- [A public model](/quest/m1/public-model/README.md) - every model item a built-in session reaches is public, unnecessary, or marked deliberately private, so a custom publisher, subscriber, or relay builds from the public model alone - [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break has its MIGRATE entry - [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 diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index da4058b2942..7bf35290c22 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -8,9 +8,8 @@ or is removed or made private. Overlapping operations are collapsed into one. Names mirror across Rust, JS, and the bindings. Every break has its `MIGRATE.md` entry. -Non-goals: redesigning surfaces that already have their own quest (the -[public model](/quest/m1/public-model/README.md), -[subscribe ranges](/quest/m1/subscribe-ranges/README.md), +Non-goals: redesigning surfaces that already have their own quest +([subscribe ranges](/quest/m1/subscribe-ranges/README.md), [archives](/quest/m1/archive/README.md), [compressed tracks](/quest/m1/compress/README.md), [A/V clock](/quest/m1/av-clock.md)), and inherited debt that a later quest @@ -89,6 +88,10 @@ Rejected in planning: - [Transport config](/quest/m1/api-audit/transport-config.md) - moq-tokio and moq-quic configs have one meaning for unset, enums for modes, no order-dependent setters, and no leftover features - [Relay config](/quest/m1/api-audit/relay-config.md) - moq-relay and its embedders load config through one path that refuses old spellings, under names that do not collide - [Auth and clock names](/quest/m1/api-audit/auth-clock-names.md) - refusal and reason types have distinct names, and the Rust and JS clocks mirror each other +- [Session-only items stay private](/quest/m1/api-audit/model-private.md) - model items only the built-in sessions use are private, and the pending and private marks are gone +- [Only sessions accept a request](/quest/m1/api-audit/claim-request-type.md) - accepting an application claim's request fails to compile, and the peer-route hatches are crate-private +- [One range setter](/quest/m1/api-audit/serve.md) - `with_range` replaces seven cursor setters and nothing else of the serving cursor goes public +- [A request's source enforces its validity](/quest/m1/api-audit/answer.md) - a stale source refuses new track requests by itself, with no public additions - [moq-transport stays private](/quest/m1/api-audit/ietf-private.md) - no public moq-net item exists only for moq-transport, and a client names its request target once as a URL - [One lifecycle vocabulary](/quest/m1/api-audit/lifecycle.md) - finish, abort, closed, and close mean one thing on every handle, and Rust and JS observers fire alike - [moq-net names say one thing](/quest/m1/api-audit/net-names.md) - demand says `wanted`, colliding stats and transport names are renamed, and Rust and JS info lookups mirror @@ -96,7 +99,7 @@ Rejected in planning: ## Related -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - removes `accept` from application requests; [the origin announces](/quest/m1/api-audit/announce-owner.md) narrows `Prepared` beside it +- [Claim request type](/quest/m1/api-audit/claim-request-type.md) - removes `accept` from application requests; [the origin announces](/quest/m1/api-audit/announce-owner.md) narrows `Prepared` beside it - [Readers follow restarts](/quest/m1/sink-restart-follow.md) - moves readers onto the follow operation that [two broadcast resolutions](/quest/m1/api-audit/resolve.md) names - [API-diff check on release PRs](/quest/m1/api-diff.md) - the backstop that catches breaks this epic misses - [Main is no slower than release](/quest/m1/release-perf/README.md) - the other release-readiness check diff --git a/quest/m1/api-audit/announce-owner.md b/quest/m1/api-audit/announce-owner.md index 8b06e16b98f..28c7f9c6f79 100644 --- a/quest/m1/api-audit/announce-owner.md +++ b/quest/m1/api-audit/announce-owner.md @@ -28,5 +28,4 @@ trap). Wire: none. ## Related -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - splits `accept` off application requests in the same file; coordinate the order -- [Public routes and answering](/quest/m1/public-model/answer.md) - later makes the forwarding claim public +- [Claim request type](/quest/m1/api-audit/claim-request-type.md) - splits `accept` off application requests in the same file; coordinate the order diff --git a/quest/m1/api-audit/answer.md b/quest/m1/api-audit/answer.md new file mode 100644 index 00000000000..9d6ae251a5a --- /dev/null +++ b/quest/m1/api-audit/answer.md @@ -0,0 +1,27 @@ +# [S] A request's source enforces its validity + +## Goal + +The source minted for a dynamic request refuses new track requests with +`Unroutable` once its route is replaced or its selection period retires, so +no session can serve a stale instance by forgetting a check. Tracks it +already accepted keep running and finish. Nothing becomes public. + +## Plan + +Decided 2026-10-23 (maintainer), reshaping +[moq-dev/moq#6411](https://github.com/moq-dev/moq/pull/6411) after the +public-model goal was dropped +([session-only items stay private](/quest/m1/api-audit/model-private.md)): + +- Keep only the internal simplification: `broadcast::RequestValidity` and + `origin::Selection` checks move into the source. The answering session + learns of it when the source's request stream ends. +- Drop every planned public addition: `answer_source`, the unused-race + family (`poll_unheld`, `close_unheld`, group `poll_unused` and + `abort_unused`, `reject_unused`), `Producer::empty`/`Consumer::empty`, + `Dynamic::withdrawn`, JS `Origin.Producer.fallback()`, and JS + `Broadcast.requested` (deferred until a JS app needs it). +- Delete `wire.ts` `OriginConsumer.broadcasts`: registered, never read. + +Public API: none. Wire: none. diff --git a/quest/m1/api-audit/claim-request-type.md b/quest/m1/api-audit/claim-request-type.md new file mode 100644 index 00000000000..07112d9ecaf --- /dev/null +++ b/quest/m1/api-audit/claim-request-type.md @@ -0,0 +1,33 @@ +# [S] Only sessions accept a request + +## Goal + +Answering an application claim's request with `accept` fails to compile in +Rust and to typecheck in TypeScript instead of panicking or throwing at +runtime. The peer-route hatches are crate-private. + +## Plan + +Decided 2026-10-21 (maintainer), after a downstream application's handler +called `request.accept(..)` on a `dynamic()` claim, which compiled and would +have panicked on the first request. Reshaped 2026-10-23 after the +public-model goal was dropped +([session-only items stay private](/quest/m1/api-audit/model-private.md)), +for [moq-dev/moq#6496](https://github.com/moq-dev/moq/pull/6496): + +- `origin::Producer::dynamic_route`, `origin::Dynamic::update_route`, and + `origin::Request::accept` become `pub(crate)` (JS `dynamicRoute`, + `updateRoute`, and `Request.accept` are already `@internal`). An + application answers with `prepare` then `publish`, or `reject`. +- Move the out-of-crate tests and benches that call them (moq-mux, moq-hls, + moq-cli, moq-gst, moq-c, moq-ffi, moq-net integration, js/watch) onto + `prepare`/`publish`. +- No `Forward`/`ForwardRequest` split: visibility already makes the call + unreachable. Delete the runtime assert and its test; a compile-fail + doctest and a `@ts-expect-error` line replace them. + +Kept from [moq-dev/moq#5678](https://github.com/moq-dev/moq/pull/5678): +prepare and publish, one epoch per answer, and the cold-start reset until +[announce takeover](/quest/m1/announce-takeover.md) lands. + +Public API: moq-net breaking (three hidden items go private). Wire: none. diff --git a/quest/m1/api-audit/ietf-private.md b/quest/m1/api-audit/ietf-private.md index 29c7e1ecc30..97430df84d4 100644 --- a/quest/m1/api-audit/ietf-private.md +++ b/quest/m1/api-audit/ietf-private.md @@ -10,10 +10,9 @@ URL, and each version sends the part it carries. Decided 2026-10-23 (API audit follow-up, maintainer): -- `track::Request::stage` and `track::Staged` (`rs/moq-net/src/model/track.rs`), - used only by the moq-transport subscriber, become `pub(crate)`. JS - `Track.Staged` and `Request.stage` (`js/net/src/track.ts`), used only by - `ietf/subscriber.ts`, become `@internal`. +- `track::Request::stage` and `track::Staged`, used only by the moq-transport + subscriber, go private with the other session-only items in + [session-only items stay private](/quest/m1/api-audit/model-private.md). - `Client::with_path` and `Client::with_authority` become one `Client::with_url(url::Url)`. moq-net gains the `url` dependency and owns what moq-tokio's `setup_path` and `setup_authority` decide today @@ -38,5 +37,5 @@ Recorded exceptions, so the next audit skips them: [token in band](/quest/m1/auth/token-in-band.md) gives lite-07 a SETUP token, which makes them shared and drops the moq-transport wording. -Public API: moq-net and `@moq/net` breaking (`with_url`, `Address`, `Staged` -private), and every transport crate and binding that sets a path. Wire: none. +Public API: moq-net breaking (`with_url`, `Address`), and every transport +crate and binding that sets a path. Wire: none. diff --git a/quest/m1/api-audit/js-prepared.md b/quest/m1/api-audit/js-prepared.md index 84108f75c22..47810bb8681 100644 --- a/quest/m1/api-audit/js-prepared.md +++ b/quest/m1/api-audit/js-prepared.md @@ -33,4 +33,4 @@ Public API: `@moq/net` breaking (`Prepared.reject` to `close`). Wire: none. ## Related -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - the JS `accept` half lives there +- [Claim request type](/quest/m1/api-audit/claim-request-type.md) - the JS `accept` half lives there diff --git a/quest/m1/api-audit/lifecycle.md b/quest/m1/api-audit/lifecycle.md index 4a416ec06c6..60e1acd57e2 100644 --- a/quest/m1/api-audit/lifecycle.md +++ b/quest/m1/api-audit/lifecycle.md @@ -33,10 +33,14 @@ handle's writer verbs and observers: Changes: -- Rust `track::Producer::close()` folds into `abort(Error::SessionClosed)`. - Readers that drained a closed copy to a clean end now get `SessionClosed`: - update `resume.rs`'s clean-end mapping and the sessions that call it; - failover is unchanged since `route_failed` already lists both errors. +- Rust `track::Producer::close()`, crate-private after + [session-only items stay private](/quest/m1/api-audit/model-private.md), + folds into `abort` with the clean-close error + ([moq-dev/moq#6482](https://github.com/moq-dev/moq/pull/6482) makes it + `Stream(Session(Cancel))`). Readers that drained a closed copy to a clean + end now get that error: update `resume.rs`'s clean-end mapping and the + sessions that call it; failover is unchanged since `route_failed` already + accepts it. - Rust `group::Producer::closed`/`poll_closed` fire on finish; sessions that need abort-only get a private poll. - JS `Group.Producer.close(abort?)` and `Track.Producer.close(abort?)` split diff --git a/quest/m1/api-audit/model-private.md b/quest/m1/api-audit/model-private.md new file mode 100644 index 00000000000..ab1dc02c16c --- /dev/null +++ b/quest/m1/api-audit/model-private.md @@ -0,0 +1,36 @@ +# [M] Session-only model items stay private + +## Goal + +moq-net and `@moq/net` model items that only the built-in sessions use are +private. The public model serves applications: publish, subscribe, and answer +on demand. Nothing in the source marks items as pending or deliberately +private. + +## Plan + +Decided 2026-10-23 (maintainer): the old public-model goal, "a third party +builds its own publisher, subscriber, or forwarding relay from the public +model alone", is dropped. It made every item a built-in session reaches +public, with no consumer outside the sessions. Items go public again only +for a named consumer. + +- Revert the session-only items + [moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111) made public: + `track::Producer::{received, drop_groups, close}`, + `track::Request::{request_start, stage}`, `track::Staged`, and + `group::Producer::info` (it duplicates `Deref`) become + `pub(crate)`. JS `Track.Request.stage`, `Track.Staged`, and + `Track.Producer.dropGroups` become internal again (`dropGroups` returns to + `hooks.dropTrackGroups`). Keep `create_frame(self)`: hang and moq-mux use it. +- Delete every `// pending:` and `// private:` mark in `rs/moq-net` and + `js/net`; their lint is already gone. The moq-transport passthrough + exception stays documented in + [moq-transport passthrough](/quest/m1/ietf-extension/README.md). + +Rejected: the toy-transport integration tests proving a third party could +build from the public model; the goal they proved is dropped. + +Public API: moq-net and `@moq/net` removals, all main-only (none reached +`release`), so no MIGRATE entry beyond fixing the unreleased #6111 one. +Wire: none. diff --git a/quest/m1/api-audit/net-exports.md b/quest/m1/api-audit/net-exports.md index 7400c8e3d3f..7171c7921a9 100644 --- a/quest/m1/api-audit/net-exports.md +++ b/quest/m1/api-audit/net-exports.md @@ -59,11 +59,27 @@ Added 2026-10-23 (API audit follow-up, maintainer): - `announce::Consumer::is_synced` (tests only) goes test-only, and `announce::Consumer::synced` goes; `poll_synced` stays for `Follow`. - `varint::MAX_QUIC`: `pub(crate)`. -- `track::Producer::drop_groups` and JS `Track.Producer.dropGroups`: internal. - Dropping a range publicly belongs on `track::Fill` - ([dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md)). - JS `Group.Demand` and `ConnectProps`/`AcceptProps.extensions`: `@internal`. -- Dead code: `Error::TooManyParameters` is never constructed; delete it. +- Dead code, found by marking every pub item `#[deprecated]` and building + the workspace. Delete what nothing references or constructs: + `Error::TooManyParameters`, `DecodeError::{ExpectedData, Missing, + Unsupported}`, `frame::Buffer::{is_full, filled_mut}`, + `frame::Consumer::poll_read_all`, `track::Ordered::poll_finished`, + `Path::next_part`, `stats::Registry::exclude`, + `stats::Traffic::{is_announced, total_bytes}`, `stats::Tier::is_default`, + `transport::poll::Buffered::get_mut`, `setup::Token::CAT`; JS + `Varint.sizeLeadingOnes`, `SetupOptions.{removeBytes, removeVarint}`, and + the `deliveryTimeout` accessors. +- Test-only plumbing, deleted with its tests moved to what remains: the whole + `broadcast::Demand` with both `demand()` getters (the Rust twin of the JS + item above), `goaway::Consumer::{peek, recv}`, + `bandwidth::Producer::{abort, closed, used, unused}`, + `Version::{from_alpn, is_ietf, names}`, `Server::with_origin`, + `stats::Handle::{parent, tier}`, and group + `poll_unused`/`abort_unused` (their `allow(dead_code)` waited on an + answering API that is now dropped). Test-only reader and writer + conveniences (`group::Consumer::read_frames`, `frame::Consumer::read_all`, + and the like) stay: applications are their consumer. Kept, so the next audit skips them: `transport::memory` stays public and ungated, since an in-process transport is real API that other crates' tests diff --git a/quest/m1/api-audit/net-names.md b/quest/m1/api-audit/net-names.md index 3026fb3c97d..b7f4110a65b 100644 --- a/quest/m1/api-audit/net-names.md +++ b/quest/m1/api-audit/net-names.md @@ -10,16 +10,14 @@ docs that expose it. Decided 2026-10-23 (API audit follow-up, maintainer): -- **Demand says `wanted`.** `group::Demand` and `broadcast::Demand` drop +- **Demand says `wanted`.** `group::Demand` drops `is_used`/`used`/`unused`/`poll_used`/`poll_unused` for `track::Demand`'s - `wanted()` snapshot plus `wanted_changed()` (and its poll). JS mirrors it - (`Broadcast.Demand`'s `used` getter and `unused()` promise). + `wanted()` snapshot plus `wanted_changed()` (and its poll). + `broadcast::Demand` is deleted instead + ([net-exports](/quest/m1/api-audit/net-exports.md)). - **Allocator.** `Session::bandwidth()` returns the send allocator beside `send_bandwidth()`/`recv_bandwidth()` estimates: rename it `allocator()`, and `Client::with_bandwidth` `with_allocator`. -- **Received copies.** `track::Request::request_start(floor)` is a builder - that requests nothing: `with_floor(floor)`. `track::Producer::received()` - reads as a predicate: `set_received()`. - **Stats and transport collisions.** `stats::Control` (vs `track::Control`) becomes `stats::Message`; `stats::Bytes` (vs `bytes::Bytes`) becomes `stats::Totals`; the trait `transport::Stats` (vs `session::Stats` and the @@ -37,8 +35,8 @@ Decided 2026-10-23 (API audit follow-up, maintainer): `Pending` stays generic over the task so poll callers and `track()` keep working; awaiting it still yields `Result`. - **Reservation.** `bandwidth::Reservation::consumer()` becomes `consume()`; - JS `Reservation.grant` (vs `Auth.Grant`) becomes `rate`, as - [binding trim](/quest/m1/api-audit/binding-trim.md) names it. + JS `Reservation.grant` (vs `Auth.Grant`) becomes `rate`, as the bindings + name it. - **JS groups.** `Track.Producer.writeGroup` becomes `createGroup`, Rust's name. Kept, so the next audit skips them: `largest()` stays a snapshot on diff --git a/quest/m1/api-audit/requests.md b/quest/m1/api-audit/requests.md index fc79617e167..fdad1be8405 100644 --- a/quest/m1/api-audit/requests.md +++ b/quest/m1/api-audit/requests.md @@ -15,10 +15,10 @@ mechanism; `auth::Handle::requests() -> auth::Requests` is the precedent. `track::Request::dynamic()`) to `requests()`, and each `Dynamic` type to `Requests`. JS `dynamic()`/`Dynamic` mirror it. moq-ffi and its wrappers follow; the frozen hand-written moq-c ABI stays as it is. -- The hidden peer-route hatches (`dynamic_route`, `update_route`) follow the - rename only if [routes](/quest/m1/public-model/routes.md) has not made - them private by then. -- Quests that say `Dynamic` (subscribe ranges, public model, archive) switch +- The peer-route hatches (`dynamic_route`, `update_route`) are crate-private + after [claim request type](/quest/m1/api-audit/claim-request-type.md); + rename them with the rest. +- Quests that say `Dynamic` (subscribe ranges, archive) switch to the new name in this PR. About 240 Rust, 30 binding, and 10 JS call sites; land it in one sweep. @@ -27,5 +27,5 @@ Public API: moq-net, `@moq/net`, moq-ffi and wrappers renamed. Wire: none. ## Related -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - reshapes the origin claim this renames +- [Claim request type](/quest/m1/api-audit/claim-request-type.md) - reshapes the origin claim this renames - [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - adds `requested_fill` to the track handle this renames diff --git a/quest/m1/api-audit/serve.md b/quest/m1/api-audit/serve.md new file mode 100644 index 00000000000..0cc48b9f85b --- /dev/null +++ b/quest/m1/api-audit/serve.md @@ -0,0 +1,31 @@ +# [M] One range setter + +## Goal + +A reader moves its cursor with one consuming `with_range(self, range)` (JS +`withRange`) on `track::Subscriber`, `track::Ordered`, `track::Subscription`, +and `group::Consumer`, replacing `set_groups`, `with_groups`, `start_at`, +`raise_start_to`, `end_at`, `subscription::Cap`, and +`group::Consumer::set_frames`. Nothing else becomes public. + +## Plan + +Decided 2026-10-23 (maintainer), reshaping +[moq-dev/moq#6138](https://github.com/moq-dev/moq/pull/6138) after the +public-model goal was dropped +([session-only items stay private](/quest/m1/api-audit/model-private.md)): + +- Keep `with_range` and the bug fixes #6138 carries: js/json and js/flate + `latest()` keeping the caller's cap, `group::Consumer::poll_finished` + ending cleanly at a frame cap, and a closed JS subscriber's groups still + expiring. +- Everything else #6138 made public for custom publishers stays internal: + `track::Subscriber::{poll_start, poll_drop}`, + `group::Consumer::poll_expired`, `Position::final_sequence`, + `Subscription::{start, end, with_start, with_end}`, and the JS `Recv` + union, `groupChanged`, `Ordered.tryNextGroup`, `finalSequence`, + `Consumer.info({ hold })`, and `Group.Consumer.expired`. The sessions call + them through `pub(crate)` and `@internal` (or the existing hooks). + +Public API: moq-net and `@moq/net` breaking (`with_range` replaces the +setters). Wire: none. diff --git a/quest/m1/audit-lints.md b/quest/m1/audit-lints.md index adb0b75607e..787f18809e4 100644 --- a/quest/m1/audit-lints.md +++ b/quest/m1/audit-lints.md @@ -68,9 +68,6 @@ benches and examples. Biome reports an unused `biome-ignore` as typescript 6 and eslint 9. Files outside any `tsconfig` project can't be typed-linted. Check whether typescript-eslint supports TS 7 before repeating that setup; if it does, consider joining CI. -- `sh/rs/model-markers.py` walks attributes above a model marker, including - ones rustfmt wraps over several lines. Run its tests if a long reason moves - near a marker. ### Results (m0, 2026-10-20) diff --git a/quest/m1/ietf-extension/README.md b/quest/m1/ietf-extension/README.md index 14a9094d7bd..29769718eb5 100644 --- a/quest/m1/ietf-extension/README.md +++ b/quest/m1/ietf-extension/README.md @@ -59,10 +59,9 @@ cannot answer unambiguously is refused. are present. Draft 17 is a separate family because it forbids 7-byte varints. Rewriting them would violate immutability; forwarding unchanged across those families would change their meaning. -- **Public model carve-out.** These fields stay crate-private on purpose, - an exception to [a public model](/quest/m1/public-model/README.md). A - third-party relay built on the public model drops them, as plain lite - does. +- **Crate-private on purpose.** These fields never become public API + ([moq-transport stays private](/quest/m1/api-audit/ietf-private.md)); a + relay that is not moq-relay drops them, as plain lite does. - **Verification:** Rust integration tests per feature in each child, plus the nightly moxygen job owned here. @@ -85,4 +84,4 @@ Work owned by this README, after the children land: ## Related - [#4926](https://github.com/moq-dev/moq/issues/4926) - the declined request for an object-level API; this carries the fields for relays only -- [A public model](/quest/m1/public-model/README.md) - these fields are its deliberate exception +- [moq-transport stays private](/quest/m1/api-audit/ietf-private.md) - no public moq-net item exists only for moq-transport diff --git a/quest/m1/public-model/README.md b/quest/m1/public-model/README.md deleted file mode 100644 index 2d3df7a7f04..00000000000 --- a/quest/m1/public-model/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# A public model that builds any publisher or subscriber - -## Goal - -A third party builds its own publisher, subscriber, or forwarding relay (a -custom transport, relay, or wire) from the public moq-net and @moq/net model -alone. Every model item a built-in session reaches is public, made -unnecessary, or marked deliberately private with its reason. - -Non-goals: changing the wire, or exposing session internals that are not part -of the model (`ietf/`, `lite/`, connection plumbing, wire codecs). - -## Plan - -Planned 2026-10-16 from the inventory below; the per-item decisions live in -the children's Plans, and the maintainer settled the open ones in #5717. - -Rules every child follows: - -- Track metadata is never private (decided 2026-10-11 in the #5283 triage). -- Each item is made public under a role name mirrored in Rust and JS, - replaced by a smaller public handle that makes misuse unrepresentable, or - made unnecessary. Each child lists its public API additions and removals - in its PR for review. -- Deliberately private, marked in a comment on the item so the next - inventory skips it (`// private: passthrough`, `tuning`, - `capability gap`, `test`, or `plumbing`; `sh/rs/model-markers.py` - enforces the marks): - - "moq-transport passthrough only" (decided 2026-10-13; subgroups were - dropped from it in #5704): object IDs and `skip`, object and unknown track properties, END_OF_GROUP status - objects, datagram object IDs, and upstream request signaling. Dynamic - groups left this list when #5930 made `Demand::wanted` public. Owned by [moq-transport passthrough](/quest/m1/ietf-extension/README.md). - A relay built on the public model drops them, as plain moq-lite does. - - "tunes the built-in sessions": constants and wake or allocation batching - that change cost, never behavior a third party can observe. - - "capability gap": a built-in behavior a third party can observe but - not reproduce yet, kept private by a recorded decision that names what - would close it (the idle copy, `track::Producer::set_idle` and `set_live`: - a custom subscriber ends its track when its upstream subscription ends - instead of keeping a lingering copy). - - "plumbing" (JS only): session or wire plumbing hosted beside the model, - such as `wire.ts` `Established`, `wireOf`, `tail.ts`, `consume.ts`, and - `priority.ts`. -- An item still owed public carries `// pending: `. Delete - the mark as the item goes public; the lint fails on a mark left behind, or - on one naming a deleted quest. -- Items replaced by another quest's new API wait for that quest instead of - growing a public shape it deletes: fetch and per-group requests - ([Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md)). - -Inventory method, to rerun before each child starts (counts drift). Rust: -replace every `pub(crate)` under `rs/moq-net/src/model/` with -`pub(in crate::model)`, then run `cargo check -p moq-net --features fuzz` -and restore each item the compiler names until it builds; repeat with -`--tests` for test-only reach. JS: list what `src/{ietf,lite,connection}/` -import from `internal.ts`, `wire.ts`, and modules `index.ts` does not -export, plus `@internal` members (`stripInternal` hides them from the -published types). - -Inventory on `main` at b4eab0db1: - -- Rust has 347 `pub(crate)` items in `model/` and no `#[doc(hidden)]` model - item. 106 are reached by session code, 16 only by session tests, and 225 - only inside `model/`. -- The 106 split into ingest (landed in [moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111)), - [serving](/quest/m1/public-model/serve.md), - [answering](/quest/m1/public-model/answer.md) (with [routes](/quest/m1/public-model/routes.md) split out later), and the - passthrough carve-out (listed in each child where it shares a type). -- The 16 test-only items (`group::Info::produce`, `group::Producer::is_aborted` - and `is_finished`, `track::Info::timed`, `track::Producer::new`, - `publisher_priority` and `start_sequence`, `track::Consumer::poll_group` - and `resume_position`, `FetchOutcome::rejected`, - `origin::Producer::announce` with `AnnounceProducer` and `ProduceTest`, - `origin::Consumer::get_broadcast`) stay crate-private test helpers: a third - party tests through the public API it uses. -- The 225 model-internal items stay private. -- JS publishes only `src/index.ts`, with `stripInternal` on. Sessions reach - past it through `internal.ts` (helpers and 22 `hooks`, 7 of them called - by sessions: `endTrack`, `pendingTrackProducer`, `tryRecvGroup`, - `groupChanged`, `replaceGroups`, `guardGroup`, `readGroupFrame`), the - `wire.ts` views (`Broadcast`, `OriginProducer`, `OriginConsumer`, - `Established`), `@internal` members (`Track.Subscriber.fork`, - `Group.Producer.mirror`), and modules the index does not export - (`hop.ts` values, `announced.ts` `Producer`, `consume.ts`, `tail.ts`, - `priority.ts`). Model-internal hooks (sequence namespaces, timestamps, - arrival, eviction, announcer stamping) stay private. Each session-reached - item is assigned in a child, except `wire.ts` `Established` (`consume`, - `goaway`). It is connection plumbing outside the model, a session handle - the connection layer uses, and stays private like the other excluded - plumbing. - -Work owned by this README, after the children land: - -- Rerun the inventory and confirm every `pub(crate)` left in `model/` is a - marked passthrough, tuning, capability-gap, or test-only item, so the - `// pending:` marks are all gone. -- Prove the result with a toy transport that uses only the public API: an - integration test under `rs/moq-net/tests/` (outside the crate, so the - compiler enforces it) and a JS test importing only the package entrypoint. - Each runs a minimal publisher, subscriber, and forwarding relay with route - ingestion and answering, on mock time, in CI. - -## Required - -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - accepting an application claim's request fails to compile instead of panicking -- [Public serving](/quest/m1/public-model/serve.md) - a custom publisher reads a track's start, cursor, and group expiry for its wire through public calls -- [Public answering](/quest/m1/public-model/answer.md) - a custom relay answers broadcast and track requests on demand and closes them once unused -- [Public routes](/quest/m1/public-model/routes.md) - a custom relay ingests routes under a peer's identity and advertises without echoing them back, once cluster routing lands -- [Public model after ranges, handover, and the JS tail](/quest/m1/public-model/deferred.md) - fetch and track-info answering, the cache peeks, JS `fork`/`mirror`, and JS `endTrack`, once the quests that reshape them land - -## Related - -- [moq-transport passthrough](/quest/m1/ietf-extension/README.md) - the deliberate exception to this rule -- [#5283](https://github.com/moq-dev/moq/pull/5283) - made the END_OF_TRACK location public, the first item under this rule diff --git a/quest/m1/public-model/answer.md b/quest/m1/public-model/answer.md deleted file mode 100644 index 6e702b6f6fd..00000000000 --- a/quest/m1/public-model/answer.md +++ /dev/null @@ -1,142 +0,0 @@ -# [L] Public answering - -## Goal - -A custom relay or session does what the built-in ones do with an origin, -through public moq-net and @moq/net calls only: answer broadcast and track -requests on demand and close them once unused without racing a returning -reader. - -## Plan - -Rules, carve-outs, and the inventory method are in the -[epic](/quest/m1/public-model/README.md). Rerun the inventory first; the -recommendations below are the planning quest's, and the ones marked -**decision** were settled by the maintainer on 2026-10-16 (#5717). - -Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1: - -- `origin::Producer::create_source` (both subscribers): public under a role - name (for example `answer_source`). It mints a broadcast with the origin's - cache policy and stats that answers a `Dynamic` request without entering - the route table, which `broadcast::Producer::new` cannot do. -- `broadcast::SourceGuard` (both subscribers): not model. Move it to the - sessions; a third party drops or closes its own source. -- **Request validity** (`broadcast::RequestValidity`, `origin::Selection`; - both subscribers). **Decision**: - - ✅ Remove the need. The source minted for a `Dynamic` request enforces - validity itself, so no session can serve a stale instance by forgetting - a check. Revised in review (#5717): once its route is replaced or its - selection period retires, the source refuses new track requests with - `Unroutable`, but tracks it already accepted keep running and finish. - `origin::Dynamic::update` and the subscriber drain paths rely on that, - so producer writes must not start failing. The answering session learns - it separately: the source's request stream (`poll_requested_track`) - ends, so it stops answering and drops the source once its accepted - tracks are done. - - A public validity handle every answerer must poll. -- **Unused races** (`broadcast::Producer::poll_unheld`, `close_unheld`, - `group::Producer::poll_unused`, `abort_unused`, - `track::Request::reject_unused`; both subscribers). **Decision**: - - ✅ Public, as one family matching the public - `track::Producer::abort_unused(self, err) -> Result<(), Self>`: each - answerer has `demand()` for the wait and an atomic `*_unused` that - refuses when a reader returned in the gap. Rename `close_unheld` and - `poll_unheld` to the `unused` vocabulary where their semantics match - `broadcast::Demand`, or document the difference. - - Keep them private. -- `origin::Dynamic::withdrawn` (lite subscriber): public, as the explicit - peer withdrawal that differs from a session simply dropping its handle. -- `origin::interest_prefixes` (both subscribers): remove the need through - Rust lazy discovery ([moq-dev/moq#6165](https://github.com/moq-dev/moq/pull/6165)), which gives Rust - the origin's watched interests the way JS has them; make that public in - both languages there or here, whichever lands second. -- `origin::Consumer::discovery`, `includes_hidden` (IETF publisher): fold - into the public `with_hidden` plus a getter, unless Lazy discovery deletes - them first. `beyond` (topping up a feed): re-inventory after Lazy - discovery; public only if it survives. -- `origin::Consumer::request` (lite publisher): remove the need; it is the - body of the public `request_broadcast`. -- `origin::Producer::empty`, `origin::Consumer::empty` (sessions): public. - A scope with no patterns is not an equivalent: `scope` refuses an empty - intersection with `Unauthorized`, which stays, since failing loud on a - scope that names nothing is the right default. The drivers fill an omitted - session half with an empty producer, and answer an out-of-scope announce - interest with an empty consumer, so a custom session needs the same - handles. Restructuring the drivers to run without a half is the - alternative, but it is a bigger change for no public gain. -- `origin::Producer::stats`, `origin::Consumer::stats` (sessions): remove - the need; the caller keeps the `stats::Session` it passed to the public - `with_stats`. -- Test-only: `origin::Producer::announce`, `AnnounceProducer`, - `ProduceTest`, `origin::Consumer::get_broadcast`. - -JS items (`js/net/src/`): - -- `wire.ts` `OriginProducer.requests`, `changed`, `blind`, `answer`, - `routes`, `origin.ts` `RequestSlot`, `internal.ts` `coveringPrefixes` - (the blind-answer loop in `connection/forward.ts`). **Decision**: - - ✅ Replace with the Rust shape. A session answers through the public - `Origin.Dynamic` requests and mints a source per request, as Rust - sessions do, so the slot loop and its view go. - - Make the `wire.ts` views public. -- **Blind answers** (the same loop, raised in review of #5717). Every JS - session answers each request no route covers by subscribing on its peer: - always, and as the only path after a peer refuses or resets discovery. - Lazy discovery keeps a plain JS - `request(path)` blind, so blind answering stays in JS and needs a public - shape before the loop goes. Rust has no blind answers (decided 2026-10-20, - maintainer), so no Rust session needs this shape. - **Decision** (settled 2026-10-16, narrowed to JS 2026-10-20): - - ✅ A fallback answerer, `Origin.Producer.fallback()` in JS. The name is still open: propose - alternatives in this quest's PR before shipping it. It returns the same - `Dynamic` request stream, scoped to the handle's allowed paths. It - receives only requests that no announced or served route covers. Once - the table routes a path, the new route takes only unresolved and new - requests for it. A request the fallback already resolved ends with an - error, since a blind answer has no epoch to prove the route serves the - same instance (never stitch). It is never - advertised, since it claims nothing. The first fallback to answer a - request wins. Dropping the handle ends the requests it resolved with an - error and never moves them to another fallback, which may be another - publisher instance (never stitch). A standby answers only requests - still unresolved, and new ones. This is what `serveRequests` in - `connection/forward.ts` does today. - - A `dynamic(prefix, route)` flagged blind and ranked below every - announced route. This reuses one call but puts an unadvertised, - claim-free entry in the route table, where selection and split-horizon - must special-case it. - Delete the JS loop only once the fallback answers the same requests, - including after a downgrade. -- `wire.ts` `OriginProducer.expect` (`connection/reload.ts`, keeps requests - pending while a reconnect is in flight): public if Rust has the same need - across a reconnect, else private to the connection. -- `wire.ts` `OriginProducer.interests`, `attach`, `accepts`: `interests` - is public with Lazy discovery; `attach(discovery)` goes with the - `discovery` option Lazy discovery removes; `accepts` is a check on the - public `allowed()` patterns. -- `wire.ts` `Broadcast.requested` (with its `served` flag) and - `overrideBroadcastWire` (both subscribers). Part of the JS answering - decision above: ✅ public, mirroring Rust's - `broadcast::Producer::dynamic()`: a broadcast producer hands out track - requests on demand, and a track's info and fetches are answered through - its `Track.Request` and `Track.Dynamic`. A subclass of - `Broadcast.Consumer` cannot do either today. The track-request handout - lands here. Fetch and info answering through ranges (`overrideBroadcastWire`) - moved to [the deferred quest](/quest/m1/public-model/deferred.md), which - waits on Model ranges. -- `wire.ts` `OriginConsumer.broadcasts`: registered and never read; delete. -- `announced.ts` `Announce.Producer`: remove the need; a session's route - ingestion goes through the origin. -- `wireOf`, `registerWire`, `trackOf`, `registerTrackConsumer`: stay - package-private plumbing, shrinking as the views above empty out. Delete - `wire.ts` if nothing is left. - -Tests: each public call gets a mock-time test in its language, and the -built-in sessions move onto the public calls, so their existing suites -cover the behavior. - -## Related - -- [Public routes](/quest/m1/public-model/routes.md) - route ingest and advertising, split out of this quest to follow cluster routing -- [Public model after ranges, handover, and the JS tail](/quest/m1/public-model/deferred.md) - the track info and fetch answers, split out of this quest diff --git a/quest/m1/public-model/claim-request-type.md b/quest/m1/public-model/claim-request-type.md deleted file mode 100644 index 91c37ff4a85..00000000000 --- a/quest/m1/public-model/claim-request-type.md +++ /dev/null @@ -1,41 +0,0 @@ -# [S] An application claim's request cannot be accepted - -## Goal - -Answering an application claim's request with `accept` fails to compile in -Rust and to typecheck in TypeScript, instead of panicking (Rust) or throwing -(JS) at runtime. `origin::Producer::dynamic(prefix)` hands out requests that -only `prepare` and `reject`; the session-forwarded claim from -`dynamic_route` / `dynamicRoute` keeps `accept`. Behavior is otherwise -unchanged: the escape hatches stay hidden until -[Public routes](/quest/m1/public-model/routes.md) gives them a -public shape. - -## Plan - -Decided 2026-10-21 (maintainer), while moving a downstream application onto -[#5678](https://github.com/moq-dev/moq/pull/5678): its handler's -`request.accept(..)` on a `dynamic()` claim still compiled against the new -API and would have panicked on the first request in production. #5678's -decision 1 kept `accept` `#[doc(hidden)] pub` as the interim, guarded only by -the runtime check. Rejected: folding this into answer.md, which leaves the -trap until that [L] lands; opening the code PR without a quest. - -Kept from #5678: prepare and publish, one epoch per answer, and the accepted -cold-start reset until -[announce takeover](/quest/m1/announce-takeover.md) lands. The downstream -check confirmed the reset is recoverable: a reader that follows the -announcement reaches the published answer, while a one-shot reader (an HTTP -fetch) cannot and gets `Unroutable`. - -Guidance: split the request type by the handle that yields it, for example -`dynamic_route` returning its own handle whose request carries `accept`, -rather than one `Request` with an optional publisher. Delete the runtime -assert and its test once the type makes the call unreachable; a -compile-fail doctest (Rust) and a `@ts-expect-error` line (JS) take their -place. Bindings (moq-ffi, moq-c) already expose no `accept` and need no -change. - -## Related - -- [Public routes](/quest/m1/public-model/routes.md) - later removes the hidden peer-claim API this quest narrows diff --git a/quest/m1/public-model/deferred.md b/quest/m1/public-model/deferred.md deleted file mode 100644 index 9dd1977a3ff..00000000000 --- a/quest/m1/public-model/deferred.md +++ /dev/null @@ -1,58 +0,0 @@ -# [M] Public model after ranges, handover, and the JS tail - -## Goal - -The hidden model items that other quests reshape are public, unnecessary, -or marked private once those quests land: fetch ingest and answering, the -IETF cache peeks, JS `Track.Subscriber.fork` and `Group.Producer.mirror`, -and JS `hooks.endTrack` and `hooks.releaseTrack`. A custom session then -answers fetches and track info, and declares or withholds a received end in -JS, through public calls only. - -## Plan - -Split from the ingest ([moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111)), -[serving](/quest/m1/public-model/serve.md), and -[answering](/quest/m1/public-model/answer.md) quests in review of #5717, -so those three do not wait on the large quests that replace these items. -Rules and the inventory method are in the -[epic](/quest/m1/public-model/README.md); rerun the inventory first, since -the prerequisites will have deleted or renamed most of these. - -- Fetch ingest (Rust `group::Request` fields `state` and `result`, - `track::TrackState` with `TrackState::abort`, `track::FetchOutcome`, - `group::Request::finish_track_at`, `track::Consumer::poll_landed`): remove - the need. Model ranges replaces per-group requests with ranges, and its - public range answer is the ingest path. -- Cache peeks (Rust `track::Consumer::peek_latest`, `peek_before`, - `peek_group`, `is_live`; IETF publisher, for Largest Object and FETCH) - and JS `Track.Subscriber.fork`: remove the need, since IETF FETCH is served - from ranges and the largest location is public. If either still has a - caller, bring the shape back to the maintainer rather than publishing - peeks. -- JS `Group.Producer.mirror` (lite subscriber fetch coalescing): remove the - need through Model ranges. JS track handover landed without a fan-out copy - of its own: it reads each route's copy directly. -- JS track info and fetch answering (`wire.ts` `overrideBroadcastWire`): - answered through the public `Track.Request` and `Track.Dynamic` range - requests that Model ranges defines, completing the answering quest's - `Broadcast.requested` decision. -- JS `hooks.endTrack`, `hooks.releaseTrack`, and `hooks.startTrack` (the JS - `finish_at_pending`, `close`, and `start_at`): public under the received-end - shape Rust settled in - [moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111): `finish_at` - and `close` on a `received()` copy, and `start_at` after - `track::Request::request_start`. Mirror the names - [lifecycle](/quest/m1/api-audit/lifecycle.md) and - [net names](/quest/m1/api-audit/net-names.md) settle: `close` becomes - `abort`, `received()` becomes `set_received()`, and `request_start` - becomes `with_floor`. - -Tests: as in the other children, mock time in each language, with the -built-in sessions moved onto the public calls. - -## Required - -- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - replaces per-group fetch requests, the peeks, and `fork` -- [JS ranges](/quest/m1/subscribe-ranges/js.md) - the JS `Track.Request`/`Track.Dynamic` range requests that answer track info and fetches -- [Public answering](/quest/m1/public-model/answer.md) - the public `Broadcast.requested` handout this completes diff --git a/quest/m1/public-model/routes.md b/quest/m1/public-model/routes.md deleted file mode 100644 index 0b95b164003..00000000000 --- a/quest/m1/public-model/routes.md +++ /dev/null @@ -1,88 +0,0 @@ -# [M] Public routes - -## Goal - -A custom relay or session does what the built-in ones do with routes, -through public moq-net and @moq/net calls only: ingest a peer's routes under -the peer's identity, and advertise routes to a peer without echoing the -peer's own routes back. - -## Plan - -Split out of [Public answering](/quest/m1/public-model/answer.md) on -2026-10-22 (maintainer, from an external API audit): every item below hangs -off the `Hop`/`Cost` route model that [Routes and -announces](/quest/m1/cluster-routing/routes.md) replaces with node ids, -seqnos, and a down-only metric in this same release. Publishing them first -would freeze machinery already scheduled for removal, so this waits for that -quest and then reruns the inventory against the route layer it lands. - -The items and decisions below are as they stood at b4eab0db1. Keep each -decision's intent: the announcing identity lives on the origin handle and a -`Route` can never forward it, a publisher advertises split-horizon from a -consumer that excludes the peer, and the peer-route escape hatches go -private once a peer's route has a public shape. Expose the roles that survive -the route layer under their new types, and drop items whose need it removes -(for example `Hop::from_wire` and `Cost::charged` if `Hops` and saturating -`Cost` are gone). Rules, carve-outs, and the inventory method are in the -[epic](/quest/m1/public-model/README.md). - -- `origin::Hop::from_wire` (IETF cluster): public as the decode-side - constructor that accepts 0 (`Hop::UNKNOWN`), beside `Hop::new`. -- `origin::Cost::charged` (both subscribers, IETF cluster): public. - `Cost::UNKNOWN` goes; it equals `Cost::default()`. -- **Announcing identity** (`origin::Route::via`, `Route::source`, - `Route::with_via`; both subscribers, server). **Decision**: - - ✅ Replace. The identity moves onto the origin handle a session - announces through, as `origin::Producer::via(hop)` beside the public - `Producer::peer` (the name `via` stays), so every route it ingests - carries it and a `Route` can never forward it. `Route.via` and - `Route.source` stay private fields the handle sets. - - A public `Route::with_via`, which a caller could forward by mistake. -- `origin::Consumer::excluding` (both publishers, client): public; - split-horizon toward a peer, the counterpart of the identity above. -- **Peer-route escape hatches** (`origin::Producer::dynamic_route`, - `origin::Dynamic::update_route`, `origin::Request::accept`; JS - `Origin.Producer.dynamicRoute`, `Dynamic.updateRoute`, `Request.accept`). - Settled as the interim in [#5678](https://github.com/moq-dev/moq/pull/5678): - an application claim (`dynamic(prefix)`) carries no route and answers - through `Request::prepare` then `Prepared::publish`, and these stay - `#[doc(hidden)] pub` / `@internal` for the sessions, tests, and benches that - model a peer's route (a covering claim with hops, cost, or an epoch, - answered with a broadcast the session already routes). Make them private - (`pub(crate)`, and private in JS) once the identity handle above and the - answer source give a peer's route a public shape, and move the - out-of-crate tests and benches onto it. -- `wire.ts` `OriginProducer.receive` (`connection/forward.ts`): mirror the - identity decision. A received route is a public `Origin.Producer.dynamic` - on a handle marked with the peer's identity, so `receive` goes and - `Origin.Request.accept` stops special-casing session-delivered broadcasts - (`Broadcast.fromSession`). -- `wire.ts` `OriginConsumer.advertised`, `local`, `demand`, with - `Advertised`, `Advertisements`, `sameInstance`, and `internal.ts` - `presented`, `hiddenBelow`, `scopeHead`, `scopeCaptures`, `scopeOverlaps` - (both publishers and subscribers): replace with the Rust shape. A - publisher advertises from the public `announced()` stream of an - `excluding(peer)` consumer and resolves with the public request taking an - epoch, as Rust does. The helpers stay private to `origin.ts`. Check that - the public announce event carries what `sameInstance` and a scoped - route's claim need; if it does not, that is track metadata and goes public. -- `hop.ts` `HopSchema`, `randomHop`, `UNKNOWN_HOP`, `MAX_HOPS`, - `Cost.zero`, `Route.default`, `Route.normalize`, `routesEqual`: public - values under `Origin`, mirroring Rust's `Hop::new`, `Hop::UNKNOWN`, - `Route::default` and `PartialEq`. `Hop` is branded, so a third party - cannot build `Route.hops` without a cast today. Today `Route.default` is - one shared mutable object (`{ hops: [], cost: 0n }`), so a caller's - `hops.push` would pollute every later route. Publish it as a factory - returning a fresh route (`Route.empty()`, like Rust's `Route::default()`), - and move the internal readers onto it, rather than exporting the shared - object. - -Tests: each public call gets a mock-time test in its language, and the -built-in sessions move onto the public calls, so their existing suites -cover the behavior. - -## Required - -- [Routes and announces](/quest/m1/cluster-routing/routes.md) - replaces the hop and cost model these items expose -- [Public answering](/quest/m1/public-model/answer.md) - the answer source the escape hatches' replacement builds on diff --git a/quest/m1/public-model/serve.md b/quest/m1/public-model/serve.md deleted file mode 100644 index e08d6b52774..00000000000 --- a/quest/m1/public-model/serve.md +++ /dev/null @@ -1,156 +0,0 @@ -# [L] Public serving - -## Goal - -A custom publisher reads a track for its wire through public moq-net and -@moq/net calls only: where the feed starts, a read cursor its peer can move in -either direction, the track's end in wire terms, and when a group it is still -writing has expired. The built-in lite and IETF publishers use the same calls. - -## Plan - -Rules, carve-outs, and the inventory method are in the -[epic](/quest/m1/public-model/README.md). Rerun the inventory first; the -recommendations below are the planning quest's, and the ones marked -**decision** were settled by the maintainer on 2026-10-16 (#5717). - -Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1: - -- `track::Subscriber::poll_start` (lite publisher): public. A publisher - reports the resolved start to its peer (SUBSCRIBE_START, SUBSCRIBE_OK). -- `track::Subscriber::poll_live` (both publishers): remove the need through - the public `poll_largest` (#5725). It now waits out an idle copy too - (#5924), but it follows the source's reported largest, which can name a - frame the cache does not hold yet, so the publishers still answer with the - buffered edge. A public live signal is - [its own quest](/quest/m1/track-live.md). Decide here whether wire answers move to it, and - with them `latest_sequence` on `track::{Producer,Consumer,Subscriber}`, - the private snapshot that replaced the public `latest()` (#5725). -- **Cursor** (`track::Subscriber::start_at`, `raise_start_to`, `end_at`, - `subscription::Cap`; both publishers). **Decision**: - - ✅ One public consuming builder assigns the floor and the cap in either - direction: `with_range(self, range) -> Self` on `track::Subscriber` and - `track::Ordered` (JS `withRange`), replacing `set_groups` and the - chaining `withGroups`. The maintainer decided "range" on 2026-10-19, - replacing the 2026-10-18 `limit` and the earlier `set_bounds`. Each - reader already hands a sequence out once (#5446), so a SUBSCRIBE_UPDATE - that widens the start re-offers only unread groups. - `start_at`, `raise_start_to`, `end_at`, and `Cap` go. Precondition: - lowering the floor re-offers unread cached groups in every mode. Today - only the resume cursor does; the arrival-order (Plain) cursor walks a - monotonic arrival index, so it must rescan below its old floor for - groups it never handed out. Rewrite the doc ("without rewinding read - progress") to say what lowering does. - - Rebinding: a consuming builder fits readers bounded once at creation, - but these callers move a live reader's bounds through `&mut` today, so - the rename needs a way to rebind (take and replace the reader, or a - crate-private setter the builder wraps): - - `rs/moq-c/src/consume.rs` `RawReader::apply` re-applies the range on - every subscription update, counting a relative start from the track - `Consumer`'s unbounded largest. `set_groups` only raises the floor, so an - update cannot lower a floor an earlier one put above the live edge - (Codex on #5924). Making the floor assign fixes it; land that with a - moq-c test where a later relative update delivers the next group. - - The lite and IETF publishers move the cursor on SUBSCRIBE_UPDATE - (`start_at`, `end_at`; JS `hooks.replaceGroups` and the IETF - publisher's `setGroups({ start })`). - - `js/json` snapshot and `js/flate` `latest()` raise the floor of the same - reader on each read; `js/room` chat sets it after `largest()` resolves. - The snapshot consumers (`js/json/src/snapshot/consumer.ts` `#read`, - `js/flate/src/snapshot/consumer.ts` `latest()`) call - `setGroups({ start: { included: latest.group } })`, whose omitted end - removes the caller's cap. The rebinding must keep that cap; land it with - a regression test where a capped snapshot reader stays below its cap. - A datagram also advances `largest()` past every group, stranding the - read. The Rust consumers instead drain the groups the reader already - has and keep the newest (#5677), which needs no floor at all. - - `rs/moq-hls` export narrows a fetched group with `set_frames(..end)`. - - Readers that set a floor once right after `poll_largest` (moq-audio, - moq-video, moq-cli play, moq-json window, moq-mux) fit the builder. - - ✅ The range floor counts as a local bound (maintainer, 2026-10-19): - `largest()` reports within both the floor and the cap, so a reader whose - floor sits above the live edge reports nothing until a group reaches it - (#5924). - - ✅ `largest()` reports only upstream-confirmed state (maintainer, - 2026-10-19): the source's reported largest and what its live feed - delivered since, never a cache-derived stand-in. It waits while the copy - is idle, though cached groups are still served, and past the cap it - reports nothing rather than the newest cached group below it (#5924). - - A second public `replace_groups`. -- **Bounds naming** (decided 2026-10-16 in #5717, changed to "range" by the - maintainer on 2026-10-19): ✅ the siblings follow: - `Subscription::with_range` replaces `with_groups` as the single-range form - of `with_ranges`, and `group::Consumer::set_frames` becomes the consuming - `with_range`, with the JS mirrors. -- `group::Consumer::start_at`, `end_at` (both publishers): remove the need; - the public `with_range` (today `set_frames`) and `skip_to` cover them. -- `Subscription::{start, end, set_bounds, with_start, with_end}` (both - publishers, IETF subscriber, from #5732): the covering span a wire without - ranges carries. Derive it from the public `ranges` instead, or make it - public. -- `subscription::Position::final_sequence` (lite publisher): public, the - one-line wire translation of an exclusive end that both languages - already duplicate. `track::Consumer::final_sequence` (IETF publisher) goes; - callers read `poll_finished` and translate. -- **Group expiry while writing** (`group::Consumer::poll_expired`, - `poll_expired_while_pending`; both publishers): public as one - `poll_expired`. A publisher that already read frames off a group must - still abandon its stream once the subscription's max delay expires the - group. Fold the `pending` flag in, or keep it private as tuning if it only - saves a poll. -- **Cache peeks** (`track::Consumer::peek_latest`, `peek_before`, - `peek_group`, `is_live`; IETF publisher, for Largest Object and FETCH): - moved to [the deferred quest](/quest/m1/public-model/deferred.md), which - waits on Model ranges. -- Passthrough, stay private and marked: `group::Consumer::wire`, - `end_object`, `last_object`, `first_frame`, `poll_read_object`, - `poll_read_objects`, `frame::Object`, `frame::Consumer::fields`, - `track::Subscriber::poll_recv_wire_datagram`. -- Test-only: `track::Consumer::poll_group`, `resume_position`. - -JS items (`js/net/src/`): - -- `hooks.replaceGroups` (lite publisher): mirror the cursor decision, so - `Track.Subscriber.withRange` assigns the floor and the hook goes. -- `hooks.tryRecvGroup` with its `Recv` union, and `hooks.groupChanged` - (lite publisher). The public `Subscriber.tryRecvGroup()` returns - `undefined` for both a held boundary and a drained end, so a synchronous - publisher cannot tell them apart or park. **Decision**: - - ✅ Public. Return the `Recv` union from the public method and expose - the change wake (a signal or a promise, matching the package's style). - The union deliberately has no Rust type: Rust's lite publisher builds - its private `Recv` from the public `poll_recv_group` (Pending versus - `Ready(None)`) and `poll_finished`, which a third party composes the - same way. - - Mirror Rust exactly: a distinct drained result and a synchronous end - check instead of the union, plus the wake. -- `hooks.guardGroup`, `hooks.readGroupFrame` with `ReadGroupFrame` (both - publishers): mirror the Rust group expiry as one public call. Reading a - frame the publisher `complete()`s holds the group's `closed` until it is - written; keep that behavior behind the public read rather than a second - method if it can be. -- `Track.Subscriber.fork` (`@internal`, IETF publisher fill and cache - reads): moved to the deferred quest with the cache peeks. -- `wire.ts` `Broadcast.subscribe`, `fetchGroup`: remove the need; the public - `Broadcast.Consumer.track(name)` paths cover them. -- `wire.ts` `Broadcast.largest`, `hooks.cachedLargest`, - `hooks.bufferedLargest`, and `hooks.latestSequence`: remove the need through the public `largest()` - stream (#5725), under the same caveat as `poll_live`. -- `wire.ts` `Broadcast.resolveTrackInfo(name, hold)` (both publishers): - public as an option on the track's info lookup (an `AbortSignal` that keeps - the track wanted until it fires), mirrored by whatever Rust's `query` - holds. Check Rust parity first; drop the hold if Rust has no equivalent - need. -- `internal.ts` `groupBounds`: stays a private helper; a third party passes - the public `Groups`. -- `priority.ts` `sendOrder`, `Priority`: stay private. They rank streams on - a `stream.ts` writer, which is wire plumbing in both languages. - -Tests: lowering the floor re-offers an unread cached group in both cursor -modes. Each public call gets a mock-time test in its language, and the -built-in publishers move onto the public calls, so their existing suites -cover the behavior. - -## Related - -- [Public model after ranges, handover, and the JS tail](/quest/m1/public-model/deferred.md) - the cache peeks and `fork`, split out of this quest diff --git a/quest/m1/track-live.md b/quest/m1/track-live.md index f5c5532e5d9..40fdc374ec5 100644 --- a/quest/m1/track-live.md +++ b/quest/m1/track-live.md @@ -18,14 +18,9 @@ what an ended feed left; this quest makes the signal itself public. `resume` `poll_live`, and the crate-private `Producer::is_live`. Expose one reader-side `poll_live`/`live()` (or `is_live`) on `track::Subscriber` and `track::Ordered`, and decide whether the publishers' crate-private - `poll_live` folds into it (see - [Public serving](/quest/m1/public-model/serve.md)). + `poll_live` folds into it. - JS has no equivalent: a track never goes idle, so its reader is live from the start. Mirror the name, and the idle state if JS gains warm copies. - Decide what a reader does with groups served before live: keep serving them (today) and let the caller skip by the signal, or offer a mode that starts at the live edge. - -## Related - -- [Public serving](/quest/m1/public-model/serve.md) - the publishers' `poll_live` and the `largest()` decision diff --git a/quest/m2/audit-dead-code.md b/quest/m2/audit-dead-code.md index 229b165a74b..e0d8d3bbbca 100644 --- a/quest/m2/audit-dead-code.md +++ b/quest/m2/audit-dead-code.md @@ -35,7 +35,7 @@ Start by comparing the live tree against these baselines: through `cfg_attr`. The unconditional ones are drop guards, compile-time Send assertions, generated settings registries, shared test and benchmark harnesses, deserialization fixtures, FFI tables, and two `GroupConsumer` - helpers waiting on the [answering API](/quest/m1/public-model/answer.md). + helpers waiting on the [answering API](/quest/m1/api-audit/answer.md). Once the lint audit lands, every unconditional one is an `expect`, so a stale one fails the build; check what the conditional ones still guard. `cargo shear` found no unused dependencies. diff --git a/quest/m2/release/api-review.md b/quest/m2/release/api-review.md index a85ac07bc26..e07bf477094 100644 --- a/quest/m2/release/api-review.md +++ b/quest/m2/release/api-review.md @@ -25,7 +25,7 @@ against AGENTS.md's Public API rules: names mirrored across languages, no callbacks or 4+ args, misuse unrepresentable, private until a consumer needs it. Propose 2-3 shapes with a recommendation, ask the maintainer, and file each approved change as a quest in m1, which holds the cut until it lands. -Related: [public model](/quest/m1/public-model/README.md), which decides what +Related: [session-only items stay private](/quest/m1/api-audit/model-private.md), which decides what moq-net exposes; this review judges the shape of what ships. ## Required diff --git a/rs/justfile b/rs/justfile index 7f0b795083e..4d14f41ccd6 100644 --- a/rs/justfile +++ b/rs/justfile @@ -45,7 +45,6 @@ check-test *args: [positional-arguments] lint *args: python3 sh/rs/stats-docs.py - python3 sh/rs/model-markers.py # The environment is process-wide, so nothing writes it. A text check, because the # clock lint's test allowances also silence clippy's disallowed_methods. git grep --untracked -nE '(^|[^[:alnum:]_])(set_var|remove_var)[[:space:]]*\(' -- 'rs/*.rs'; [ $? -eq 1 ] || { echo "error: a write to the process environment: pass the value down, or run a test in a child with test_env::Env" >&2; exit 1; } diff --git a/sh/js/workspace.py b/sh/js/workspace.py index 70115f5c5a4..b8d7845c5f1 100644 --- a/sh/js/workspace.py +++ b/sh/js/workspace.py @@ -157,7 +157,6 @@ def scripts(verb): raise RuntimeError("add these cross-tree JS inputs to the js pattern in sh/dispatch.sh: " + " ".join(missing)) command("python3", "sh/rs/stats-docs.py") - command("python3", "sh/rs/model-markers.py") # These inexpensive shared checks still protect every package's imports. result = subprocess.run(["grep", "-rn", "--include=*.ts", "--include=*.tsx", "--exclude-dir=node_modules", "--exclude-dir=dist", diff --git a/sh/rs/model-markers.py b/sh/rs/model-markers.py deleted file mode 100644 index 3e6988b01e5..00000000000 --- a/sh/rs/model-markers.py +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env python3 -"""Fail when session code reaches a private model item that carries no marker. - -The public model (`quest/m1/public-model/`) shrinks what the built-in sessions reach -past the public API to a marked set. Each item they reach is either pending, naming -the quest that makes it public, or deliberately private with its reason: - - // pending: /quest/m1/public-model/serve.md - // private: passthrough | tuning | capability gap | test | plumbing (JS only) - -The marker sits in the comment and attribute lines directly above the item. A marker -anywhere else is stale and fails too, so a quest that publishes an item must delete -its mark, and a `pending` mark must name a quest file that still exists. - -Rust: under `rs/moq-net/src/model/`, every restricted visibility needs a marker unless -it stays inside the model: `pub(self)`, `pub(super)` outside `mod.rs`, or -`pub(in crate::model...)`. The compiler then refuses any unmarked reach. - -JS: nothing compiles reach, so this follows the import edges instead. A non-test file -under `js/net/src/{ietf,lite,connection}/` reaching a model module's name the package -entrypoint does not publish (an import, a namespace member, a `hooks.` read, or -an `@internal` member call) needs a marker on that declaration, as does every member -of the `wire.ts` views. An import or read form it cannot follow, like a default import or -a destructured namespace, fails instead. Run from `just rs lint` and `just js check`. -""" - -from pathlib import Path -import re -import sys - -ROOT = Path(__file__).resolve().parents[2] - -PRIVATE = {"passthrough", "tuning", "capability gap", "test", "plumbing"} -# Rust hosts no session plumbing beside the model, so `plumbing` is JS only. -RUST_PRIVATE = PRIVATE - {"plumbing"} -MARKER = re.compile(r"^\s*// (private|pending): (.*?)\s*$") - -errors = [] - - -def fail(path, line, message): - errors.append(f"{path.relative_to(ROOT)}:{line + 1}: {message}") - - -def marker(path, index, line, private): - """Validate a marker line against the `private` reasons, returning whether it is one.""" - found = MARKER.match(line) - if not found: - return False - kind, value = found.groups() - if kind == "private" and value not in private: - fail(path, index, f"unknown private marker {value!r}; use one of: {', '.join(sorted(private))}") - if kind == "pending" and not (value.startswith("/quest/") and value.endswith(".md") and (ROOT / value[1:]).is_file()): - fail(path, index, f"pending marker names {value!r}, which is not an existing quest file") - return True - - -def indent(line): - return len(line) - len(line.lstrip()) - - -def wrapped(lines, end): - """The opening line of the attribute rustfmt wrapped to close on `lines[end]`, or None. - - rustfmt puts a wrapped attribute's `)]` alone at the `#[` line's indent, with every - line between indented deeper. Matching that shape rather than balancing brackets - leaves strings and comments alone, and code that merely ends in `)]`, like `)];`, - never borrows an earlier attribute and its marker. - """ - if lines[end].strip() != ")]": - return None - for start in range(end - 1, -1, -1): - line = lines[start] - if line.strip() and indent(line) <= indent(lines[end]): - if indent(line) == indent(lines[end]) and line.lstrip().startswith(("#[", "#!")): - return start - return None - return None - - -def block(lines, index, prefixes): - """The indices of the comment and attribute lines directly above `index`.""" - above = [] - while index > 0: - line = lines[index - 1].lstrip() - start = wrapped(lines, index - 1) if "#[" in prefixes else None - if line.startswith(prefixes): - index -= 1 - above.append(index) - elif start is not None: - above.extend(range(index - 1, start - 1, -1)) - index = start - else: - break - return above - - -def markers(path, lines, private): - """Every marker line in a file, validated.""" - return {i for i, line in enumerate(lines) if marker(path, i, line, private)} - - -def require(path, lines, index, prefixes, what, used): - """Fail unless a marker sits directly above `index`; record the markers it consumes.""" - found = [i for i in block(lines, index, prefixes) if MARKER.match(lines[i])] - used.update(found) - if not found: - fail(path, index, f"{what} needs a `// private: ` or `// pending: ` marker") - - -def stale(path, all_markers, used): - for i in sorted(all_markers - used): - fail(path, i, "marker is not on an item that needs one; delete it") - - -# Rust - -RUST = ROOT / "rs/moq-net/src/model" -VISIBILITY = re.compile(r"\bpub\s*\(([^)]*)\)") -RUST_BLOCK = ("//", "#[", "#!") - - -def rust(): - for path in sorted(RUST.rglob("*.rs")): - lines = path.read_text().split("\n") - used = set() - for i, line in enumerate(lines): - if line.lstrip().startswith("//"): - continue - for scope in VISIBILITY.findall(line): - scope = " ".join(scope.split()) - if scope == "self" or (scope == "super" and path != RUST / "mod.rs"): - continue - if re.fullmatch(r"in crate::model(::\w+)*", scope): - continue - require(path, lines, i, RUST_BLOCK, f"`pub({scope})` reaches past the model and", used) - stale(path, markers(path, lines, RUST_PRIVATE), used) - - -# JS - -SRC = ROOT / "js/net/src" -SESSIONS = ("ietf", "lite", "connection") -# Root modules that are wire or session plumbing rather than model; sessions reach them freely. -PLUMBING = {"auth_session.ts", "datagram_stream.ts", "error.ts", "index.ts", "mock.ts", "stream.ts"} -JS_BLOCK = ("//", "/*", "*", "@") - -DECLARATION = r"^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\*?|class|interface|type|const|let|var|enum|namespace)\s+{}\b" -VALUE = r"^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|enum|namespace)\s+{}\b" -REEXPORT = re.compile(r"export\s+(type\s+)?(\*(?:\s+as\s+\w+)?|\{[^}]*\})\s+from\s+\"(\.[^\"]+)\"", re.S) -IMPORT = re.compile(r"import\s+(type\s+)?(\{[^}]*\}|\*\s+as\s+\w+)\s+from\s+\"(\.[^\"]+)\"", re.S) -# Every relative import, so a form IMPORT does not parse (like a default import) fails instead of slipping by. -ANY_IMPORT = re.compile(r"\bimport\s+[^\"';]*?\s+from\s+\"(\.[^\"]+)\"", re.S) - - -def module(source, specifier): - return (source.parent / specifier).resolve() - - -def names(spec): - """(name, local binding, type-only) for each entry of an `{ a, type b as c }` list.""" - for entry in spec.strip("{}").split(","): - entry = " ".join(entry.split()) - if entry: - kind = entry.startswith("type ") - name, _, local = entry.removeprefix("type ").partition(" as ") - yield name, local or name, kind - - -def internal(lines, index): - """Whether the JSDoc directly above `index` tags it `@internal`.""" - return any("@internal" in lines[i] for i in block(lines, index, JS_BLOCK)) - - -def declaration(lines, name): - pattern = re.compile(DECLARATION.format(re.escape(name))) - for i, line in enumerate(lines): - if pattern.match(line): - return i - return None - - -def published(): - """What the package entrypoint publishes: whole modules, and (module, name) -> type-only.""" - whole, named = set(), {} - pending = [SRC / "index.ts"] - seen = set() - while pending: - source = pending.pop() - if source in seen: - continue - seen.add(source) - for type_only, spec, specifier in REEXPORT.findall(source.read_text()): - target = module(source, specifier) - if spec.startswith("*"): - whole.add(target) - pending.append(target) - continue - for name, _, kind in names(spec): - key = (target, name) - named[key] = named.get(key, True) and bool(type_only or kind) - return whole, named - - -def js(): - whole, named = published() - models = { - path for path in SRC.glob("*.ts") if ".test" not in path.name and "test-util" not in path.name and path.name not in PLUMBING - } - files = {path: path.read_text().split("\n") for path in models} - # The declarations sessions need marked, by file and line. - needed = {path: {} for path in models} - - def public(path, name, type_only): - lines = files[path] - index = declaration(lines, name) - if path in whole: - # A name it re-exports from a package has no declaration here, and is public. - return index is None or not internal(lines, index) - # A name declared only as an interface or type alias has no value, so any reach of it is a type reach. - type_only = type_only or not any(re.match(VALUE.format(re.escape(name)), line) for line in lines) - kind = named.get((path, name)) - return kind is not None and (type_only or not kind) - - def reach(path, name, user): - lines = files[path] - index = declaration(lines, name) - if index is None: - fail(user, 0, f"cannot find the declaration of `{name}` in {path.relative_to(ROOT)}") - return - needed[path].setdefault(index, f"`{name}`, reached by session code,") - - # Members tagged `@internal` inside exported classes, which `stripInternal` hides. - members = {} - for path, lines in files.items(): - for i, line in enumerate(lines): - found = re.match(r"^\t+(?:readonly\s+|static\s+|async\s+|get\s+)*(\w+)\s*[(<:?]", line) - if found and internal(lines, i): - members.setdefault(found.group(1), []).append((path, i)) - - hooks_path = SRC / "internal.ts" - hooks_lines = files[hooks_path] - hooks_start = hooks_lines.index("export const hooks: {") - hooks_end = hooks_lines.index("} = {", hooks_start) - hooks = {} - for i in range(hooks_start + 1, hooks_end): - found = re.match(r"^\t(\w+)\??:", hooks_lines[i]) - if found: - hooks[found.group(1)] = i - - def code(text): - """Session source without its comments and imports.""" - return IMPORT.sub("", re.sub(r"//[^\n]*|/\*.*?\*/", "", text, flags=re.S)) - - def reads(text, binding): - """The members session code reads off `binding`, a local name or a dotted path.""" - return sorted(set(re.findall(rf"(?` reads are followed, so refuse any other use, like destructuring. - if re.search(rf"(?`, so the lint can follow each hook") - for hook in reads(text, binding): - if hook not in hooks: - fail(user, 0, f"`hooks.{hook}` is not declared in internal.ts") - continue - needed[hooks_path].setdefault(hooks[hook], f"`hooks.{hook}`, reached by session code,") - - for directory in SESSIONS: - for user in sorted((SRC / directory).rglob("*.ts")): - if ".test" in user.name or "test-util" in user.name: - continue - text = user.read_text() - for statement in ANY_IMPORT.finditer(text): - if module(user, statement.group(1)) in models and not IMPORT.fullmatch(statement.group(0)): - fail(user, 0, f"import a model module by name or namespace only: {' '.join(statement.group(0).split())}") - for type_only, spec, specifier in IMPORT.findall(text): - target = module(user, specifier) - if target not in models: - continue - if spec.startswith("*"): - alias = spec.split()[-1] - if re.search(rf"\}}\s*=\s*{alias}\s*(?:;|$)", code(text), re.M): - fail(user, 0, f"read `{alias}` only as `{alias}.`, so the lint can follow each member") - # A local variable may shadow the alias, so only the module's own declarations count. - for name in reads(text, alias): - if declaration(files[target], name) is not None and not public(target, name, bool(type_only)): - reach(target, name, user) - if target == hooks_path: - reach_hooks(text, f"{alias}.hooks", user) - continue - for name, local, kind in names(spec): - if not public(target, name, bool(type_only or kind)): - reach(target, name, user) - if target == hooks_path and name == "hooks": - reach_hooks(text, local, user) - for name, declared in members.items(): - if re.search(rf"\.{name}\b", text): - for path, index in declared: - needed[path].setdefault(index, f"`@internal` member `{name}`, reached by session code,") - - # Every member of a `wire.ts` view exists for the protocol layers. - wire_path = SRC / "wire.ts" - wire_lines = files[wire_path] - views = re.search(r"^type View = (.*);$", "\n".join(wire_lines), re.M).group(1) - for view in (name.strip() for name in views.split("|")): - start = wire_lines.index(f"export interface {view} {{") - for i in range(start + 1, wire_lines.index("}", start)): - found = re.match(r"^\t(?:readonly\s+)?(\w+)\??\s*[(:<]", wire_lines[i]) - if found: - needed[wire_path].setdefault(i, f"`{view}.{found.group(1)}` view member") - - for path in sorted(models): - lines = files[path] - used = set() - for index, what in sorted(needed[path].items()): - require(path, lines, index, JS_BLOCK, what, used) - stale(path, markers(path, lines, PRIVATE), used) - - -rust() -js() -if errors: - print("\n".join(errors), file=sys.stderr) - print(f"model-markers: {len(errors)} problem(s); see sh/rs/model-markers.py", file=sys.stderr) - sys.exit(1) diff --git a/sh/rs/model-markers.test.py b/sh/rs/model-markers.test.py deleted file mode 100644 index 3bb932eac73..00000000000 --- a/sh/rs/model-markers.test.py +++ /dev/null @@ -1,188 +0,0 @@ -"""The model marker lint passes a marked tree and fails each kind of unmarked or stale reach.""" - -from pathlib import Path -import shutil -import subprocess -import sys -import tempfile -import unittest - - -SCRIPT = Path(__file__).resolve().parent / "model-markers.py" - -TREE = { - "quest/m1/open.md": "# [S] Open\n", - "rs/moq-net/src/model/mod.rs": "mod group;\n// pending: /quest/m1/open.md\npub(crate) use group::Open;\n", - "rs/moq-net/src/model/group.rs": ( - "pub(super) struct Inner;\n" - "pub(in crate::model) fn helper() {}\n" - "/// Reached by a session test.\n" - "#[cfg(test)]\n" - "// private: test\n" - "pub(crate) struct Open;\n" - ), - "js/net/src/index.ts": 'export * as Track from "./track.ts";\nexport type { Box, Shape } from "./box.ts";\n', - "js/net/src/box.ts": "export class Box {}\nexport interface Shape {}\n", - "js/net/src/track.ts": ( - "export class Subscriber {\n" - "\t// pending: /quest/m1/open.md\n" - "\t/** @internal */\n" - "\tfork(): void {}\n" - "}\n" - ), - "js/net/src/internal.ts": ( - "// private: plumbing\n" - "export const hooks: {\n" - "\t// private: plumbing\n" - "\tused: () => void;\n" - "\tunused: () => void;\n" - "} = {\n" - "\tused: () => {},\n" - "\tunused: () => {},\n" - "};\n" - ), - "js/net/src/wire.ts": ( - "export interface Broadcast {\n" - "\t// private: plumbing\n" - "\tsubscribe(): void;\n" - "}\n" - "type View = Broadcast;\n" - "// private: plumbing\n" - "export function wireOf(handle: object): View {\n" - "\treturn handle as View;\n" - "}\n" - ), - "js/net/src/lite/session.ts": ( - 'import { wireOf } from "../wire.ts";\n' - 'import * as Box from "../box.ts";\n' - 'import { hooks } from "../internal.ts";\n' - "hooks.used();\n" - "let shape: Box.Shape;\n" - "wireOf({}).subscribe();\n" - "subscriber.fork();\n" - ), - "js/net/src/lite/session.test.ts": 'import { hooks } from "../internal.ts";\nhooks.unused();\n', -} - -HOOKS = 'import { hooks } from "../internal.ts";\nhooks.used();' - - -def run(**changes): - """Run the lint over TREE with `changes` ({path: (old, new)}) applied; return its stderr, or None on success.""" - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - for path, text in TREE.items(): - if path in changes: - old, new = changes[path] - assert old in text, (path, old) - text = text.replace(old, new) - (root / path).parent.mkdir(parents=True, exist_ok=True) - (root / path).write_text(text) - (root / "sh/rs").mkdir(parents=True) - shutil.copy(SCRIPT, root / "sh/rs/model-markers.py") - result = subprocess.run([sys.executable, root / "sh/rs/model-markers.py"], capture_output=True, text=True) - return None if result.returncode == 0 else result.stderr - - -class TestModelMarkers(unittest.TestCase): - def test_marked_tree_passes(self): - self.assertIsNone(run()) - - def test_unmarked_crate_visibility_fails(self): - error = run(**{"rs/moq-net/src/model/group.rs": ("pub(super) struct Inner", "pub(crate) struct Inner")}) - self.assertIn("group.rs:1: `pub(crate)`", error) - - def test_super_super_reaches_the_crate(self): - error = run(**{"rs/moq-net/src/model/group.rs": ("pub(super) struct Inner", "pub(in super::super) struct Inner")}) - self.assertIn("group.rs:1: `pub(in super::super)`", error) - - def test_super_in_mod_rs_reaches_the_crate(self): - error = run(**{"rs/moq-net/src/model/mod.rs": ("mod group;", "pub(super) mod group;")}) - self.assertIn("mod.rs:1: `pub(super)`", error) - - def test_pending_on_a_missing_quest_fails(self): - error = run(**{"rs/moq-net/src/model/mod.rs": ("/quest/m1/open.md", "/quest/m1/done.md")}) - self.assertIn("not an existing quest file", error) - - def test_unknown_private_reason_fails(self): - error = run(**{"rs/moq-net/src/model/group.rs": ("private: test", "private: later")}) - self.assertIn("unknown private marker", error) - - def test_marker_above_a_wrapped_attribute_passes(self): - wrapped = '// private: test\n#[cfg_attr(\n\tnot(feature = "async"),\n\tallow(dead_code, reason = "why")\n)]\npub(crate) struct Open' - self.assertIsNone(run(**{"rs/moq-net/src/model/group.rs": ("// private: test\npub(crate) struct Open", wrapped)})) - - def test_delimiters_in_attribute_strings_and_comments_are_ignored(self): - wrapped = '// private: test\n#[cfg_attr(\n\tnot(feature = "async"), /* /* ) */ ) */\n\tallow(dead_code, reason = r#"why " )"#)\n)]\npub(crate) struct Open' - self.assertIsNone(run(**{"rs/moq-net/src/model/group.rs": ("// private: test\npub(crate) struct Open", wrapped)})) - - def test_code_ending_like_an_attribute_does_not_lend_its_marker(self): - table = "// private: test\n#[cfg(test)]\npub(crate) const TABLE: [u8; 1] = [\n\tu8::pow(1, 1\n)];\npub(crate) struct Inner" - error = run(**{"rs/moq-net/src/model/group.rs": ("pub(super) struct Inner", table)}) - self.assertIn("group.rs:6: `pub(crate)`", error) - - def test_code_ending_in_a_bare_attribute_close_does_not_lend_its_marker(self): - table = "// private: test\n#[cfg(test)]\npub(crate) const TABLE: [u8; 1] = [u8::pow(\n\t1, 1\n)]\npub(crate) struct Inner" - error = run(**{"rs/moq-net/src/model/group.rs": ("pub(super) struct Inner", table)}) - self.assertIn("group.rs:6: `pub(crate)`", error) - - def test_stale_rust_marker_fails(self): - error = run(**{"rs/moq-net/src/model/group.rs": ("pub(super) struct Inner", "// private: test\npub(super) struct Inner")}) - self.assertIn("group.rs:1: marker is not on an item", error) - - def test_session_reaching_an_unmarked_hook_fails(self): - error = run(**{"js/net/src/lite/session.ts": ("hooks.used();", "hooks.used();\nhooks.unused();")}) - self.assertIn("internal.ts:5: `hooks.unused`", error) - - def test_aliased_hook_read_fails(self): - error = run(**{"js/net/src/lite/session.ts": (HOOKS, 'import { hooks as h } from "../internal.ts";\nh.used();\nh.unused();')}) - self.assertIn("internal.ts:5: `hooks.unused`", error) - - def test_namespaced_hook_read_fails(self): - namespaced = 'import * as Internal from "../internal.ts";\nInternal.hooks.used();\nInternal.hooks.unused();' - error = run(**{"js/net/src/lite/session.ts": (HOOKS, namespaced)}) - self.assertIn("internal.ts:5: `hooks.unused`", error) - - def test_destructured_hooks_fail(self): - error = run(**{"js/net/src/lite/session.ts": ("hooks.used();", "const { unused } = hooks;")}) - self.assertIn("session.ts:1: read `hooks` only as `hooks.`", error) - - def test_destructured_namespace_fails(self): - error = run(**{"js/net/src/lite/session.ts": ("let shape: Box.Shape;", "const { Box: Made } = Box;")}) - self.assertIn("session.ts:1: read `Box` only as `Box.`", error) - - def test_default_import_of_a_model_module_fails(self): - error = run(**{"js/net/src/lite/session.ts": ('import * as Box from "../box.ts";', 'import Box, { Shape } from "../box.ts";')}) - self.assertIn("import a model module by name or namespace only", error) - - def test_namespace_value_reach_of_a_type_export_fails(self): - error = run(**{"js/net/src/lite/session.ts": ("let shape: Box.Shape;", "new Box.Box();")}) - self.assertIn("box.ts:1: `Box`", error) - - def test_type_namespace_reach_of_a_type_export_passes(self): - typed = 'import type * as Box from "../box.ts";\nlet box: Box.Box;' - self.assertIsNone(run(**{"js/net/src/lite/session.ts": ('import * as Box from "../box.ts";', typed)})) - - def test_plumbing_on_rust_fails(self): - error = run(**{"rs/moq-net/src/model/group.rs": ("private: test", "private: plumbing")}) - self.assertIn("group.rs:5: unknown private marker 'plumbing'", error) - - def test_unmarked_internal_member_fails(self): - error = run(**{"js/net/src/track.ts": ("\t// pending: /quest/m1/open.md\n", "")}) - self.assertIn("track.ts:3: `@internal` member `fork`", error) - - def test_unmarked_unpublished_import_fails(self): - error = run(**{"js/net/src/wire.ts": ("// private: plumbing\nexport function", "export function")}) - self.assertIn("wire.ts:6: `wireOf`", error) - - def test_unmarked_view_member_fails(self): - error = run(**{"js/net/src/wire.ts": ("\t// private: plumbing\n", "")}) - self.assertIn("wire.ts:2: `Broadcast.subscribe` view member", error) - - def test_stale_js_marker_fails(self): - error = run(**{"js/net/src/internal.ts": ("\tunused:", "\t// private: plumbing\n\tunused:")}) - self.assertIn("internal.ts:5: marker is not on an item", error) - - -if __name__ == "__main__": - unittest.main() From f11fb72fa774ac05ca2f2e43512fa31e750dfc3c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 11:36:02 -0700 Subject: [PATCH 3/3] chore(net): remove the public-model pending and private marks They pointed at deleted quests once the marker lint went. Co-Authored-By: Claude Opus 5.5 --- js/net/src/announced.ts | 4 -- js/net/src/auth_gate.ts | 1 - js/net/src/consume.ts | 1 - js/net/src/group.ts | 1 - js/net/src/hop.ts | 6 --- js/net/src/internal.ts | 30 --------------- js/net/src/ledger.ts | 1 - js/net/src/listing.ts | 1 - js/net/src/origin.ts | 3 -- js/net/src/priority.ts | 2 - js/net/src/tail.ts | 1 - js/net/src/track.ts | 3 -- js/net/src/wire.ts | 32 ---------------- quest/m1/api-audit/README.md | 2 +- quest/m1/api-audit/model-private.md | 8 ++-- rs/moq-net/src/model/bandwidth.rs | 4 -- rs/moq-net/src/model/broadcast.rs | 10 ----- rs/moq-net/src/model/datagram.rs | 2 - rs/moq-net/src/model/frame.rs | 9 ----- rs/moq-net/src/model/group.rs | 39 ------------------- rs/moq-net/src/model/listing.rs | 3 -- rs/moq-net/src/model/mod.rs | 8 ---- rs/moq-net/src/model/object.rs | 6 --- rs/moq-net/src/model/origin.rs | 40 -------------------- rs/moq-net/src/model/subscription.rs | 7 ---- rs/moq-net/src/model/track.rs | 56 ---------------------------- 26 files changed, 4 insertions(+), 276 deletions(-) diff --git a/js/net/src/announced.ts b/js/net/src/announced.ts index 689a2a26d79..35ba251bf62 100644 --- a/js/net/src/announced.ts +++ b/js/net/src/announced.ts @@ -42,7 +42,6 @@ export interface Announce { */ export type Event = { kind: "start" | "update" | "restart" | "end" } & Announce; -// pending: /quest/m1/public-model/answer.md /** * Options for an announcement stream. * @@ -89,7 +88,6 @@ function closeState(state: AnnounceState, abort?: Error) { state.synced.set(true); } -// pending: /quest/m1/public-model/answer.md /** * The write side of an announcement stream. * @@ -119,7 +117,6 @@ export class Producer { }); } - // pending: /quest/m1/public-model/answer.md /** * Marks the initial set as delivered: the stream syncs once the reader has taken * everything queued so far. A no-op until {@link behind} once marked. @@ -133,7 +130,6 @@ export class Producer { settle(state); } - // pending: /quest/m1/public-model/answer.md /** * Takes sync back until the next {@link synced}, as when another initial set starts. * diff --git a/js/net/src/auth_gate.ts b/js/net/src/auth_gate.ts index b32dd0fe718..299e5d9cf28 100644 --- a/js/net/src/auth_gate.ts +++ b/js/net/src/auth_gate.ts @@ -2,7 +2,6 @@ import type { Dispose, Getter } from "@moq/signals"; import type { Grant } from "./auth.ts"; import type * as Path from "./path.ts"; -// private: plumbing /** Whether our grant still lets us serve one incoming request, built when it is dispatched. @internal */ export class Gate { #grant?: Getter; diff --git a/js/net/src/consume.ts b/js/net/src/consume.ts index d216712a534..d3c90ac1fde 100644 --- a/js/net/src/consume.ts +++ b/js/net/src/consume.ts @@ -31,7 +31,6 @@ class Blind implements Selection { readonly validity = new Validity(); } -// private: plumbing /** Consumed broadcasts shared only within the selected serving instance. @internal */ export class BroadcastCache { #cache = new Map>(); diff --git a/js/net/src/group.ts b/js/net/src/group.ts index 4bb02d5c0a1..2a44f5ee6c9 100644 --- a/js/net/src/group.ts +++ b/js/net/src/group.ts @@ -270,7 +270,6 @@ export class Producer { return makeConsumer(this.#state); } - // pending: /quest/m1/public-model/deferred.md /** * Create an independent read handle that receives every frame written here. * diff --git a/js/net/src/hop.ts b/js/net/src/hop.ts index 318530c8cbb..aff9b312b7e 100644 --- a/js/net/src/hop.ts +++ b/js/net/src/hop.ts @@ -11,7 +11,6 @@ import * as z from "zod/mini"; import type { Auth } from "./auth.ts"; import type * as Epoch from "./epoch.ts"; -// pending: /quest/m1/public-model/answer.md /** * One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire. * @@ -31,7 +30,6 @@ export const HopSchema = z /** A relay's hop id, validated by {@link HopSchema}. */ export type Hop = z.infer; -// pending: /quest/m1/public-model/answer.md /** * The reserved id 0, meaning "no identity". * @@ -42,7 +40,6 @@ export type Hop = z.infer; */ export const UNKNOWN_HOP: Hop = HopSchema.parse(0n); -// pending: /quest/m1/public-model/answer.md /** * Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`. * @@ -51,7 +48,6 @@ export const UNKNOWN_HOP: Hop = HopSchema.parse(0n); */ export const MAX_HOPS = 32; -// pending: /quest/m1/public-model/answer.md /** * Generate a fresh hop with a random non-zero id. * @@ -73,7 +69,6 @@ export function randomHop(): Hop { return HopSchema.parse(raw === 0n ? 1n : raw); } -// pending: /quest/m1/public-model/answer.md /** The static cost of pulling content through a route; lower wins. */ export type Cost = bigint; @@ -143,7 +138,6 @@ export function isAnonymous(route: Route): boolean { return route.hops.includes(UNKNOWN_HOP); } -// pending: /quest/m1/public-model/answer.md /** Whether two routes name the same epoch, hop chain, cost, and auth. */ export function routesEqual(a: Route | undefined, b: Route | undefined): boolean { if (a === b) return true; diff --git a/js/net/src/internal.ts b/js/net/src/internal.ts index 5c79dbb77de..a0fec998645 100644 --- a/js/net/src/internal.ts +++ b/js/net/src/internal.ts @@ -19,7 +19,6 @@ import type { Timestamp } from "./time.ts"; import type { Groups, Info, Location, Producer, Request, Subscriber, Subscription } from "./track.ts"; import type { Advertised, Advertisements } from "./wire.ts"; -// private: plumbing /** Normalize public group bounds into an inclusive start and exclusive end. */ export function groupBounds(groups: Groups = {}): { start: number; end?: number } { const bound = (value: Groups["start"] | undefined, start: boolean): number | undefined => { @@ -36,7 +35,6 @@ export function groupBounds(groups: Groups = {}): { start: number; end?: number return { start: bound(groups.start, true) ?? 0, end: bound(groups.end, false) }; } -// pending: /quest/m1/public-model/answer.md /** * The announce-interest prefix a scope needs on a prefix-shaped wire: its literal head. * The peer echoes every suffix beneath it, and the caller filters what arrives. @@ -45,7 +43,6 @@ export function scopeHead(scope: Path.Pattern): Path.Valid { return Path.from(scope.head); } -// pending: /quest/m1/public-model/answer.md /** * Whether a segment of `path` below `prefix` starts with `.`, which hides it from announce * discovery unless the request opts in. A path at or above the prefix never hides. @@ -55,7 +52,6 @@ export function hiddenBelow(prefix: Path.Valid, path: Path.Valid): boolean { return below !== null && Path.parts(below).some((part) => part.startsWith(".")); } -// pending: /quest/m1/public-model/answer.md /** * Where each carried route lands under the requested prefix: its suffix beneath the * prefix, or the empty suffix for a route above it, where the most specific such route @@ -86,26 +82,22 @@ export function presented( return out; } -// pending: /quest/m1/public-model/answer.md /** Whether the announced prefix's subtree overlaps `scope`. */ export function scopeOverlaps(scope: Path.Pattern, prefix: Path.Valid): boolean { return scope.overlaps(Path.Pattern.subtree(prefix)); } -// pending: /quest/m1/public-model/answer.md /** What `scope` captures from an exact announced prefix, if it pins every wildcard. */ export function scopeCaptures(scope: Path.Pattern, prefix: Path.Valid): Path.Pattern[] | undefined { return scope.captures(Path.Pattern.literal(prefix)); } -// private: plumbing /** A half-open run of group sequences, `[start, end)`. */ export interface Run { start: number; end: number; } -// private: plumbing /** Add `[start, end)` to sorted, disjoint, non-adjacent `runs`, merging every run it touches. */ export function insertRun(runs: Run[], start: number, end: number): void { if (start >= end) return; @@ -160,7 +152,6 @@ export function dropAt(drops: readonly DropRun[], sequence: number): Error | und return drops.find((run) => run.start <= sequence && sequence < run.end)?.error; } -// private: plumbing /** The parts of `[start, end)` that sorted, disjoint `runs` do not cover, in order. */ export function runGaps(runs: readonly Run[], start: number, end: number): Run[] { const gaps: Run[] = []; @@ -387,16 +378,13 @@ export interface TrackRequestOptions { /** Maximum buffered datagrams per subscriber; mirrors Rust's bounded send buffer. */ export const MAX_DATAGRAMS = 64; -// pending: /quest/m1/public-model/serve.md /** The first group an exclusive track end excludes whole: one past the last group that may exist. */ export function finalSequence(end: Location): number { return end.group + (end.frame === 0 ? 0 : 1); } -// private: plumbing /** Hooks assigned in static blocks by the owning class. */ export const hooks: { - // pending: /quest/m1/public-model/deferred.md /** * Declare the end a wire subscription received: repeating it is a no-op, and a mid-group * end may refine the group boundary it falls just before. Throws `ProtocolViolation` on @@ -405,24 +393,20 @@ export const hooks: { * a higher one. Mirrors the Rust `finish_at` on a `received()` copy; assigned by `track.ts`. */ endTrack: (producer: Producer, end: Location) => void; - // pending: /quest/m1/public-model/deferred.md /** * End a wire subscription's copy cleanly without declaring an end, for a session closed * locally: a dead route, not finished content, so a resumed reader's replacement route * continues past it. Mirrors the Rust `close` on a `received()` copy; assigned by `track.ts`. */ releaseTrack: (producer: Producer) => void; - // private: capability gap /** The producer's largest reported location and its subsequent changes. */ producerLargest: (producer: Producer) => Getter; - // private: passthrough /** Re-evaluate a live-edge join at this subscriber's copy. */ joinSubscriber: (subscriber: Subscriber) => void; /** Re-evaluate a live-edge join at a producer. */ joinProducer: (producer: Producer) => void; - // pending: /quest/m1/public-model/deferred.md /** * Declare the first group a wire subscription's live feed serves (lite SUBSCRIBE_START, or * IETF SUBSCRIBE_OK's Largest) for the floor its SUBSCRIBE requested: while the demand keeps @@ -442,24 +426,19 @@ export const hooks: { * cache window once it closed cleanly, else zero; assigned by `track.ts`. */ linger: (producer: Producer) => Milli; - // private: capability gap /** Seed a received track from the publisher's reported edge; assigned by `track.ts`. */ reportedLargest: (producer: Producer, largest: Location | undefined) => void; - // pending: /quest/m1/public-model/serve.md /** Snapshot the latest group sequence for wire positioning; assigned by `track.ts`. */ latestSequence: (subscriber: Subscriber) => number | undefined; - // pending: /quest/m1/public-model/serve.md /** * The source's declared start, raised to the subscriber's cursor floor, or `"pending"` while * the upstream START that resolves it has yet to arrive; assigned by `track.ts`. */ sourceStart: (subscriber: Subscriber) => number | "pending" | undefined; - // pending: /quest/m1/public-model/serve.md /** The newest frame buffered for a subscriber, which wire answers name; assigned by `track.ts`. */ bufferedLargest: (subscriber: Subscriber) => Location | undefined; /** Call `fn` with a group's frame count now and on every change; assigned by `group.ts`. */ groupProgress: (group: GroupProducer, fn: (total: number) => void) => Dispose; - // pending: /quest/m1/public-model/serve.md /** * Take the next group the subscriber's cursor allows, without waiting; assigned by `track.ts`. * @@ -497,7 +476,6 @@ export const hooks: { givenUp: (subscriber: Subscriber, sequence: number) => { error?: Error; changed: Getter }; /** Whether a group may still land on the live feed of the subscriber's track; see {@link Land}. */ mayLand: (subscriber: Subscriber, land: Land) => boolean; - // private: capability gap /** A subscriber's largest location now, and whether it can still advance. */ peekLargest: (subscriber: Subscriber) => { largest?: Location; ended: boolean }; /** Wake once a subscriber's largest location or its end may have changed; fires at most once. */ @@ -508,12 +486,10 @@ export const hooks: { subscription: Subscription | undefined, source: (shell: ResumeShell) => ResumeSource, ) => Subscriber; - // pending: /quest/m1/public-model/serve.md /** Wake once a subscriber's group cursor may read differently; assigned by `track.ts`. */ groupChanged: (subscriber: Subscriber, fn: () => void) => Dispose; /** Apply a FETCH reader budget without inheriting SUBSCRIBE's zero default. */ fetchBudget: (subscriber: Subscriber, maxDelay: Milli | undefined) => void; - // pending: /quest/m1/public-model/serve.md /** * Replace a serving cursor. An omitted start keeps the current floor; a provided start * can lower it. Wire publishers apply SUBSCRIBE_UPDATE here rather than through @@ -526,13 +502,10 @@ export const hooks: { */ bindGroupTimed: (group: GroupProducer, timed: boolean) => void; /** Admit a datagram in its stored wire form. */ - // private: passthrough writeDatagram: (producer: Producer, datagram: Datagram, inflated?: number) => void; /** Bind the group to its immutable track codec. */ - // private: passthrough bindGroupCompression: (group: GroupProducer, compressed: boolean) => void; /** Admit a received stored slice without recompressing it. */ - // private: passthrough writeGroupFrame: (group: GroupProducer, frame: Frame, inflated?: number) => void; /** Payload footprint including a shared inflated copy, for measurements. */ groupCacheBytes: (group: GroupProducer) => number; @@ -566,10 +539,8 @@ export const hooks: { expireGroup: (group: GroupConsumer, expiry: Expiry) => void; /** Carry a handed-out group across its logical track's route changes. */ recoverGroup: (group: GroupConsumer, recover: GroupRecover) => void; - // pending: /quest/m1/public-model/serve.md /** Start a group operation unless the handed-out group has expired, and stop it if the group expires mid-flight. */ guardGroup: (group: GroupConsumer, operation: () => Promise) => Promise; - // pending: /quest/m1/public-model/serve.md /** Read a frame the wire publisher completes (or skips) once written. */ readGroupFrame: (group: GroupConsumer, from?: number, compressed?: boolean) => Promise; /** Make an evicted mirror terminal while its track timeline still contains it. */ @@ -792,7 +763,6 @@ export function spreadHash(path: string, hops: readonly bigint[]): bigint { return hash; } -// pending: /quest/m1/public-model/answer.md /** Covering prefixes, most specific first, including the empty root. */ export function* coveringPrefixes(path: Path.Valid): Generator { for (;;) { diff --git a/js/net/src/ledger.ts b/js/net/src/ledger.ts index b8462819471..42b73e5f263 100644 --- a/js/net/src/ledger.ts +++ b/js/net/src/ledger.ts @@ -10,7 +10,6 @@ import { Instant, Milli, Nano } from "@moq/clock"; */ export const GRACE = Milli(1000); -// private: plumbing /** The grace for a subscription with `maxDelay`: how long it was willing to wait for a late group anyway, or {@link GRACE} without one. */ export function grace(maxDelay: Milli): Milli { return maxDelay > 0 ? maxDelay : GRACE; diff --git a/js/net/src/listing.ts b/js/net/src/listing.ts index 8c17af94293..dddec0d06f2 100644 --- a/js/net/src/listing.ts +++ b/js/net/src/listing.ts @@ -386,7 +386,6 @@ export class Tracks { } } -// pending: /quest/m1/public-model/answer.md /** * Answers a broadcast's listings for a handler that knows the tracks it would serve, as a * session lists its peer's. A broadcast has at most one. diff --git a/js/net/src/origin.ts b/js/net/src/origin.ts index 8606a3a4c4e..c1160f4b679 100644 --- a/js/net/src/origin.ts +++ b/js/net/src/origin.ts @@ -241,7 +241,6 @@ class CoveringRoot { } } -// pending: /quest/m1/public-model/answer.md /** * One requested path: the notify node for everything watching it. * @@ -2297,7 +2296,6 @@ export class Dynamic { * * @internal */ - // pending: /quest/m1/public-model/answer.md updateRoute( route: Route | { epoch?: Route["epoch"]; hops?: Route["hops"]; cost?: Route["cost"]; auth?: Route["auth"] }, ): void { @@ -2433,7 +2431,6 @@ export class Request { * @throws Error when handed a broadcast a session delivered. The request stays open. * @internal */ - // pending: /quest/m1/public-model/answer.md accept(source: broadcast.Producer | broadcast.Consumer): void { if (this.#done) { // A withdrawn request takes no answer. Only the handed-off consumer is ours; diff --git a/js/net/src/priority.ts b/js/net/src/priority.ts index 42d1e4c31b8..26879c007ba 100644 --- a/js/net/src/priority.ts +++ b/js/net/src/priority.ts @@ -38,7 +38,6 @@ export interface Rank { position?: number; } -// private: plumbing /** * The transport send order for a group, where HIGHER values are transmitted first. * @@ -61,7 +60,6 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(Math.trunc(value), min), max); } -// private: plumbing /** * Ranks one subscription's group streams against each other, and re-ranks them whenever the * set changes or the subscription is updated. diff --git a/js/net/src/tail.ts b/js/net/src/tail.ts index b155976a05a..209ff90d924 100644 --- a/js/net/src/tail.ts +++ b/js/net/src/tail.ts @@ -8,7 +8,6 @@ export interface TailOptions { grace?: () => Milli; } -// private: plumbing /** * The group streams a subscription has received, so its end can wait for the ones still owed. * diff --git a/js/net/src/track.ts b/js/net/src/track.ts index 02000b5d4f7..f8081e58f36 100644 --- a/js/net/src/track.ts +++ b/js/net/src/track.ts @@ -132,7 +132,6 @@ export interface Info { */ refresh?: number; /** The original moq-transport publisher's draft, whose timestamps may rewind. @internal */ - // private: passthrough ietf?: IetfVersion; } @@ -160,7 +159,6 @@ function priorityValue(value: number): number { return value; } -// private: passthrough /** The timescale a moq-lite hop serves a track at: none for an {@link Info.ietf} one. @internal */ export function liteTimescale(info: Info): Timescale | undefined { return info.ietf === undefined ? (info.timescale ?? undefined) : undefined; @@ -2265,7 +2263,6 @@ export class Subscriber { return undefined; } - // pending: /quest/m1/public-model/deferred.md /** * An independent subscriber to the same track, with its own cursor and its own replay of * the retained window. diff --git a/js/net/src/wire.ts b/js/net/src/wire.ts index 709302ca558..dd1d244fb3f 100644 --- a/js/net/src/wire.ts +++ b/js/net/src/wire.ts @@ -20,26 +20,18 @@ import type * as track from "./track.ts"; /** The protocol-facing operations behind a broadcast handle. */ export interface Broadcast { - // pending: /quest/m1/public-model/serve.md subscribe(name: string, options?: track.Subscription): track.Subscriber; - // pending: /quest/m1/public-model/serve.md /** The track's properties; `hold` keeps the track wanted until it aborts, the way a held TRACK stream does, and aborting it before the answer abandons the lookup. */ resolveTrackInfo(name: string, hold?: AbortSignal): Promise; - // pending: /quest/m1/public-model/serve.md /** The newest frame the track's cache holds, read without subscribing; `undefined` while it holds none. */ largest(name: string): track.Location | undefined; - // pending: /quest/m1/public-model/serve.md fetchGroup(name: string, sequence: number, options?: track.FetchGroupOptions): Promise; - // pending: /quest/m1/public-model/answer.md requested(): Promise; - // private: capability gap // Sessions dedup a peer's listings per broadcast; a custom session has no public owner yet. /** List the broadcast's tracks for `owner`, ending its older listing of this broadcast with CANCELLED. */ tracks(owner?: object): Tracks; - // pending: /quest/m1/public-model/answer.md /** Answer the broadcast's listings, as a session does with its peer's; throws while another answers. */ lister(): Lister; - // pending: /quest/m1/public-model/answer.md /** * A session delivered this broadcast. An originated route refuses to serve it: * publishing it would name this origin's hop for upstream content. @@ -52,7 +44,6 @@ export type InterestListener = (prefix: Path.Valid, wanted: boolean) => void; /** The protocol-facing operations behind an origin producer. */ export interface OriginProducer { - // pending: /quest/m1/public-model/answer.md /** * Hear the root-relative prefixes readers of this origin are watching, narrowed to its * scope: each one currently wanted first, then every change. A prefix may be reported @@ -60,55 +51,39 @@ export interface OriginProducer { * ask their peer for announcements under exactly these. Returns the unsubscribe. */ interests(listener: InterestListener): Dispose; - // pending: /quest/m1/public-model/answer.md /** Whether an advertised prefix overlaps the handle's allowed paths. */ accepts(prefix: Path.Valid): boolean; - // pending: /quest/m1/public-model/answer.md receive( prefix: Path.Valid, route?: Route | { epoch?: Route["epoch"]; hops?: Route["hops"]; cost?: Route["cost"]; auth?: Route["auth"] }, ): origin.Dynamic; - // pending: /quest/m1/public-model/answer.md attach(discovery: boolean): Dispose; - // pending: /quest/m1/public-model/answer.md expect(): Dispose; - // pending: /quest/m1/public-model/answer.md /** * Hold the origin's announcement streams overlapping `prefix` unsynced until * the returned dispose: a session holds one while its peer's initial set is in flight. */ snapshot(prefix: Path.Valid): Dispose; - // pending: /quest/m1/public-model/answer.md readonly requests: Getter | undefined>; - // pending: /quest/m1/public-model/answer.md changed(): GetPromise; - // pending: /quest/m1/public-model/answer.md /** Whether sessions should answer the request with a blind subscription right now. */ blind(slot: origin.RequestSlot): boolean; - // pending: /quest/m1/public-model/answer.md answer(path: Path.Valid, front: broadcast.Consumer): Dispose | undefined; - // pending: /quest/m1/public-model/answer.md routes(path: Path.Valid): boolean; } /** The protocol-facing operations behind an origin consumer. */ export interface OriginConsumer { - // pending: /quest/m1/public-model/answer.md routes(path: Path.Valid): boolean; - // pending: /quest/m1/public-model/answer.md readonly broadcasts: Getter | undefined>; - // pending: /quest/m1/public-model/answer.md readonly advertised: Getter; - // pending: /quest/m1/public-model/answer.md /** The announced local broadcast at `path`, when it is the route peers are offered there. * A request naming an `epoch` only matches a route serving it. */ local(path: Path.Valid, epoch?: Epoch.Valid): broadcast.Consumer | undefined; - // pending: /quest/m1/public-model/answer.md /** Resolve `path` for serving; throws `Unroutable` when the route serves another `epoch`. */ demand(path: Path.Valid, epoch?: Epoch.Valid, signal?: AbortSignal): Promise; } -// pending: /quest/m1/public-model/answer.md /** One originated advertisement exposed to the publishing wire. */ export interface Advertised { readonly identity: object; @@ -123,7 +98,6 @@ export interface Instance { readonly route: Pick; } -// pending: /quest/m1/public-model/answer.md /** * Whether two advertisements serve one publisher instance: the same epoch, or, without one, * the same advertisement, since nothing says another serves the same bytes. @@ -133,7 +107,6 @@ export function sameInstance(a: Instance, b: Instance): boolean { return a.identity === b.identity; } -// pending: /quest/m1/public-model/answer.md /** * Every originated advertisement per prefix, most preferred first. A reader takes the first * one its scope admits, so a cheaper route it cannot use never hides one it can. @@ -142,9 +115,7 @@ export type Advertisements = ReadonlyMap; /** The protocol-facing operations behind an established session. */ export interface Established { - // private: plumbing consume(path: Path.Valid): broadcast.Consumer; - // private: plumbing /** Settles with the peer's GOAWAY; the session keeps serving until it closes. */ readonly goaway: GetPromise; } @@ -152,13 +123,11 @@ export interface Established { type View = Broadcast | OriginProducer | OriginConsumer | Established; const views = new WeakMap(); -// private: plumbing /** Register the package-private view for a handle. */ export function registerWire(handle: object, view: View): void { views.set(handle, view); } -// pending: /quest/m1/public-model/deferred.md /** * Replace selected operations on a broadcast's package-private view. * @@ -174,7 +143,6 @@ export function overrideBroadcastWire( views.set(handle, { ...(view as Broadcast), ...overrides, fromSession: true }); } -// private: plumbing export function wireOf(handle: broadcast.Producer | broadcast.Consumer): Broadcast; export function wireOf(handle: origin.Producer): OriginProducer; export function wireOf(handle: origin.Consumer): OriginConsumer; diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 7bf35290c22..fe15b25c4c0 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -88,7 +88,7 @@ Rejected in planning: - [Transport config](/quest/m1/api-audit/transport-config.md) - moq-tokio and moq-quic configs have one meaning for unset, enums for modes, no order-dependent setters, and no leftover features - [Relay config](/quest/m1/api-audit/relay-config.md) - moq-relay and its embedders load config through one path that refuses old spellings, under names that do not collide - [Auth and clock names](/quest/m1/api-audit/auth-clock-names.md) - refusal and reason types have distinct names, and the Rust and JS clocks mirror each other -- [Session-only items stay private](/quest/m1/api-audit/model-private.md) - model items only the built-in sessions use are private, and the pending and private marks are gone +- [Session-only items stay private](/quest/m1/api-audit/model-private.md) - model items only the built-in sessions use are private - [Only sessions accept a request](/quest/m1/api-audit/claim-request-type.md) - accepting an application claim's request fails to compile, and the peer-route hatches are crate-private - [One range setter](/quest/m1/api-audit/serve.md) - `with_range` replaces seven cursor setters and nothing else of the serving cursor goes public - [A request's source enforces its validity](/quest/m1/api-audit/answer.md) - a stale source refuses new track requests by itself, with no public additions diff --git a/quest/m1/api-audit/model-private.md b/quest/m1/api-audit/model-private.md index ab1dc02c16c..ab8cfcc6f48 100644 --- a/quest/m1/api-audit/model-private.md +++ b/quest/m1/api-audit/model-private.md @@ -4,8 +4,7 @@ moq-net and `@moq/net` model items that only the built-in sessions use are private. The public model serves applications: publish, subscribe, and answer -on demand. Nothing in the source marks items as pending or deliberately -private. +on demand. ## Plan @@ -23,9 +22,8 @@ for a named consumer. `pub(crate)`. JS `Track.Request.stage`, `Track.Staged`, and `Track.Producer.dropGroups` become internal again (`dropGroups` returns to `hooks.dropTrackGroups`). Keep `create_frame(self)`: hang and moq-mux use it. -- Delete every `// pending:` and `// private:` mark in `rs/moq-net` and - `js/net`; their lint is already gone. The moq-transport passthrough - exception stays documented in +- The `// pending:` and `// private:` marks and their lint are already gone. + The moq-transport passthrough exception stays documented in [moq-transport passthrough](/quest/m1/ietf-extension/README.md). Rejected: the toy-transport integration tests proving a third party could diff --git a/rs/moq-net/src/model/bandwidth.rs b/rs/moq-net/src/model/bandwidth.rs index df3d781ec5b..8a619a98897 100644 --- a/rs/moq-net/src/model/bandwidth.rs +++ b/rs/moq-net/src/model/bandwidth.rs @@ -231,7 +231,6 @@ impl Allocator { /// The same registry dividing `estimate` instead, or nothing when it is `None`. #[cfg(feature = "async")] - // private: capability gap // Sessions follow their allocator's target; a custom session has no public way to yet. pub(crate) fn with_estimate(&self, estimate: Option) -> Self { Self { @@ -258,7 +257,6 @@ impl Allocator { /// Follow the probe target, for the session that probes on the reservations' behalf. #[cfg(any(feature = "async", test))] - // private: capability gap // Sessions follow their allocator's target; a custom session has no public way to yet. pub(crate) fn target(&self) -> Target { Target { @@ -485,7 +483,6 @@ struct Entry { /// An [`Allocator`]'s probe target, followed by the session that probes for it. #[cfg(any(feature = "async", test))] -// private: capability gap // Sessions follow their allocator's target; a custom session has no public way to yet. pub(crate) struct Target { /// `None` once every allocator is gone. @@ -498,7 +495,6 @@ impl Target { /// The target once it differs from the last one returned, arming `waiter` otherwise. /// /// Zero once every allocator is gone, since nothing is left reserving. - // private: capability gap // Sessions follow their allocator's target; a custom session has no public way to yet. pub(crate) fn poll_changed(&mut self, waiter: &kio::Waiter) -> Poll { let Some(registry) = &self.registry else { diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index 06af68791cf..8fd74fdf2a5 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -24,7 +24,6 @@ use super::{Requests, WeakCache}; #[path = "listing.rs"] mod listing; -// private: capability gap // Sessions dedup a peer's listings per broadcast; a custom session has no public owner yet. #[cfg(feature = "async")] pub(crate) use listing::Owner; @@ -68,7 +67,6 @@ pub struct Info { // What the frames this broadcast's groups are still receiving may allocate up front, // inherited from the origin handle that created it. - // private: tuning pub(crate) frames: super::frame::Budget, } @@ -530,7 +528,6 @@ impl Producer { } /// `Ready(Ok)` once no [`Consumer`] handle is left, `Ready(Err)` once the broadcast ended. - // pending: /quest/m1/public-model/answer.md pub(crate) fn poll_unheld(&self, waiter: &kio::Waiter) -> Poll> { self.alive.token.poll_unused(waiter) } @@ -538,7 +535,6 @@ impl Producer { /// [`Self::close`], unless a [`Consumer`] holds the broadcast or a track it asked for /// still waits for a handler. Atomic with minting a consumer, so a holder either keeps /// the broadcast or only ever sees it closed. Returns whether the broadcast is closed. - // pending: /quest/m1/public-model/answer.md pub(crate) fn close_unheld(&self) -> bool { self.alive.close_unheld() } @@ -720,7 +716,6 @@ impl Drop for Alive { /// Whether a path request still names the source that handed out its track. #[derive(Clone, Default)] #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] -// pending: /quest/m1/public-model/answer.md pub(crate) enum RequestValidity { #[default] Unrestricted, @@ -732,13 +727,11 @@ pub(crate) enum RequestValidity { impl RequestValidity { #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn token(route: Option>) -> Self { route.map(Self::Route).unwrap_or_default() } #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn for_source(route: &crate::origin::Dynamic, path: &crate::Path) -> Self { route .request_validity(path) @@ -748,7 +741,6 @@ impl RequestValidity { /// Capture one selection period so a pending request never revives after a replacement. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn start(&self) -> Self { match self { Self::Selected(selected) => Self::Period(selected.current(), selected.clone()), @@ -757,7 +749,6 @@ impl RequestValidity { } #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn check(&self, waiter: &kio::Waiter) -> Result<(), crate::Error> { match self { Self::Route(route) if route.poll_closed(waiter).is_ready() => Err(crate::Error::Unroutable), @@ -792,7 +783,6 @@ impl Producer { /// A peer's retraction and a dead session end the source the same way: a broadcast /// carries no end cause past this hop. Shared by the lite and IETF subscribers. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] -// pending: /quest/m1/public-model/answer.md pub(crate) struct SourceGuard(Producer); impl SourceGuard { diff --git a/rs/moq-net/src/model/datagram.rs b/rs/moq-net/src/model/datagram.rs index 7f1637b94f2..9a4ede400f3 100644 --- a/rs/moq-net/src/model/datagram.rs +++ b/rs/moq-net/src/model/datagram.rs @@ -43,7 +43,6 @@ pub struct Datagram { } #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) struct Wire { pub object_id: Option, pub priority: Option, @@ -53,7 +52,6 @@ pub(crate) struct Wire { } #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) struct Stored { pub datagram: Datagram, pub wire: Option>, diff --git a/rs/moq-net/src/model/frame.rs b/rs/moq-net/src/model/frame.rs index c93bc1dbf96..eb6947aeb24 100644 --- a/rs/moq-net/src/model/frame.rs +++ b/rs/moq-net/src/model/frame.rs @@ -76,7 +76,6 @@ impl Frame { /// A frame with the relay metadata its group keeps beside it, read only by wire egress. #[cfg(feature = "async")] #[derive(Clone, Debug)] -// private: passthrough pub(crate) struct Object { pub frame: Frame, pub fields: Option>, @@ -94,7 +93,6 @@ impl std::ops::Deref for Object { } /// A streamed frame's private header, alongside the unchanged public header. -// private: passthrough pub(crate) struct Header { pub info: Info, pub fields: Option>, @@ -207,7 +205,6 @@ impl Buffer { /// Every broadcast, track, and group minted from an [`crate::origin::Producer`] handle shares /// its budget. The built-in sessions each ingest through a handle with its own. #[derive(Clone, Default)] -// private: tuning pub(crate) struct Budget(Arc); struct Remaining(AtomicUsize); @@ -232,7 +229,6 @@ impl Budget { /// A budget of `bytes`, for a benchmark sweeping it. #[cfg(any(test, feature = "fuzz"))] - // private: tuning pub(crate) fn new(bytes: usize) -> Self { Self(Arc::new(Remaining(AtomicUsize::new(bytes)))) } @@ -531,7 +527,6 @@ impl Producer { /// Transfer a receive handler's cancellation cleanup into its in-flight frame. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap // A dropped receive handler cannot name its error; a public driver aborts explicitly. pub(crate) fn cancel_on_drop(mut self) -> Self { self.cancel_on_drop = true; @@ -566,7 +561,6 @@ impl Producer { /// /// `coding::Reader::poll_read_frame` owns the pairing and is the only caller. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: tuning pub(crate) fn write_deferred(&mut self, chunk: B) -> Result<()> { let len = chunk.as_ref().len(); if len > self.remaining() { @@ -593,7 +587,6 @@ impl Producer { /// Publish what has been written so far, waking consumers parked on the group. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: tuning pub(crate) fn notify(&self) { self.group().frame_notify(); } @@ -705,12 +698,10 @@ pub struct Consumer { state: kio::Consumer, info: Info, #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fields: Option>, /// The inflated size of a compressed frame read in its stored form (see /// [`group::Consumer::compressed`]), whose payload is then the DEFLATE slice. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) inflated: Option, source: Source, // Byte offset consumed so far. diff --git a/rs/moq-net/src/model/group.rs b/rs/moq-net/src/model/group.rs index 1d59be2aef0..046881fb043 100644 --- a/rs/moq-net/src/model/group.rs +++ b/rs/moq-net/src/model/group.rs @@ -78,7 +78,6 @@ impl Info { /// supplies the parent track's [`track::Info`]. This helper exists for in-crate /// tests that don't exercise timestamps. #[cfg(test)] - // private: test pub(crate) fn produce(self) -> Producer { Producer::new( self, @@ -503,7 +502,6 @@ enum Source { /// The IETF header state that cannot be inferred from a plain frame list. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] #[derive(Clone, Debug)] -// private: passthrough pub(crate) struct Wire { /// Object IDs differ from the public frame ordinals, so ordinal recovery cannot stitch. pub sparse: bool, @@ -959,7 +957,6 @@ pub struct Producer { /// Shared group state without permission to write frames or finish the group. #[derive(Clone)] -// private: capability gap // Sessions need an abortable observer while public callers use Demand. pub(crate) struct Handle { state: kio::Producer, @@ -1028,13 +1025,11 @@ impl std::ops::Deref for Producer { impl Producer { #[cfg(feature = "async")] - // private: passthrough pub(crate) fn wire(&self) -> Option { self.handle.state.read().wire().cloned() } #[cfg(feature = "async")] - // private: passthrough pub(crate) fn set_wire(&self, mut wire: Wire) -> Result<()> { let mut state = modify(&self.handle.state)?; // A head adopted before the header arrived holds the objects below the stream's. @@ -1048,7 +1043,6 @@ impl Producer { } #[cfg(feature = "async")] - // private: passthrough pub(crate) fn mark_gap(&self, id: u64, gap: Option) -> Result<()> { if gap.is_some_and(|gap| gap > id) { return Err(Error::MalformedTrack); @@ -1073,7 +1067,6 @@ impl Producer { } #[cfg(feature = "async")] - // private: passthrough pub(crate) fn set_end_object(&self, fields: super::object::Fields) -> Result<()> { let mut state = modify(&self.handle.state)?; if state.fin.is_some() || state.relay.as_ref().is_some_and(|relay| relay.end_object.is_some()) { @@ -1154,7 +1147,6 @@ impl Producer { /// Whether the parent track compresses, so a received frame arrives as a stored slice /// with its inflated size ([`frame::Header::inflated`]). #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn compressed(&self) -> bool { self.handle.track.compress } @@ -1404,7 +1396,6 @@ impl Producer { /// [`Self::create_frame`] with the moq-transport object fields a relay forwards. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn create_object(mut self, frame: frame::Header) -> Result { let meter = self.chunk_meter(frame.inflated); let size = frame.info.size as usize; @@ -1654,7 +1645,6 @@ impl Producer { kio::wait(|waiter| self.handle.poll_closed(waiter)).await } /// The non-writing observer used by the cache and built-in sessions. - // private: capability gap // Sessions need an abortable observer while public callers use Demand. pub(crate) fn handle(&self) -> Handle { self.handle.clone() @@ -1676,7 +1666,6 @@ impl Producer { dead_code, reason = "the answering API retains these on the writer as well as the session observer" )] - // pending: /quest/m1/public-model/answer.md pub(crate) fn abort_unused(&self, err: Error) -> bool { self.handle.abort_unused(err) } @@ -1688,7 +1677,6 @@ impl Producer { /// track lock never takes the group's. Monotone, and only ever conservative: a /// concurrent abort can still read as live for the length of [`Self::abort`], /// which hands out a group whose consumer then surfaces the abort. - // private: test pub(crate) fn is_aborted(&self) -> bool { self.handle.is_aborted() } @@ -1714,7 +1702,6 @@ impl Producer { dead_code, reason = "the answering API retains these on the writer as well as the session observer" )] - // pending: /quest/m1/public-model/answer.md pub(crate) fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> { self.handle.poll_unused(waiter) } @@ -1722,7 +1709,6 @@ impl Producer { impl Handle { /// Abort the group without taking ownership of its writer. - // private: capability gap // Sessions cancel a group while its writer is in a frame. pub(crate) fn abort(&self, err: Error) -> Result<()> { self.close_aborted(err) @@ -1730,7 +1716,6 @@ impl Handle { /// Abort the group with `err` unless it already holds every frame: the publisher gave /// up a group still arriving, while one that finished here stays whole. - // private: capability gap // Sessions cancel an unfinished FETCH while its writer belongs to its data stream. pub(crate) fn abort_open(&self, err: Error) { if let Ok(state) = self.state.write() @@ -1823,7 +1808,6 @@ impl Handle { /// /// Counts any frames the group [started past](Producer::start_at), so it's the group's /// logical length rather than the number of frames this handle holds. - // private: capability gap // Sessions observe a group while its sole writer belongs to a frame. pub(crate) fn frame_count(&self) -> usize { self.state.read().next_index @@ -1848,7 +1832,6 @@ impl Handle { /// closed. Consumer creation and the check share a lock, so a reader arriving after /// [`poll_unused`](Self::poll_unused) keeps the group alive instead of reading the abort. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn abort_unused(&self, err: Error) -> bool { match self.state.write_unused() { kio::Unused::Idle(guard) => { @@ -1903,7 +1886,6 @@ impl Handle { /// track lock never takes the group's. Monotone, and only ever conservative: a /// concurrent abort can still read as live for the length of [`Self::abort`], /// which hands out a group whose consumer then surfaces the abort. - // private: test pub(crate) fn is_aborted(&self) -> bool { self.alive.aborted.load(Ordering::Acquire) } @@ -1917,7 +1899,6 @@ impl Handle { } /// Whether the group was finished: it holds every frame it will ever have. - // private: test pub(crate) fn is_finished(&self) -> bool { self.state.read().fin.is_some() } @@ -2096,7 +2077,6 @@ impl Handle { } /// Create a new consumer for the group. - // private: capability gap // Sessions observe a group while its sole writer belongs to a frame. pub(crate) fn consume(&self) -> Consumer { self.consumer(self.state.consume()) @@ -2151,14 +2131,12 @@ impl Handle { /// Poll until the group is aborted, including by eviction; ready with the cause. /// Finishing does not close it. - // private: capability gap // Sessions observe a group while its sole writer belongs to a frame. pub(crate) fn poll_closed(&self, waiter: &kio::Waiter) -> Poll { self.state.poll_closed(waiter).map(|()| self.abort_reason()) } /// Watch reader demand without keeping the group alive. - // private: capability gap // Sessions observe a group while its sole writer belongs to a frame. pub(crate) fn demand(&self) -> Demand { Demand { @@ -2169,7 +2147,6 @@ impl Handle { /// Poll for the group becoming unused (every consumer dropped). #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> { self.state.poll_unused(waiter).map(|_| ()) } @@ -2521,7 +2498,6 @@ impl std::ops::Deref for Consumer { impl Consumer { #[cfg(feature = "async")] - // private: passthrough pub(crate) fn last_object(&self) -> Option { let state = self.cursor.state.read(); match state.wire() { @@ -2531,7 +2507,6 @@ impl Consumer { } #[cfg(feature = "async")] - // private: passthrough pub(crate) fn wire(&self) -> Option { self.cursor.state.read().wire().cloned() } @@ -2546,7 +2521,6 @@ impl Consumer { /// frame's inflated size beside it ([`frame::Object::inflated`], /// [`frame::Consumer::inflated`]), for egress that carries compression. A no-op on a /// plain group. - // private: passthrough pub(crate) fn compressed(mut self) -> Self { self.cursor.raw = true; self.cursor.prefetch = Prefetch::default(); @@ -2554,20 +2528,17 @@ impl Consumer { } /// The index of the first frame the group holds (see [`Producer::start_at`]). - // private: passthrough pub(crate) fn first_frame(&self) -> u64 { self.cursor.state.read().offset as u64 } #[cfg(feature = "async")] - // private: passthrough pub(crate) fn end_object(&self) -> Option> { self.cursor.state.read().relay.as_ref()?.end_object.clone() } /// Fill a private batch without losing metadata through the public frame view. #[cfg(feature = "async")] - // private: passthrough pub(crate) fn poll_read_objects( &mut self, waiter: &kio::Waiter, @@ -2702,7 +2673,6 @@ impl Consumer { } /// Check the parent subscription while a wire publisher drains detached payload. - // pending: /quest/m1/public-model/serve.md pub(crate) fn poll_expired(&mut self, waiter: &kio::Waiter) -> bool { self.poll_expired_while_pending(waiter, false) } @@ -2731,7 +2701,6 @@ impl Consumer { } /// Keep checking expiry while a wire publisher still owns buffered group data. - // pending: /quest/m1/public-model/serve.md pub(crate) fn poll_expired_while_pending(&mut self, waiter: &kio::Waiter, pending: bool) -> bool { if !self.expired && (pending || self.expiry_pending()) @@ -2754,7 +2723,6 @@ impl Consumer { /// /// A machine parked on stream credit reads nothing, so without this an aborted group, /// which never expires, would wait for credit it could only spend on a reset. - // pending: /quest/m1/public-model/serve.md #[cfg(feature = "async")] pub(crate) fn poll_abandoned(&mut self, waiter: &kio::Waiter, pending: bool) -> Option { if self.poll_expired_while_pending(waiter, pending) { @@ -2810,7 +2778,6 @@ impl Consumer { /// The timescale a moq-lite hop serves the parent track at (see /// [`track::Info::lite_timescale`]). #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn lite_timescale(&self, ietf: bool) -> Option { self.track.lite_timescale(ietf) } @@ -2818,7 +2785,6 @@ impl Consumer { /// The draft the parent track's moq-transport publisher spoke, which serialized every /// object's properties, or `None` for a moq-lite one. #[cfg(feature = "async")] - // private: passthrough pub(crate) fn ietf(&self) -> Option { self.track.ietf() } @@ -2851,7 +2817,6 @@ impl Consumer { /// actually landed. /// Only moves forward; a lower `index` is ignored, since the frames behind the /// cursor may already have been handed out. - // pending: /quest/m1/public-model/serve.md pub(crate) fn start_at(&mut self, index: u64) { self.cursor.start_at(index); } @@ -2871,7 +2836,6 @@ impl Consumer { /// no frame is delivered. Reads past the cap end cleanly (`None`), as if the group /// finished there. The cap can move in either direction: raising it re-offers /// frames that are still cached. - // pending: /quest/m1/public-model/serve.md pub(crate) fn end_at(&mut self, end: impl Into) { let end = end.into().exclusive(); self.cursor.end = end.map(|end| usize::try_from(end).unwrap_or(usize::MAX)); @@ -2931,7 +2895,6 @@ impl Consumer { /// Read the next frame with its relay metadata, for wire egress. #[cfg(feature = "async")] - // private: passthrough pub(crate) fn poll_read_object(&mut self, waiter: &kio::Waiter) -> Poll>> { // Every frame is served through the cursor's prefetch, which holds its fields. let frame = ready!(self.poll_read_frame(waiter))?; @@ -3307,14 +3270,12 @@ impl Fetch { /// carries its own producer handle, so it works the same whether or not the track /// has been accepted yet. pub struct Request { - // pending: /quest/m1/public-model/deferred.md pub(crate) state: kio::Producer, pub(super) fetch: kio::Shared, pub(super) sequence: u64, pub(super) priority: i8, pub(super) max_delay: Option, pub(super) frame_start: u64, - // pending: /quest/m1/public-model/deferred.md pub(crate) result: kio::Producer, pub(super) done: bool, } diff --git a/rs/moq-net/src/model/listing.rs b/rs/moq-net/src/model/listing.rs index 9a76c4d5a99..a7583f6c563 100644 --- a/rs/moq-net/src/model/listing.rs +++ b/rs/moq-net/src/model/listing.rs @@ -66,12 +66,10 @@ struct Entry { /// [`Error::Cancel`], so an owner's listings cost one queue per broadcast however many /// paths or requests reach it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -// private: capability gap // Sessions dedup a peer's listings per broadcast; a custom session has no public owner yet. pub(crate) struct Owner(u64); impl Owner { - // private: capability gap // Sessions dedup a peer's listings per broadcast; a custom session has no public owner yet. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] pub(crate) fn new() -> Self { @@ -512,7 +510,6 @@ impl Consumer { /// [`Self::tracks`] for `owner`, ending its older listing of this broadcast with /// [`Error::Cancel`]. - // private: capability gap // Sessions dedup a peer's listings per broadcast; a custom session has no public owner yet. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] pub(crate) fn tracks_as(&self, owner: Owner) -> Result { diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index 5fb671b4c18..23774659ae7 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -19,13 +19,11 @@ mod front; mod aborts; mod bytes; mod clock; -// private: passthrough pub(crate) mod datagram; mod delivered; mod expiry; // The DEFLATE codec behind compressed tracks. mod flate; -// private: passthrough pub(crate) mod object; mod requests; mod resume; @@ -41,7 +39,6 @@ use requests::Requests; use weak_cache::{WeakCache, WeakEntry}; pub use bytes::*; -// pending: /quest/m1/public-model/serve.md pub(crate) use subscription::Cap; // Datagram stays flat at the crate root (a small track-adjacent wire type), // not under a role module. @@ -70,24 +67,19 @@ pub use origin_impl::{Consume, Hop, Hops, InvalidHop}; // The announce-interest prefixes a scope needs on a prefix-shaped wire. #[cfg(feature = "async")] -// pending: /quest/m1/public-model/answer.md pub(crate) use origin_impl::interest_prefixes; // Held by a session while its peer's initial announcements arrive. #[cfg(feature = "async")] -// pending: /quest/m1/public-model/answer.md pub(crate) use origin_impl::Snapshot; // What a client session asks its peer to announce, as the origin's readers change. #[cfg(feature = "async")] -// pending: /quest/m1/public-model/answer.md pub(crate) use origin_impl::{InterestChange, Interests}; // The advertise-only route guard, for tests shaping the route table. #[cfg(all(test, feature = "async"))] -// private: test pub(crate) use origin_impl::AnnounceProducer; #[cfg(all(test, feature = "async"))] -// private: test pub(crate) use origin_impl::ProduceTest; diff --git a/rs/moq-net/src/model/object.rs b/rs/moq-net/src/model/object.rs index 0350544bf54..8e08bc7b05e 100644 --- a/rs/moq-net/src/model/object.rs +++ b/rs/moq-net/src/model/object.rs @@ -4,14 +4,12 @@ use bytes::Bytes; use crate::Timestamp; #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) enum Value { Int(u64), Bytes(Bytes), } #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) struct Property { pub id: u64, pub value: Value, @@ -22,7 +20,6 @@ pub(crate) struct Property { /// On a timed track, the mutable Timestamp is never held: it is the frame's, which egress /// restates. On an untimed one it has no units, so it stays in the block like any other. #[derive(Clone, Debug, Default, PartialEq, Eq)] -// private: passthrough pub(crate) struct Properties { /// The Timestamp the block's Immutable Properties hold on a timed track, which keeps /// its ingress units. @@ -36,7 +33,6 @@ pub(crate) struct Properties { /// Kept encoded because a decoded list costs many times its wire size, and a relay holds /// one per cached object. Egress decodes it only to re-encode for another draft. #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) struct Block { /// The properties in ascending ID order, serialized as the track's /// [`TrackProperties::version`] does. Never empty. @@ -59,7 +55,6 @@ impl Properties { /// The moq-transport state of a track a moq-transport publisher published, which a relay /// forwards unchanged. #[derive(Clone, Debug, PartialEq, Eq)] -// private: passthrough pub(crate) struct TrackProperties { /// The draft the publisher spoke, which serialized these and every object's [`Block`] /// and gives each property its meaning. A publisher on another draft is a new instance. @@ -71,7 +66,6 @@ pub(crate) struct TrackProperties { /// Object numbering is separate from the public frame's ordinal in its group. #[derive(Clone, Debug)] -// private: passthrough pub(crate) struct Fields { pub id: u64, pub properties: Properties, diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 03c3cf2f3d6..6c0ce2ff7ba 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -80,7 +80,6 @@ impl Hop { /// Build a hop from an id read off the wire, where 0 is legal. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn from_wire(id: u64) -> Result { if id >= 1u64 << 62 { return Err(DecodeError::InvalidValue); @@ -423,12 +422,10 @@ impl Cost { /// A peer without a wire cost: the same as [`Cost::default`], so only the /// local link price counts. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) const UNKNOWN: Self = Self(0); /// Add a link's static price without overflowing the wire ceiling. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn charged(self, link_cost: u64) -> Self { Self::new(self.0.saturating_add(link_cost)) } @@ -518,11 +515,9 @@ pub struct Route { /// Local selection state: split-horizon matches this as well as the chain, so a /// route is never advertised back to the session it came from even when that /// session withheld an identity (hop 0). Never forwarded. - // pending: /quest/m1/public-model/answer.md pub(crate) via: Hop, /// Where the route entered this origin; see [`Self::source`]. Never forwarded. - // pending: /quest/m1/public-model/answer.md pub(crate) source: Source, } @@ -584,7 +579,6 @@ impl Route { /// Not part of the advertised route: an assigned identity is private selection /// state and must not be forwarded. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn with_via(mut self, via: Hop) -> Self { self.via = via; self @@ -1825,7 +1819,6 @@ impl Default for OriginScope { /// The announce-interest prefixes that cover a pattern scope on a prefix-only /// wire: each member's literal head, minus heads another already covers. -// pending: /quest/m1/public-model/answer.md pub(crate) fn interest_prefixes(allowed: &Patterns) -> Vec { let mut heads: Vec = allowed .iter() @@ -2022,7 +2015,6 @@ impl Producer { /// Give this handle a frame budget of its own, so what one session allocates up front /// never crowds out another's. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: tuning pub(crate) fn with_own_frames(mut self) -> Self { self.frames = Default::default(); self @@ -2030,7 +2022,6 @@ impl Producer { /// The ingress stats context this handle was tagged with. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn stats(&self) -> &stats::Session { &self.stats } @@ -2101,7 +2092,6 @@ impl Producer { } // Sessions enforce the policy configured once on the origin. - // private: tuning #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn max_hops(&self) -> HopLimit { self.max_hops @@ -2112,7 +2102,6 @@ impl Producer { /// subscriber issues no ANNOUNCE_PLEASE). Used to fill an unset session half /// so both the publisher and subscriber loops still run. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn empty(hop: Hop) -> Self { // No allowed prefixes means no broadcast is ever created, so nothing will // ever be queued on the detached submission handle. @@ -2244,7 +2233,6 @@ impl Producer { /// [`Dynamic`] requests with one of these; the requester already holds /// the request's result channel, so the table never needs to resolve it. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn create_source(&self, path: impl AsPath) -> broadcast::Producer { let path = path.as_path(); let full = self.root.join(&path).to_owned(); @@ -2269,7 +2257,6 @@ impl Producer { /// advertises through a broadcast ([`broadcast::Producer::announce`]) or a /// [`Dynamic`] handler, which serve what they claim. #[cfg(test)] - // private: test pub(crate) fn announce(&self, prefix: impl AsPath, route: Route) -> Result { Announcing::new(self, prefix)?.announce( route, @@ -2285,7 +2272,6 @@ impl Producer { /// synced until the returned guard drops. Taken before the request goes out, so /// nothing reads as synced in between. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn snapshot(&self, prefix: impl AsPath) -> Snapshot { let prefix = self.root.join(prefix.as_path()).to_owned(); self.shared.lock().snapshot(&self.shared, prefix, self.link) @@ -2295,7 +2281,6 @@ impl Producer { /// prefixes readers of the origin want discovered, mapped onto this handle's /// scope, starting with the current ones. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn interests(&self) -> Interests { let queue = kio::Queue::new(); let mut state = self.shared.lock(); @@ -2335,7 +2320,6 @@ impl Producer { /// Whether routes arrive here on a link between relays ([`Self::peer`] or /// [`Self::upstream`]), whose sessions discover everything they may. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn relays(&self) -> bool { self.link != Link::Local } @@ -2684,7 +2668,6 @@ impl Announcer { /// retracts the route, which [`AnnounceConsumer`]s observe and sessions withdraw /// from their peers. #[must_use = "dropping an announcement retracts the route"] -// private: test pub(crate) struct AnnounceProducer { shared: SendCell>, /// The table entries this advertisement created, by prefix and id. A prefix @@ -4174,7 +4157,6 @@ impl Selected { /// The lifetime of path requests while this peer still selects the held instance. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] -// pending: /quest/m1/public-model/answer.md pub(crate) struct Selection { shared: SendCell>, path: PathOwned, @@ -4266,7 +4248,6 @@ struct SnapshotEntry { /// Every announce consumer whose scope the prefix overlaps is unsynced /// (see [`AnnounceConsumer::poll_synced`]) until this drops: once the session /// has inserted the announcements its peer counted, or it gave up on them. -// pending: /quest/m1/public-model/answer.md pub(crate) struct Snapshot { shared: Option>>, prefix: PathOwned, @@ -4276,7 +4257,6 @@ pub(crate) struct Snapshot { impl Snapshot { /// A snapshot nothing waits on. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn landed() -> Self { Self { shared: None, @@ -4362,7 +4342,6 @@ impl Listener { /// What a session asks its peer about, as it changes: the broadest prefixes /// readers of the origin want discovered, mapped onto the session's scope. -// pending: /quest/m1/public-model/answer.md pub(crate) struct Interests { shared: SendCell>, id: u64, @@ -4372,7 +4351,6 @@ pub(crate) struct Interests { impl Interests { /// The next change, or `None` once the origin closed. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn poll_next(&self, waiter: &kio::Waiter) -> Poll> { Poll::Ready(ready!(self.queue.poll_pop(waiter)).ok()) } @@ -4391,7 +4369,6 @@ impl Drop for Interests { not(feature = "async"), expect(dead_code, reason = "only async sessions consume discovery changes") )] -// pending: /quest/m1/public-model/answer.md pub(crate) struct InterestChange { /// The prefix, relative to the session's root. pub prefix: PathOwned, @@ -5435,7 +5412,6 @@ impl Dynamic { /// Retire an explicit peer withdrawal, hiding derived paths only once its /// last live advertisement at the prefix is gone. A lost session just drops. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn withdrawn(self) { self.announcement.retract(true); } @@ -6061,7 +6037,6 @@ impl Consumer { /// anonymous route from echoing back. Pass [`Hop::UNKNOWN`] for an anonymous /// peer. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn excluding(mut self, peer: Hop) -> Self { self.horizon.exclude = Some(peer); self @@ -6070,7 +6045,6 @@ impl Consumer { /// A clone whose announcements skip the routes this origin cannot forward: a /// chain already at [`Config::max_hops`] has no room for our hop. Sessions whose /// wire carries the hop path apply this to the routes they advertise. - // private: tuning #[cfg_attr(not(feature = "async"), allow(dead_code))] pub(crate) fn forwardable(mut self) -> Self { self.horizon.max_hops = Some(self.max_hops); @@ -6091,7 +6065,6 @@ impl Consumer { /// A clone for a session serving its peer: its reads are the peer's, not a /// local reader's, so they never make a session ask its own peer about them. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn serving(mut self) -> Self { self.interested = false; self @@ -6109,7 +6082,6 @@ impl Consumer { /// A clone whose [`announced`](Self::announced) reports only the routes a feed /// from `outer` hides, for a stream topping up that feed. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn beyond(mut self, outer: &Consumer) -> Self { self.hidden.beyond = Some( outer @@ -6124,7 +6096,6 @@ impl Consumer { /// Set wire discovery visibility relative to this consumer's requested root. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn discovery(mut self, hidden: bool) -> Self { self.hidden.include = hidden; self.hidden.from = Some(self.root.clone()); @@ -6133,14 +6104,12 @@ impl Consumer { /// Whether [`announced`](Self::announced) reports hidden routes too. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn includes_hidden(&self) -> bool { self.hidden.include } /// The egress stats context this handle was tagged with. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn stats(&self) -> &stats::Session { &self.stats } @@ -6168,7 +6137,6 @@ impl Consumer { /// announce-interest for a prefix outside our scope by announcing nothing, /// rather than tearing the stream down. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn empty(&self) -> Self { Self { scope: OriginScope::empty(), @@ -6298,7 +6266,6 @@ impl Consumer { /// The newest broadcast published on this origin at exactly `path`, if any. /// Test-only: a request goes through the table like any other. #[cfg(test)] - // private: test pub(crate) fn get_broadcast(&self, path: impl AsPath) -> Option { let full = self.root.join(path).to_owned(); if !self.scope.permits(&full) { @@ -6496,7 +6463,6 @@ impl Consumer { /// [`Self::request_broadcast`], refused with [`Error::Unroutable`] unless the /// winning route serves `epoch`: a peer asking for one publisher instance must /// not be handed another. - // pending: /quest/m1/public-model/answer.md pub(crate) fn request(&self, path: Path<'_>, epoch: Option<&crate::Epoch>) -> kio::Pending { // The path as this handle names it, absolute: what its scope authorizes and // what its egress is counted under, so a read through a mount bills where @@ -6691,14 +6657,12 @@ impl AnnounceConsumer { } #[cfg(test)] - // private: test pub(crate) async fn expect_next_active(&mut self, expected: impl AsPath) -> Route { let event = self.expect_next().await; Self::active(event, &expected.as_path()) } #[cfg(test)] - // private: test pub(crate) async fn expect_next_restarted(&mut self, expected: impl AsPath) -> Route { match self.expect_next().await { AnnounceEvent::Restart(announce) => { @@ -6710,7 +6674,6 @@ impl AnnounceConsumer { } #[cfg(test)] - // private: test pub(crate) async fn expect_next_ended(&mut self, expected: impl AsPath) { match self.expect_next().await { AnnounceEvent::End(announce) => assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"), @@ -6985,14 +6948,12 @@ impl AnnounceConsumer { } // The wire sender uses this only to bound its initial replay. - // private: tuning #[cfg(feature = "async")] pub(crate) fn pending_len(&self) -> usize { self.state.read().pending.len() } // A wider wire grant must reuse pending slots, rather than bypass the initial queue. - // private: tuning #[cfg(feature = "async")] pub(crate) fn replay<'a>( &mut self, @@ -7141,7 +7102,6 @@ impl AnnounceConsumer { /// the test executor, mirroring what `moq_tokio::origin::spawn` does for /// applications. #[cfg(test)] -// private: test pub(crate) trait ProduceTest { fn produce(self) -> Producer; } diff --git a/rs/moq-net/src/model/subscription.rs b/rs/moq-net/src/model/subscription.rs index 0554c86003a..26d9cf27aa7 100644 --- a/rs/moq-net/src/model/subscription.rs +++ b/rs/moq-net/src/model/subscription.rs @@ -307,7 +307,6 @@ impl Subscription { /// /// The form a wire without ranges carries: a floor below which nothing is requested. #[cfg(any(feature = "async", test))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn start(&self) -> Option { let mut floor: Option = None; for range in &self.ranges { @@ -324,7 +323,6 @@ impl Subscription { /// /// The form a wire without ranges carries, alongside [`Self::start`]. #[cfg(any(feature = "async", test))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn end(&self) -> Option { let mut end = Some(Position::default()); for range in &self.ranges { @@ -339,7 +337,6 @@ impl Subscription { /// Replace the ranges with one from `start` (or [`Range::oldest`]'s) to `end`: the form /// a wire without ranges carries. #[cfg(any(feature = "async", test))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn set_bounds(&mut self, start: Option, end: Option) { self.ranges = vec![Range { start: start.map_or(Range::oldest().start, Anchor::Absolute), @@ -349,7 +346,6 @@ impl Subscription { /// Replace the start of [`Self::set_bounds`]'s one range, keeping its end. #[cfg(feature = "async")] - // pending: /quest/m1/public-model/serve.md pub(crate) fn with_start(mut self, start: impl Into>) -> Self { self.set_bounds(start.into(), self.end()); self @@ -357,7 +353,6 @@ impl Subscription { /// Replace the end of [`Self::set_bounds`]'s one range, keeping its start. #[cfg(any(feature = "async", test))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn with_end(mut self, end: impl Into>) -> Self { self.set_bounds(self.start(), end.into()); self @@ -524,7 +519,6 @@ impl Position { } /// The first group this exclusive end excludes whole, `None` past the last group. - // pending: /quest/m1/public-model/serve.md pub(crate) fn final_sequence(self) -> Option { self.group.checked_add(u64::from(self.frame != 0)) } @@ -536,7 +530,6 @@ impl Position { /// before 5, `..=5` reads through it, and `..` removes the cap. A [`Bound`] converts /// too, for callers holding one (a decoded wire field, or [`Position::group_end`]). #[derive(Clone, Copy, Debug, PartialEq, Eq)] -// pending: /quest/m1/public-model/serve.md pub(crate) struct Cap(Option); impl Cap { diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index 33cf960b66e..12a960f663e 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -81,7 +81,6 @@ pub(super) struct ExpiryScan { /// consumer: HLS polls every `TARGETDURATION` seconds, commonly 6 or 10. A lingering /// track holds no upstream subscription, so waiting longer costs cached state, not a /// viewer. -// private: capability gap pub(crate) const IDLE_LINGER: Duration = Duration::from_secs(30); /// Publisher-side properties of a track. @@ -167,7 +166,6 @@ pub struct Info { /// Relay-only: moq-lite carries them only between relays, with the `ietf` extension. /// Shared, since every group holds a copy of the info. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) properties: Option>, } @@ -222,14 +220,12 @@ impl Info { /// The timescale served on a moq-lite hop, preserving moq-transport timestamps only /// when the hop negotiated the `ietf` extension. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn lite_timescale(&self, ietf: bool) -> Option { self.timescale.filter(|_| self.ietf().is_none() || ietf) } /// The original moq-transport publisher's draft, whose timestamps may rewind, or /// `None` for a native moq-lite track. - // private: passthrough pub(crate) fn ietf(&self) -> Option { self.properties.as_ref().map(|properties| properties.version) } @@ -238,7 +234,6 @@ impl Info { #[cfg(test)] impl Info { /// A millisecond track, for tests that write timestamps. - // private: test pub(crate) fn timed() -> Self { Self::default().with_timescale(Timescale::MILLI) } @@ -304,7 +299,6 @@ enum Feed { } #[derive(Default)] -// pending: /quest/m1/public-model/deferred.md pub(crate) struct TrackState { // The publisher's properties, once known; always Some for Subscriber/Producer. // Copied by value into each group it creates. @@ -726,9 +720,7 @@ pub(super) struct PendingFetch { /// Whether a fetch attempt was accepted, or why it was rejected. #[derive(Default)] -// pending: /quest/m1/public-model/deferred.md pub(crate) struct FetchOutcome { - // private: test pub(crate) rejected: Option, // The budget the rejected attempt asked its handler for, so a looser joiner can tell // that an `Old` refusal was not its own. @@ -2414,7 +2406,6 @@ impl TrackState { } /// Observe a shared track's terminal cause without extending its publisher lifetime. - // private: capability gap // Sessions retain shared state while a FETCH owns its group writer. pub(crate) fn poll_closed(producer: &kio::Producer, waiter: &kio::Waiter) -> Poll { producer @@ -2424,7 +2415,6 @@ impl TrackState { /// Abort shared track state without extending its publisher lifetime. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn abort(producer: &kio::Producer, err: Error) -> Result<()> { commit_abort(Self::modify(producer)?, err).abort(); Ok(()) @@ -2618,7 +2608,6 @@ impl Producer { /// The immutable publisher priority committed when this track was accepted. #[cfg(test)] #[cfg(feature = "async")] - // private: test pub(crate) fn publisher_priority(&self) -> i8 { self.info.priority } @@ -2630,7 +2619,6 @@ impl Producer { /// [`Request`]), which threads the broadcast's `Arc` down. The /// track opens its cache account against that broadcast's origin pool, and every /// group it creates charges into it. - // private: test pub(crate) fn new( broadcast: Arc, name: impl Into>, @@ -2819,7 +2807,6 @@ impl Producer { ) } - // private: passthrough pub(crate) fn insert_wire_datagram(&mut self, mut datagram: super::datagram::Stored) -> Result<()> { if datagram.payload.len() > super::datagram::MAX_DATAGRAM_PAYLOAD { return Err(Error::FrameTooLarge); @@ -2853,7 +2840,6 @@ impl Producer { /// stream that must inflate to exactly `size` bytes. Kept as it arrived, beside the /// inflated payload. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn insert_deflated_datagram(&mut self, mut datagram: super::datagram::Stored, size: u64) -> Result<()> { if !self.info.compress { return Err(Error::ProtocolViolation); @@ -3041,7 +3027,6 @@ impl Producer { /// Readers get nothing from the cache until [`Self::set_live`]: the upstream /// subscription ended with this copy still held, so what it cached may go stale. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn set_idle(&mut self) { if let Ok(mut state) = self.modify() { state.set_idle(); @@ -3052,7 +3037,6 @@ impl Producer { /// group its feed serves when the answer names it; see [`Self::set_idle`]. Readers /// resume once the cache shows the largest position. A no-op unless idle. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn set_live(&mut self, largest: Option, start: Option) { if let Ok(mut state) = self.modify() { state.set_live(largest, start); @@ -3062,14 +3046,12 @@ impl Producer { /// The source reported its largest frame, which [`Self::largest`] follows before the /// frame itself arrives. A hint about the edge: it never makes the frame readable. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn report_largest(&self, largest: Position) { self.state.read().cache.advance_largest(largest); } /// Whether readers may take from the cache; see [`Self::set_idle`]. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn is_live(&self) -> bool { self.state.read().feed == Feed::Live } @@ -3077,7 +3059,6 @@ impl Producer { /// While idle, the newest group cached when the track went idle: a route asked from /// its head sends it again, and its first frame says whether the cache is current. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn idle_newest(&self) -> Option { let state = self.state.read(); state.idle_newest.filter(|_| state.feed == Feed::Idle) @@ -3086,7 +3067,6 @@ impl Producer { /// [`Request::request_start`] for a copy already accepted, which a held copy's next /// subscription asks for again. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn request_start(&mut self, sequence: Option) -> Result<()> { self.modify()?.set_start(sequence, true); Ok(()) @@ -3096,7 +3076,6 @@ impl Producer { /// declared one. `None` while nothing was declared. #[cfg(test)] #[cfg(feature = "async")] - // private: test pub(crate) fn start_sequence(&self) -> Option { self.state.read().start_sequence } @@ -3183,7 +3162,6 @@ impl Producer { } #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn latest_sequence(&self) -> Option { self.state.read().max_sequence } @@ -3197,7 +3175,6 @@ impl Producer { /// first frame opened or that finished, or a later datagram. A frameless group header /// does not count, since nothing says the copy reached it yet. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn poll_past(&self, sequence: u64, waiter: &kio::Waiter) -> Poll<()> { let Some(next) = sequence.checked_add(1) else { return Poll::Pending; @@ -3344,7 +3321,6 @@ impl Producer { not(feature = "async"), allow(dead_code, reason = "Only session adapters request upstream.") )] - // private: capability gap pub(crate) fn widen_compressed(&self, subscription: &mut Subscription) { if !self.info.compress { return; @@ -3990,7 +3966,6 @@ impl Demand { #[cfg(all(test, feature = "async"))] impl Demand { /// Wait until [`Self::wanted`] satisfies `f`, returning it. Errors once the track closes. - // private: test pub(crate) async fn until(&mut self, f: impl Fn(Option) -> bool) -> Result> { kio::wait(|waiter| { loop { @@ -4019,7 +3994,6 @@ pub(super) enum DemandState { /// A watch-only handle to a track's live arrivals. // Legacy fetch batches share one ledger read per poll. -// private: tuning pub(crate) struct Arrivals { state: kio::ConsumerWeak, } @@ -4028,14 +4002,12 @@ pub(crate) struct Arrivals { impl Arrivals { /// Whether both handles watch the same track. // A fetch batch may share only one track's arrival ledger. - // private: tuning pub(crate) fn same_channel(&self, other: &Self) -> bool { self.state.same_channel(&other.state) } /// Read the ledger and wake its owner when arrivals or terminal bounds change. // One owner watch opens batch deadlines without waking each held fetch. - // private: tuning pub(crate) fn poll(&self, waiter: &kio::Waiter) -> ArrivalRead<'_> { let _ = self.state.poll(waiter, |_| Poll::<()>::Pending); let state = self.state.read(); @@ -4048,7 +4020,6 @@ impl Arrivals { } /// A locked arrival ledger, so a batch never copies or re-locks its runs per fetch. -// private: tuning pub(crate) struct ArrivalRead<'a> { state: kio::Ref<'a, TrackState>, grace: Duration, @@ -4058,14 +4029,12 @@ pub(crate) struct ArrivalRead<'a> { impl ArrivalRead<'_> { /// Whether the live feed can no longer deliver any missing group. // The batch checks closure without re-locking the track. - // private: tuning pub(crate) fn is_closed(&self) -> bool { self.state.is_closed() } /// Gaps that can expire held fetches, including the gap before the first arrival. // Only the session's held-fetch deadline index needs all live gaps. - // private: tuning pub(crate) fn folds(&self) -> impl Iterator, crate::time::Instant)> + '_ { self.state.arrivals.folds(self.grace) } @@ -4073,7 +4042,6 @@ impl ArrivalRead<'_> { /// Judge one live group and register only for its own arrival or terminal bound: the /// group, the error it was given up with, or `None` once it is never coming. // Legacy wires lack FETCH, so held requests need the live-feed verdict. - // private: capability gap pub(crate) fn poll_group( &self, sequence: u64, @@ -4256,7 +4224,6 @@ impl Consumer { /// counting as a fetch or a delivery. The IETF publisher snapshots its frame count to /// resolve Largest Object; a group that is not immediately available reads as no edge. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn peek_latest(&self) -> Option { if let Some(serving) = self.serving() { return serving.peek_latest(); @@ -4269,7 +4236,6 @@ impl Consumer { /// [`Self::peek_group`]. Walks the cache's own order, so gaps in the group numbering /// are crossed and aborted (evicted) entries are skipped. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn peek_before(&self, sequence: u64) -> Option { if let Some(serving) = self.serving() { return serving.peek_before(sequence); @@ -4291,7 +4257,6 @@ impl Consumer { /// group alive over one a subscriber actually read; an aborted (evicted) group is a /// miss. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn peek_group(&self, sequence: u64) -> Option { if let Some(serving) = self.serving() { return serving.peek_group(sequence); @@ -4309,14 +4274,12 @@ impl Consumer { /// The live arrivals of the track this handle reads now, without joining its demand. // This batch view avoids per-fetch ledger copies and locks in the session. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: tuning pub(crate) fn arrivals(&self) -> Option { self.with_state(|state| state.map(|state| Arrivals { state: state.clone() })) } /// Poll for a cached group, a terminal disposition, or a gap outliving its grace: the /// group, the error a dropped or cut group was given up with, or `None` once it is gone. - // private: test pub(crate) fn poll_group(&self, sequence: u64, waiter: &kio::Waiter) -> Poll>> { self.with_state(|state| { let Some(state) = state else { @@ -4487,7 +4450,6 @@ impl Consumer { /// Whether the cache is fed live, so the newest object it holds is the track's largest. /// Fetch-only, closed, and parked copies are not, even once they learn the end. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn is_live(&self) -> bool { if let Some(serving) = self.serving() { return serving.is_live(); @@ -4656,7 +4618,6 @@ impl Consumer { kio::wait(|waiter| self.poll_largest(waiter)).await } - // pending: /quest/m1/public-model/serve.md pub(crate) fn latest_sequence(&self) -> Option { if let Some(serving) = self.serving() { return serving.latest_sequence(); @@ -4666,7 +4627,6 @@ impl Consumer { /// The first group the declared end excludes whole, or `None` while the track is open ended. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn final_sequence(&self) -> Option { if let Some(serving) = self.serving() { return serving.final_sequence(); @@ -4679,7 +4639,6 @@ impl Consumer { /// /// Survives the track aborting, which is when a route change asks. #[cfg(test)] - // private: test pub(crate) fn resume_position(&self) -> Option { self.with_state(|state| state?.read().resume_position()) } @@ -4897,7 +4856,6 @@ impl group::Request { /// Declare the track's exclusive final sequence, as the publisher answering this /// fetch reported it. A no-op once the track declared an end, or holds a later group. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/deferred.md pub(crate) fn finish_track_at(&self, final_sequence: u64) { if let Ok(mut state) = TrackState::modify(&self.state) && state.end.is_none() @@ -6695,7 +6653,6 @@ impl Subscriber { /// Count a datagram handed on as `bytes` of payload, the single-frame group it stands in /// for. Unlike a group (metered lazily as its frames are read), a datagram goes whole. - // private: passthrough pub(crate) fn meter_datagram(&self, bytes: u64) { self.stats.meter().datagram(bytes); } @@ -6703,7 +6660,6 @@ impl Subscriber { /// The next datagram as the track stores it: its wire metadata, any status, and a /// compressed track's DEFLATE slice. Unmetered: the caller meters the form it hands on /// with [`Self::meter_datagram`]. - // private: passthrough pub(crate) fn poll_recv_stored_datagram( &mut self, waiter: &kio::Waiter, @@ -6716,7 +6672,6 @@ impl Subscriber { /// [`Self::poll_recv_stored_datagram`], metered as its plain payload: for a session that /// never carries compression. - // private: passthrough pub(crate) fn poll_recv_wire_datagram( &mut self, waiter: &kio::Waiter, @@ -6770,7 +6725,6 @@ impl Subscriber { /// resolved; see [`Subscriber::poll_start`]. A feed starting below the floor serves the /// floor's group too. `None` when the source declares none. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn poll_start(&mut self, waiter: &kio::Waiter) -> Poll> { let cursor = match &mut self.inner { Inner::Plain(cursor) => cursor, @@ -6836,7 +6790,6 @@ impl Subscriber { /// Stale skips are recorded only once this has been polled, so a cursor no wire /// publisher forwards drops for keeps none. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn poll_drop(&mut self, waiter: &kio::Waiter) -> Poll<(ops::Range, StreamError)> { match &mut self.inner { Inner::Plain(cursor) => cursor.poll_drop(waiter), @@ -6848,7 +6801,6 @@ impl Subscriber { /// largest position it holds; `None` for a track with nothing yet. A front's logical /// track answers for the route serving it. See [`Producer::set_idle`]. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn poll_live(&mut self, waiter: &kio::Waiter) -> Poll> { let cursor = match &mut self.inner { Inner::Plain(cursor) => cursor, @@ -6907,7 +6859,6 @@ impl Subscriber { /// to start there too, set [`Subscription::ranges`] via [`Self::update`]. /// See [Local cursor vs wire preference](Self#local-cursor-vs-wire-preference). #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn start_at(&mut self, sequence: u64) { match &mut self.inner { Inner::Plain(cursor) => { @@ -6923,7 +6874,6 @@ impl Subscriber { } /// Raise the read cursor's floor to `sequence`, keeping any higher floor already set. - // pending: /quest/m1/public-model/serve.md pub(crate) fn raise_start_to(&mut self, sequence: u64) { match &mut self.inner { Inner::Plain(cursor) => { @@ -6948,7 +6898,6 @@ impl Subscriber { /// with a higher bound (or unbounded) offers the ones still cached. Lowering the cap /// below the consumer's current cursor waits until the cap is raised, or the track /// finishes. - // pending: /quest/m1/public-model/serve.md pub(crate) fn end_at(&mut self, end: impl Into) { match &mut self.inner { Inner::Plain(cursor) => { @@ -7054,7 +7003,6 @@ impl Subscriber { /// or moving [`Self::poll_largest`] on: past what the cache holds once the source /// reported a newer one. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn peek_largest(&mut self) -> Option { let mut seen = None; match &mut self.inner { @@ -7067,7 +7015,6 @@ impl Subscriber { } #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/serve.md pub(crate) fn latest_sequence(&self) -> Option { match &self.inner { Inner::Plain(cursor) => cursor.state.read().max_sequence, @@ -7078,7 +7025,6 @@ impl Subscriber { /// Join again at the live edge, judged by [`Info::refresh`] like a new subscriber: a /// moq-transport NEW_GROUP_REQUEST. A front's reader joins the copy serving it. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: passthrough pub(crate) fn join(&self) { match &self.inner { Inner::Plain(cursor) => TrackState::join(cursor.state.read()), @@ -7387,7 +7333,6 @@ impl Request { /// Accept the track idle, as a copy that no live feed fills yet: readers get nothing /// from it until the feed goes live. Applied atomically with [`Self::accept`]. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // private: capability gap pub(crate) fn idle(mut self) -> Self { self.idle = true; self @@ -7452,7 +7397,6 @@ impl Request { /// share one lock, so demand returning after `Demand::wanted` went to `None` wins the race, and the /// close under that lock stops a later consumer attaching to a dead request. #[cfg_attr(not(feature = "async"), allow(dead_code, reason = "async-only"))] - // pending: /quest/m1/public-model/answer.md pub(crate) fn reject_unused(&self, err: Error) -> bool { match self.state.write_unused() { kio::Unused::Idle(guard) => {