Skip to content

feat(moq-net): hook up the rest of moq-lite-05 wire (TRACK_INFO, SUBSCRIBE_END, frame timestamps)#1963

Merged
kixelated merged 2 commits into
mainfrom
claude/festive-franklin-3f5961
Jun 30, 2026
Merged

feat(moq-net): hook up the rest of moq-lite-05 wire (TRACK_INFO, SUBSCRIBE_END, frame timestamps)#1963
kixelated merged 2 commits into
mainfrom
claude/festive-franklin-3f5961

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

Builds on #1954 (SETUP + PATH) to hook up the rest of the moq-lite-05 wire changes on both the Rust (rs/moq-net) and JS (js/net) sides. Everything stays gated behind the opt-in Lite05Wip version (still excluded from ALPNS / Versions::all), so this is additive: no shipping wire-format, model, or public-API change (the lite module is private), which is why it targets main like #1954.

All branches are version-gated on a new lite::Version::has_track_stream() (Rust) / hasTrackStream() (JS).

Wire changes

  • Track stream + TRACK_INFO (ControlType/StreamId 0x6): carries the publisher's immutable priority / ordered / max_latency / timescale. The publisher serves it (recv_track); decode rejects a zero timescale. The subscriber does not proactively open it yet (properties remain unused, as before).
  • Trimmed SUBSCRIBE_OK + new SUBSCRIBE_END: lite-05 SUBSCRIBE_OK carries only the resolved start group (publisher properties live in TRACK_INFO). Response discriminators become OK=0x0 / END=0x1 / DROP=0x2 on lite-05 (OK=0x0 / DROP=0x1 on 03/04). The publisher sends a resolved-group OK eagerly and a SUBSCRIBE_END before FIN (run_track now returns the last sequence); the subscriber drains OK/END/DROP until FIN.
  • Per-frame timestamp: the publisher stamps each frame with a wall-clock millisecond timestamp (Time::now() / Date.now()) and writes a zigzag delta before the frame size; the subscriber decodes and discards it.

JS-specific

  • Adds the SETUP stream that feat(moq-net): moq-lite-05 SETUP message + PATH parameter #1954 never mirrored to JS: each endpoint sends an (empty) SETUP on a unidirectional stream (DataId.Setup = 1) and reads the peer's. The browser conveys its path via the WebTransport URL, so it never sends a Path parameter (per spec).
  • connect.ts maps the negotiated lite-05 ALPN → DRAFT_05_WIP. It stays opt-in (not advertised by default), matching Rust.

Design decisions

  • Frame timestamp = auto wall-clock ms. Stamped purely at the moq-net wire layer, so hang / moq-mux / ffi are untouched. The moq-net wire timestamp is redundant with hang's in-payload (microsecond) timestamp but lets the media-agnostic relay see timing without parsing payloads (per the "timestamp on the wire" intent).
  • Timescale fixed at 1000 (ms) in TRACK_INFO.
  • Datagram group delivery deferred to a follow-up.

PATH status

The SETUP-PATH wire is fully piped for the realistic transports: WebTransport (path from the URL), native raw (Client::with_path → SETUP → Server::accept_request().path()), and the relay's internal qmux listener (scopes by it).

Known gap (not addressed here): a qmux/raw session to the main relay can't scope by the in-band SETUP path — moq_native::Request::url() is None for the WebSocket backend and Request doesn't surface the SETUP path, so only the internal listener honors it. Closing it needs a moq-native two-phase accept + relay wiring and has a rejection-timing tension (HTTP status vs reading SETUP after the handshake).

Test plan

  • cargo clippy -p moq-net -p moq-native --all-targets -- -D warnings clean (via nix toolchain)
  • cargo test -p moq-net — 374 pass, incl. new round-trip tests for TRACK_INFO, lite-05 SUBSCRIBE_OK, SUBSCRIBE_END, and response discriminators
  • just js check green (all packages, biome clean)
  • Not yet e2e-tested against a live relay
  • Docs (doc/concept, doc/bin/relay) not yet updated

Note: bare just check reports a pre-existing large_enum_variant clippy error on moq-native's RequestKind (untouched here); it's a system-clippy false positive that the nix toolchain CI uses doesn't flag.

🤖 Generated with Claude Code

(Written by Claude Opus 4.8)

…CRIBE_END, frame timestamps)

Builds on #1954 (SETUP + PATH) to implement the remaining moq-lite-05 wire
changes on both the Rust (`rs/moq-net`) and JS (`js/net`) sides. Everything
stays gated behind the opt-in `Lite05Wip` version (still excluded from `ALPNS`
and `Versions::all`), so it is additive: no shipping wire-format, model, or
public-API change.

