From 342bf251ff83e6bbeb7059c8981342806029d879 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 05:09:55 -0700 Subject: [PATCH] chore(quest): retire flate-binary on main and track the JS closed-track leak flate-binary landed on dev (#4425); main's references now point there. Adds the dev-only JS late-subscriber cache leak found in #4428. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/ffi-shape/README.md | 2 +- quest/m1/ffi-shape/json.md | 2 +- quest/m1/flate-binary.md | 50 -------------------------------- quest/m1/js-closed-track-leak.md | 18 ++++++++++++ quest/m2/flate/README.md | 2 +- quest/m2/flate/bindings.md | 6 +--- quest/m2/teleop/mavlink.md | 2 +- quest/m2/teleop/robot.md | 2 +- 9 files changed, 25 insertions(+), 61 deletions(-) delete mode 100644 quest/m1/flate-binary.md create mode 100644 quest/m1/js-closed-track-leak.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 19dad3cbf7..63cb0a6250 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -22,7 +22,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Exact scope](/quest/m1/exact-scope.md) - a reader never sees an exact broadcast outside its scope, in Rust or JS; prefix routes above it still present as the empty path - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay -- [moq-binary folds into moq-flate](/quest/m1/flate-binary.md) - on dev, moq-flate and @moq/flate own the opaque snapshot and stream tracks and moq-binary is deleted - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below - [Track demand](/quest/m1/track-demand.md) - Rust and JS watch a track's subscribers through `demand()` alone - [Error messages](/quest/m1/error-display.md) - Python, Go, and Dart print `MoqError` with Rust's message, as Kotlin and Swift do @@ -41,6 +40,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - every stream group in a lite subscription arrives or is dropped by name, and lite-07 drops its stream count for it - [FIN wait expiry](/quest/m1/fin-wait-expiry.md) - a group awaiting its FIN ack still expires and follows priority updates on lite and IETF - [Cross-relay bursts](/quest/m1/cross-relay-bursts.md) - bursty small-group tracks cross two relays without lost groups, unanswered FETCHes, or stalls +- [JS closed-track leak](/quest/m1/js-closed-track-leak.md) - on dev, a subscriber that joins a closed JS track with unlimited retention is released instead of cached forever - [Session death parity](/quest/m1/session-death.md) - a local close ends tracks cleanly in both languages, and JS group readers see the session's error on session death - [Watch video guards](/quest/m1/watch-video-guards.md) - promoting a video track holds the last picture, and an older group never reaches the codec between live deltas - [Watch decoder recovery](/quest/m1/watch-decoder-recovery.md) - one malformed packet rebuilds the audio or video decoder instead of ending playback diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index 346223a423..878b592172 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -21,7 +21,7 @@ Settled shape: consumers); `json`, `flate`, `audio`, and `video` own their producers and consumers. `flate` holds the opaque snapshot and stream tracks moq-ffi publishes as `publish_binary_*` today (#4137), named after the crate they - fold into in [moq-binary folds into moq-flate](/quest/m1/flate-binary.md). + fold into in moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`). - A layer's type is constructed from the handles its Rust constructor takes, not reached through an accessor on the broadcast: JSON wraps a track (`moq_json::snapshot::Producer::new(track, config)`), so it also works on a diff --git a/quest/m1/ffi-shape/json.md b/quest/m1/ffi-shape/json.md index 3cbf8969d6..b664519c22 100644 --- a/quest/m1/ffi-shape/json.md +++ b/quest/m1/ffi-shape/json.md @@ -29,7 +29,7 @@ the root must not import it back. `flate` is the same shape over opaque bytes: moq-ffi's `binary.rs` (`publish_binary_snapshot`, `publish_binary_stream`, #4137) moves under it, mirroring `moq_flate::{snapshot, stream}` once -[moq-binary folds into moq-flate](/quest/m1/flate-binary.md). If that fold has +moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`). If that fold has not landed, name the namespace `flate` anyway rather than `binary`. Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/flate-binary.md b/quest/m1/flate-binary.md deleted file mode 100644 index 812c11c613..0000000000 --- a/quest/m1/flate-binary.md +++ /dev/null @@ -1,50 +0,0 @@ -# [M] moq-binary folds into moq-flate - -## Goal - -Opaque binary tracks live in `moq-flate` and `@moq/flate`: the `snapshot` and -`stream` modes `moq-binary` and `@moq/binary` provide today move there beside -the group-scoped codec, and `moq-binary` and `@moq/binary` are deleted. One -package owns compressed and opaque tracks, so there is no second "compressed -track" wrapper to build. - -## Plan - -Decided in the 2026-09-28 quest audit: `moq-binary` already composes -`moq-flate` into per-group windows (each group one sync-flushed DEFLATE -stream), which is what the m2 flate line planned to add as a new track wrapper. -Folding the two removes the duplicate instead of building it. - -- Rust: move `rs/moq-binary/src/{snapshot,stream}` and `Compression` into - `moq-flate` as `moq_flate::{snapshot, stream}`, keeping the codec - (`Encoder`/`Decoder`) at the root. `moq-flate` gains the `moq-net` - dependency. Delete `rs/moq-binary` and its workspace member, and repoint - `moq-mux` (`src/binary.rs`, `src/error.rs`) and `rs/libmoq` if it still - exists. -- JS: move `js/binary/src/{snapshot,stream,compression.ts}` into `@moq/flate` - as `Snapshot` and `Stream`, adding the `@moq/net` and `@moq/signals` - dependencies, and delete `js/binary`. -- Wire and catalog: unchanged. The hang catalog's `binary` section and - `moq_mux::binary` keep their names; they describe the track's content, and - the catalog section is wire. -- moq-ffi: rename `binary.rs` and its `publish_binary_*`, `MoqBinaryConfig`, - and producer types after `flate`, so every binding names the crate it wraps. - If [FFI shape](/quest/m1/ffi-shape/README.md) has already given them a - `flate` namespace, follow it instead. -- Open: whether `Compression::None` survives the move. An uncompressed opaque - track still needs a home, so the recommendation is to keep it and document - that the crate name is not a promise every track is deflated. -- Docs: fold `doc/lib/rs/moq-binary.md` into a `moq-flate` page and - `doc/lib/js/binary.md` into a `@moq/flate` page, fix `doc/.vitepress/config.ts`, - `doc/lib/{rs,js}/index.md`, `doc/concept/hang.md`, the android workflow path - filter, and add an upgrade note in `doc/setup/upgrade.md`. Grep for - `moq-binary`, `moq_binary`, and `@moq/binary`. - -Public API: breaking. `moq-binary` and `@moq/binary` are published and -deleted, and moq-ffi's binary names change, so this lands on `dev`. -`moq-flate` and `@moq/flate` grow additively. Wire: none. - -## Related - -- [Compressed tracks](/quest/m2/flate/README.md) - the hand-written wrappers expose these tracks -- [FFI shape](/quest/m1/ffi-shape/README.md) - gives the flate tracks their binding namespace diff --git a/quest/m1/js-closed-track-leak.md b/quest/m1/js-closed-track-leak.md new file mode 100644 index 0000000000..bc1755302c --- /dev/null +++ b/quest/m1/js-closed-track-leak.md @@ -0,0 +1,18 @@ +# [XS] A late subscriber to a closed JS track is released + +## Goal + +On `dev`, where `@moq/net`'s track retention can be unlimited, a subscriber +that joins a track after its producer closed is removed from the track cache +once it's done, instead of being held forever. + +## Plan + +Found while landing the main-into-dev sync (#4428). That PR stopped caching +a subscriber the producer closes when retention is unlimited, since nothing +ages it out, but a subscriber that arrives after the close takes a different +path in `js/net/src/track.ts` and is never cleaned up. Release it on the same +terms, and add a test that a late subscriber to a closed track leaves no +cache entry once it drops. + +Public API: none. Wire: none. Targets `dev`, where unlimited retention lives. diff --git a/quest/m2/flate/README.md b/quest/m2/flate/README.md index 58936a70b3..abb54d689d 100644 --- a/quest/m2/flate/README.md +++ b/quest/m2/flate/README.md @@ -9,7 +9,7 @@ are identical across all of them. ## Plan `moq-flate` and `@moq/flate` absorb `moq-binary`'s snapshot and stream modes -in [moq-binary folds into moq-flate](/quest/m1/flate-binary.md), so the crate +in moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`), so the crate already owns the per-group window a caller could otherwise desynchronize. The track wrapper this line once planned was dropped for that reason. What remains is reaching those tracks from the hand-written binding wrappers. diff --git a/quest/m2/flate/bindings.md b/quest/m2/flate/bindings.md index ef28b41d31..bf6f8042b9 100644 --- a/quest/m2/flate/bindings.md +++ b/quest/m2/flate/bindings.md @@ -11,7 +11,7 @@ decodes in the browser with `@moq/flate` and vice versa. moq-ffi publishes opaque tracks today (`publish_binary_snapshot` and `publish_binary_stream`, #4137), renamed after `flate` by -[moq-binary folds into moq-flate](/quest/m1/flate-binary.md). Only the +moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`). Only the generated bindings reach them; no wrapper does. This quest binds the existing track modes, not the bare codec: a `frame()` call across the FFI boundary invites the window desync the track modes exist to prevent. @@ -30,7 +30,3 @@ invites the window desync the track modes exist to prevent. `just test interop --all`. Public API: additive on moq-ffi and every wrapper. Wire: none. - -## Required - -- [moq-binary folds into moq-flate](/quest/m1/flate-binary.md) - the flate snapshot and stream tracks and their moq-ffi names diff --git a/quest/m2/teleop/mavlink.md b/quest/m2/teleop/mavlink.md index 5d545ee4d3..aa50bb5853 100644 --- a/quest/m2/teleop/mavlink.md +++ b/quest/m2/teleop/mavlink.md @@ -43,7 +43,7 @@ skipped and head-of-line blocking is back. So the lossy class is a latest-value snapshot of opaque bytes: `moq_binary::snapshot` (moving to `moq_flate::snapshot` in -[moq-binary folds into moq-flate](/quest/m1/flate-binary.md)), with the raw +moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`)), with the raw frame as the value. Every update is a self-contained group, so a newer value never waits behind an older one. One detail decides whether it actually delivers latest-value: diff --git a/quest/m2/teleop/robot.md b/quest/m2/teleop/robot.md index 5dd2735138..3ebd5b0079 100644 --- a/quest/m2/teleop/robot.md +++ b/quest/m2/teleop/robot.md @@ -73,7 +73,7 @@ The framing is where the guarantee lives, not the subscription flags: - The two delivery classes, as the snapshot and stream modes with the group structure and `Info::max_age` each one needs: `moq-json`'s for JSON, and the opaque-bytes ones for binary frames (`moq-binary`, folding into `moq-flate` - per [moq-binary folds into moq-flate](/quest/m1/flate-binary.md)). + per moq-binary's fold into moq-flate ([#4425](https://github.com/moq-dev/moq/pull/4425), on `dev`)). - Per-stage timestamp instrumentation, generalised from moq-boy's `status` track. Check it against the publisher-reported stats broadcast ([client stats](/quest/m1/qos/stats/schema.md), moq#2734) before adding a