Skip to content

moq-net: split TrackConsumer into a track handle + TrackSubscriber#1576

Merged
kixelated merged 2 commits into
devfrom
claude/nifty-margulis-b5f697
Jun 1, 2026
Merged

moq-net: split TrackConsumer into a track handle + TrackSubscriber#1576
kixelated merged 2 commits into
devfrom
claude/nifty-margulis-b5f697

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

Picks up the next step after #1540. Separates a handle to a track you can subscribe to from a live subscription you read from, and moves the SUBSCRIBE_OK await off the broadcast lookup and onto the subscription itself.

  • Rename the old read+demand handle TrackConsumerTrackSubscriber. It keeps everything (recv_group/next_group/read_frame, update, subscription) and still carries the per-subscriber Subscription that feeds the producer's aggregate.
  • Add a new lightweight TrackConsumer { broadcast, name }: a cheap, cloneable handle that names a track but sends nothing until you call it.
    • async fn subscribe(Subscription) -> Result<TrackSubscriber> blocks on SUBSCRIBE_OK.
    • subscribe_pending(Subscription) -> TrackPending for poll-based callers (moq-mux's container source.rs).
    • fetch is reserved (TODO) for one-shot range retrieval; not added yet.
  • BroadcastConsumer::subscribe_track(name, sub) -> TrackPending becomes consume_track(name) -> TrackConsumer; the await now lives on .subscribe():
let track: TrackConsumer = broadcast.consume_track("video");
let sub: TrackSubscriber = track.subscribe(Subscription { priority: 5, ..Default::default() }).await?;
sub.recv_group().await?;

All ~30 call sites across the workspace migrated.

Cross-package

  • FFI (moq-ffi / libmoq): unchanged surface. MoqBroadcastConsumer::subscribe_track keeps its name and async signature; only its internal self.inner.consume_track(...) calls were renamed. No py/swift/kt/go regeneration needed.
  • js/net: no change. Wire format is unchanged, and it has its own subscribe(name, priority) API.
  • docs: updated the Rust example in doc/lib/rs/env/native.md. Python doc examples are unchanged (FFI API unchanged).

Test plan

  • cargo check --workspace --all-targets clean
  • cargo clippy --workspace --all-targets clean (via pinned nix toolchain)
  • cargo test for moq-net (328), moq-native, moq-relay, moq-mux — all pass
  • cargo fmt via nix

Follow-ups (not in this PR)

  • The stale field in aggregate_subscription looks inverted (forwards the soonest skip deadline upstream rather than the most patient subscriber's). Flagged for a separate change once confirmed.
  • SUBSCRIBE_UPDATE aggregate plumbing: the relay still doesn't re-aggregate downstream preferences and push an upstream SUBSCRIBE_UPDATE when they change.

(Written by Claude)

Separate "a handle to a track you can subscribe to" from "a live
subscription you read from", and move the SUBSCRIBE_OK await off the
broadcast lookup and onto the subscription itself.

- Rename the old read+demand handle `TrackConsumer` -> `TrackSubscriber`.
  It keeps everything (recv_group/next_group/read_frame, update,
  subscription) and still carries the per-subscriber Subscription that
  feeds the producer's aggregate.
- Add a new lightweight `TrackConsumer { broadcast, name }`: a cheap,
  cloneable handle that names a track but sends nothing until you call
  `subscribe(Subscription).await -> TrackSubscriber` (blocks on
  SUBSCRIBE_OK) or `subscribe_pending` for poll-based callers. `fetch`
  is reserved (TODO) for one-shot range retrieval.
- `BroadcastConsumer::subscribe_track(name, sub) -> TrackPending` becomes
  `consume_track(name) -> TrackConsumer`; the await now lives on
  `.subscribe()`. All call sites migrated.

The FFI surface is unchanged: `MoqBroadcastConsumer::subscribe_track`
keeps its name and async signature, so the py/swift/kt/go bindings need
no regeneration. The wire format is unchanged, so `js/net` (which has its
own `subscribe(name, priority)` API) needs no change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread doc/lib/rs/env/native.md Outdated
let track_consumer = broadcast
.subscribe_track(&track_name, moq_net::Subscription::new(1))
.consume_track(&track_name)
.subscribe(moq_net::Subscription::new(1))

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.

Can you remove ::new? It's not obvious this is priority.

Force struct construction and/or make a builder. moq_net::Subscription::default().with_priority(1)

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.

Done in f6e7b71. Replaced Subscription::new(1) (which didn't actually exist) with builder methods: Subscription::default().with_priority(1). Added with_priority/with_ordered/with_stale/with_group_start/with_group_end and updated hang::Catalog::default_subscription too.

(Written by Claude)

Comment thread rs/moq-net/src/model/broadcast.rs Outdated
Comment on lines +691 to +700
pub async fn subscribe(&self, subscription: Subscription) -> Result<TrackSubscriber, Error> {
self.subscribe_pending(subscription).ok().await
}

/// Like [`Self::subscribe`], but returns the unresolved [`TrackPending`] so a
/// poll-based caller can drive it via [`TrackPending::poll_ok`].
pub fn subscribe_pending(&self, subscription: Subscription) -> TrackPending {
self.broadcast.request_subscribe(&self.name, subscription)
}
}

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.

I think we just combine these. TrackPending could impl Future

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.

Done in f6e7b71. TrackPending now impls Future, so TrackConsumer::subscribe(sub) returns it directly: .await it for the TrackSubscriber, or drive it with poll_ok from a poll loop (moq-mux's container source). Dropped subscribe_pending and the redundant ok().

(Written by Claude)

Address review feedback on #1576:

- Combine `TrackConsumer::subscribe` and `subscribe_pending` into one
  method. `TrackPending` now impls `Future`, so `subscribe(sub)` returns
  it directly: `.await` it for the `TrackSubscriber`, or drive it with
  `poll_ok` from a poll loop. Drops the redundant `ok()` method.
- Add `Subscription` builder methods (`with_priority`, `with_ordered`,
  `with_stale`, `with_group_start`, `with_group_end`) so the priority is
  named at the call site instead of a bare positional arg. Update the
  native doc and `hang::Catalog::default_subscription` to use them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) June 1, 2026 16:40
@kixelated kixelated merged commit 0e5c26d into dev Jun 1, 2026
2 checks passed
@kixelated kixelated deleted the claude/nifty-margulis-b5f697 branch June 1, 2026 17:00
kixelated added a commit that referenced this pull request Jun 1, 2026
CI caught the Lite05 timestamp tests still using the old
`bc.subscribe_track(name, sub).ok().await` form, which #1576 removed
from `BroadcastConsumer` in favor of the explicit
`consume_track(name).subscribe(sub).await` two-step. Update both
Lite05 integration tests to match dev's new pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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