Wire changes (version-gated on a new `Version::has_track_stream()` /
`hasTrackStream()`):

- Track stream (`ControlType`/`StreamId` 0x6) + `TRACK_INFO` carrying the
  publisher's immutable `priority` / `ordered` / `max_latency` / `timescale`.
  The publisher serves it; decode rejects a zero timescale.
- `SUBSCRIBE_OK` trimmed to just the resolved start group (publisher properties
  moved to `TRACK_INFO`); new `SUBSCRIBE_END`; the response discriminators
  become OK=0x0 / END=0x1 / DROP=0x2 on lite-05 (OK=0x0 / DROP=0x1 on 03/04).
  The publisher sends a resolved-group OK eagerly and a `SUBSCRIBE_END` before
  FIN; the subscriber drains OK/END/DROP until FIN.
- Per-frame timestamp: the publisher stamps each frame with a wall-clock
  millisecond timestamp (`Time::now()` / `Date.now()`) and writes a zigzag delta
  before the frame size; the subscriber decodes and discards it. The timescale
  is fixed at 1000, so `hang`/`moq-mux`/`ffi` are untouched (the moq-net wire
  timestamp is redundant with hang's in-payload timestamp but lets the
  media-agnostic relay see timing without parsing payloads).

The JS side additionally gains the SETUP stream that #1954 never mirrored: each
endpoint sends an (empty) SETUP on a unidirectional stream and reads the peer's.
The browser conveys its path via the WebTransport URL, so it never sends a Path
parameter. `connect.ts` maps the negotiated lite-05 ALPN to the right version;
it stays opt-in (not advertised by default), matching Rust.

Datagram delivery is deferred to a follow-up. Docs (`doc/concept`,
`doc/bin/relay`) and a live e2e test are still outstanding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @kixelated, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d5642e7-5cef-4c78-a0b1-59dd313081f9

📥 Commits

Reviewing files that changed from the base of the PR and between 933449c and 4ee3a37.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • js/net/src/lite/subscribe.ts
  • js/net/src/lite/track.ts
  • rs/moq-net/src/lite/publisher.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • js/net/src/lite/track.ts
  • rs/moq-net/src/lite/publisher.rs
  • js/net/src/lite/subscribe.ts

Walkthrough

This PR adds moq-lite Draft 05 WIP support across the Rust and JavaScript networking stacks. It introduces Track and TrackInfo messages, a Setup message, and a SubscribeEnd response. SubscribeOk is adjusted to carry only the resolved start group in Draft 05. Publisher and subscriber loops are updated for Track handling, trailing subscribe responses, and per-frame timestamp deltas. ALPN routing is extended to recognize the Draft 05 protocol.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: wiring moq-lite-05 TRACK_INFO, SUBSCRIBE_END, and frame timestamps.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the Rust and JS moq-lite-05 updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/festive-franklin-3f5961

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
js/net/src/lite/track.ts (1)

75-78: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a timescale === 0 guard on encode to mirror Rust.

Rust TrackInfo::encode_msg rejects a zero timescale (EncodeError::InvalidState), but the JS encode only validates the version, so a TrackInfo constructed with timescale: 0 would be sent and only rejected by the peer's decoder. Guard on encode for symmetry and to fail fast locally.

