Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ lands before the cut.
- [qmux credit](/quest/m0/qmux-credit.md) - dropped/stopped streams return credit and close is delivered

- [Per-package MIGRATE.md](/quest/m1/migrate.md) - every published package documents how to migrate each breaking change, and breaking PRs add their entry
- [moq-net follows announced instances](/quest/m1/net-announced-follow.md) - the Rust equivalent of JS `request(path, { announced: true })`, replacing the follow loops in moq-ffi and moq-c
- [Sink restart follow](/quest/m1/sink-restart-follow.md) - moq-cli's server and push sinks follow an epochless `Restart` where their output can mark it, and fail loud otherwise
- [OBS restart follow](/quest/m1/obs-restart-follow.md) - the OBS source keeps playing across a GOAWAY drain or a publisher replacement
- [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters
Expand Down Expand Up @@ -84,7 +85,10 @@ lands before the cut.
- [IETF takeover](/quest/m1/ietf-takeover.md) - carry the shared takeover contract through the negotiated IETF epoch extension
- [Identity and recovery guide](/quest/m1/identity-guide.md) - explain pinned catalogs, lazy jobs, takeover, restart and version compatibility
- [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped`; a dead route ends Unroutable, and the TS follower lingers on announcements instead of matching errors
- [A partial group is never cached as whole](/quest/m1/partial-group-reset.md) - every reader, local or served, gets `Old` when an unfinished subscription group expires, so no hop caches the prefix as complete
- [Generated C bindings](/quest/m1/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written libmoq
- [No rustls panic on an http dial](/quest/m1/http-dial-crypto-provider.md) - the `http://` certificate fetch installs a crypto provider first
- [moq-ffi reports why a first dial fails](/quest/m1/ffi-first-dial-failure.md) - OBS names the reason while the first dial retries, as moq-c already can
- [Typed refusal reason](/quest/m1/refusal-reason.md) - an auth server's 403 names the reason (including `expired`) and the root and tier, so the relay's session outcomes count and attribute refusals
- [JS track handover](/quest/m1/js-group-handover.md) - a JS track subscription resumes across a route swap from the first frame it lacks, so `test/drain` passes at zero latency budget
- [Scope track priority](/quest/m1/track-priority-scope.md) - priority orders one owner's streams, and a shared cluster session is fair across tenants
Expand All @@ -96,6 +100,7 @@ lands before the cut.
- [Container reads drop backfill](/quest/m1/read-backfill.md) - the JS container consumer drops groups below its read cursor like Rust
- [Rust request credit](/quest/m1/rs-request-credit.md) - a moq-net request fails at once without stream credit instead of waiting
- [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome
- [Timed-out requests clean up](/quest/m1/legacy-request-timeout-cleanup.md) - on drafts 14-16, Rust and JS send PUBLISH_NAMESPACE_DONE or UNSUBSCRIBE_NAMESPACE for a timed-out request before its local reset
- [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide
- [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails
- [Received timestamps are media time](/quest/m1/lite-time-provenance.md) - a received timed track's timestamps are media time, so the rewind check can run across hops
Expand Down
28 changes: 28 additions & 0 deletions quest/m1/ffi-first-dial-failure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [S] moq-ffi reports why a first dial fails

## Goal

While a moq-ffi client's first dial keeps failing and retrying, the caller can
read why, so the OBS dock names the reason instead of showing only
"Connecting…". moq-c already does this: `moq_session_connect` returns a
session before the first connect, and `moq_session_failure` names the
retrying dial.

Today the native `connect` in `rs/moq-ffi/src/session.rs` awaits
`connection.established()` before it returns a `MoqSession`, so
`MoqSession::failure()` (#5723) is unreachable until the first success.

## Plan

Decided with the maintainer on 2026-10-17: the goal as drafted, in m1
because OBS needs it.

The API shape is the maintainer's call; bring options with a recommendation.
Candidates: a failure accessor on `MoqClient` readable while `connect()` is
pending, returning the session before the first connect as moq-c does, or
failing the first dial fast with its error. The first is additive and keeps
`connect()` surfacing auth refusals. Follow the moq-ffi row of the
Cross-Package Sync table, ending with the dock in `cpp/obs`.

Test: a dial to a listener that hangs up reports its reason before any
session connects, and the report clears once one does.
24 changes: 24 additions & 0 deletions quest/m1/http-dial-crypto-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [XS] No rustls panic on an http dial

## Goal

An `http://` dial through moq-tokio's QUIC backend never panics for want of a
rustls crypto provider: it installs one before fetching the certificate
fingerprint. moq-ffi and moq-c reach this path.

Today the fetch of `/certificate.sha256` (`reqwest::get` in
`rs/moq-tokio/src/quic/backend.rs`) panics inside reqwest when the build
unifies reqwest's `rustls` feature and no process default is installed, for
example `cargo test -p moq-tokio -p moq-ffi -p moq-c --all-features`.

## Plan

Decided with the maintainer on 2026-10-17: the goal as drafted, from the
follow-ups of [#5723](https://github.com/moq-dev/moq/pull/5723).

`moq_tokio::crypto::install_default()` installs the provider the crate
features select, and returns an error that can be ignored when one is already
installed. Add a regression test that dials `http://` under the unified
features and fails without the fix; moq-c's
`session_failure_names_a_failing_dial` dials `ws://` only to stay clear of
this. Make sure a CI lane builds that feature combination.
8 changes: 8 additions & 0 deletions quest/m1/lazy-discovery/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Facts (2026-10-10, `origin/main` e5446b036):
equivalent.
- `OriginConsumer::follow` exists (`rs/moq-net/src/model/origin.rs:4473`);
Rust has no announced-gated request like JS's, so the fold's shape is open.
- Added 2026-10-17: [moq-net follows announced instances](/quest/m1/net-announced-follow.md)
lands first and gives Rust its announced request. This quest then makes
that request register discovery interest for its path, since the session
stops asking for its whole scope.
- moq-relay reads `origin.consume().announced()` for the root in its web,
internal, and cluster paths, so a relay's watch set is the root.

Expand All @@ -50,3 +54,7 @@ fallout (see the Cross-Package Sync table in `AGENTS.md`).
Tests must pin the relay side: an origin in relay mode (cluster and upstream
sessions on the client subscriber path) still sends a root request, so
lazy discovery never narrows what a relay discovers.

## Required

- [moq-net follows announced instances](/quest/m1/net-announced-follow.md) - the Rust announced request this quest registers discovery interest for
39 changes: 39 additions & 0 deletions quest/m1/legacy-request-timeout-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [S] Timed-out requests clean up on drafts 14-16

## Goal

When a request on moq-transport drafts 14-16 times out after it reached the
peer, Rust and JS both send the legacy cleanup message before the local
reset: PUBLISH_NAMESPACE_DONE for an advertisement, and UNSUBSCRIBE_NAMESPACE
for a namespace subscription on drafts 14-15. Unanswered advertisements stay
terminal, as #5741 decided; only the cleanup changes.

On drafts 14-16 a request rides a virtual control-stream stream whose reset
never reaches the peer, so today a timed-out PUBLISH_NAMESPACE is never
withdrawn and the peer can hold the namespace until the session closes:

- JS: `Deadline` (`js/net/src/util/deadline.ts`) aborts every stream it
recorded when the deadline fires, before the caller can write the cleanup.
- Rust: when `read_response` in `rs/moq-net/src/ietf/publisher.rs` gives up
on an advertisement, the request is dropped without PUBLISH_NAMESPACE_DONE.

## Plan

Decided with the maintainer on 2026-10-17:

- The goal comes from the review of
[#5741](https://github.com/moq-dev/moq/pull/5741), which put the JS fix in
`Deadline`'s timeout path rather than in each caller.
- Widened to Rust and JS in one quest, rather than a separate Rust quest,
since both have the same gap on the same drafts.

The JS legacy SUBSCRIBE path opens its virtual stream outside `Deadline`, and
its timeout cleanup already sends UNSUBSCRIBE (`#cancelSubscribe`); keep that
covered by a test while moving the mechanism. Check every other timeout on
these drafts, in both languages, for the same gap. Draft 17 and later keep
the stream reset alone.

Test with mocked time on drafts 14, 15 and 16, in Rust and JS: a timed-out
PUBLISH_NAMESPACE whose message was sent is followed by PUBLISH_NAMESPACE_DONE
on the control stream, and the same for UNSUBSCRIBE_NAMESPACE on drafts
14-15. Wire: no new messages.
53 changes: 53 additions & 0 deletions quest/m1/net-announced-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# [M] moq-net follows announced instances

## Goal

moq-net offers the Rust equivalent of JS `request(path, { announced: true })`
(`js/net/src/origin.ts`): one request that follows each publisher instance
serving a path, unless an epoch is pinned. moq-ffi's
Comment thread
kixelated marked this conversation as resolved.
Resolved
`MoqAnnouncedBroadcast::available()` (`rs/moq-ffi/src/origin.rs`) and moq-c's
`moq_origin_announced_broadcast` (`rs/moq-c/src/origin.rs`) use it and drop
their hand-rolled follow loops.

## Plan

Decided with the maintainer on 2026-10-17: the goal as drafted, from
decision 5 of [#5700](https://github.com/moq-dev/moq/pull/5700), which kept
the loops per binding until moq-net owns them. Mirror the JS name.

The sink follow helper waits on this quest so it does not add a third loop;
the OBS source keeps calling `available()`, whose API does not change.

The loops #5700 added drain `origin::Consumer::follow`, resolve the path with
`routed_broadcast`, and settle three cases moq-net should own once:

- An `End` forgets the last instance, so the same instance returning after
a gap is handed out again.
- The last instance is held weakly, so a handle the caller let go of never
keeps its instance from retiring.
- A change that arrives while a resolution is in flight, such as an end and
a start for the instance it resolved, resolves afresh; and a resolution
that runs ahead of its held `Restart` is not handed out twice.

Two JS behaviors have no Rust counterpart to mirror. JS falls back to blind
resolution while a `consume` session lacks discovery; moq-net has no blind
requests. Decided with the maintainer on 2026-10-17: no blind fallback. The
mode waits on an announced route as `routed_broadcast` does, and blind
requests stay out of scope, so it differs from JS when discovery is
unavailable. JS also watches the path so a `consume` session asks its peer about
it. A moq-net session asks for its whole granted scope at setup today
(`interest_prefixes` in `rs/moq-net/src/lite/subscriber.rs`), so this quest
registers no interest. [Rust lazy discovery](/quest/m1/lazy-discovery/rust.md)
narrows that to watched prefixes, lands after this quest, and owns making
this mode watch its path, as JS does.

JS returns a handle whose current broadcast changes per instance
(`Request.active`); Rust's `request_broadcast` takes only an optional epoch
today. Propose the Rust shape in the PR. Keep #5700's binding tests passing
unchanged as the regression, and add moq-net tests for the three cases plus
a pinned epoch: an announced request that pins epoch A refuses a replacement
announced with epoch B instead of following it, mirroring JS `an
epoch-pinned announced request refuses another instance`
(`js/net/src/origin-request.test.ts`).
Public API: a new moq-net request mode or handle, and simpler binding
internals with no binding API change. Wire: none.
42 changes: 42 additions & 0 deletions quest/m1/partial-group-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# [S] A partial group is never cached as whole

## Goal

A subscription group whose max-delay budget expires before the publisher
finishes it fails with `Old` instead of ending normally, for every reader:
a local reader at the group's tail and a publisher serving it alike, on every
hop, in Rust and JS. A serving publisher resets the stream with `Old`, so a
relay that receives it never caches the prefix as the complete group.

Today a reader that has taken every frame the group holds treats expiry as
losing nothing and ends the group cleanly: `expired_truncates` in
`rs/moq-net/src/model/group.rs` and `#expire` in `js/net/src/group.ts`. The
lite publisher already resets with `Old` when a group expires before its
stream opens or while a write is blocked, but when it is parked at the tail
waiting for the next frame, `poll_next_frame` returns `Ok(None)` and
`GroupServe::poll_serve` (`rs/moq-net/src/lite/publisher.rs`) finishes the
stream. The next hop then stores the truncated group as finished.

## Plan

Decided with the maintainer on 2026-10-17: the goal as drafted, from the
follow-up to [#5680](https://github.com/moq-dev/moq/pull/5680), which made an
expiring FETCH reset with `OLD` for the same reason.

Decided with the maintainer on 2026-10-17: every reader, local or served,
gets `Old` when an unfinished group expires. Rejected: resetting only on the
serving path, which leaves a local reader unable to tell a truncated group
from a whole one, and a separate expired-at-tail state, which adds API for
the same outcome. The clean end was chosen for a local reader stalled at a
group's tail; it goes, along with `expired_truncates`' distinction. Readers
already treat `Old` like a latency skip.

Add regression tests that fail without the fix:

- A Rust model test in `rs/moq-net/src/model/group.rs` and its JS twin for
`js/net/src/group.ts`: a local reader that has taken every frame of an
unfinished group receives `Old` when the group expires.
- An upstream group stalls past the budget after its last frame, and the
downstream hop holds no finished group for it.

Wire: no new codes; lite already resets with `Old`.
4 changes: 4 additions & 0 deletions quest/m1/sink-restart-follow.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ rather than one rule per sink. Each fix lands with a test that replaces the
publisher while the sink runs.

Public API: possibly a shared follow helper in `moq-mux`. Wire: none.

## Required

- [moq-net follows announced instances](/quest/m1/net-announced-follow.md) - the shared follow helper builds on moq-net's announced request, not a third hand-rolled loop
1 change: 1 addition & 0 deletions quest/m1/ts-reliability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ remain with their current quests.
- [Plan: semantic fidelity](/quest/m1/ts-reliability/fidelity.md) - explicit profiles and PMT lifecycle contracts produce implementation quests
- [Whole-PES import](/quest/m1/ts-reliability/pes-import.md) - private PES travels whole at its source placement, a missing PTS stays missing, and the `pes` framing is deleted
- [Section placement](/quest/m1/ts-reliability/section-placement.md) - sections share the PCR placement clock instead of the video PTS
- [Sequence holes get a time budget](/quest/m1/ts-reliability/hole-budget.md) - a hole gives up on max(wall, pts), so a break marker behind it on a paused track is delivered
- [Loss and recovery tests](/quest/m1/ts-reliability/recovery.md) - actual MoQ omissions exercise reacquisition and unaffected tracks

## Related
Expand Down
44 changes: 44 additions & 0 deletions quest/m1/ts-reliability/hole-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# [S] Sequence holes get a time budget

## Goal

A container consumer waiting on a missing group sequence gives up on it once
a budget expires on max(wall clock, pts), then reports the gap and what
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
follows. A break marker behind a hole on a paused track is delivered without
waiting for media to resume or the track to finish. Rust
(`rs/moq-mux/src/container/consumer.rs`) and JS
(`js/hang/src/container/consumer.ts`) agree.

## Plan

Decided with the maintainer on 2026-10-17: the goal as drafted, from the
review of [#5815](https://github.com/moq-dev/moq/pull/5815). The budget covers
every kind of hole, not only one before a data marker, and runs on
max(wall, pts) so it fires on a paused track.

Today a marker carries no timestamp, so a hole before it on an open, paused
track waits indefinitely. A buffered higher sequence alone proves nothing,
since a late group is not a lost one; the budget is what decides.

The two anchors, matching the timers the TS line already uses:

- PTS: the existing age measure, how far the newest content
(`max_timestamp`) runs past where the next stamped group begins
(`next_start`), in `rs/moq-mux/src/container/consumer.rs`, and its
counterpart in `js/hang/src/container/consumer.ts`. A bare marker has no
timestamp, so this side never fires behind one.
- Wall: the `moq_clock` instant the reader first found itself blocked on
the hole, the way the TS export anchors a frame's arrival at the instant
its source was last found empty (`Track::empty` in
`rs/moq-mux/src/container/ts/export.rs`, judged by `jitter::Buffer` in
`rs/moq-mux/src/jitter.rs`). A timer wakes the reader at the anchor plus
the budget, with no new group needed.

The hole is lost once either side reaches the budget. Decided with the
maintainer on 2026-10-17: the budget is the subscription's max delay, which
already drives latency skips, so both sides share one number. With the
default of zero, a hole counts as lost as soon as the reader blocks on it.

Test with mocked time in both languages: read group N, lose N+1, receive a
closed marker as N+2 on a paused track; after the budget the reader reports a
gap, then the end. It fails without the fix.
Loading