Skip to content

moq-lite-05: implement FETCH for past groups via TrackConsumer::fetch#1601

Merged
kixelated merged 7 commits into
devfrom
claude/silly-colden-96e186
Jun 5, 2026
Merged

moq-lite-05: implement FETCH for past groups via TrackConsumer::fetch#1601
kixelated merged 7 commits into
devfrom
claude/silly-colden-96e186

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

@kixelated kixelated commented Jun 3, 2026

What

Adds a one-shot fetch on TrackConsumer, so you can retrieve a single past group without holding a live subscription:

TrackConsumer::fetch(sequence: u64, options: impl Into<Option<Fetch>>) -> Result<TrackFetchPending>
pub struct Fetch { pub priority: u8 }   // defaults priority 0

TrackFetchPending resolves to a GroupConsumer: immediately if the group is already cached, otherwise after a wire moq-lite FETCH completes (blocking on a new FETCH_OK, mirroring how subscribe blocks on SUBSCRIBE_OK). FETCH was previously a wire-only definition the publisher rejected with UnexpectedStream.

Why

The only way to read a group was to subscribe to a track and walk its cache. There was no first-class way to grab one past group. fetch lives on TrackConsumer (not a live subscription) on purpose: you no longer have to subscribe just to fetch.

How it works (model)