♻️ Proposed guard
 	async encode(w: Writer, version: Version): Promise<void> {
 		if (!hasTrackStream(version)) throw new Error("TRACK_INFO requires moq-lite-05+");
+		if (this.timescale === 0) throw new Error("TRACK_INFO timescale must be non-zero");
 		return Message.encode(w, this.#encode.bind(this));
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/net/src/lite/track.ts` around lines 75 - 78, Add a local encode-time
validation in TrackInfo.encode so it rejects a timescale of 0 before calling
Message.encode, matching the Rust TrackInfo::encode_msg behavior. Keep the
existing hasTrackStream(version) check, then add a guard that throws an error
when this.timescale is zero so invalid TrackInfo instances fail fast on the
sender side instead of being emitted and rejected later by the peer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/net/src/lite/publisher.ts`:
- Around line 175-179: The lite-05 subscribe flow in publisher.ts is using the
request startGroup or 0 when building SUBSCRIBE_OK, while the track reader is
positioned separately, so the acknowledged start can differ from actual
delivery. In the SUBSCRIBE handling path that builds the SubscribeOk and opens
the track reader, resolve the absolute start group once, pass that resolved
value into the track reader/positioning logic, and reuse the same value when
constructing SubscribeOk so both paths stay aligned.

In `@js/net/src/lite/subscribe.ts`:
- Line 332: The exported response helper functions are missing required
documentation. Add JSDoc for encodeSubscribeResponse and
decodeSubscribeResponse, and also document the other exported symbols referenced
in the same area (the helpers around lines 402-404) so every exported symbol in
subscribe.ts has a clear description, parameters, and return behavior where
applicable.
- Line 330: The exported SubscribeResponse union in subscribe.ts has been
widened with a new end variant, which changes the public API and can break
exhaustive downstream type handling. Update the SubscribeResponse and related
subscribe wire types so the stable exported type stays versioned, or move the
end case into an internal wire-only type used by the Lite05Wip path; keep the
public re-export from index.ts aligned with the stable API and use a separate
version-specific response type if needed.

In `@rs/moq-net/src/lite/publisher.rs`:
- Around line 471-473: The subscribe flow in publisher::run should use one
resolved start value for both the SUBSCRIBE_OK response and track playback.
Compute the resolved Option<u64> once from subscribe.start_group or
track.latest(), then pass that value into run_track instead of recomputing
track.latest() inside run_track. Update the start_at call in run_track to use
the passed-in resolved start so the advertised group and the actual served start
stay consistent.

In `@rs/moq-net/src/lite/subscribe.rs`:
- Around line 321-324: `SubscribeResponse` is a public enum whose variants are
part of the stable API, so adding `End` directly expands the exhaustiveness
surface for downstream matches. Update `SubscribeResponse` in `subscribe.rs` to
either mark the enum with `#[non_exhaustive]` and keep `Ok`, `End`, and `Drop`
as the public shape, or remove `End` from the public enum if it should not be
part of the API. If `End` remains public, add rustdoc to the `SubscribeResponse`
and `SubscribeEnd` symbols so the new variant is documented.

---

Nitpick comments:
In `@js/net/src/lite/track.ts`:
- Around line 75-78: Add a local encode-time validation in TrackInfo.encode so
it rejects a timescale of 0 before calling Message.encode, matching the Rust
TrackInfo::encode_msg behavior. Keep the existing hasTrackStream(version) check,
then add a guard that throws an error when this.timescale is zero so invalid
TrackInfo instances fail fast on the sender side instead of being emitted and
rejected later by the peer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3aa45db0-8970-4ad6-83cb-c8960362cb47

📥 Commits

Reviewing files that changed from the base of the PR and between b963472 and 933449c.

📒 Files selected for processing (16)
  • js/net/src/connection/connect.ts
  • js/net/src/lite/connection.ts
  • js/net/src/lite/publisher.ts
  • js/net/src/lite/setup.ts
  • js/net/src/lite/stream.ts
  • js/net/src/lite/subscribe.ts
  • js/net/src/lite/subscriber.ts
  • js/net/src/lite/track.ts
  • js/net/src/lite/version.ts
  • rs/moq-net/src/lite/mod.rs
  • rs/moq-net/src/lite/publisher.rs
  • rs/moq-net/src/lite/stream.rs
  • rs/moq-net/src/lite/subscribe.rs
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/lite/track.rs
  • rs/moq-net/src/lite/version.rs

Comment thread js/net/src/lite/publisher.ts
Comment thread js/net/src/lite/subscribe.ts
Comment thread js/net/src/lite/subscribe.ts
Comment thread rs/moq-net/src/lite/publisher.rs Outdated
Comment thread rs/moq-net/src/lite/subscribe.rs
@kixelated kixelated enabled auto-merge (squash) June 30, 2026 17:24
@kixelated kixelated disabled auto-merge June 30, 2026 17:25
- publisher: resolve the lite-05 start group once in run_subscribe and use it to
  position the track there, instead of recomputing track.latest() inside
  run_track. The SUBSCRIBE_OK value and the served start can no longer diverge if
  a new group arrives between the two reads.
- js subscribe: document the exported encodeSubscribeResponse /
  decodeSubscribeResponse helpers.
- js track: reject a zero timescale on TrackInfo encode too (mirrors the Rust
  side) so an invalid value fails fast on the sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) June 30, 2026 17:32
@kixelated kixelated merged commit fbc3ab1 into main Jun 30, 2026
2 checks passed
@kixelated kixelated deleted the claude/festive-franklin-3f5961 branch June 30, 2026 17:40
@moq-bot moq-bot Bot mentioned this pull request Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant