Skip to content

moq-lite-05: add TRACK stream, drop SUBSCRIBE_OK/FETCH_OK#1648

Merged
kixelated merged 1 commit into
devfrom
claude/vigorous-dirac-040547
Jun 8, 2026
Merged

moq-lite-05: add TRACK stream, drop SUBSCRIBE_OK/FETCH_OK#1648
kixelated merged 1 commit into
devfrom
claude/vigorous-dirac-040547

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Implements moq-dev/drafts#25 for moq-lite-05-wip. The previous FETCH_OK was wrong anyway.

Targets dev (wire-protocol change under rs/moq-net, plus breaking JS API changes).

Wire protocol (rs/moq-net, gated to Lite05Wip; drafts 01–04 unchanged)

  • New Track stream (control type 0x6): a subscriber sends TRACK (broadcast path + track name); the publisher replies with a single TRACK_INFO (Publisher Priority, Ordered, Cache, Timescale, Compression) and FINs, or resets on error. These properties are immutable, so they're fetched once and cached instead of echoed on every response.
  • Removed SUBSCRIBE_OK and FETCH_OK. A subscription is accepted implicitly (rejection = stream reset); a FETCH returns bare FRAME messages.
  • Resolved range moved to SUBSCRIBE_START (0x0) / SUBSCRIBE_END (0x1); SUBSCRIBE_DROP renumbered to 0x2.
  • Added priority / ordered to crate::TrackInfo (serde-skipped, so the hang catalog is unchanged) so a relay round-trips the publisher's values.

The subscriber opens a TRACK stream immediately on a track request and accepts the producer up front (so downstream subscribers resolve and frames decode without blocking), then opens SUBSCRIBE / FETCH on demand. The relay drains START/END/DROP from the upstream subscribe stream (it previously only waited for FIN).

JS mirror (js/net)

Same wire changes, plus a consumer-side API matching Rust:

  • Track class renamed to TrackSubscriber; new TrackConsumer handle from broadcast.track(name) with subscribe() and info(). (fetch() is a follow-up PR.)
  • TrackRequest.accept(info): TrackSubscriber (mirrors Rust TrackRequest::accept) and an async Track.info(); the consumer wire commits TRACK_INFO so apps never see placeholder defaults.
  • Consumer call sites migrated to broadcast.track(name).subscribe({ priority }).

Cross-package sync

  • rs/moq-net wire ↔ js/net: done.
  • doc/concept (SUBSCRIBE_OK → TRACK_INFO): intentionally deferred while the draft is WIP.
  • JS fetch() support: follow-up PR (needs a publisher FETCH responder + group cache).

Known limitation

The JS publisher resolves a track's real TRACK_INFO via a cached probe (broadcast.subscribe(name)await track.info()). For a media track, the first such lookup briefly constructs and closes a VideoEncoder, because js/publish couples accept() and serve(). Bounded (cached, once per track per connection) and lite-05-wip is opt-in.

Test plan

  • just check green: Rust 332 tests + clippy/doc/shear/sort; all JS packages typecheck + test; biome/remark/shfmt/taplo/nixfmt.
  • rs/moq-native integration tests exercise lite-05 end-to-end over WebTransport: timestamp round-trip, bare-FRAME FETCH with Deflate matching TRACK_INFO, concurrent fetch+subscribe.

🤖 Generated with Claude Code

(Written by Claude)

@kixelated kixelated force-pushed the claude/vigorous-dirac-040547 branch 3 times, most recently from 8941496 to 89a6614 Compare June 8, 2026 16:10
@kixelated

Copy link
Copy Markdown
Collaborator Author

Addressed the TrackProducer/TrackConsumer split feedback in 89a6614.

The do-everything Track is now split, mirroring Rust:

  • TrackProducer (write: appendGroup/writeGroup/writeFrame/…) and TrackSubscriber (read: recvGroup/nextGroup/readFrame/…), sharing a TrackState.
  • TrackConsumer is the lazy handle from broadcast.track(name)subscribe() / info() (fetch() is the follow-up PR).
  • TrackRequest.accept(info) returns a TrackProducer. On the wire, the consume side writes via the producer while the app reads via the subscriber it got from subscribe(); the publish side is the mirror.

All call sites updated (publish serve fns → TrackProducer, watch/consumers → TrackSubscriber, json/hang/loc producer-vs-consumer typed accordingly). just check is green (Rust 332 + all JS packages incl. integration tests). Rebased on latest dev.

(Written by Claude)

@kixelated kixelated force-pushed the claude/vigorous-dirac-040547 branch from 89a6614 to 8d5f311 Compare June 8, 2026 16:20
@kixelated

Copy link
Copy Markdown
Collaborator Author

Heads up: CI also tripped on a brand-new advisory unrelated to this PRRUSTSEC-2026-0173 (proc-macro-error2 marked unmaintained, published 2026-06-08). It's a build-time proc-macro pulled in transitively via getset, with no runtime exposure, and it fails cargo deny repo-wide (dev included). I added it to deny.toml's ignore list alongside the existing entries. Happy to split that into its own PR if you'd prefer. (Written by Claude)