Fetch fits the track model right where the // TODO fetch placeholders were:

  • TrackConsumer::fetch(sequence, options) registers a want in the track's shared state (only if the group isn't already determined by the cache) and returns a TrackFetchPending that resolves via poll_get_group.
  • Producer side — a relay learns of fetch requests via TrackRequest::requested_fetch() (pre-accept) / TrackProducer::requested_fetch() (post-accept), then makes the group available with TrackRequest::serve_fetch(sequence, timescale) -> GroupProducer (which doesn't require the track to be accepted, so a fetch-only track needs no TrackInfo).
  • A cached group resolves with no wire round-trip; an aborted/evicted group goes to the wire.

The FETCH response streams on the same bidi control stream as the request: subscriber writes Fetch; publisher replies FetchOk (codec + timescale) then streams the group's frames; stream FIN ends the group. Errors propagate via stream reset.

Changes

  • model (model/track.rs): Fetch options + FetchRequest; TrackConsumer::fetchTrackFetchPending; TrackState.fetches; producer/request requested_fetch() + TrackRequest::serve_fetch(). GroupConsumer::timescale() accessor (model/group.rs).
  • wire (lite/fetch.rs): new FetchOk (group echo + compression + timescale); Fetch.frame_start (lite-05+, honored by the publisher).
  • publisher (lite/publisher.rs): recv_fetch / run_fetch serve the group on the fetch stream (gated to lite-05+).
  • subscriber (lite/subscriber.rs): run_subscribe is now a demand loop that serves both subscriptions and fetches off one TrackRequest; a fetch opens a wire FETCH upstream and fills the group (spawned).
  • kio (kio/producer.rs): expose poll_unused (poll sibling of the existing async unused), used by the relay's demand loop to stop serving a track once every consumer drops.
  • relay (moq-relay/src/web.rs): GET /fetch/<broadcast>/<track>?group=N uses fetch() instead of subscribe(). ?group=latest keeps the subscribe path (fetch needs a concrete group).

Out of scope (follow-ups)

  • frame_start resume reseed. The publisher honors frame_start on the wire, but fetch() always requests 0; an aborted/evicted group is re-fetched whole. True resume (request frame_start = N, stitch onto cached frames) is a separate change.
  • Compressed fetch. v1 streams fetched groups uncompressed (FetchOk.compression = None).
  • Fetch of an old group while already subscribed. The subscribe lifecycle loop doesn't watch for fetch requests, so a fetch of an evicted group on an actively-subscribed track isn't served (recent groups still resolve from cache). Easy to add later.
  • JS + docs. Cross-Package Sync pairs rs/moq-net wire/API with js/net and doc/concept; deferred.

Test plan

  • cargo test -p moq-net — wire roundtrip (FetchOk, frame_start gating) + model unit tests (cache hit, miss-signals-producer, past-final NotFound, abort).
  • cargo test -p moq-native --test broadcastbroadcast_moq_lite_05_fetch_webtransport: end-to-end fetch over a real session (client fetches a past group; the relay forwards a wire FETCH upstream). WebTransport only, since Lite05Wip isn't ALPN-advertised.
  • cargo test -p moq-relay -p kio, cargo clippy --workspace, cargo fmt --check.

Targets dev per branch targeting (wire-protocol + model change under rs/moq-net). Rebased onto the TrackInfo / async-TrackConsumer reshape (#1631) and the frame_start wire field (#1595).

🤖 Generated with Claude Code

(Written by Claude)

kixelated and others added 3 commits June 2, 2026 17:55
Add a one-shot `TrackConsumer::fetch(group, options) -> GroupConsumer` that
retrieves a single past group without holding a subscription. A cached group is
returned directly; otherwise the request bridges to a wire moq-lite FETCH,
blocking on a new FETCH_OK (mirroring SUBSCRIBE/SUBSCRIBE_OK).

- model: a dynamic group-request channel parallel to dynamic track requests
  (request_fetch / FetchPending / BroadcastDynamic::requested_group / GroupRequest).
  Cache hit returns the group; an aborted cached group is bypassed.
- wire: new FetchOk message; Fetch.frame_start (lite-05+); the publisher honors
  frame_start by skipping earlier frames.
- publisher: recv_fetch / run_fetch serve the group's frames on the fetch stream.
- subscriber: serve group requests by issuing FETCH upstream and routing frames
  into the producer that resolves the fetcher.
- relay web.rs: /fetch?group=N uses fetch() instead of subscribe().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-colden-96e186

# Conflicts:
#	rs/moq-net/src/lite/fetch.rs
…y-colden-96e186

# Conflicts:
#	rs/moq-relay/src/web.rs
kixelated and others added 4 commits June 3, 2026 20:51
…y-colden-96e186

# Conflicts:
#	rs/moq-net/src/lite/publisher.rs
…y-colden-96e186

# Conflicts:
#	rs/moq-net/src/lite/subscriber.rs
#	rs/moq-net/src/model/broadcast.rs
#	rs/moq-relay/src/web.rs
Implementing std Future on a kio-backed type means stashing the strong Waiter in
an Option<Waiter> field and replacing it every poll, because the channel's
WaiterList only holds a Weak (a dropped waiter loses its wakeup). kio::wait()
already hides this for closures, but named futures returned from sync methods had
to repeat the dance by hand.

Add `kio::Future` (a poll-based trait, method `poll(&mut self, waiter)`) and
`kio::Pending<F>`, which carries the retained Waiter and provides the std Future
impl once. Pending derefs to the inner value, so any inherent methods you put on
it are reachable through the awaitable handle.

Convert `TrackFetchPending` to `kio::Pending<TrackFetch>` as the first user: the
hand-written waiter field and std Future impl are gone; `TrackFetch` writes only
`kio::Future::poll`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the hand-written `waiter: Option<Waiter>` field and `impl std::future::Future`
from `TrackSubscriberPending` and `TrackInfoPending`, mirroring `TrackFetchPending`:
each is now `kio::Pending<Inner>` where `Inner` implements `kio::Future`.

`kio::Future::poll` takes `&self` (kio channels poll immutably), so a pollable can
be driven through a shared borrow. `Pending` derefs to the inner, so the existing
`poll_ok` / `update` surface keeps working through the wrapper with no caller
changes (moq-mux polls `poll_ok` through an `&self`-borrowed enum, and the
broadcast test macro calls it on a shared handle).

No hand-written future plumbing or retained-waiter fields remain in moq-net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kixelated kixelated merged commit a812808 into dev Jun 5, 2026
1 check passed
@kixelated kixelated deleted the claude/silly-colden-96e186 branch June 5, 2026 14:26
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