Skip to content
Closed
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: 2 additions & 3 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [Session close](/quest/m1/session-close.md) - a graceful session end withdraws announces and waits one second for the ack
- [Drain before close](/quest/m1/drain-before-close.md) - a closing client delivers its queued stream finishes, so `moq import` ends the catalog cleanly over a real relay
- [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one
- [Live in apps](/quest/m1/announce-live-apps.md) - the demo and `@moq/room` show "no broadcasts" from the `live` marker, which waits for the first session on page load
- [Page-load marker](/quest/m1/announce-page-load.md) - an announcement stream opened before the first connection waits for its replay before `live`
- [Empty state](/quest/m1/announce-empty-state.md) - watch, room, and the demo show "no broadcasts" once `live` arrives with nothing announced
- [Live and offline](/quest/m1/announce-offline.md) - announce streams toggle between `live` and `offline`, holding the set across a reconnect
- [Empty state](/quest/m1/announce-empty-state.md) - watch, room, and the demo show loading, "no broadcasts", or reconnecting from the `live`/`offline` toggle
- [JS active count](/quest/m1/js-active-count.md) - @moq/net speaks MoQ Active Count, so its IETF announce consumers go live without a timer
- [Watch refusal](/quest/m1/watch-refusal.md) - `<moq-watch>` shows an origin refusal as an error instead of sitting offline
- [kio waiter overflow](/quest/m1/kio-waiter-lost.md) - a retained `Waiter` past 8 lists stops adding a duplicate entry to lists it already recorded
Expand Down
32 changes: 26 additions & 6 deletions quest/m1/announce-empty-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,35 @@

## Goal

Once an announcement stream reports `live` with nothing announced, `js/watch`,
`js/room`, and `demo/web` show a "no broadcasts" state instead of a spinner
that never resolves.
`js/watch`, `js/room`, and `demo/web` follow the announcement stream's
`live`/`offline` toggle instead of a spinner that never resolves: loading
before the first `live`, a "no broadcasts" state after `live` with nothing
announced, and a reconnecting state on `offline` that keeps the held list on
screen. Never an empty state before a connection has answered.

## Plan

- Today each consumer skips the `live` marker. Track it next to the
announced set and render the empty state only after it arrives.
- Today each consumer skips the `live` marker (`demo/web/src/index.ts`,
`js/room/src/room.ts`, `js/moq-boy`). Track the toggle next to the
announced set.
- A connection that fails keeps the stream not-live, so the page stays in
loading or reconnecting; show the connection's own error or retry status
there (`Reload.status`, or the rejected one-shot `connect`), never the empty
state.
- `js/watch` is different: its main broadcast waits on
`origin.request(name, { announced: true })` in `#runBroadcast`, and its
announcement stream opens only for relative catalog references. The named
request needs its own caught-up-and-absent and offline states, driven by
the same toggle, for watch to show "not live" or "reconnecting" instead of
waiting.
- Libraries expose the state as a signal; wording stays in the demo.
- Tests in `js/room/src/room.test.ts` and `js/watch/src/broadcast.test.ts`:
loading before `live`, empty once live with nothing announced, reconnecting
on `offline` with the list kept, and never empty when the connection fails.

Public API: any state signal on `@moq/room` or `@moq/watch` is additive.
Wire: none.

## Required

- [Page-load marker](/quest/m1/announce-page-load.md) - otherwise the empty state flashes on page load before the first connection replays
- [Live and offline](/quest/m1/announce-offline.md) - the toggle these states are driven by
36 changes: 0 additions & 36 deletions quest/m1/announce-live-apps.md

This file was deleted.

102 changes: 102 additions & 0 deletions quest/m1/announce-offline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# [XL] Announce streams toggle between live and offline

## Goal

An announcement stream in `@moq/net` and `rs/moq-net` reports when it stops
being live, not only when it starts. `live` fires once a connection's replay
has landed; a new `offline` event fires when no connection feeding the stream
is up. Events strictly alternate from an implicit not-live start: `live`,
`offline`, `live`, and so on.

- Every connection path follows the same rule (JS `Reload`, one-shot
`connect`/`accept`, and reconnects), so page load is no special case: a
stream opened before any connection starts not-live, and a failed attempt
never lands, so it never yields `live`.
- On `offline` the stream emits no `end`: the set is held as it was. When the
next replay lands, the stream emits only the real diffs (`end` what's gone,
`start` what's new, `update` the rest), then `live`. A reconnect blip tears
nothing down in a player.
- JS delivers through one pending entry per prefix, like Rust's `pending`
map, so both sides share the per-prefix `live` barrier and the same
coalescing.

## Plan

Decided by the maintainer (2026-09-29):

- Toggle, with no special case for the first connection. Why: treating the
first connection as special is weird; a UI needs a marker for when things
stop being live (reconnecting), not only for when they are.
- `offline` is an event in the announce stream, not app state read from
`Reload.status`. Why: the stream is what knows whether its set is current.
- Never report `live` until there's actually a live connection: a failed
one-shot `connect`/`accept` leaves the stream not-live. Why: a false `live`
on an empty set makes a player conclude "offline" too early.
- The initial state is implicit: no `offline` at open. Why: a stream starts
not-live, so the first event is either a route or `live`.
- Hold and reconcile: on `offline`, emit no `end`; reconcile when the next
replay lands. Why: a blip must not tear down every player.
- Rust mirrors the same shape in `OriginConsumer`: `offline` when the last
network source feeding the consumer's scope goes away, `live` when a new
one's replay lands. A local-only origin with no connections is live at
once, as today. Why: one model across Rust, JS, and the bindings.
- Rewrite the JS queue now instead of waiting for generated lite. Why: the
per-prefix pending map gives the reconcile, and Rust's per-prefix barrier
comes with it, so JS matches Rust today.

Implementation:

- Sources. Rust's `Producer::replaying` guard (`rs/moq-net/src/model/origin.rs`)
lives only while a session replays. Widen it into a per-session source that
lives for the whole session and marks when its replay landed; the lite and
IETF subscribers (`rs/moq-net/src/{lite,ietf}/subscriber.rs`) hold it. A
cursor is live once every connected source overlapping its scope has landed
(the per-prefix barrier: `LiveState::Owed` as today), and offline once none
Comment on lines +53 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Define the transition when another source starts replaying

When source A has landed and the cursor has emitted live, then an overlapping source B connects and begins replaying, the “every connected source ... has landed” predicate becomes false, but the offline predicate is also false because A remains connected. The cursor therefore stays visibly live with an incomplete set and cannot emit another live after B lands under the strict alternation rule. Decide whether adding B transitions the cursor to offline or whether live only requires one landed source, and cover this source-join case in the tests. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

is connected. JS's `replaying` signal (`js/net/src/origin.ts`) widens the
same way. `Reload` (`js/net/src/connection/reload.ts`) registers its source
before its first dial and keeps it across reconnects; one-shot
`Connection.connect`/`accept` register theirs before the handshake and
drop it on failure.
- Local-only: an origin no connection has fed is live at once. Once a
connection has been attached, only a landed replay makes a stream live, so
a failed one-shot never releases into `live`.
Comment on lines +60 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep pre-connect cursors pending

When a caller polls origin.announced() before invoking connect or accept, this local-only rule classifies the origin as live and can deliver live immediately; registering the source inside the later connection call cannot retract an event already observed. Fresh evidence beyond the earlier page-load discussion is this replacement quest's explicit “no connection has fed” rule while retaining the pre-connect regression test at lines 90-92. Preserve a way to register connection intent before the cursor can become live, or narrow the guarantee so the core page-load test is implementable. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L29-L30

Useful? React with 👍 / 👎.

- Reconcile. While offline, a cursor stops delivering and keeps folding
changes per prefix. Across the gap, a prefix that ends and comes back is
compared by route (hops, cost), not by the session serving it: unchanged
delivers nothing, changed delivers `update`. Rust's pending map folds
`Unannounce` then `Announce` into an `UnannounceAnnounce` today; it needs
this offline fold. Prefixes still pending when the replay lands are owed
ahead of `live`.
- JS queue. Replace the append-only `AnnounceState.queue`
(`js/net/src/announced.ts`) and the per-tick table diff in
`#runAnnounced` with one pending entry per prefix, taken in path order,
folding and cancelling as Rust's `apply_announce`/`apply_unannounce` do.
- Open, ask the maintainer before building: whether the hold applies to
session egress cursors (`rs/moq-net/src/{lite,ietf}/publisher.rs`). Held
retractions there keep advertising a dead route to peers during the gap,
which delays failover. Recommendation: egress forwards retractions at once
and only app-facing cursors hold.
- Bindings: moq-ffi's announce event (`rs/moq-ffi/src/origin.rs`) gains
`Offline`; follow the moq-ffi row of the cross-package sync table.
- Docs, updated with the code: `doc/concept/moq-lite.md`,
`doc/lib/js/net.md`, `doc/lib/rs/moq-net.md`, and the binding pages that
describe the `live` marker.
- Benchmark: the pending map grows with both prefixes and announcement
consumers. Add a `js/net/bench` case swept over prefix count and consumer
count, with slow consumers draining it, and wire it into
`.github/workflows/nightly.yml` next to the other JS origin benchmarks.
Add an offline-then-reconnect case over the same sweep to
`rs/moq-net/benches/origin.rs`.
- Tests, in Rust and JS: a stream opened before the first connection gets
`live` only after its replay; a failed first connection (one-shot and
`Reload`) yields no `live`; a disconnect yields `offline` with no `end`; a
reconnect with the same set yields only `live`, and with a changed set the
real diffs then `live`; a second connection dropping while another is up
yields nothing; barrier cases for a change to an owed prefix (folded ahead
of `live`), a retraction of one (cancelled, `live` still follows), and a
new prefix sorting ahead of an owed one (may precede `live`).

Public API: breaks `@moq/net`'s announce `Event` type and `rs/moq-net`'s
announce consumer (`AnnounceEvent`), and the moq-ffi bindings, so it lands on
`dev`. Wire: none; `offline` is local connection state, and the drafts carry
only the initial-set count (`Active Count`), no live marker.
19 changes: 0 additions & 19 deletions quest/m1/announce-page-load.md

This file was deleted.