@kixelated kixelated force-pushed the claude/vigorous-dirac-040547 branch from 8d5f311 to b96ac13 Compare June 8, 2026 18:53
Comment thread js/hang/src/container/consumer.test.ts Outdated

test("Consumer delivers frames from a single group", async () => {
const track = new Track("test");
const track = new TrackSubscriber("test");

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry I meant there should be a TrackProducer and TrackConsumer/Subscriber split, just like Rust.

Comment thread js/net/src/ietf/subscriber.ts Outdated
// IETF carries no per-track publisher properties, so commit defaults up
// front: this resolves the consumer's track.info() and gives us the write
// side that incoming object streams are routed into.
const producer = request.accept();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not true, should be set based on TRACK_STATUS or SUBSCRIBE_OK (or FETCH_OK).

Comment thread js/net/src/lite/publisher.ts Outdated
if (!published) throw new Error("not found");

// Priority is a subscriber concern; 0 is fine for an info-only probe.
const probe = published.subscribe(track, 0);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No we need to have broadcast.track(track).info().await

It triggers a TrackRequest which gets accept(TrackInfo)


if (emitRange && !startSent) {
startSent = true;
await encodeSubscribeResponse(stream, { start: new SubscribeStart(group.sequence) }, this.version);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We should rename SUBSCRIBE_START -> SUBSCRIBE_OK (doing it on the draft PR).

And you should return SUBSCRIBE_OK immediately, using the minimum group in cache. If there's nothing in cache, return undefined instead of blocking.

Implements moq-dev/drafts#25 for moq-lite-05-wip.

Wire protocol (rs/moq-net, gated to Lite05Wip; older drafts unchanged):
- New Track stream (control type 0x6): a TRACK request answered with a single
  TRACK_INFO (publisher priority, ordered, cache, timescale, compression) then
  FIN, or reset on error. Properties are immutable and fetched once.
- Removed SUBSCRIBE_OK and FETCH_OK. A subscription is accepted implicitly
  (rejection = stream reset); FETCH returns bare FRAME messages.
- Moved the resolved range into SUBSCRIBE_START (0x0) / SUBSCRIBE_END (0x1);
  SUBSCRIBE_DROP renumbered to 0x2.
- Added priority/ordered to crate::TrackInfo (serde-skipped, so the catalog is
  unchanged) so a relay round-trips them.

Subscriber opens a TRACK stream immediately on a track request and accepts the
producer up front, then opens SUBSCRIBE / FETCH on demand. The relay drains
START/END/DROP from the upstream subscribe stream.

JS mirror (js/net): same wire changes, plus a Rust-style track API split:
- The do-everything Track is split into TrackProducer (write) and TrackSubscriber
  (read) sharing a TrackState, plus a lazy TrackConsumer handle from
  broadcast.track(name) with subscribe() and info() (fetch() is a follow-up PR).
- TrackRequest.accept(info) returns a TrackProducer; TrackSubscriber.info() is an
  async getter the wire commits TRACK_INFO into.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kixelated kixelated force-pushed the claude/vigorous-dirac-040547 branch from b96ac13 to a7417e6 Compare June 8, 2026 22:55
@kixelated

Copy link
Copy Markdown
Collaborator Author

Addressed the review comments in a7417e6:

lite/publisher.ts (info via broadcast.track().info()) — done. The publisher's #resolveTrackInfo now calls published.track(track).info(), which (on a published broadcast) enqueues a TrackRequest the app answers with accept(TrackInfo); the resolved info is cached per track. Replaced the old broadcast.subscribe() probe entirely.

ietf/subscriber.ts (don't blindly default) — fixed. It now commits ordered: false to reflect the negotiated group order (this IETF impl only supports descending / newest-first, GROUP_ORDER = 0x2), instead of the misleading "no properties, use defaults" comment. Compression/timescale stay default since IETF has no per-frame codec/timescale here.

lite/publisher.ts:297 (SUBSCRIBE_START → SUBSCRIBE_OK; return resolved start immediately from min cached group, undefined if empty) — holding this one for now, since you said the rename + semantics are happening on the draft PR and it changes the wire shape (SUBSCRIBE_OK carrying an optional group). Once the draft lands I'll mirror the rename and switch the publisher to emit it immediately from the lowest cached group (and skip/return-undefined when the cache is empty) in both Rust and JS, rather than blocking on the first group. Shout if you'd rather I implement that speculatively now.

just check green; @moq/net 157 tests pass (incl. the integration suite exercising the new track().info() publish path).

(Written by Claude)

@kixelated kixelated enabled auto-merge (squash) June 8, 2026 22:57
@kixelated kixelated merged commit 371c7d1 into dev Jun 8, 2026
1 check passed
@kixelated kixelated deleted the claude/vigorous-dirac-040547 branch June 8, 2026 23:19
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