From cac0c13853068add463120ba4377376ffa54e120 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 5 Oct 2026 23:45:09 -0700 Subject: [PATCH 1/4] quest: claim announce-live-removal Co-Authored-By: Claude Opus 5.5 From 8174014c3cd3527ec4d7a576eca5c3725db32ab6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 00:02:18 -0700 Subject: [PATCH 2/4] feat!: delete the announce Live marker Announce streams yield only route events (start, update, end). Removes AnnounceEvent::Live, { kind: "live" }, MoqAnnounceEvent::Live, MOQ_ANNOUNCE_KIND_LIVE, and the binding aliases, plus everything that only produced the marker: origin replay holds and landing bookkeeping, the JS forwarder holds, and the quiet-gap timers on lite-03/04 and moq-transport. The wire is unchanged: Active Count is still sent and validated. Co-Authored-By: Claude Opus 5.5 --- dart/moq/README.md | 2 - dart/moq/lib/src/aliases.dart | 5 +- dart/moq/lib/src/client.dart | 3 - dart/moq/test/moq_test.dart | 32 +- dart/moq_ffi/lib/src/moq.dart | 34 -- demo/web/src/index.ts | 1 - demo/web/src/stats.ts | 1 - doc/concept/moq-lite.md | 12 +- doc/lib/c/index.md | 2 +- doc/lib/dart/index.md | 5 +- doc/lib/go/index.md | 6 +- doc/lib/js/net.md | 8 +- doc/lib/kt/index.md | 5 +- doc/lib/py/index.md | 6 +- doc/lib/rs/index.md | 2 +- doc/lib/rs/moq-net.md | 6 +- doc/lib/swift/index.md | 4 +- go/wrapper/README.md | 5 +- go/wrapper/example_test.go | 6 +- go/wrapper/moq_test.go | 78 +--- go/wrapper/origin_internal_test.go | 6 +- go/wrapper/types.go | 5 +- js/moq-boy/src/element.tsx | 1 - js/net/examples/discovery.ts | 4 - js/net/src/announced.test.ts | 16 - js/net/src/announced.ts | 58 +-- js/net/src/connection/close.test.ts | 1 - js/net/src/connection/established.ts | 3 +- js/net/src/connection/forward.test.ts | 49 --- js/net/src/connection/forward.ts | 11 - js/net/src/connection/pool.ts | 6 +- js/net/src/connection/reload.ts | 4 +- js/net/src/connection/scope.test.ts | 2 - js/net/src/ietf/subscriber.test.ts | 69 ++-- js/net/src/ietf/subscriber.ts | 10 - js/net/src/integration.test.ts | 88 +---- js/net/src/lite/connection.test.ts | 1 - js/net/src/lite/subscriber.test.ts | 144 ++----- js/net/src/lite/subscriber.ts | 27 +- js/net/src/origin.test.ts | 168 ++------- js/net/src/origin.ts | 59 +-- js/net/src/wire.ts | 5 - js/room/src/room.ts | 1 - js/watch/src/broadcast.ts | 1 - kt/README.md | 1 - .../kotlin/dev/moq/Aliases.kt | 4 +- .../jvmAndAndroidMain/kotlin/dev/moq/Flows.kt | 3 +- .../kotlin/dev/moq/SmokeTest.kt | 33 +- py/moq-rs/README.md | 4 +- py/moq-rs/docs/index.md | 1 - py/moq-rs/examples/announced.py | 2 - py/moq-rs/moq/__init__.py | 2 - py/moq-rs/moq/origin.py | 10 +- py/moq-rs/tests/test_local.py | 32 +- py/moq-rs/tests/test_server.py | 9 +- quest/m1/README.md | 1 - quest/m1/announce-live-removal.md | 47 --- rs/hang/examples/subscribe.rs | 11 +- rs/moq-bench/src/connection.rs | 4 +- rs/moq-boy/src/input.rs | 1 - rs/moq-c/src/api.rs | 8 +- rs/moq-c/src/origin.rs | 8 - rs/moq-c/src/test.rs | 77 +--- rs/moq-ffi/src/origin.rs | 4 - rs/moq-ffi/src/test.rs | 43 +-- rs/moq-net/src/client.rs | 31 -- rs/moq-net/src/ietf/publisher.rs | 21 +- rs/moq-net/src/ietf/session.rs | 8 +- rs/moq-net/src/ietf/subscriber.rs | 93 +---- rs/moq-net/src/lite/publisher.rs | 7 - rs/moq-net/src/lite/subscriber.rs | 102 +---- rs/moq-net/src/model/mod.rs | 3 - rs/moq-net/src/model/origin.rs | 357 ++---------------- rs/moq-net/tests/announce_covering_route.rs | 4 +- rs/moq-net/tests/announce_to_serve.rs | 1 - rs/moq-net/tests/caught_up.rs | 104 ----- rs/moq-net/tests/legacy_reconnect.rs | 2 +- rs/moq-net/tests/mesh_withdraw.rs | 1 - rs/moq-relay/src/cluster.rs | 22 +- rs/moq-relay/src/internal.rs | 11 +- rs/moq-relay/tests/auth_lifetime.rs | 11 +- rs/moq-relay/tests/cluster_unknown.rs | 11 +- rs/moq-relay/tests/drills.rs | 11 +- rs/moq-relay/tests/embed.rs | 11 +- rs/moq-relay/tests/goaway_cluster.rs | 11 +- rs/moq-relay/tests/public_rules.rs | 10 +- rs/moq-relay/tests/runtime_uring.rs | 11 +- rs/moq-relay/tests/runtime_workers.rs | 11 +- rs/moq-relay/tests/smoke.rs | 11 +- rs/moq-room/src/room.rs | 1 - rs/moq-rtc/src/lib.rs | 11 +- rs/moq-stats/src/aggregate.rs | 12 +- rs/moq-stats/src/consume.rs | 11 +- rs/moq-stats/src/produce.rs | 11 +- rs/moq-tokio/examples/clock.rs | 1 - rs/moq-tokio/src/origin.rs | 11 +- rs/moq-tokio/src/server.rs | 11 +- rs/moq-tokio/tests/backend.rs | 11 +- rs/moq-tokio/tests/broadcast.rs | 11 +- swift/README.md | 3 +- swift/Sources/Moq/Aliases.swift | 5 +- swift/Sources/Moq/Origin.swift | 3 +- swift/Tests/MoqTests/SmokeTests.swift | 35 +- 103 files changed, 339 insertions(+), 1930 deletions(-) delete mode 100644 quest/m1/announce-live-removal.md delete mode 100644 rs/moq-net/tests/caught_up.rs diff --git a/dart/moq/README.md b/dart/moq/README.md index 99e8b8335c..eab774edb8 100644 --- a/dart/moq/README.md +++ b/dart/moq/README.md @@ -13,8 +13,6 @@ await for (final event in connection.announcements( // Prefix stays origin-relative; captures reports wildcard matches. print(event.announce.prefix); print(event.announce.captures); - } else if (event is AnnounceEventLive) { - break; // every route live at subscribe time has been delivered } } ``` diff --git a/dart/moq/lib/src/aliases.dart b/dart/moq/lib/src/aliases.dart index 742fbe54f4..39accb60e6 100644 --- a/dart/moq/lib/src/aliases.dart +++ b/dart/moq/lib/src/aliases.dart @@ -53,7 +53,7 @@ typedef AnnouncedBroadcast = MoqAnnouncedBroadcast; typedef Announce = MoqAnnounce; /// What an [AnnounceConsumer] yields: [AnnounceEventStart], -/// [AnnounceEventUpdate], [AnnounceEventEnd], or [AnnounceEventLive]. +/// [AnnounceEventUpdate], or [AnnounceEventEnd]. typedef AnnounceEvent = MoqAnnounceEvent; /// A route now covers the prefix; the stream had none there. @@ -65,9 +65,6 @@ typedef AnnounceEventUpdate = UpdateMoqAnnounceEvent; /// No route covers the prefix any more; carries its last route. typedef AnnounceEventEnd = EndMoqAnnounceEvent; -/// Every route live at subscribe time has been delivered; what follows is live changes. -typedef AnnounceEventLive = LiveMoqAnnounceEvent; - /// The write side of a broadcast: publish tracks into it. typedef BroadcastProducer = MoqBroadcastProducer; diff --git a/dart/moq/lib/src/client.dart b/dart/moq/lib/src/client.dart index 6faef4b2de..fb65eefae5 100644 --- a/dart/moq/lib/src/client.dart +++ b/dart/moq/lib/src/client.dart @@ -163,9 +163,6 @@ final class Moq { session.publish().createBroadcast(path: path); /// Stream announce events matching [options]; prefixes stay relative to the origin. - /// - /// A [AnnounceEventLive] follows the routes live at subscribe time, so a - /// listener can collect what is live and stop there. Stream announcements({ AnnounceOptions options = const AnnounceOptions(), }) async* { diff --git a/dart/moq/test/moq_test.dart b/dart/moq/test/moq_test.dart index 5e38e1f117..296e8ba896 100644 --- a/dart/moq/test/moq_test.dart +++ b/dart/moq/test/moq_test.dart @@ -6,13 +6,11 @@ import 'package:test/test.dart'; const timeout = Duration(seconds: 10); -/// The next announce event that is not Live, which lands wherever the backlog ends. +/// The next announce event. Future nextRoute(AnnounceConsumer announced) async { - while (true) { - final event = await announced.next().timeout(timeout); - if (event == null) throw StateError('announce stream ended'); - if (event is! AnnounceEventLive) return event; - } + final event = await announced.next().timeout(timeout); + if (event == null) throw StateError('announce stream ended'); + return event; } void main() { @@ -214,28 +212,6 @@ void main() { expect(update.announce.captures, ['alice']); }); - test('announced yields Live once caught up', () async { - final origin = MoqOriginProducer(config: MoqOriginConfig()); - final consumer = origin.consume(); - - final empty = consumer.announced(config: MoqAnnounceConfig()); - expect(await empty.next().timeout(timeout), isA()); - empty.cancel(); - empty.dispose(); - - final broadcast = origin.createBroadcast(path: 'cam'); - broadcast.announce(route: MoqRoute()); - await consumer.announcedBroadcast(path: 'cam').available().timeout(timeout); - - final announced = consumer.announced(config: MoqAnnounceConfig()); - final first = await announced.next().timeout(timeout); - expect((first as AnnounceEventStart).announce.prefix, 'cam'); - expect(await announced.next().timeout(timeout), isA()); - announced.cancel(); - announced.dispose(); - broadcast.close(); - }); - test('dynamic serves a request under a prefix', () async { final origin = MoqOriginProducer(config: MoqOriginConfig()); final dynamic = origin.dynamic_(prefix: 'live', route: MoqRoute()); diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 3e5e353de9..1d706ffad9 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -3806,12 +3806,6 @@ class FfiConverterMoqAnnounceEvent { lifted.value, lifted.bytesRead - subview.offsetInBytes + 4, ); - case 4: - final lifted = LiveMoqAnnounceEvent.read(subview); - return LiftRetVal( - lifted.value, - lifted.bytesRead - subview.offsetInBytes + 4, - ); default: throw UniffiInternalError( UniffiInternalError.unexpectedEnumCase, @@ -3947,34 +3941,6 @@ class EndMoqAnnounceEvent extends MoqAnnounceEvent { } } -class LiveMoqAnnounceEvent extends MoqAnnounceEvent { - LiveMoqAnnounceEvent(); - LiveMoqAnnounceEvent._(); - static LiftRetVal read(Uint8List buf) { - int new_offset = buf.offsetInBytes; - return LiftRetVal(LiveMoqAnnounceEvent._(), new_offset); - } - - @override - RustBuffer lower() { - final buf = Uint8List(allocationSize()); - write(buf); - return toRustBuffer(buf); - } - - @override - int allocationSize() { - return 4; - } - - @override - int write(Uint8List buf) { - buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 4); - int new_offset = buf.offsetInBytes + 4; - return new_offset; - } -} - enum MoqTransport { quic, iroh, webSocket, tcp, unix } class FfiConverterMoqTransport { diff --git a/demo/web/src/index.ts b/demo/web/src/index.ts index 768c56424f..e946de64fb 100644 --- a/demo/web/src/index.ts +++ b/demo/web/src/index.ts @@ -187,7 +187,6 @@ discovery.run((effect) => { for (;;) { const entry = await effect.race(announced.next()); if (!entry) break; - if (entry.kind === "live") continue; const path = entry.prefix; // Only catalog-backed broadcasts are watchable streams; this skips the relay's // `.stats` broadcast (see the stats dashboard demo for that one). diff --git a/demo/web/src/stats.ts b/demo/web/src/stats.ts index 534f854e7c..bd80adc9b9 100644 --- a/demo/web/src/stats.ts +++ b/demo/web/src/stats.ts @@ -131,7 +131,6 @@ discovery.run((effect) => { for (;;) { const entry = await effect.race(announced.next()); if (!entry) break; - if (entry.kind === "live") continue; const path = entry.prefix; const node = Net.Path.stripPrefix(prefix, path); if (!node) continue; diff --git a/doc/concept/moq-lite.md b/doc/concept/moq-lite.md index bf3817736c..f7d0a34724 100644 --- a/doc/concept/moq-lite.md +++ b/doc/concept/moq-lite.md @@ -228,14 +228,10 @@ and TypeScript `Announce.Event`, whose `kind` is `"start"`, `"update"`, or `"end"` alongside the same `Announce.Announce` fields. An update is a reprice in place. Captures are present when the announced prefix pins every wildcard in the most-specific matching scope member. The Rust consumer is a -`Stream` and the TypeScript one an async iterable. Both, and every binding over -moq-ffi or moq-c, also yield one `Live` marker (TypeScript `{ kind: "live" }`) -once the routes live at subscribe time have all been delivered, including those -a peer session was still sending: moq-lite-05+ counts them in `ANNOUNCE_OK`, -moq-lite-01/02 send them in `ANNOUNCE_INIT`, Rust IETF draft-16+ sessions count -them in `REQUEST_OK` when both sides speak -[active-count](/draft/moq-active-count), and anything else waits for the -stream to go quiet. +`Stream` and the TypeScript one an async iterable. Neither marks when the +routes live at subscribe time have all arrived: an origin merges many sessions +that connect and reconnect independently, so drive loading and offline UI from +the connection status instead. Announcements are hints; requests are the authority. When a subscriber asks for a covered path the advertiser will not serve, the advertiser refuses that diff --git a/doc/lib/c/index.md b/doc/lib/c/index.md index f49b96850e..cfd0fb660e 100644 --- a/doc/lib/c/index.md +++ b/doc/lib/c/index.md @@ -52,7 +52,7 @@ pkg-config file included. - **Server.** `moq_server_listen` binds before it returns (a bad address or certificate fails there) and hands each incoming session to `on_request` as a request handle. Read `moq_session_request_path` and `_query` to route and authenticate, then `moq_session_request_accept` (a session handle, with origins like `moq_session_connect`) or `moq_session_request_reject` with an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports `1` once SETUP completes and never reconnects. `moq_server_addr` reports an ephemeral port and `moq_server_fingerprints` the hashes a client pins for a `tls_generate` certificate. `moq_server_close` stops listening; its terminal callback fires once the sockets are released. - **Demand.** A watcher on a published track (`moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, `moq_encode_audio_demand`) calls `on_demand` with `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED` right away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it. `moq_publish_demand_cancel` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current. - **Requests.** `moq_publish_dynamic` serves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name with `moq_track_request_name`, then `moq_track_request_accept` (a raw track handle), `moq_track_request_video` / `_audio` (the media handle `moq_publish_video` / `_audio` return), or `moq_track_request_abort` with an application code the subscriber sees. Without a live handler an unknown name is refused. `moq_publish_track_dynamic` does the same for fetches of groups a track no longer has cached, delivered as `moq_group_request_*` (`sequence`, `priority`, `frame_start`); `moq_group_request_accept` starts the producer at `frame_start` so written frames keep their group indices. Register it with `moq_track_request_dynamic` before accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop with `moq_publish_dynamic_cancel`. -- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_LIVE` once every route live when the listener started has been delivered, with no prefix or captures); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them. +- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them. ```c moq_client_config config; diff --git a/doc/lib/dart/index.md b/doc/lib/dart/index.md index 4537f5d8b9..d78a636884 100644 --- a/doc/lib/dart/index.md +++ b/doc/lib/dart/index.md @@ -33,8 +33,6 @@ moq.announcements( if (event is AnnounceEventStart) { print(event.announce.prefix); print(event.announce.captures); - } else if (event is AnnounceEventLive) { - print('caught up; what follows is live'); } }); final broadcast = await moq.requestBroadcast('live/camera'); @@ -84,8 +82,7 @@ route is a capability, not an inventory. `announcements(options:)` takes a literal prefix plus an optional relative pattern and yields `AnnounceEvent`s: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd` carrying an `Announce`, whose `prefix` stays origin-relative and whose -`captures` reports the wildcard matches, or `AnnounceEventLive` once every route -live at subscribe time has been delivered. Paths with +`captures` reports the wildcard matches. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `hidden: true`. Sessions reconnect with backoff when the transport drops and re-announce local diff --git a/doc/lib/go/index.md b/doc/lib/go/index.md index f200cd43fc..72ed046ade 100644 --- a/doc/lib/go/index.md +++ b/doc/lib/go/index.md @@ -41,7 +41,7 @@ for event, err := range announced.All(ctx) { } ann, ok := event.(moq.AnnounceEventStart) if !ok { - continue // AnnounceEventUpdate, AnnounceEventEnd, or AnnounceEventLive + continue // AnnounceEventUpdate or AnnounceEventEnd } // Prefix stays origin-relative; Captures reports what each wildcard matched. fmt.Printf("captures: %v\n", ann.Announce.Captures) @@ -92,9 +92,7 @@ serve. A route is a capability, not an inventory. `Announced(options)` combines a literal prefix with an optional relative pattern and yields an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd` carrying an `Announce`, whose `Prefix` stays relative to the origin and whose -`Captures` reports the wildcard matches, or `AnnounceEventLive` once every route -live at subscribe time has been delivered. Break on `AnnounceEventLive` to list -what is live and stop. +`Captures` reports the wildcard matches. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `Hidden: true`. diff --git a/doc/lib/js/net.md b/doc/lib/js/net.md index aae825f85f..ce1ac745b1 100644 --- a/doc/lib/js/net.md +++ b/doc/lib/js/net.md @@ -52,7 +52,7 @@ for (;;) { - **Origins** hold the broadcasts, not the connection: closing a session unannounces them but leaves them created for the next one. `origin.request(path)` resolves an announced local broadcast with no round trip, so a page that watches what it publishes reads its own copy, unless a cheaper route announces the same path. A refusal from the most specific route ends the request: `request.closed` settles with the handler's error and no broader route is asked. Create, populate, then `announce()` for an exact path; use `dynamic(prefix, route)` when the set of paths is not known: an exact-path subscribe before the tracks exist is refused, and nobody, local or remote, can see or reach a broadcast until it announces. - **Connections** race WebTransport against WebSocket. `new Connection({ url })` pools one connection per relay URL and reconnects with backoff, which the elements use. Supplying WebTransport/WebSocket options, discovery, delay, `goaway`, or a caller-owned origin selects a private loop; explicit `share: true` refuses those options. On a relay's GOAWAY the connection dials the replacement at once while the old session keeps serving its groups until it closes or the `goaway.handover` cap (default 10s, lowered to the relay's deadline) passes; the handle and its origin stay the same. An empty GOAWAY redials the same URL through a fresh DNS resolve. A redirect is followed onto the same host by default (`goaway.redirect: "follow"` lets the relay pick the host, `"ignore"` never moves), and moves the pooled entry to the new URL; a malformed or refused one, including a host change under a pinned certificate, ends the connection with `Error.RefusedRedirect` instead of reconnecting. `closed` settles when the handle is released (`null` on a clean close); the failure that stopped retrying the current URL is `error`, and a new URL recovers the same handle. A connection owns one send-rate sampler and one `Bandwidth.Allocator`; publishers reserve against it so their encoder targets sum to the estimate instead of each matching it. - **Bandwidth** (`Bandwidth.Allocator`) divides the connection's send-rate estimate by track priority, max-min fair within a tier. An idle track claims nothing. The receive side is untouched. -- **Discovery** by any pattern scope (`origin.announced(scope)`, such as `room/*/chat`; default everything). A route event's `kind` says whether the route started, updated, or ended; its `prefix` is the covered prefix relative to the origin, and `captures` reports what the scope's wildcards matched when the prefix pins them. One `{ kind: "live" }` marker, with no prefix, follows the routes live at subscribe time. The consumer is an async iterable. `origin.broadcasts(scope)` is a live `Getter>` of the same covered prefixes for UIs that need the current set. A borrowed `Connection.origin` also exposes `dynamic(prefix, route)` for serving paths on demand. +- **Discovery** by any pattern scope (`origin.announced(scope)`, such as `room/*/chat`; default everything). A route event's `kind` says whether the route started, updated, or ended; its `prefix` is the covered prefix relative to the origin, and `captures` reports what the scope's wildcards matched when the prefix pins them. The consumer is an async iterable. `origin.broadcasts(scope)` is a live `Getter>` of the same covered prefixes for UIs that need the current set. A borrowed `Connection.origin` also exposes `dynamic(prefix, route)` for serving paths on demand. - **Publisher retention** is optional `Track.Info.maxAge`: omission sets no limit, while zero keeps the live edge. No implicit five-second window is applied. - **Subscriptions** carry a priority, a `Time.Milli` max age, and optional `groups` bounds. Groups arrive out of order and are read frame by frame, with `Error.TooFarBehind` when a reader asks for a frame the group never held and `Error.GroupTooLarge` when a write exceeds the cache budget and aborts the group. - **Track ends**: `close()` ends a track at its live edge, while `finishAt(n)` declares the exclusive end ahead of it and still accepts the groups below. A subscriber reads the end with `final()` or awaits `finished()`. Group and datagram readers end once the live edge reaches the declared boundary, even while missing lower groups wait out the grace. The `closed` signal settles separately once every group below the end has arrived or was dropped; one reset before its header arrived is skipped after the subscription's max age on moq-lite (one second without one), or after one second on IETF. @@ -106,10 +106,8 @@ A route is a capability, not an inventory. `origin.announced(scope)` yields place), or `"end"`, each also carrying an `Announce.Announce`: `prefix` is the covered prefix relative to the origin, `captures` is one pattern per scope wildcard when the prefix pins a complete match (otherwise `undefined`), -and `route` carries hops and cost (on a retraction, its last values). A single -`{ kind: "live" }` follows the routes live at subscribe time, including every -route a connected peer was still sending, so a caller listing what is live -stops there. The consumer is an async iterable. A prefix is +and `route` carries hops and cost (on a retraction, its last values). The +consumer is an async iterable. A prefix is not a broadcast name; the scope filters locally while sessions request its literal head on the wire. Paths with a `.`-prefixed segment below that head are [hidden](/concept/moq-lite#hidden-broadcasts) unless `announced(scope, { hidden: true })` opts in; diff --git a/doc/lib/kt/index.md b/doc/lib/kt/index.md index af711d99ee..bd25ea3d87 100644 --- a/doc/lib/kt/index.md +++ b/doc/lib/kt/index.md @@ -26,7 +26,7 @@ import dev.moq.* // Subscribe. The Flow is live, so run it in its own coroutine. Moq.connect("https://relay.example.com", tlsRoots = listOf("ca.pem")).use { moq -> moq.announcements(AnnounceConfig(prefix = "live/", filter = "*/camera")).collect { event -> - if (event !is AnnounceEventStart) return@collect // Update, End, or Live + if (event !is AnnounceEventStart) return@collect // Update or End // Prefixes stay origin-relative; captures reports what each wildcard matched. println(event.announce.captures) val broadcast = moq.requestBroadcast(event.announce.prefix) @@ -72,8 +72,7 @@ serve. A route is a capability, not an inventory. `announcements(config)` takes a literal prefix plus an optional relative pattern and yields `AnnounceEvent`s: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd` carrying an `Announce`, whose `prefix` stays origin-relative and whose -`captures` reports the wildcard matches, or `AnnounceEventLive` once every route -live at subscribe time has been delivered. Paths with +`captures` reports the wildcard matches. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `hidden = true`. Sessions reconnect with backoff when the transport drops and re-announce local diff --git a/doc/lib/py/index.md b/doc/lib/py/index.md index 8ca4dcaaa8..29de56c16f 100644 --- a/doc/lib/py/index.md +++ b/doc/lib/py/index.md @@ -25,7 +25,7 @@ async def main(): # The filter is relative to the literal prefix; prefixes stay origin-relative. async for event in client.announced("live/", filter="*/camera"): if not isinstance(event, moq.AnnounceEventStart): - continue # AnnounceEventUpdate, AnnounceEventEnd, or AnnounceEventLive + continue # AnnounceEventUpdate or AnnounceEventEnd announcement = event.announce print(announcement.captures) # what * matched, or None for a partial overlap broadcast = await client.request_broadcast(announcement.prefix) @@ -93,9 +93,7 @@ capability, not an inventory. `announced(prefix, filter=...)` combines a literal root with an optional relative pattern and yields `AnnounceEvent`s: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd` carrying an `Announce` as `.announce`, whose `.prefix` stays relative to the -origin and whose `.captures` reports what the pattern wildcards matched, or -`AnnounceEventLive` once every route live at subscribe time has been delivered. -Break on `AnnounceEventLive` to list what is live and stop. +origin and whose `.captures` reports what the pattern wildcards matched. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `hidden=True`. diff --git a/doc/lib/rs/index.md b/doc/lib/rs/index.md index f643a2e52e..404c247113 100644 --- a/doc/lib/rs/index.md +++ b/doc/lib/rs/index.md @@ -54,7 +54,7 @@ let session = client.with_origin(origin.clone()).connect(url); let consumer = origin.consume(); let mut announced = consumer.announced(); while let Some(event) = announced.next().await { - // Skip retractions, and `Live`, which marks the end of what was already live. + // Skip updates and retractions. let moq_net::announce::Event::Start(update) = event else { continue }; let broadcast = consumer.request_broadcast(&update.prefix).await?; let catalog = broadcast diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index 5fa5089f8b..91a85bf894 100644 --- a/doc/lib/rs/moq-net.md +++ b/doc/lib/rs/moq-net.md @@ -154,10 +154,8 @@ refused locally. A disjoint route is `Unauthorized`. `announce::Announce` with `prefix`, the covered prefix relative to the consumer's root; `captures`, what the most specific matching scope member's wildcards stood for when the prefix pins them; and `route`, its hops and cost -(on a retraction, its last values). A single `Event::Live` follows -the routes live at subscribe time, including every route a connected peer -was still sending, so a caller listing what is live stops there. The -consumer is also a `futures::Stream`. A prefix is not a broadcast name; +(on a retraction, its last values). The consumer is also a +`futures::Stream`. A prefix is not a broadcast name; sessions request each scope member's literal head and filter locally. Routes with a `.`-prefixed segment below that head are [hidden](/concept/moq-lite#hidden-broadcasts) unless `with_hidden(true)` opts the consumer in. Sessions always ask the peer diff --git a/doc/lib/swift/index.md b/doc/lib/swift/index.md index e5897ca1de..a79c185193 100644 --- a/doc/lib/swift/index.md +++ b/doc/lib/swift/index.md @@ -29,7 +29,7 @@ let client = Client() let session = try await client.connect(to: "https://relay.example.com") for try await event in try session.consume.announced(prefix: "live/", filter: "*/camera") { - guard case .start(let announcement) = event else { continue } // .update, .end, or .live + guard case .start(let announcement) = event else { continue } // .update or .end // Prefixes stay origin-relative; captures reports what each wildcard matched. print(announcement.captures ?? []) let broadcast = try await session.consume.requestBroadcast(path: announcement.prefix) @@ -74,7 +74,7 @@ route is a capability, not an inventory. `announced(prefix:filter:)` combines a literal root with an optional relative pattern and yields `AnnounceEvent`s: `.start`, `.update`, or `.end` carrying an `Announce`, whose `prefix` stays relative to the origin and whose `captures` reports what the wildcards -matched, or `.live` once every route live at subscribe time has been delivered. +matched. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `hidden: true`. diff --git a/go/wrapper/README.md b/go/wrapper/README.md index 1c876f7d91..41cf42dfd6 100644 --- a/go/wrapper/README.md +++ b/go/wrapper/README.md @@ -50,12 +50,9 @@ for event, err := range announced.All(ctx) { } log.Fatal(err) } - switch event := event.(type) { - case moq.AnnounceEventStart: + if event, ok := event.(moq.AnnounceEventStart); ok { // Prefix stays origin-relative; Captures reports wildcard matches. fmt.Println("got broadcast", event.Announce.Prefix) - case moq.AnnounceEventLive: - fmt.Println("caught up; later events are live changes") } } ``` diff --git a/go/wrapper/example_test.go b/go/wrapper/example_test.go index 1261b19e50..2f15e1704d 100644 --- a/go/wrapper/example_test.go +++ b/go/wrapper/example_test.go @@ -25,7 +25,6 @@ func ExampleClient_Announced() { } defer announced.Cancel() - // List what is live now, then stop. for event, err := range announced.All(ctx) { if err != nil { if moq.IsShutdown(err) { @@ -33,11 +32,8 @@ func ExampleClient_Announced() { } log.Fatal(err) } - switch event := event.(type) { - case moq.AnnounceEventStart: + if event, ok := event.(moq.AnnounceEventStart); ok { fmt.Println("broadcast:", event.Announce.Prefix) - case moq.AnnounceEventLive: - return } } } diff --git a/go/wrapper/moq_test.go b/go/wrapper/moq_test.go index aa8fe94287..d469f71d1f 100644 --- a/go/wrapper/moq_test.go +++ b/go/wrapper/moq_test.go @@ -1431,83 +1431,21 @@ func TestAnnouncedExactFilterCapturesEmpty(t *testing.T) { } } -// Live marks the end of the routes live at subscribe time: at once on an empty -// origin, and after the existing routes otherwise, so an app can list and stop. -func TestAnnouncedYieldsLiveOnceCaughtUp(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - origin := newOrigin(t) - consumer := origin.Consume() - - empty, err := consumer.Announced(moq.AnnounceOptions{}) - if err != nil { - t.Fatal(err) - } - defer empty.Cancel() - if event, err := empty.Next(ctx); err != nil || event != (moq.AnnounceEventLive{}) { - t.Fatalf("empty origin: event=%+v err=%v, want Live", event, err) - } - - broadcast, err := origin.CreateBroadcast("cam") - if err != nil { - t.Fatal(err) - } - defer func() { _ = broadcast.Close() }() - if err := broadcast.Announce(moq.Route{}); err != nil { - t.Fatal(err) - } - available, err := consumer.AnnouncedBroadcast("cam") - if err != nil { - t.Fatal(err) - } - defer available.Cancel() - if _, err := available.Available(ctx); err != nil { - t.Fatal(err) - } +// nextRoute returns the next announce event. +func nextRoute(t *testing.T, ctx context.Context, announced *moq.AnnounceConsumer) moq.AnnounceEvent { + t.Helper() - announced, err := consumer.Announced(moq.AnnounceOptions{}) + event, err := announced.Next(ctx) if err != nil { t.Fatal(err) } - defer announced.Cancel() - - var listed []string - for event, err := range announced.All(ctx) { - if err != nil { - t.Fatal(err) - } - if a, ok := event.(moq.AnnounceEventStart); ok { - listed = append(listed, a.Announce.Prefix) - } - if _, ok := event.(moq.AnnounceEventLive); ok { - break - } - } - if len(listed) != 1 || listed[0] != "cam" { - t.Fatalf("listed = %v, want [cam]", listed) - } -} - -// nextRoute returns the next announce event that is not Live, skipping Live wherever it lands. -func nextRoute(t *testing.T, ctx context.Context, announced *moq.AnnounceConsumer) moq.AnnounceEvent { - t.Helper() - - for { - event, err := announced.Next(ctx) - if err != nil { - t.Fatal(err) - } - if event == nil { - t.Fatal("announcement stream ended") - } - if _, live := event.(moq.AnnounceEventLive); !live { - return event - } + if event == nil { + t.Fatal("announcement stream ended") } + return event } -// nextAnnounced returns the next newly announced route, skipping Live. +// nextAnnounced returns the next newly announced route. func nextAnnounced(t *testing.T, ctx context.Context, announced *moq.AnnounceConsumer) moq.Announce { t.Helper() diff --git a/go/wrapper/origin_internal_test.go b/go/wrapper/origin_internal_test.go index 0102225472..f1c6191918 100644 --- a/go/wrapper/origin_internal_test.go +++ b/go/wrapper/origin_internal_test.go @@ -25,12 +25,8 @@ func TestOriginEndsWithItsProducer(t *testing.T) { origin.inner.Destroy() - // The teardown runs on the origin's driver; the cursor ending is its signal. An - // empty origin reports Live first. + // The teardown runs on the origin's driver; the cursor ending is its signal. update, err := announced.Next(ctx) - if _, live := update.(AnnounceEventLive); live && err == nil { - update, err = announced.Next(ctx) - } if update != nil || err != nil { t.Fatalf("Next = (%v, %v), want the end of the stream", update, err) } diff --git a/go/wrapper/types.go b/go/wrapper/types.go index 85c6274c4a..537450011e 100644 --- a/go/wrapper/types.go +++ b/go/wrapper/types.go @@ -51,7 +51,7 @@ type ( // [OriginConsumer.RequestBroadcast]. Announce = ffi.MoqAnnounce // AnnounceEvent is what an AnnounceConsumer yields: AnnounceEventStart, - // AnnounceEventUpdate, AnnounceEventEnd, or AnnounceEventLive. + // AnnounceEventUpdate, or AnnounceEventEnd. AnnounceEvent = ffi.MoqAnnounceEvent // AnnounceEventStart reports a route now covering a prefix that had none. AnnounceEventStart = ffi.MoqAnnounceEventStart @@ -59,9 +59,6 @@ type ( AnnounceEventUpdate = ffi.MoqAnnounceEventUpdate // AnnounceEventEnd reports that no route covers a prefix any more, carrying its last route. AnnounceEventEnd = ffi.MoqAnnounceEventEnd - // AnnounceEventLive reports that every route live at subscribe time has been - // delivered; what follows is live changes. Yielded once. - AnnounceEventLive = ffi.MoqAnnounceEventLive // Subscription holds subscriber-side delivery preferences: priority, ordering, max age, and group range. Subscription = ffi.MoqSubscription // TrackInfo holds publisher-side track properties: priority, ordering, max age, and timescale. diff --git a/js/moq-boy/src/element.tsx b/js/moq-boy/src/element.tsx index 66e78d9751..6a7000e879 100644 --- a/js/moq-boy/src/element.tsx +++ b/js/moq-boy/src/element.tsx @@ -137,7 +137,6 @@ export default class MoqBoy extends HTMLElement { for (;;) { const entry = await effect.race(announced.next()); if (!entry) break; - if (entry.kind === "live") continue; // A broad route that cannot pin the game id names nothing to open. const capture = entry.captures?.[0]; diff --git a/js/net/examples/discovery.ts b/js/net/examples/discovery.ts index 3eba3ec5b9..f89b9a76b2 100644 --- a/js/net/examples/discovery.ts +++ b/js/net/examples/discovery.ts @@ -10,10 +10,6 @@ async function main() { // Discover broadcasts announced by the server for await (const announcement of announced) { - if (announcement.kind === "live") { - console.log("Caught up: everything live has been listed"); - continue; - } if (announcement.kind === "end") continue; console.log("New stream available:", announcement.prefix); diff --git a/js/net/src/announced.test.ts b/js/net/src/announced.test.ts index 7763431e17..98842f1f9c 100644 --- a/js/net/src/announced.test.ts +++ b/js/net/src/announced.test.ts @@ -57,22 +57,6 @@ test("a same-name re-announce is a distinct update", async () => { expect(await consumer.next()).toEqual({ prefix: p("a"), captures: undefined, kind: "start", route }); }); -test("the live marker is delivered once", async () => { - const producer = new Announce.Producer(); - const consumer = producer.consume(); - - // A stream spanning sessions forwards each one's marker; only the first is news. - const route = Route.default; - producer.append({ kind: "live" }); - producer.append({ prefix: p("a"), captures: undefined, kind: "start", route }); - producer.append({ kind: "live" }); - producer.append({ prefix: p("b"), captures: undefined, kind: "start", route }); - - expect(await consumer.next()).toEqual({ kind: "live" }); - expect(await consumer.next()).toMatchObject({ prefix: p("a") }); - expect(await consumer.next()).toMatchObject({ prefix: p("b") }); -}); - test("closing resolves next with undefined", async () => { const producer = new Announce.Producer(); const consumer = producer.consume(); diff --git a/js/net/src/announced.ts b/js/net/src/announced.ts index 3630c290c8..0f576afd50 100644 --- a/js/net/src/announced.ts +++ b/js/net/src/announced.ts @@ -33,14 +33,11 @@ export interface Announce { * What an announcement stream yields. * * `start`: a route now covers the prefix. `update`: the route covering it changed - * hops or cost, in place. `end`: no route covers it any more. `live`: every route - * live at subscribe time has been delivered, including those a connected peer was still - * sending, so what follows is live changes; yielded at most once, and a caller listing - * what is live stops there. + * hops or cost, in place. `end`: no route covers it any more. * * @public */ -export type Event = ({ kind: "start" | "update" | "end" } & Announce) | { kind: "live" }; +export type Event = { kind: "start" | "update" | "end" } & Announce; /** * Options for an announcement stream. @@ -60,8 +57,6 @@ export interface Options { /** Reactive backing state shared by announcement producers and consumers. */ class AnnounceState { queue = new Signal([]); - // Whether the live marker was appended, so a repeat (from a stream spanning sessions) is dropped. - live = false; closed = new Once(); } @@ -95,13 +90,9 @@ export class Producer { return makeConsumer(this.#state); } - /** Writes an event to the queue. The `live` marker is written once; later ones are dropped. */ + /** Writes an event to the queue. */ append(event: Event) { if (this.#state.closed.peek() !== undefined) throw new Error("announcements are closed"); - if (event.kind === "live") { - if (this.#state.live) return; - this.#state.live = true; - } this.#state.queue.mutate((queue) => { queue.push(event); }); @@ -169,46 +160,3 @@ export class Consumer { closeState(this.#state, abort); } } - -/** - * When an initial set has landed, for wires that never say where it ends (lite-03/04, - * moq-transport): once its stream goes quiet. - * - * A peer writes its whole set back to back, so the first announcement gets a round trip's - * grace and each one after it only has to beat its siblings. Mirrors `Quiet` in `rs/moq-net`. - * - * @internal - */ -export class Quiet { - /** How long the stream may stay silent before its first announcement, in ms. */ - static readonly FIRST = 500; - /** How long the stream may stay silent between announcements, in ms. */ - static readonly GAP = 30; - - #landed: () => void; - #timer: ReturnType | undefined; - - /** Start counting from now; `landed` runs once the stream goes quiet. */ - constructor(landed: () => void) { - this.#landed = landed; - this.#timer = setTimeout(() => this.#land(), Quiet.FIRST); - } - - /** An announcement arrived: the set is still landing. */ - heard(): void { - if (this.#timer === undefined) return; - clearTimeout(this.#timer); - this.#timer = setTimeout(() => this.#land(), Quiet.GAP); - } - - /** Stop counting without landing, once the stream is gone. Idempotent. */ - close(): void { - clearTimeout(this.#timer); - this.#timer = undefined; - } - - #land(): void { - this.#timer = undefined; - this.#landed(); - } -} diff --git a/js/net/src/connection/close.test.ts b/js/net/src/connection/close.test.ts index 0a276c235a..8dc3f1e554 100644 --- a/js/net/src/connection/close.test.ts +++ b/js/net/src/connection/close.test.ts @@ -67,7 +67,6 @@ for (const protocol of [Lite.ALPN_07_WIP, Ietf.ALPN.DRAFT_19]) { let timer: ReturnType> | undefined; try { expect((await announced.next())?.kind).toBe("start"); - expect((await announced.next())?.kind).toBe("live"); fin.enable(); let closed = false; void pair.server.closed.then(() => { diff --git a/js/net/src/connection/established.ts b/js/net/src/connection/established.ts index cb761f41c0..0c6200bda2 100644 --- a/js/net/src/connection/established.ts +++ b/js/net/src/connection/established.ts @@ -37,8 +37,7 @@ export interface Established { * Subscribe to broadcast announcements matching `scope`, any pattern (`foo/**` * for a subtree, `room/* /chat` for each room's chat, default `**`). Paths are * relative to the session; captures report what the scope's wildcards stood for. - * Hidden routes are left out unless `options.hidden` opts in. The `live` marker follows - * once the peer's initial set has landed. + * Hidden routes are left out unless `options.hidden` opts in. */ announced(scope?: Path.Pattern, options?: announce.Options): announce.Consumer; diff --git a/js/net/src/connection/forward.test.ts b/js/net/src/connection/forward.test.ts index 932a703194..dfcfa25d3e 100644 --- a/js/net/src/connection/forward.test.ts +++ b/js/net/src/connection/forward.test.ts @@ -190,55 +190,6 @@ test("a request replaced across one coalesced wakeup still gets answered", async origin.close(); }); -/** Whether `next` is still waiting once everything queued has run. */ -async function pending(next: Promise): Promise { - const waiting = Symbol("waiting"); - const result = await Promise.race([next, settle().then(() => waiting)]); - return result === waiting; -} - -test("the origin's live marker waits for the session's initial set", async () => { - const origin = new OriginProducer(); - const session = new FakeSession(); - forwardAnnounced(session.session, origin); - - const announced = origin.announced(); - session.announces.append({ prefix: Path.from("a"), captures: undefined, kind: "start", route: Route.default }); - expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - - // The peer has not said its initial set is complete. - const next = announced.next(); - expect(await pending(next)).toBe(true); - - session.announces.append({ kind: "live" }); - expect(await next).toEqual({ kind: "live" }); - - // A stream opened after the set landed does not wait on it. - const later = origin.announced(); - expect(await later.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - expect(await later.next()).toEqual({ kind: "live" }); - - announced.close(); - later.close(); - origin.close(); -}); - -test("a session dying before its initial set lands releases the marker", async () => { - const origin = new OriginProducer(); - const session = new FakeSession(); - forwardAnnounced(session.session, origin); - - const announced = origin.announced(); - const next = announced.next(); - expect(await pending(next)).toBe(true); - - session.die(); - expect(await next).toEqual({ kind: "live" }); - - announced.close(); - origin.close(); -}); - test("scoped origins subscribe to disjoint literal heads with hidden routes", () => { const origin = new OriginProducer().scope( Path.empty(), diff --git a/js/net/src/connection/forward.ts b/js/net/src/connection/forward.ts index 9bdbf25218..1918e005be 100644 --- a/js/net/src/connection/forward.ts +++ b/js/net/src/connection/forward.ts @@ -52,11 +52,6 @@ export function forwardAnnounced(conn: Established, origin: OriginProducer): voi const announced = conn.announced(Path.Pattern.subtree(prefix), { hidden: true }); const inserted = new Map(); - // Taken before the session is handed out, so no announcement stream on the origin misses - // it: each overlapping one opened now withholds its live marker until this interest's - // initial set lands. - const landed = originWire.replaying(prefix); - // End the stream the moment the session closes rather than waiting for the wire to // error it, so the retractions below land promptly. void conn.closed.then(() => announced.close()); @@ -67,10 +62,6 @@ export function forwardAnnounced(conn: Established, origin: OriginProducer): voi for (;;) { const event = await announced.next(); if (!event) break; - if (event.kind === "live") { - landed(); - continue; - } if (!originWire.accepts(event.prefix)) continue; if (event.kind !== "end") { @@ -93,8 +84,6 @@ export function forwardAnnounced(conn: Established, origin: OriginProducer): voi // cleanup below retracts everything this stream fed either way. failure = err; } finally { - // A dead stream cannot hold the marker back. - landed(); for (const handle of inserted.values()) handle.close(); inserted.clear(); announced.close(); diff --git a/js/net/src/connection/pool.ts b/js/net/src/connection/pool.ts index 5acbf59bab..c2e939d7fa 100644 --- a/js/net/src/connection/pool.ts +++ b/js/net/src/connection/pool.ts @@ -276,8 +276,7 @@ export class Connection { /** * Subscribe to broadcast announcements matching `scope`, spanning reconnects * and URL switches: a switch retracts everything from - * the old relay's origin, then the new one's arrivals stream in. The `live` marker - * comes once, from the first origin; see {@link Origin.Consumer.announced}. + * the old relay's origin, then the new one's arrivals stream in. */ announced(scope: Path.Pattern = Path.Pattern.all(), options?: Announce.Options): Announce.Consumer { const producer = new Announce.Producer(); @@ -310,8 +309,7 @@ export class Connection { const entry = await effect.race(upstream.next()); if (!entry) break; if (entry.kind === "end") active.delete(entry.prefix); - else if (entry.kind !== "live") active.set(entry.prefix, entry); - // The stream delivers the marker once; a later session's is dropped. + else active.set(entry.prefix, entry); producer.append(entry); } } finally { diff --git a/js/net/src/connection/reload.ts b/js/net/src/connection/reload.ts index f597485a6b..ad4d54bdcf 100644 --- a/js/net/src/connection/reload.ts +++ b/js/net/src/connection/reload.ts @@ -569,7 +569,6 @@ export class Reload { * The same {@link Announce.Consumer} stream as {@link Established.announced}, but everything active * is retracted (an `end` event) whenever the connection drops and re-announced on * reconnect, so a consumer draining `next()` never clings to a dead route across a reconnect. - * The `live` marker comes once, from the first session. * * Stays empty while the relay lacks {@link Established.discovery}. */ @@ -603,8 +602,7 @@ export class Reload { const entry = await effect.race(upstream.next()); if (!entry) break; if (entry.kind === "end") active.delete(entry.prefix); - else if (entry.kind !== "live") active.set(entry.prefix, entry); - // The stream delivers the marker once; a later session's is dropped. + else active.set(entry.prefix, entry); producer.append(entry); } } catch { diff --git a/js/net/src/connection/scope.test.ts b/js/net/src/connection/scope.test.ts index 9c18d0753e..920bba0647 100644 --- a/js/net/src/connection/scope.test.ts +++ b/js/net/src/connection/scope.test.ts @@ -37,8 +37,6 @@ for (const protocol of [Lite.ALPN_07_WIP, Ietf.ALPN.DRAFT_19]) { received.push(update.prefix); } expect(received.sort()).toEqual(["client/other/live", "client/room/.hidden", "client/room/live"]); - // The marker waits for every interest's initial set, not just the first to land. - expect(await withTimeout(announced.next(), 1000, "live marker did not arrive")).toEqual({ kind: "live" }); expect([...consume.broadcasts().peek().keys()].sort()).toEqual([ Path.from("other/live"), Path.from("room/live"), diff --git a/js/net/src/ietf/subscriber.test.ts b/js/net/src/ietf/subscriber.test.ts index b6d33f83f6..70df1d8305 100644 --- a/js/net/src/ietf/subscriber.test.ts +++ b/js/net/src/ietf/subscriber.test.ts @@ -17,15 +17,6 @@ import { SubscribeNamespace, SubscribeNamespaceEntry, SubscribeNamespaceEntryDon import { Subscriber } from "./subscriber.ts"; import { ALPN, Version } from "./version.ts"; -/** The next route event, skipping the live marker: these tests pin routes, and the marker has its own. */ -async function nextRoute(announced: { - next(): Promise; -}): Promise | undefined> { - for (;;) { - const event = await announced.next(); - if (event?.kind !== "live") return event as Exclude | undefined; - } -} const VERSION = Version.DRAFT_19; @@ -95,7 +86,7 @@ test("an unsolicited announcement lands", async () => { stream, ); - const next = await nextRoute(announced); + const next = await announced.next(); expect(next?.prefix).toBe(Path.from("surprise")); expect(next?.kind).toBe("start"); @@ -111,10 +102,10 @@ test("an unsolicited announcement lands", async () => { new PublishNamespace({ requestId: 2n, trackNamespace: Path.from("sentinel") }), second, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("sentinel"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("sentinel"), kind: "start" }); peer.close(); await handler; - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("surprise"), kind: "end", }); @@ -141,7 +132,7 @@ test("an advertisement with no path is stamped per connection", async () => { new PublishNamespace({ requestId: 0n, trackNamespace: Path.from("legacy") }), stream, ); - const hops = (await nextRoute(announced))?.route.hops ?? []; + const hops = (await announced.next())?.route.hops ?? []; expect(hops).toHaveLength(2); expect(hops[0]).not.toBe(UNKNOWN_HOP); expect(hops[1]).toBe(UNKNOWN_HOP); @@ -181,7 +172,7 @@ async function inlineNamespace(stream: Stream, path: Path.Valid, cluster?: Clust */ async function syncInline(stream: Stream, announced: announce.Consumer, cluster?: Cluster.Advert): Promise { await inlineNamespace(stream, Path.from("sentinel"), cluster); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("sentinel"), kind: "start", }); @@ -209,7 +200,7 @@ test("an announcement survives the first of its two sources ending", async () => new PublishNamespace({ requestId: 0n, trackNamespace: Path.from("both") }), request, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("both"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("both"), kind: "start" }); await inlineNamespace(subscription, Path.from("both")); await syncInline(subscription, announced); @@ -222,7 +213,7 @@ test("an announcement survives the first of its two sources ending", async () => await handler; const next = await Promise.race([ - nextRoute(announced), + announced.next(), new Promise<"nothing">((resolve) => setTimeout(() => resolve("nothing"), 250)), ]); expect(next).toBe("nothing"); @@ -245,7 +236,7 @@ test("an announcement ends once its last source does", async () => { new PublishNamespace({ requestId: 0n, trackNamespace: Path.from("both") }), request, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("both"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("both"), kind: "start" }); await inlineNamespace(subscription, Path.from("both")); await syncInline(subscription, announced); @@ -259,7 +250,7 @@ test("an announcement ends once its last source does", async () => { await subscription.writer.u53(SubscribeNamespaceEntryDone.id); await new SubscribeNamespaceEntryDone({ suffix: Path.from("both") }).encode(subscription.writer, VERSION); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("both"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("both"), kind: "end" }); }); /** @@ -308,7 +299,7 @@ test("a duplicate legacy publish_namespace is still refused", async () => { new PublishNamespace({ requestId: 0n, trackNamespace: Path.from("twice") }), first, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("twice"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("twice"), kind: "start" }); // The same namespace again, on its own request. const second = await Stream.open(pair.server, { version: Version.DRAFT_15 }); @@ -323,7 +314,7 @@ test("a duplicate legacy publish_namespace is still refused", async () => { peer.close(); await handler; - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("twice"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("twice"), kind: "end" }); }); /** @@ -382,7 +373,7 @@ test("concurrent legacy publish_namespace requests take one reference", async () second, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("raced"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("raced"), kind: "start" }); await two; // Only one reference was taken, so the surviving request ending retracts the path. @@ -391,7 +382,7 @@ test("concurrent legacy publish_namespace requests take one reference", async () peer.close(); await one; - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("raced"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("raced"), kind: "end" }); }); /** The Hop IDs a cluster-negotiated session declared, ours first. */ @@ -430,10 +421,10 @@ test("an inline NAMESPACE that starts looping back is retracted", async () => { const subscription = await acceptSubscribeNamespace(pair.client); await inlineNamespace(subscription, Path.from("theirs"), { hops: [PEER], cost: 0n }); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); await inlineNamespace(subscription, Path.from("theirs"), { hops: [SELF, PEER], cost: 0n }); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); }); /** @@ -450,14 +441,14 @@ test("a repeated NAMESPACE reprices in place", async () => { const subscription = await acceptSubscribeNamespace(pair.client); await inlineNamespace(subscription, Path.from("theirs"), { hops: [PEER], cost: 4n }); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start", route: { hops: [PEER], cost: { warm: 4n, cold: 4n } }, }); await inlineNamespace(subscription, Path.from("theirs"), { hops: [PEER], cost: 0n }); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "update", route: { hops: [PEER], cost: { warm: 0n, cold: 0n } }, @@ -583,7 +574,7 @@ test("a PUBLISH_NAMESPACE update that starts looping back is detached", async () }), request, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); const peer = await nextStream(pair.client); if (!peer) throw new Error("no PUBLISH_NAMESPACE stream"); @@ -597,7 +588,7 @@ test("a PUBLISH_NAMESPACE update that starts looping back is detached", async () VERSION, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); expect(await peer.reader.u53()).toBe(RequestOk.id); await RequestOk.decode(peer.reader, VERSION); @@ -607,7 +598,7 @@ test("a PUBLISH_NAMESPACE update that starts looping back is detached", async () peer.writer, VERSION, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); expect(await peer.reader.u53()).toBe(RequestOk.id); await RequestOk.decode(peer.reader, VERSION); @@ -637,7 +628,7 @@ test("a PUBLISH_NAMESPACE repricing is acknowledged in place", async () => { }), request, ); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start", route: { hops: [PEER], cost: { warm: 4n, cold: 4n } }, @@ -652,7 +643,7 @@ test("a PUBLISH_NAMESPACE repricing is acknowledged in place", async () => { await new PublishNamespaceUpdate({ requestId: 3n, update: { cost: 0n } }).encode(peer.writer, VERSION); expect(await peer.reader.u53()).toBe(RequestOk.id); await RequestOk.decode(peer.reader, VERSION); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "update", route: { hops: [PEER], cost: { warm: 0n, cold: 0n } }, @@ -661,7 +652,7 @@ test("a PUBLISH_NAMESPACE repricing is acknowledged in place", async () => { // Still announced: the stream ending is what retracts it. peer.close(); await handler; - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); }); /** @@ -686,7 +677,7 @@ test("a PUBLISH_NAMESPACE update that changes the publisher applies in place", a }), request, ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); const peer = await nextStream(pair.client); if (!peer) throw new Error("no PUBLISH_NAMESPACE stream"); @@ -713,7 +704,7 @@ test("a PUBLISH_NAMESPACE update that changes the publisher applies in place", a // Still announced: the stream ending is what retracts it. peer.close(); await handler; - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); }); /** @@ -735,7 +726,7 @@ test("a repeated PUBLISH_NAMESPACE is a protocol violation", async () => { }); const request = await Stream.open(pair.server, { version: VERSION }); const handler = subscriber.runPublishNamespace(advert, request); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "start" }); const peer = await nextStream(pair.client); if (!peer) throw new Error("no PUBLISH_NAMESPACE stream"); @@ -743,7 +734,7 @@ test("a repeated PUBLISH_NAMESPACE is a protocol violation", async () => { await advert.encode(peer.writer, VERSION); await expect(handler).rejects.toThrow(ProtocolViolation); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("theirs"), kind: "end" }); }); /** @@ -1137,9 +1128,9 @@ test("local readers filter hidden unsolicited namespaces from a legacy peer", as ), ); } - expect((await nextRoute(plain))?.prefix).toBe(Path.from("visible")); - expect((await nextRoute(opted))?.prefix).toBe(Path.from(".stats/node")); - expect((await nextRoute(opted))?.prefix).toBe(Path.from("visible")); + expect((await plain.next())?.prefix).toBe(Path.from("visible")); + expect((await opted.next())?.prefix).toBe(Path.from(".stats/node")); + expect((await opted.next())?.prefix).toBe(Path.from("visible")); for (const stream of streams) stream.close(); plain.close(); opted.close(); diff --git a/js/net/src/ietf/subscriber.ts b/js/net/src/ietf/subscriber.ts index 45e53461aa..1c8ff68367 100644 --- a/js/net/src/ietf/subscriber.ts +++ b/js/net/src/ietf/subscriber.ts @@ -332,10 +332,6 @@ export class Subscriber { // reference nobody is left to release, pinning the path for the session. let released = false; - // Nothing on this wire says where the initial set ends, so it has landed once the - // stream goes quiet. - let quiet: announce.Quiet | undefined; - // v14/v15: SubscribeNamespace on control stream (via adapter virtual stream) // v16+: SubscribeNamespace on its own real bidi stream @@ -384,10 +380,6 @@ export class Subscriber { throw new Error(`SubscribeNamespace rejected: typeId=0x${respTypeId.toString(16)}`); } - quiet = new announce.Quiet(() => { - if (announced.closed.peek() === undefined) announced.append({ kind: "live" }); - }); - // Loop reading Namespace/NamespaceDone entries const readLoop = (async () => { for (;;) { @@ -395,7 +387,6 @@ export class Subscriber { if (done) break; const msgType = await stream.reader.u53(); - quiet?.heard(); if (msgType === SubscribeNamespaceEntry.id) { const entry = await SubscribeNamespaceEntry.decode( stream.reader, @@ -491,7 +482,6 @@ export class Subscriber { // each namespace keeps its count and the source never detaches, which would // pin the path for the session even after the other source withdrew. released = true; - quiet?.close(); for (const path of live) { this.#detachAnnounce(path); } diff --git a/js/net/src/integration.test.ts b/js/net/src/integration.test.ts index 722bcac4d3..37bab7d093 100644 --- a/js/net/src/integration.test.ts +++ b/js/net/src/integration.test.ts @@ -23,15 +23,6 @@ import type { Producer as TrackProducer } from "./track.ts"; import { withTimeout } from "./util/timeout.ts"; import { wireOf } from "./wire.ts"; -/** The next route event, skipping the live marker: these tests pin routes, and the marker has its own. */ -async function nextRoute(announced: { - next(): Promise; -}): Promise | undefined> { - for (;;) { - const event = await announced.next(); - if (event?.kind !== "live") return event as Exclude | undefined; - } -} function publish(origin: OriginProducer, path: Path.Valid) { const broadcast = origin.createBroadcast(path); @@ -101,14 +92,14 @@ async function runPublishSubscribeFlow(protocol: string, version?: number) { // Client discovers announced broadcast const announced = client.announced(); - const entry = await nextRoute(announced); + const entry = await announced.next(); if (!entry) throw new Error("expected entry"); expect(entry.prefix).toBe("test" as Path.Valid); expect(entry.kind).toBe("start"); // Scoped discovery only echoes the suffix on the wire, but presents the whole path. const prefixed = client.announced(Path.Pattern.subtree(Path.from("root"))); - const prefixedEntry = await nextRoute(prefixed); + const prefixedEntry = await prefixed.next(); if (!prefixedEntry) throw new Error("expected prefixed entry"); expect(prefixedEntry.prefix).toBe("root/child" as Path.Valid); expect(prefixedEntry.kind).toBe("start"); @@ -385,28 +376,28 @@ test("integration: lite draft-06 announce lifecycle", async () => { const first = publish(origin, Path.from("first")); const announced = client.announced(); - let entry = await nextRoute(announced); + let entry = await announced.next(); if (!entry) throw new Error("expected announce"); expect(entry.prefix).toBe("first" as Path.Valid); expect(entry.kind).toBe("start"); // A live announce. const second = publish(origin, Path.from("second")); - entry = await nextRoute(announced); + entry = await announced.next(); if (!entry) throw new Error("expected announce"); expect(entry.prefix).toBe("second" as Path.Valid); expect(entry.kind).toBe("start"); // Unannounce: retracted by announce id on the wire. second.close(); - entry = await nextRoute(announced); + entry = await announced.next(); if (!entry) throw new Error("expected unannounce"); expect(entry.prefix).toBe("second" as Path.Valid); expect(entry.kind).toBe("end"); // Re-announce the same path: a fresh announce assigning a fresh id. const secondAgain = publish(origin, Path.from("second")); - entry = await nextRoute(announced); + entry = await announced.next(); if (!entry) throw new Error("expected re-announce"); expect(entry.prefix).toBe("second" as Path.Valid); expect(entry.kind).toBe("start"); @@ -423,7 +414,7 @@ test("integration: lite draft-06 announce lifecycle", async () => { async function announcedUntil(announced: Announce.Consumer, until: string) { const seen: string[] = []; while (!seen.includes(until)) { - const entry = await withTimeout(nextRoute(announced), 1000, `waiting for ${until}`); + const entry = await withTimeout(announced.next(), 1000, `waiting for ${until}`); if (!entry) throw new Error("announcements ended"); seen.push(entry.prefix); } @@ -1341,7 +1332,7 @@ test("integration: ietf draft-14 subscriber teardown on last unsubscribe", async // Draft-14 only completes SUBSCRIBE_OK once the session is warmed by an announce round-trip. const announced = client.announced(); - await nextRoute(announced); + await announced.next(); const remote = wireOf(client).consume(Path.from("test")); const sub = remote.track("video").subscribe().ordered(); @@ -2016,7 +2007,7 @@ async function runOriginFlow(protocol: string, version?: number) { // The announcement lands in the client's origin. const reader = clientOrigin.consume(); const announced = reader.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("test"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("test"), kind: "start" }); // Consuming through the origin reaches the wire. const remote = await routed(reader, Path.from("test")); @@ -2026,7 +2017,7 @@ async function runOriginFlow(protocol: string, version?: number) { // Unpublishing retracts the entry over the wire and out of the origin. broadcast.close(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("test"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("test"), kind: "end" }); await until(() => !wireOf(reader).routes(Path.from("test"))); await serving; @@ -2373,7 +2364,7 @@ test("create then announce is discoverable on the wire", async () => { const broadcast = origin.createBroadcast(Path.from("later")); broadcast.createTrack("chat"); - const pending = nextRoute(announced); + const pending = announced.next(); broadcast.announce(); const entry = await pending; expect(entry?.prefix).toBe("later" as Path.Valid); @@ -2405,7 +2396,7 @@ test("a handle serves a request under live/** over the wire", async () => { ]); const announced = client.announced(); - const entry = await nextRoute(announced); + const entry = await announced.next(); expect(entry?.prefix).toBe("live" as Path.Valid); expect(entry?.kind).toBe("start"); @@ -2532,58 +2523,3 @@ test("integration: lite draft-05 ends a track with the publisher's reset", async server.abort(); origin.close(); }); - -/** - * Connect a client whose origin is fed by a peer publishing `paths`, then read the origin's - * announcement stream up to the live marker, returning the paths delivered before it. - */ -async function caughtUp(protocol: string, version: number | undefined, paths: string[]): Promise { - const pair = createMockTransportPair(protocol); - const serverOrigin = new OriginProducer(); - const clientOrigin = new OriginProducer(); - const broadcasts = paths.map((path) => publish(serverOrigin, Path.from(path))); - - const [client, server] = await Promise.all([ - connect(url, { transport: pair.client, consume: clientOrigin }), - accept(pair.server, url, { version, publish: serverOrigin.consume() }), - ]); - - const announced = clientOrigin.announced(); - const seen: string[] = []; - for (;;) { - const event = await withTimeout(announced.next(), 2000, `${protocol || version}: never caught up`); - if (!event) throw new Error("announcements ended"); - if (event.kind === "live") break; - if (event.kind !== "start") throw new Error(`only announcements before the marker: got ${event.kind}`); - seen.push(event.prefix); - } - - announced.close(); - for (const broadcast of broadcasts) broadcast.close(); - client.abort(); - server.abort(); - serverOrigin.close(); - clientOrigin.close(); - return seen.sort(); -} - -// Every version reaches the marker after the whole initial set: lite-01/02 via ANNOUNCE_INIT, -// lite-05+ via ANNOUNCE_OK's count, and lite-03/04 and moq-transport once the stream goes quiet. -for (const [name, protocol, version] of [ - ["lite draft-01", "", Lite.Version.DRAFT_01], - ["lite draft-02", "", Lite.Version.DRAFT_02], - ["lite draft-03", Lite.ALPN_03, undefined], - ["lite draft-04", Lite.ALPN_04, undefined], - ["lite draft-05", Lite.ALPN_05, undefined], - ["lite draft-06", Lite.ALPN_06, undefined], - ["ietf draft-14", "", Ietf.Version.DRAFT_14], - ["ietf draft-19", Ietf.ALPN.DRAFT_19, undefined], -] as const) { - test(`integration: ${name} is live after the whole initial set`, async () => { - expect(await caughtUp(protocol, version, ["a", "b", "c"])).toEqual(["a", "b", "c"]); - }); - - test(`integration: ${name} is live with an empty peer`, async () => { - expect(await caughtUp(protocol, version, [])).toEqual([]); - }); -} diff --git a/js/net/src/lite/connection.test.ts b/js/net/src/lite/connection.test.ts index 99ed41e2b3..5bc9afd83a 100644 --- a/js/net/src/lite/connection.test.ts +++ b/js/net/src/lite/connection.test.ts @@ -434,7 +434,6 @@ test("close waits for a request served while withdrawals are in flight", async ( }); try { expect((await announced.next())?.kind).toBe("start"); - expect((await announced.next())?.kind).toBe("live"); fin.enable(); const closing = server.close(); diff --git a/js/net/src/lite/subscriber.test.ts b/js/net/src/lite/subscriber.test.ts index 7b668639bb..9ccb5ba0fd 100644 --- a/js/net/src/lite/subscriber.test.ts +++ b/js/net/src/lite/subscriber.test.ts @@ -12,15 +12,6 @@ import { Subscriber } from "./subscriber.ts"; import { TrackInfo } from "./track.ts"; import { Version } from "./version.ts"; -/** The next route event, skipping the live marker: these tests pin routes, and the marker has its own. */ -async function nextRoute(announced: { - next(): Promise; -}): Promise | undefined> { - for (;;) { - const event = await announced.next(); - if (event?.kind !== "live") return event as Exclude | undefined; - } -} test("closing the subscriber suppresses probe stream warnings", async () => { let readable!: ReadableStreamDefaultController; @@ -162,7 +153,7 @@ test("a max-length chain plus withheld responder is dropped", async () => { Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start", route: { hops: [PUBLISHER_A, UNKNOWN_HOP] }, @@ -185,7 +176,7 @@ test("an unidentified responder keeps hop 0 on a nonempty chain", async () => { Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start", route: { hops: [PUBLISHER_A, UNKNOWN_HOP] }, @@ -205,7 +196,7 @@ test("a received hop list naming no publisher is stamped per connection", async await send((w) => encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from("room"), hops: [] }, Version.DRAFT_06), ); - const update = await nextRoute(announced); + const update = await announced.next(); expect(update).toMatchObject({ prefix: Path.from("room"), kind: "start" }); const hops = update?.route.hops ?? []; expect(hops).toHaveLength(2); @@ -236,7 +227,7 @@ test("a received chain starting with hop 0 keeps it behind the stamp", async () Version.DRAFT_06, ), ); - const update = await nextRoute(announced); + const update = await announced.next(); expect(update).toMatchObject({ prefix: Path.from("room"), kind: "start" }); const [stamp, ...rest] = update?.route.hops ?? []; expect(stamp).not.toBe(UNKNOWN_HOP); @@ -256,7 +247,7 @@ test("a restart updates the route in place, even from another publisher", async await send((w) => encodeAnnounceBroadcast(w, { status: "active", suffix: room, hops: [PUBLISHER_A] }, Version.DRAFT_06), ); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "start" }); const held = subscriber.consume(room); // Same publisher over an identical route: no metadata to forward, so nothing surfaces. @@ -271,13 +262,13 @@ test("a restart updates the route in place, even from another publisher", async Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "update" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "update" }); const same = subscriber.consume(room); expect(same.closed).toBe(held.closed); // A different publisher took the path: an in-place update, never a retraction. await send((w) => encodeAnnounceBroadcast(w, { status: "restart", id: 0n, hops: [PUBLISHER_B] }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: room, kind: "update", route: { hops: [PUBLISHER_B, PEER] }, @@ -297,7 +288,7 @@ test("a restart updates the route in place, even from another publisher", async expect(subscriber.consume(room).closed).toBe(held.closed); await send((w) => encodeAnnounceBroadcast(w, { status: "endedId", id: 0n }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "end" }); announced.close(); subscriber.close(); @@ -316,7 +307,7 @@ test("a restart that re-prices the same publisher emits the new route", async () Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start", route: { hops: [PUBLISHER_A, PEER], cost: { warm: 0n, cold: 0n } }, @@ -329,7 +320,7 @@ test("a restart that re-prices the same publisher emits the new route", async () Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "update", route: { hops: [PUBLISHER_A, PEER], cost: { warm: 4n, cold: 4n } }, @@ -349,12 +340,12 @@ test("a lite-05 duplicate announce follows the same restart rule", async () => { encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from("room"), hops }, Version.DRAFT_05); await send(active([PUBLISHER_A])); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); // On lite-05 a restart travels as a duplicate ANNOUNCE rather than its own message. await send(active([PUBLISHER_A])); await send(active([PUBLISHER_B])); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "update" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "update" }); announced.close(); subscriber.close(); @@ -372,7 +363,7 @@ test("a restart from an unidentified publisher updates in place", async () => { await send((w) => new AnnounceOk(UNKNOWN_HOP, 0).encode(w, Version.DRAFT_06)); await send((w) => encodeAnnounceBroadcast(w, { status: "active", suffix: room, hops: [] }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "start" }); const held = subscriber.consume(room); await send((w) => @@ -382,12 +373,12 @@ test("a restart from an unidentified publisher updates in place", async () => { Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "update" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "update" }); expect(subscriber.consume(room).closed).toBe(held.closed); // Naming a publisher changes the route, not the broadcast. await send((w) => encodeAnnounceBroadcast(w, { status: "restart", id: 0n, hops: [PUBLISHER_A] }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: room, kind: "update" }); + expect(await announced.next()).toMatchObject({ prefix: room, kind: "update" }); expect(subscriber.consume(room).closed).toBe(held.closed); announced.close(); @@ -517,7 +508,7 @@ test("an announce skipped as a reflected loop still holds its path", async () => ), ); - await expect(nextRoute(announced)).rejects.toThrow("duplicate announce"); + await expect(announced.next()).rejects.toThrow("duplicate announce"); // The peer has to hear about it: closing only our side would leave it announcing // into a stream nobody reads. expect(await abortReason()).toContain("duplicate announce"); @@ -550,11 +541,11 @@ test("a restart replaces an announce that was skipped as a reflected loop", asyn // The id stays live, so the peer may restart it into a route that is usable here. await send((w) => encodeAnnounceBroadcast(w, { status: "restart", id: 0n, hops: [PUBLISHER_A] }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); // Retiring the id ends what the restart attached, and nothing else. await send((w) => encodeAnnounceBroadcast(w, { status: "endedId", id: 0n }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "end" }); announced.close(); subscriber.close(); @@ -584,13 +575,13 @@ test("retiring an id whose announce was skipped ends nothing", async () => { Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("lobby"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("lobby"), kind: "start" }); // Retire the skipped one, then the live one. The first must surface nothing, so the // only end a consumer sees is "lobby". await send((w) => encodeAnnounceBroadcast(w, { status: "endedId", id: 0n }, Version.DRAFT_06)); await send((w) => encodeAnnounceBroadcast(w, { status: "endedId", id: 1n }, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("lobby"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("lobby"), kind: "end" }); announced.close(); subscriber.close(); @@ -604,10 +595,10 @@ test("a draft-02 initial announcement can still be retracted", async () => { // ANNOUNCE_INIT carries the initial set. These are advertisements like any other, so // the peer may retract one later and the consumer has to hear about it. await send((w) => new AnnounceInit([Path.from("room")]).encode(w, Version.DRAFT_02)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); await send((w) => encodeAnnounceBroadcast(w, { status: "ended", suffix: Path.from("room") }, Version.DRAFT_02)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "end" }); announced.close(); subscriber.close(); @@ -629,7 +620,7 @@ test("a duplicate start is reported even when its own route reflects", async () Version.DRAFT_06, ), ); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); // A second start for that path, carrying a route that loops back through us. Skipping // it must not pre-empt the violation: the peer sent two starts with no end between @@ -642,7 +633,7 @@ test("a duplicate start is reported even when its own route reflects", async () ), ); - await expect(nextRoute(announced)).rejects.toThrow("duplicate announce"); + await expect(announced.next()).rejects.toThrow("duplicate announce"); announced.close(); subscriber.close(); @@ -660,7 +651,7 @@ test("a draft-02 initial set naming a path twice is refused", async () => { // Erroring the stream discards what it had already queued, so the consumer sees the // violation rather than the first entry followed by it. - await expect(nextRoute(announced)).rejects.toThrow("duplicate announce"); + await expect(announced.next()).rejects.toThrow("duplicate announce"); expect(await abortReason()).toContain("duplicate announce"); // The draft makes this session-fatal: resetting only the stream would let the peer // repeat the violation on the next one. The code has to say so too, or the peer reads @@ -685,10 +676,10 @@ test("a violation on a very long path still ends the session", async () => { const long = Path.from("x".repeat(2000)); const active: AnnounceBroadcast = { status: "active", suffix: long, hops: [PUBLISHER_A] }; await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_06)); - expect(await nextRoute(announced)).toMatchObject({ prefix: long, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: long, kind: "start" }); await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_06)); - await expect(nextRoute(announced)).rejects.toThrow("duplicate announce"); + await expect(announced.next()).rejects.toThrow("duplicate announce"); const info = await sessionEnded(); expect(info?.closeCode).toBe(15); @@ -706,14 +697,14 @@ test("a draft-04 duplicate start is a violation, not a restart", async () => { const active: AnnounceBroadcast = { status: "active", suffix: Path.from("room"), hops: [PUBLISHER_A] }; await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_04)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_04)); // Session first: it is the bounded assertion, so a version that treats this as a // restart fails here in 250ms instead of hanging on a rejection that never comes. expect((await sessionEnded())?.closeCode).toBe(15); - await expect(nextRoute(announced)).rejects.toThrow("duplicate announce"); + await expect(announced.next()).rejects.toThrow("duplicate announce"); }); test("a draft-05 duplicate start is still a restart", async () => { @@ -725,7 +716,7 @@ test("a draft-05 duplicate start is still a restart", async () => { const active: AnnounceBroadcast = { status: "active", suffix: Path.from("room"), hops: [PUBLISHER_A] }; await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_05)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); // Same publisher over a new route: transparent, and emphatically not an error. await send((w) => encodeAnnounceBroadcast(w, active, Version.DRAFT_05)); @@ -733,80 +724,7 @@ test("a draft-05 duplicate start is still a restart", async () => { // A different publisher surfaces as an update, which is what proves the reroute above passed. const replaced: AnnounceBroadcast = { status: "active", suffix: Path.from("room"), hops: [PUBLISHER_B] }; await send((w) => encodeAnnounceBroadcast(w, replaced, Version.DRAFT_05)); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "update" }); - - announced.close(); - subscriber.close(); -}); - -// ANNOUNCE_OK's count lands the initial set at exactly that many ANNOUNCE_STARTs: an unknown -// message does not count toward it, and a live update right behind the set comes after the -// marker. Mirrors the Rust `the_count_lands_at_the_last_initial_start`. -test("the count lands at the last initial start", async () => { - const { subscriber, send, settle } = announceHarness(Version.DRAFT_06); - const announced = subscriber.announced(); - await settle(); - - const start = (suffix: string) => (w: Writer) => - encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from(suffix), hops: [] }, Version.DRAFT_06); - await send((w) => new AnnounceOk(PEER, 1).encode(w, Version.DRAFT_06)); - // An unknown announce type with an empty body, which decodes as skipped. - await send((w) => w.write(new Uint8Array([0x3f, 0x00]))); - await send(start("a")); - await send(start("b")); - - expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - expect(await announced.next()).toEqual({ kind: "live" }); - expect(await announced.next()).toMatchObject({ prefix: Path.from("b"), kind: "start" }); - - announced.close(); - subscriber.close(); -}); - -test("a count of zero is live before any announcement", async () => { - const { subscriber, send, settle } = announceHarness(Version.DRAFT_05); - const announced = subscriber.announced(); - await settle(); - - await send((w) => new AnnounceOk(PEER, 0).encode(w, Version.DRAFT_05)); - expect(await announced.next()).toEqual({ kind: "live" }); - - await send((w) => - encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from("a"), hops: [] }, Version.DRAFT_05), - ); - expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - - announced.close(); - subscriber.close(); -}); - -test("a draft-02 initial set is live after its ANNOUNCE_INIT", async () => { - const { subscriber, send, settle } = announceHarness(Version.DRAFT_02); - const announced = subscriber.announced(); - await settle(); - - await send((w) => new AnnounceInit([Path.from("a"), Path.from("b")]).encode(w, Version.DRAFT_02)); - expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - expect(await announced.next()).toMatchObject({ prefix: Path.from("b"), kind: "start" }); - expect(await announced.next()).toEqual({ kind: "live" }); - - announced.close(); - subscriber.close(); -}); - -// Lite-03/04 say nothing about where the initial set ends, so it lands once the stream goes quiet. -test("a draft-04 initial set is live once the stream goes quiet", async () => { - const { subscriber, send, settle } = announceHarness(Version.DRAFT_04); - const announced = subscriber.announced(); - await settle(); - - const start = (suffix: string) => (w: Writer) => - encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from(suffix), hops: [] }, Version.DRAFT_04); - await send(start("a")); - await send(start("b")); - expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); - expect(await announced.next()).toMatchObject({ prefix: Path.from("b"), kind: "start" }); - expect(await announced.next()).toEqual({ kind: "live" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "update" }); announced.close(); subscriber.close(); diff --git a/js/net/src/lite/subscriber.ts b/js/net/src/lite/subscriber.ts index 9a6236b581..0094095424 100644 --- a/js/net/src/lite/subscriber.ts +++ b/js/net/src/lite/subscriber.ts @@ -217,8 +217,6 @@ export class Subscriber { // Opened outside the try so the catch can reach it: a protocol violation below has // to reset the stream, not just close our side of it. let stream: Stream; - // Lite03/04: lands the initial set once the stream goes quiet. - let quiet: announce.Quiet | undefined; try { stream = await Stream.open(this.#quic, { version: this.version }); } catch (err: unknown) { @@ -235,15 +233,12 @@ export class Subscriber { // It no longer stamps itself onto each hop chain, so we append it here to // keep the reflected-announce loop check seeing the full chain. let responderOrigin: Hop | undefined; - // Lite05+: the initial set ends after this many more ANNOUNCE_STARTs. - let remaining: number | undefined; if (hasAnnounceOk(this.version)) { const ok = await AnnounceOk.decode(stream.reader, this.version); // Keep a withheld 0: it names nobody for loop detection, but it is the // anonymous mark and must travel the reconstructed chain. Assigned identities // stay off this hop and are never forwarded. responderOrigin = ok.hop; - remaining = ok.active; } // Every advertisement the peer currently has live, keyed by suffix (at most one @@ -285,18 +280,10 @@ export class Subscriber { console.debug(`announced: broadcast=${path} active=true`); announced.append({ prefix: path, captures, kind: "start", route }); } - announced.append({ kind: "live" }); break; } - case Version.DRAFT_03: - case Version.DRAFT_04: - // No AnnounceInit and no count: the initial set has landed once the stream goes quiet. - quiet = new announce.Quiet(() => { - if (announced.closed.peek() === undefined) announced.append({ kind: "live" }); - }); - break; default: - // Lite05+: initial state comes via Announce messages, counted by AnnounceOk. + // Draft03+: no AnnounceInit, initial state comes via Announce messages. break; } @@ -308,12 +295,6 @@ export class Subscriber { // Receive announce updates (for Draft03+, this includes initial state) for (;;) { - // Land before decoding past the boundary, so no live update arrives ahead of the marker. - if (remaining === 0) { - announced.append({ kind: "live" }); - remaining = undefined; - } - const announce = await race([ decodeAnnounceBroadcastMaybe(stream.reader, this.version), announced.closed, @@ -322,10 +303,6 @@ export class Subscriber { if (!announce) break; if (announce instanceof Error) throw announce; - quiet?.heard(); - // The count is of ANNOUNCE_STARTs, not every message. - if (remaining !== undefined && announce.status === "active") remaining -= 1; - let path: Path.Valid; let active: boolean; // Present on active/restart; ended messages never carry hops worth checking. @@ -486,8 +463,6 @@ export class Subscriber { if (e instanceof ProtocolViolation) { this.#quic.close({ closeCode: PROTOCOL_VIOLATION_CODE, reason: closeReason(reason(e)) }); } - } finally { - quiet?.close(); } } diff --git a/js/net/src/origin.test.ts b/js/net/src/origin.test.ts index 1220225b69..141b464fa3 100644 --- a/js/net/src/origin.test.ts +++ b/js/net/src/origin.test.ts @@ -8,15 +8,6 @@ import { Producer } from "./origin.ts"; import * as Path from "./path.ts"; import { wireOf } from "./wire.ts"; -/** The next route event, skipping the live marker: these tests pin routes, and the marker has its own. */ -async function nextRoute(announced: { - next(): Promise; -}): Promise | undefined> { - for (;;) { - const event = await announced.next(); - if (event?.kind !== "live") return event as Exclude | undefined; - } -} function publish(origin: Producer, path: Path.Valid) { const broadcast = origin.createBroadcast(path); @@ -325,103 +316,6 @@ test("the table is reactive", async () => { origin.close(); }); -/** Whether `next` is still waiting once everything queued has run. */ -async function pending(next: Promise): Promise { - const waiting = Symbol("waiting"); - const result = await Promise.race([next, settle().then(() => waiting)]); - return result === waiting; -} - -test("an empty origin is live at once, and the marker never repeats", async () => { - const origin = new Producer(); - const announced = origin.announced(); - expect(await announced.next()).toEqual({ kind: "live" }); - - // Later routes are live changes, even past a session that replays and lands. - const landed = wireOf(origin).replaying(Path.empty()); - landed(); - const alice = publish(origin, Path.from("alice")); - expect(await announced.next()).toMatchObject({ prefix: Path.from("alice"), kind: "start" }); - expect(await pending(announced.next())).toBe(true); - - alice.close(); - origin.close(); -}); - -test("live follows the replayed routes", async () => { - const origin = new Producer(); - const b = publish(origin, Path.from("b")); - const c = publish(origin, Path.from("c")); - - const announced = origin.announced(); - const seen: string[] = []; - for (;;) { - const event = await announced.next(); - if (event?.kind === "live") break; - expect(event?.kind).toBe("start"); - if (event?.kind === "start") seen.push(event.prefix); - } - expect(seen.sort()).toEqual(["b", "c"]); - - b.close(); - c.close(); - origin.close(); -}); - -test("a replaying session withholds live until it lands", async () => { - const origin = new Producer(); - const first = wireOf(origin).replaying(Path.empty()); - const second = wireOf(origin).replaying(Path.empty()); - const announced = origin.announced(); - - // Routes a session lands arrive before the marker. - serve(origin, Path.from("room/alice"), () => new BroadcastProducer().consume()); - expect(await announced.next()).toMatchObject({ prefix: Path.from("room/alice"), kind: "start" }); - - // Every session replaying when the stream opened has to land, and a release is idempotent. - first(); - first(); - const next = announced.next(); - expect(await pending(next)).toBe(true); - - // A session that starts replaying after the stream opened is not waited on. - const late = wireOf(origin).replaying(Path.empty()); - second(); - expect(await next).toEqual({ kind: "live" }); - - late(); - announced.close(); - origin.close(); -}); - -test("a stream waits only on sessions replaying into its scope", async () => { - const origin = new Producer(); - const room = wireOf(origin).replaying(Path.from("room")); - const other = wireOf(origin).replaying(Path.from("other")); - - // Nothing replays under `lobby`, and `other` cannot announce into `room/**`. - const lobby = origin.announced(Path.Pattern.parse("lobby/**")); - expect(await lobby.next()).toEqual({ kind: "live" }); - const announced = origin.announced(Path.Pattern.parse("room/**")); - const next = announced.next(); - expect(await pending(next)).toBe(true); - room(); - expect(await next).toEqual({ kind: "live" }); - - // A scoped handle rebases the prefix under its root before comparing. - const scoped = origin.scope(Path.from("tenant"), new Path.Patterns([Path.Pattern.all()])); - const inner = wireOf(scoped).replaying(Path.from("room")); - const outer = origin.announced(Path.Pattern.parse("room/**")); - expect(await outer.next()).toEqual({ kind: "live" }); - const tenant = origin.announced(Path.Pattern.parse("tenant/room/**")); - expect(await pending(tenant.next())).toBe(true); - - inner(); - other(); - for (const stream of [lobby, announced, outer, tenant]) stream.close(); - origin.close(); -}); - test("announced streams the table under a scope with origin-relative paths", async () => { const origin = new Producer(); const consumer = origin.consume(); @@ -431,20 +325,20 @@ test("announced streams the table under a scope with origin-relative paths", asy const announced = consumer.announced(Path.Pattern.subtree(Path.from("room"))); // The initial state arrives first, named from the origin rather than the scope. - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room/a"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room/a"), kind: "start" }); // Additions under the scope stream in; paths outside it are invisible. const b = publish(origin, Path.from("room/b")); publish(origin, Path.from("lobby/c")); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room/b"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room/b"), kind: "start" }); // Removals retract. b.close(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room/b"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room/b"), kind: "end" }); // The stream ends when the origin closes. origin.close(); - expect(await nextRoute(announced)).toBeUndefined(); + expect(await announced.next()).toBeUndefined(); a.close(); }); @@ -461,9 +355,9 @@ test("hidden paths need an opt-in or a scope naming the dot segment", async () = expect([...consumer.broadcasts(Path.Pattern.parse(".stats/**")).peek().keys()]).toEqual([Path.from(".stats/node")]); const plain = consumer.announced(); - expect(await nextRoute(plain)).toMatchObject({ prefix: Path.from("room/catalog.pro") }); + expect(await plain.next()).toMatchObject({ prefix: Path.from("room/catalog.pro") }); const opted = consumer.announced(Path.Pattern.parse("room/**"), { hidden: true }); - const seen = [(await nextRoute(opted))?.prefix, (await nextRoute(opted))?.prefix].sort(); + const seen = [(await opted.next())?.prefix, (await opted.next())?.prefix].sort(); expect(seen).toEqual([Path.from("room/.internal"), Path.from("room/catalog.pro")]); plain.close(); @@ -489,10 +383,10 @@ test("a remote entry resolves by path and retracts on dispose", async () => { // Announced streams include remote entries. const announced = consumer.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start", route: Route.default }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start", route: Route.default }); dispose(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); expect(wireOf(consumer).routes(path)).toBe(false); announced.close(); @@ -510,10 +404,10 @@ test("announced keeps a broader covering route at its prefix", async () => { // The scope filters the route without changing the prefix it claims. const scope = Path.Pattern.subtree(Path.from("room/alice")); const announced = consumer.announced(scope); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "start" }); dispose(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("room"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("room"), kind: "end" }); announced.close(); upstream.close(); @@ -541,7 +435,7 @@ test("a local publish shadows a remote entry", async () => { // One path, one announcement, even though both tables route it. const announced = consumer.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start", route: Route.default }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start", route: Route.default }); // Dropping the local publish falls back to the remote entry without a retraction. local.close(); @@ -665,12 +559,12 @@ test("disposing the newest remote route promotes the fallback", async () => { const disposeNewer = serve(origin, path, provider(newer)); const announced = consumer.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start" }); // The newer session dies: consumers see a retract then the promoted fallback. disposeNewer(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start" }); const handle = await routed(consumer, path); const track = handle?.track("chat").subscribe(); @@ -738,7 +632,7 @@ test("requests never appear in announced or the table", async () => { const announced = consumer.announced(); publish(origin, Path.from("real")); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("real"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("real"), kind: "start" }); announced.close(); request.close(); @@ -752,12 +646,12 @@ test("a republish retracts then re-announces the path", async () => { publish(origin, path); const announced = consumer.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start" }); // A new broadcast takes the path: consumers must let go of the superseded one. publish(origin, path); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start" }); announced.close(); origin.close(); @@ -1160,7 +1054,7 @@ test("createBroadcast is invisible to everyone until announce", async () => { broadcast.announce({ cost: 4n }); expect(wireOf(consumer).routes(path)).toBe(true); expect(wireOf(consumer).advertised.peek()?.has(path)).toBe(true); - expect(await nextRoute(announced)).toMatchObject({ + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start", route: { hops: [], cost: { warm: 4n, cold: 4n } }, @@ -1172,18 +1066,18 @@ test("createBroadcast is invisible to everyone until announce", async () => { broadcast.unannounce(); expect(wireOf(consumer).routes(path)).toBe(false); expect(wireOf(consumer).advertised.peek()?.has(path)).toBe(false); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); expect(request.active.peek()).toBeUndefined(); // Back on the air through a fresh handle. broadcast.announce(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start" }); const again = request.active.peek(); expect(again).toBeDefined(); expect(again).not.toBe(first); broadcast.close(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); request.close(); announced.close(); origin.close(); @@ -1205,12 +1099,12 @@ test("a request prefers a cheaper received route over an announced local broadca const remote = request.active.peek(); expect(remote).toBeDefined(); const announced = consumer.announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, route: { hops: [PEER] } }); + expect(await announced.next()).toMatchObject({ prefix: path, route: { hops: [PEER] } }); // Re-priced below it, the local broadcast wins at once, and the remote front it replaced closes. local.announce({ cost: 0n }); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "end" }); - expect(await nextRoute(announced)).toMatchObject({ prefix: path, kind: "start", route: Route.default }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: path, kind: "start", route: Route.default }); expect(request.active.peek()).not.toBe(remote); expect(remote?.closed.peek()).not.toBeUndefined(); @@ -1256,7 +1150,7 @@ test("a handle serves a request under live", async () => { const consumer = origin.consume(); const handle = origin.dynamic(Path.from("live")); - expect(await nextRoute(consumer.announced())).toMatchObject({ prefix: Path.from("live"), kind: "start" }); + expect(await consumer.announced().next()).toMatchObject({ prefix: Path.from("live"), kind: "start" }); const waiting = handle.requested().next(); const request = consumer.request(Path.from("live/cam")); @@ -1283,11 +1177,11 @@ test("a re-priced route is delivered as an update", async () => { const origin = new Producer(); const handle = origin.dynamic(Path.from("live")); const announced = origin.consume().announced(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("live"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("live"), kind: "start" }); handle.update({ cost: 5n }); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("live"), kind: "update" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("live"), kind: "update" }); handle.close(); - expect(await nextRoute(announced)).toMatchObject({ prefix: Path.from("live"), kind: "end" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("live"), kind: "end" }); announced.close(); origin.close(); }); @@ -1659,7 +1553,7 @@ test("announced filters by arbitrary patterns and reports captures", async () => const broadcast = publish(origin, Path.from("room/alice")); const announced = consumer.announced(Path.Pattern.parse("room/*")); - const update = await nextRoute(announced); + const update = await announced.next(); expect(update?.prefix).toBe(Path.from("room/alice")); expect(update?.captures?.map((capture) => capture.text)).toEqual(["alice"]); @@ -1807,7 +1701,7 @@ test("a scoped reader picks the best route its scope can see at a prefix", async expect(reader.broadcasts().peek().get(Path.empty())?.cost.warm).toBe(5n); expect(origin.broadcasts(Path.Pattern.parse("room/video")).peek().get(Path.empty())?.cost.warm).toBe(5n); const announced = reader.announced(); - expect((await nextRoute(announced))?.route.cost.warm).toBe(5n); + expect((await announced.next())?.route.cost.warm).toBe(5n); announced.close(); // A session publishing the scoped view offers the video route too. diff --git a/js/net/src/origin.ts b/js/net/src/origin.ts index 380a7afd97..2416ce44dd 100644 --- a/js/net/src/origin.ts +++ b/js/net/src/origin.ts @@ -457,11 +457,6 @@ class OriginState { // this is coming" and "nothing here can ever serve you". answerers = new Signal(0); - // Sessions still replaying the peer's initial announce set into the table, each with the - // absolute paths it may announce under. An announcement stream opened while an - // overlapping one replays withholds its live marker until it lands. - replaying = new Signal>(new Set()); - closed = new Once(); /** @@ -693,7 +688,6 @@ export class Producer implements Table { this.#scope.allowed.overlaps(Path.Pattern.subtree(Path.join(this.#scope.root, prefix))), attach: (discovery) => this.#attach(discovery), expect: () => this.#expect(), - replaying: (prefix) => this.#replaying(prefix), get requests() { if (thisProducer.#scope === Scope.all) return thisProducer.#state.requests; thisProducer.#requests ??= new Derived([thisProducer.#state.requests], (requests) => @@ -924,33 +918,6 @@ export class Producer implements Table { }; } - /** - * Register a session replaying the peer's initial announce set under `prefix`, relative - * to this handle's root. Only announcement streams overlapping it wait. Returns the - * release; call it once the set has landed, or the session dies. Idempotent. - * - * @internal - */ - #replaying(prefix: Path.Valid): Dispose { - let scope: Path.Patterns; - try { - scope = this.#scope.patterns(Path.Pattern.subtree(prefix)); - } catch { - // A subtree too complex to intersect waits on the whole scope instead, like Rust. - scope = this.#scope.allowed ?? new Path.Patterns([Path.Pattern.all()]); - } - const source = { scope }; - this.#state.replaying.mutate((sources) => { - sources.add(source); - }); - return () => { - if (!this.#state.replaying.peek().has(source)) return; - this.#state.replaying.mutate((sources) => { - sources.delete(source); - }); - }; - } - /** * Resolves once anything a serving session scans changes: the open requests, either * side of the routing table, or the attached sessions that decide which requests are blind. @@ -1386,9 +1353,7 @@ export class Consumer { /** * The announced routes matching `scope`, as a live stream: every currently advertised - * route arrives first as `start`, then the `live` marker, then changes as they - * happen. The marker also waits for every session still replaying its peer's initial - * set when the stream opened, so a caller listing what is live stops there. + * route arrives first as `start`, then changes as they happen. * Any pattern is accepted. A local broadcast appears once it announces, exactly as a * peer sees it. A dynamic or received route announces the prefix it covers when its * subtree overlaps the scope. The stream ends when the origin closes or the consumer is @@ -1441,12 +1406,6 @@ export class Consumer { // update. let active = new Map(); - // The sessions replaying into this stream's scope when it opened; the live marker - // follows the diff after the last of them lands. Undefined once it was delivered. - let waiting: Set<{ scope: Path.Patterns }> | undefined = new Set( - [...this.#state.replaying.peek()].filter(({ scope }) => [...scope].some((p) => patterns.overlaps(p))), - ); - try { for (;;) { const local = this.#state.local.peek(); @@ -1481,21 +1440,7 @@ export class Consumer { } active = next; - if (waiting) { - const replaying = this.#state.replaying.peek(); - for (const source of waiting) { - if (!replaying.has(source)) waiting.delete(source); - } - if (waiting.size === 0) { - producer.append({ kind: "live" }); - waiting = undefined; - } - } - - // Replay holds only matter until the marker is out; past it, a reconnect's holds - // must not rescan the table for every stream already live. - const table = [this.#state.local, this.#state.advertisedLocal, this.#state.routes, producer.closed]; - await (waiting ? Signal.race(...table, this.#state.replaying) : Signal.race(...table)); + await Signal.race(this.#state.local, this.#state.advertisedLocal, this.#state.routes, producer.closed); if (producer.closed.peek() !== undefined) return; } } catch { diff --git a/js/net/src/wire.ts b/js/net/src/wire.ts index a71c9ab1e4..e25b023ad7 100644 --- a/js/net/src/wire.ts +++ b/js/net/src/wire.ts @@ -36,11 +36,6 @@ export interface OriginProducer { ): origin.Dynamic; attach(discovery: boolean): Dispose; expect(): Dispose; - /** - * Hold the live marker of announcement streams opened now that overlap `prefix`, until the - * peer's initial set under it lands. - */ - replaying(prefix: Path.Valid): Dispose; readonly requests: Getter | undefined>; changed(): GetPromise; /** Whether sessions should answer the request with a blind subscription right now. */ diff --git a/js/room/src/room.ts b/js/room/src/room.ts index dd8fcb0f2b..c9e038438a 100644 --- a/js/room/src/room.ts +++ b/js/room/src/room.ts @@ -82,7 +82,6 @@ export class Room { for (;;) { const update = await effect.race(announced.next()); if (!update) break; - if (update.kind === "live") continue; // The scope's `**` captures what lies beneath the prefix. A broad route // that cannot pin that suffix names no participant to open. diff --git a/js/watch/src/broadcast.ts b/js/watch/src/broadcast.ts index 15418a974b..17b7882160 100644 --- a/js/watch/src/broadcast.ts +++ b/js/watch/src/broadcast.ts @@ -166,7 +166,6 @@ export class Broadcast { for (;;) { const entry = await effect.race(announced.next()); if (!entry) break; - if (entry.kind === "live") continue; this.#announced.mutate((active) => { if (!active) return; if (entry.kind === "end") active.delete(entry.prefix); diff --git a/kt/README.md b/kt/README.md index c4187827fe..0a0085498e 100644 --- a/kt/README.md +++ b/kt/README.md @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.collect // connect() wires up an internal origin and returns a live connection. Moq.connect("https://relay.example.com").use { moq -> moq.announcements(AnnounceConfig(prefix = "demos/", filter = "*/camera")).collect { event -> - // AnnounceEventLive follows the routes live at subscribe time. if (event !is AnnounceEventStart) return@collect // Prefix stays origin-relative; captures reports what * matched. println("got broadcast ${event.announce.prefix}") diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt index 3d237b19e4..349e8ec8c4 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt @@ -39,7 +39,7 @@ typealias AnnouncedBroadcast = uniffi.moq.MoqAnnouncedBroadcast typealias Announce = uniffi.moq.MoqAnnounce /** * What an [AnnounceConsumer] yields: [AnnounceEventStart], [AnnounceEventUpdate], - * [AnnounceEventEnd], or [AnnounceEventLive]. + * or [AnnounceEventEnd]. */ typealias AnnounceEvent = uniffi.moq.MoqAnnounceEvent // Kotlin cannot reach a sealed class's subtypes through its typealias, so each @@ -50,8 +50,6 @@ typealias AnnounceEventStart = uniffi.moq.MoqAnnounceEvent.Start typealias AnnounceEventUpdate = uniffi.moq.MoqAnnounceEvent.Update /** No route covers the prefix any more; carries its last route. */ typealias AnnounceEventEnd = uniffi.moq.MoqAnnounceEvent.End -/** Every route live at subscribe time has been delivered; what follows is live changes. */ -typealias AnnounceEventLive = uniffi.moq.MoqAnnounceEvent.Live // Broadcast / track / group producers and consumers. /** The write side of a broadcast: publish tracks into it. */ typealias BroadcastProducer = uniffi.moq.MoqBroadcastProducer diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt index f84127ee98..339ceb4dde 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt @@ -193,8 +193,7 @@ fun GroupConsumer.frames(): Flow = flow { } /** - * Stream of announce events matching [config]. An [AnnounceEventLive] follows the - * routes live at subscribe time, so a collector can gather what is live and stop. + * Stream of announce events matching [config]. * * Acquires the subscription on first collection and cancels it when collection * ends, so callers never touch the underlying handle. Use the raw diff --git a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt index e4a090b4f7..ba21628c2f 100644 --- a/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt +++ b/kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt @@ -3,8 +3,6 @@ package dev.moq import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield @@ -23,13 +21,9 @@ import kotlin.time.Duration.Companion.milliseconds @Serializable private data class Status(val state: String) -/** The next announce event that is not Live, which lands wherever the backlog ends. */ -private suspend fun AnnounceConsumer.nextRoute(): AnnounceEvent { - while (true) { - val event = checkNotNull(next()) { "announce stream ended" } - if (event !is AnnounceEventLive) return event - } -} +/** The next announce event. */ +private suspend fun AnnounceConsumer.nextRoute(): AnnounceEvent = + checkNotNull(next()) { "announce stream ended" } private fun opusHead(): ByteArray = "OpusHead".encodeToByteArray() + byteArrayOf( @@ -391,27 +385,6 @@ class SmokeTest { } } - @Test - fun `announced yields Live once caught up`() = runTest { - OriginProducer(OriginConfig()).use { origin -> - val consumer = origin.consume() - val empty = consumer.announced(AnnounceConfig()) - assertEquals(AnnounceEventLive, empty.next()) - empty.cancel() - - origin.createBroadcast("cam").use { broadcast -> - broadcast.announce(Route()) - consumer.announcedBroadcast("cam").available() - - val listed = consumer.announcements() - .takeWhile { it !is AnnounceEventLive } - .map { (it as AnnounceEventStart).announce.prefix } - .toList() - assertEquals(listOf("cam"), listed) - } - } - } - @Test fun `dynamic serves a request under a prefix`() = runTest { OriginProducer(OriginConfig()).use { origin -> diff --git a/py/moq-rs/README.md b/py/moq-rs/README.md index 69339fe6ce..9ae5871ee4 100644 --- a/py/moq-rs/README.md +++ b/py/moq-rs/README.md @@ -30,7 +30,7 @@ async def main(): async with moq.connect("https://cdn.moq.dev/anon") as client: async for event in client.announced(): if not isinstance(event, moq.AnnounceEventStart): - continue # AnnounceEventUpdate, AnnounceEventEnd, or AnnounceEventLive + continue # AnnounceEventUpdate or AnnounceEventEnd # A route covers a prefix and carries no broadcast, so resolve the path. broadcast = await client.request_broadcast(event.announce.prefix) catalog = await broadcast.catalog() @@ -205,7 +205,7 @@ Every handle whose cleanup is `cancel()` is an async context manager, so exiting - `await .requested_broadcast() → BroadcastRequest`. Call `.accept(broadcast)` to serve it, or `.reject(code)` to fail the requester. - Async iterator yielding `BroadcastRequest` - **`OriginConsumer`**. Discover broadcasts. - - `.announced(prefix, filter=None) → AnnounceConsumer` (async iterator of `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd` carrying an `Announce`, or `AnnounceEventLive` once caught up); `filter` is a pattern relative to the literal prefix, while each `Announce.prefix` stays origin-relative and `.captures` reports wildcard matches + - `.announced(prefix, filter=None) → AnnounceConsumer` (async iterator of `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd`, each carrying an `Announce`); `filter` is a pattern relative to the literal prefix, while each `Announce.prefix` stays origin-relative and `.captures` reports wildcard matches - `.announced_broadcast(path) → AnnouncedBroadcast` (awaitable, waits until something serves the path) - `.request_broadcast(path) → BroadcastConsumer` (awaitable; announced now or a dynamic fallback, else raises) diff --git a/py/moq-rs/docs/index.md b/py/moq-rs/docs/index.md index c7282ed47d..14fafb6291 100644 --- a/py/moq-rs/docs/index.md +++ b/py/moq-rs/docs/index.md @@ -106,7 +106,6 @@ asyncio.run(main()) AnnounceEventStart AnnounceEventUpdate AnnounceEventEnd - AnnounceEventLive ``` ## Data types diff --git a/py/moq-rs/examples/announced.py b/py/moq-rs/examples/announced.py index 66bff90515..666dda911c 100644 --- a/py/moq-rs/examples/announced.py +++ b/py/moq-rs/examples/announced.py @@ -18,8 +18,6 @@ async def run(url: str, prefix: str, tls_verify: bool) -> None: print(f" + {event.announce.prefix}") elif isinstance(event, moq.AnnounceEventEnd): print(f" - {event.announce.prefix}") - elif isinstance(event, moq.AnnounceEventLive): - print(" (caught up; what follows is live)") def main() -> None: diff --git a/py/moq-rs/moq/__init__.py b/py/moq-rs/moq/__init__.py index 7c4f836e0e..35eb0dfd09 100644 --- a/py/moq-rs/moq/__init__.py +++ b/py/moq-rs/moq/__init__.py @@ -14,7 +14,6 @@ AnnouncedBroadcast, AnnounceEvent, AnnounceEventEnd, - AnnounceEventLive, AnnounceEventStart, AnnounceEventUpdate, BroadcastRequest, @@ -100,7 +99,6 @@ "AnnouncedBroadcast", "AnnounceEvent", "AnnounceEventEnd", - "AnnounceEventLive", "AnnounceEventStart", "AnnounceEventUpdate", "Audio", diff --git a/py/moq-rs/moq/origin.py b/py/moq-rs/moq/origin.py index eaa4887963..e1c0fc9ac2 100644 --- a/py/moq-rs/moq/origin.py +++ b/py/moq-rs/moq/origin.py @@ -32,8 +32,8 @@ AnnounceEvent = MoqAnnounceEvent """What :class:`AnnounceConsumer` yields: :data:`AnnounceEventStart`, -:data:`AnnounceEventUpdate`, or :data:`AnnounceEventEnd` carrying an -:data:`Announce` as ``announce``, or :data:`AnnounceEventLive`. +:data:`AnnounceEventUpdate`, or :data:`AnnounceEventEnd`, each carrying an +:data:`Announce` as ``announce``. """ AnnounceEventStart = MoqAnnounceEvent.START @@ -45,16 +45,12 @@ AnnounceEventEnd = MoqAnnounceEvent.END """No route covers the prefix any more; carries its last route.""" -AnnounceEventLive = MoqAnnounceEvent.LIVE -"""Every route live at subscribe time has been delivered; what follows is live changes.""" - class AnnounceConsumer: """Async-iterable stream of :data:`AnnounceEvent` as they arrive. Usable as an async context manager; iterate with ``async for`` and it keeps - yielding events until cancelled. Break on :data:`AnnounceEventLive` to list - what is live and stop. + yielding events until cancelled. """ def __init__(self, inner: MoqAnnounceConsumer) -> None: diff --git a/py/moq-rs/tests/test_local.py b/py/moq-rs/tests/test_local.py index 64d4b7b4e9..f45efd9881 100644 --- a/py/moq-rs/tests/test_local.py +++ b/py/moq-rs/tests/test_local.py @@ -15,18 +15,15 @@ def create_announced(origin: moq.OriginProducer, path: str) -> moq.BroadcastProd async def routes(announced: moq.AnnounceConsumer): - """Yield each newly announced route, skipping the other events such as LIVE.""" + """Yield each newly announced route, skipping updates and ends.""" async for event in announced: if isinstance(event, moq.AnnounceEventStart): yield event.announce async def next_route(announced: moq.AnnounceConsumer) -> moq.AnnounceEvent: - """The next announce event that is not LIVE, which lands wherever the backlog ends.""" - while True: - event = await asyncio.wait_for(anext(announced), timeout=5.0) - if not isinstance(event, moq.AnnounceEventLive): - return event + """The next announce event.""" + return await asyncio.wait_for(anext(announced), timeout=5.0) def opus_head() -> bytes: @@ -1181,29 +1178,6 @@ async def test_broadcast_is_reachable_only_while_announced(): broadcast.close() -async def test_announced_yields_live_once_caught_up(): - """LIVE ends the backlog: at once on an empty origin, after existing routes otherwise.""" - origin = moq.OriginProducer() - consumer = origin.consume() - - empty = consumer.announced() - assert isinstance(await asyncio.wait_for(anext(empty), timeout=5.0), moq.AnnounceEventLive) - empty.cancel() - - broadcast = create_announced(origin, "cam") - await asyncio.wait_for(consumer.announced_broadcast("cam"), timeout=5.0) - - listed = [] - async with consumer.announced() as announced: - async for event in announced: - if isinstance(event, moq.AnnounceEventLive): - break - assert isinstance(event, moq.AnnounceEventStart) - listed.append(event.announce.prefix) - assert listed == ["cam"] - broadcast.close() - - async def test_announced_pattern_captures(): origin = moq.OriginProducer() consumer = origin.consume() diff --git a/py/moq-rs/tests/test_server.py b/py/moq-rs/tests/test_server.py index a146a8b590..c981e9785f 100644 --- a/py/moq-rs/tests/test_server.py +++ b/py/moq-rs/tests/test_server.py @@ -10,18 +10,15 @@ async def routes(announced: moq.AnnounceConsumer): - """Yield each newly announced route, skipping the other events such as LIVE.""" + """Yield each newly announced route, skipping updates and ends.""" async for event in announced: if isinstance(event, moq.AnnounceEventStart): yield event.announce async def next_route(announced: moq.AnnounceConsumer) -> moq.AnnounceEvent: - """The next announce event that is not LIVE, which lands wherever the backlog ends.""" - while True: - event = await asyncio.wait_for(anext(announced), timeout=5.0) - if not isinstance(event, moq.AnnounceEventLive): - return event + """The next announce event.""" + return await asyncio.wait_for(anext(announced), timeout=5.0) def opus_head() -> bytes: diff --git a/quest/m1/README.md b/quest/m1/README.md index 5cd69305a1..4bbd1539b3 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -48,7 +48,6 @@ blocks. The quests that gated m0 lines moved under them. - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [moq-net owns its transport seam](/quest/m1/transport-seam.md) - moq-net names only its own transport traits, so a web-transport-trait or qmux major bump is a patch for it - [fMP4 init from the catalog](/quest/m1/fmp4-catalog-init.md) - fMP4 export writes avc3/hev1 entries from the catalog, so an Annex-B H.264 or H.265 init no longer waits for the first keyframe -- [Delete the live marker](/quest/m1/announce-live-removal.md) - announce streams yield only route events; the `Live` marker and its hold machinery are gone - [WebTransport close backends](/quest/m1/wt-close-backends.md) - web-transport-noq and -quinn keep the session alive while closing, and iroh's client reads a capsule close - [Enabled flag](/quest/m1/catalog-enabled.md) - one `enabled` rendition flag replaces `stalled` and pausing, and the flapping encoder-lag detector is deleted - [DTX timestamps](/quest/m1/opus-dtx-timestamps.md) - Opus DTX keeps the capture timeline, so voice enables it by default again diff --git a/quest/m1/announce-live-removal.md b/quest/m1/announce-live-removal.md deleted file mode 100644 index a5d232b810..0000000000 --- a/quest/m1/announce-live-removal.md +++ /dev/null @@ -1,47 +0,0 @@ -# [L] Delete the announce live marker - -## Goal - -Announce streams yield only route events (start, update, end) in -`rs/moq-net`, `@moq/net`, moq-ffi, and every binding. The `Live` marker and -everything that exists to produce it are deleted: the per-session replay -holds and landing bookkeeping on the origin, the quiet-period guesses on -wires without an exact count, and the marker in the bindings' event types. - -## Plan - -Decided 2026-09-29 by the maintainer, after walking through a page-load fix, -a Live/Offline toggle, and a per-session `live()`: - -- The marker answers "has the initial list arrived?" Only one-shot listing - (`moq ls`, shell completion, both since removed) used it, and no app does: room, watch, and the - demo skip it. An origin merges many sessions and local publishers, so - "caught up" there needs aggregation across connections that start, fail, - and reconnect independently. That produced the page-load race, the special - first connection, and the offline question. With no customer, delete it - rather than patch it. -- No replacement for now. A single-connection listing (a `session.live()` - returning the peer's initial set) can come back when a customer needs it. -- Apps drive loading and offline UI from connection status, which `@moq/net` - already exposes. No empty-state work is planned. -- Wire unchanged. lite's ANNOUNCE_OK Active Count and the IETF Active Count - extension (`drafts/draft-lcurley-moq-active-count.md`) stay on the wire, - unused; a future listing would read exactly that count. Peers still send - and validate it. Only the local consumption goes. - -Guidance: - -- The marker is unreleased, on `main` only (Rust since #4059, JS since #4261, bindings - since #4266), so this deletion breaks nothing published. -- Look for code that only exists to produce the marker, and delete it rather - than stubbing it: replay/landing counters in the origin, holds taken by the - JS forwarder and reconnect loop, quiet-gap timers on lite-03/04 and - moq-transport, and the batching of the initial set if nothing else needs - it. Keep what other features rely on (for example, request answering - through `expect()`). -- Update `doc/lib/{rs,js}`, `doc/concept`, the binding docs, and examples - (`js/net/examples/discovery.ts`) inline. Delete marker-only tests. -- Run `just test interop --all`. - -Public API: removes `AnnounceEvent::Live` / `{ kind: "live" }` / -`MoqAnnounceEvent::Live` and the binding aliases. Wire: none. diff --git a/rs/hang/examples/subscribe.rs b/rs/hang/examples/subscribe.rs index e1f784652b..b213db674d 100644 --- a/rs/hang/examples/subscribe.rs +++ b/rs/hang/examples/subscribe.rs @@ -43,13 +43,10 @@ async fn run_session(origin: moq_net::origin::Producer) -> anyhow::Result<()> { async fn run_subscribe(consumer: moq_net::origin::Consumer) -> anyhow::Result<()> { // Wait for a route to be announced, then resolve the broadcast at its path. // The convention is that a publisher announces each broadcast's exact path. - let mut announced = consumer.announced(); - let path = loop { - match announced.next().await.context("origin closed")? { - moq_net::announce::Event::Start(announce) => break announce.prefix, - // Nothing else can come before the first announcement. - event => anyhow::ensure!(matches!(event, moq_net::announce::Event::Live), "unexpected {event:?}"), - } + let path = match consumer.announced().next().await.context("origin closed")? { + moq_net::announce::Event::Start(announce) => announce.prefix, + // Nothing else can come before the first announcement. + event => anyhow::bail!("unexpected {event:?}"), }; tracing::info!(%path, "broadcast announced"); diff --git a/rs/moq-bench/src/connection.rs b/rs/moq-bench/src/connection.rs index 569154968b..c90bf21bea 100644 --- a/rs/moq-bench/src/connection.rs +++ b/rs/moq-bench/src/connection.rs @@ -337,7 +337,7 @@ async fn subscribe( update = announced.next() => { let update = match update { Some(moq_net::announce::Event::Start(update) | moq_net::announce::Event::Update(update)) => update, - Some(moq_net::announce::Event::End(_) | moq_net::announce::Event::Live) => continue, + Some(moq_net::announce::Event::End(_)) => continue, None => break, }; let path = update.prefix.to_string(); @@ -363,7 +363,7 @@ async fn subscribe( while selected < want { let update = match announced.next().await { Some(moq_net::announce::Event::Start(update) | moq_net::announce::Event::Update(update)) => update, - Some(moq_net::announce::Event::End(_) | moq_net::announce::Event::Live) => continue, + Some(moq_net::announce::Event::End(_)) => continue, None => break, }; let path = update.prefix.to_string(); diff --git a/rs/moq-boy/src/input.rs b/rs/moq-boy/src/input.rs index 526df9f9ca..61202b1127 100644 --- a/rs/moq-boy/src/input.rs +++ b/rs/moq-boy/src/input.rs @@ -69,7 +69,6 @@ pub async fn handle_viewers( let (update, active) = match announced.next().await { Some(moq_net::announce::Event::Start(update) | moq_net::announce::Event::Update(update)) => (update, true), Some(moq_net::announce::Event::End(update)) => (update, false), - Some(moq_net::announce::Event::Live) => continue, None => break, }; diff --git a/rs/moq-c/src/api.rs b/rs/moq-c/src/api.rs index 4f74abdaf4..18d766b785 100644 --- a/rs/moq-c/src/api.rs +++ b/rs/moq-c/src/api.rs @@ -726,13 +726,9 @@ pub enum moq_announce_kind { MOQ_ANNOUNCE_KIND_UPDATE = 1, /// No route covers the prefix any more. MOQ_ANNOUNCE_KIND_END = 2, - /// Every route live when the listener started has been delivered; what - /// follows is live changes. Delivered once, with no prefix or captures. - MOQ_ANNOUNCE_KIND_LIVE = 3, } -/// An announce event from an origin: a route starting, updating, or ending, -/// or the listener catching up. +/// An announce event from an origin: a route starting, updating, or ending. #[repr(C)] #[allow(non_camel_case_types)] pub struct moq_announce_event { @@ -747,7 +743,7 @@ pub struct moq_announce_event { pub captures_len: usize, pub has_captures: bool, - /// Which event this is. A LIVE event carries no prefix or captures. + /// Which event this is. pub kind: moq_announce_kind, } diff --git a/rs/moq-c/src/origin.rs b/rs/moq-c/src/origin.rs index d251718642..ca3f4b3183 100644 --- a/rs/moq-c/src/origin.rs +++ b/rs/moq-c/src/origin.rs @@ -60,14 +60,6 @@ impl AnnouncedRecord { Event::Start(update) => (update, moq_announce_kind::MOQ_ANNOUNCE_KIND_START), Event::Update(update) => (update, moq_announce_kind::MOQ_ANNOUNCE_KIND_UPDATE), Event::End(update) => (update, moq_announce_kind::MOQ_ANNOUNCE_KIND_END), - Event::Live => { - return Self { - prefix: String::new(), - captures: None, - capture_views: Vec::new(), - kind: moq_announce_kind::MOQ_ANNOUNCE_KIND_LIVE, - }; - } }; let captures = update.captures.map(|captures| { captures diff --git a/rs/moq-c/src/test.rs b/rs/moq-c/src/test.rs index 00539f4ec1..b348e97e3e 100644 --- a/rs/moq-c/src/test.rs +++ b/rs/moq-c/src/test.rs @@ -123,28 +123,10 @@ impl Callback { } impl Callback { - /// Wait for the next announce event that is not `LIVE`, freeing any `LIVE` ahead of it. + /// Wait for the next announce event. fn recv_route(&self) -> (u32, moq_announce_event) { - loop { - let announced = id(self.recv()); - let info = announce_info(announced); - if info.kind != moq_announce_kind::MOQ_ANNOUNCE_KIND_LIVE { - return (announced, info); - } - assert_eq!(moq_origin_announced_free(announced), 0); - } - } - - /// Like [`recv_terminal`](Self::recv_terminal), but first frees any announce - /// events (such as a late `LIVE`) still queued ahead of it. - fn recv_announce_terminal(&self) -> i32 { - loop { - let code = self.recv(); - if code <= 0 { - return code; - } - assert_eq!(moq_origin_announced_free(id(code)), 0); - } + let announced = id(self.recv()); + (announced, announce_info(announced)) } } @@ -156,7 +138,7 @@ fn announce_info(announced: u32) -> moq_announce_event { captures: std::ptr::null(), captures_len: 0, has_captures: false, - kind: moq_announce_kind::MOQ_ANNOUNCE_KIND_LIVE, + kind: moq_announce_kind::MOQ_ANNOUNCE_KIND_START, }; assert_eq!(unsafe { moq_origin_announced_info(announced, &mut info) }, 0); info @@ -1890,7 +1872,7 @@ fn announced_free_lifecycle() { // Stop the listener and drain its terminal callback before the Callback drops. assert_eq!(moq_origin_announced_cancel(ann_task), 0); - ann_cb.recv_announce_terminal(); + ann_cb.recv_terminal(); assert_eq!(moq_origin_close(origin), 0); assert_eq!(moq_publish_close(broadcast), 0); @@ -2017,45 +1999,6 @@ fn unknown_format() { assert!(ret < 0, "an out-of-range format code should fail"); } -/// `LIVE` marks the end of the routes live when the listener started: at once on -/// an empty origin, and after the existing routes otherwise. -#[test] -fn announced_delivers_live_once_caught_up() { - let origin = id(moq_origin_create()); - - let empty_cb = Callback::new(); - let empty = id(unsafe { moq_origin_announced(origin, std::ptr::null(), Some(channel_callback), empty_cb.ptr) }); - let live = id(empty_cb.recv()); - let info = announce_info(live); - assert_eq!(info.kind, moq_announce_kind::MOQ_ANNOUNCE_KIND_LIVE); - assert_eq!(info.prefix_len, 0); - assert!(!info.has_captures); - assert_eq!(moq_origin_announced_free(live), 0); - assert_eq!(moq_origin_announced_cancel(empty), 0); - assert_eq!(empty_cb.recv_announce_terminal(), 0); - - let path = b"cam"; - let broadcast = publish_broadcast(origin, path); - let _ = request_broadcast(origin, path); - - let cb = Callback::new(); - let task = id(unsafe { moq_origin_announced(origin, std::ptr::null(), Some(channel_callback), cb.ptr) }); - let announced = id(cb.recv()); - let info = announce_info(announced); - assert_eq!(info.kind, moq_announce_kind::MOQ_ANNOUNCE_KIND_START); - let got = unsafe { std::slice::from_raw_parts(info.prefix.cast::(), info.prefix_len) }; - assert_eq!(got, path); - let live = id(cb.recv()); - assert_eq!(announce_info(live).kind, moq_announce_kind::MOQ_ANNOUNCE_KIND_LIVE); - assert_eq!(moq_origin_announced_free(announced), 0); - assert_eq!(moq_origin_announced_free(live), 0); - - assert_eq!(moq_origin_announced_cancel(task), 0); - assert_eq!(cb.recv_announce_terminal(), 0); - assert_eq!(moq_publish_close(broadcast), 0); - assert_eq!(moq_origin_close(origin), 0); -} - #[test] fn local_announce() { let origin = id(moq_origin_create()); @@ -2075,7 +2018,7 @@ fn local_announce() { assert_eq!(moq_origin_announced_free(announced_id), 0); assert_eq!(moq_origin_announced_cancel(announced_task), 0); - assert_eq!(cb.recv_announce_terminal(), 0, "announced close delivers terminal 0"); + assert_eq!(cb.recv_terminal(), 0, "announced close delivers terminal 0"); assert_eq!(moq_publish_close(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); } @@ -2110,7 +2053,7 @@ fn announced_filters_patterns_and_reports_captures() { assert_eq!(moq_origin_announced_free(announced_id), 0); assert_eq!(moq_origin_announced_cancel(announced_task), 0); - assert_eq!(cb.recv_announce_terminal(), 0); + assert_eq!(cb.recv_terminal(), 0); assert_eq!(moq_publish_close(audio), 0); assert_eq!(moq_publish_close(chat), 0); assert_eq!(moq_origin_close(origin), 0); @@ -2146,7 +2089,7 @@ fn announced_hides_dot_paths_unless_asked() { assert_eq!(moq_origin_announced_free(announced), 0); assert_eq!(moq_origin_announced_cancel(task), 0); // Later events may be queued ahead of the terminal; free them. - assert_eq!(cb.recv_announce_terminal(), 0); + assert_eq!(cb.recv_terminal(), 0); } assert_eq!(moq_publish_close(stats), 0); @@ -2183,7 +2126,7 @@ fn announced_deactivation() { let _ = request_broadcast(origin, path); assert_eq!(moq_origin_announced_cancel(announced_task), 0); - assert_eq!(cb.recv_announce_terminal(), 0, "announced close delivers terminal 0"); + assert_eq!(cb.recv_terminal(), 0, "announced close delivers terminal 0"); assert_eq!(moq_publish_close(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); } @@ -2206,7 +2149,7 @@ fn create_broadcast_is_unroutable_until_announced() { assert_eq!(moq_origin_announced_free(announced_id), 0); assert_eq!(moq_origin_announced_cancel(announced_task), 0); - assert_eq!(cb.recv_announce_terminal(), 0); + assert_eq!(cb.recv_terminal(), 0); assert_eq!(moq_publish_close(broadcast), 0); assert_eq!(moq_origin_close(origin), 0); } diff --git a/rs/moq-ffi/src/origin.rs b/rs/moq-ffi/src/origin.rs index 9b23722764..4ddbd3f202 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -206,9 +206,6 @@ pub enum MoqAnnounceEvent { Update { announce: MoqAnnounce }, /// No route covers the prefix any more. Carries its last advertised route. End { announce: MoqAnnounce }, - /// Every route live at subscribe time has been delivered; what follows is - /// live changes. Yielded once. - Live, } impl From for MoqAnnounceEvent { @@ -224,7 +221,6 @@ impl From for MoqAnnounceEvent { Event::End(announce) => Self::End { announce: announce.into(), }, - Event::Live => Self::Live, } } } diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index c0f5ee44be..6f6b1a751f 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -200,14 +200,11 @@ async fn next_event(announced: &MoqAnnounceConsumer) -> MoqAnnounceEvent { .expect("origin ended while waiting for an announce event") } -/// The next newly announced route, skipping the `Live` marker wherever it lands. +/// The next newly announced route. async fn next_announced(announced: &MoqAnnounceConsumer) -> MoqAnnounce { - loop { - match next_event(announced).await { - MoqAnnounceEvent::Live => continue, - MoqAnnounceEvent::Start { announce } => return announce, - other => panic!("expected an announcement, got {other:?}"), - } + match next_event(announced).await { + MoqAnnounceEvent::Start { announce } => announce, + other => panic!("expected an announcement, got {other:?}"), } } @@ -1535,7 +1532,6 @@ async fn create_broadcast_is_invisible_until_announced() { assert!(unroutable.is_err(), "an unannounced broadcast is unroutable"); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); - assert_eq!(next_event(&announced).await, MoqAnnounceEvent::Live); let pending = tokio::spawn(async move { announced.next().await }); let waiting = consumer.announced_broadcast("live".into()).unwrap(); let waited = tokio::spawn(async move { waiting.available().await }); @@ -1709,28 +1705,6 @@ async fn announced_filters_patterns_and_reports_captures() { chat.close().unwrap(); } -/// `Live` marks the end of the routes live at subscribe time: at once on an empty -/// origin, and after the existing routes otherwise, so an app can list and stop. -#[tokio::test] -async fn announced_yields_live_once_caught_up() { - let origin = MoqOriginProducer::new(MoqOriginConfig::default()); - let consumer = origin.consume(); - - let empty = consumer.announced(MoqAnnounceConfig::default()).unwrap(); - assert_eq!(next_event(&empty).await, MoqAnnounceEvent::Live); - - let _cam = create_announced(&origin, "cam"); - await_announced(&consumer, "cam").await; - - let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); - let first = next_event(&announced).await; - assert!( - matches!(&first, MoqAnnounceEvent::Start { announce } if announce.prefix == "cam"), - "{first:?}" - ); - assert_eq!(next_event(&announced).await, MoqAnnounceEvent::Live); -} - /// A `.`-named broadcast is listed only when the config opts in or the prefix names it. #[tokio::test] async fn announced_hides_dot_paths_unless_asked() { @@ -3348,12 +3322,9 @@ fn without_runtime() { .unwrap(); let announced = consumer.announced(MoqAnnounceConfig::default()).unwrap(); - let announcement = loop { - match pollster::block_on(announced.next()).unwrap().unwrap() { - MoqAnnounceEvent::Live => continue, - MoqAnnounceEvent::Start { announce } => break announce, - other => panic!("expected an announcement, got {other:?}"), - } + let announcement = match pollster::block_on(announced.next()).unwrap().unwrap() { + MoqAnnounceEvent::Start { announce } => announce, + other => panic!("expected an announcement, got {other:?}"), }; assert_eq!(announcement.prefix, "test"); let _bc = pollster::block_on(consumer.request_broadcast("test".into())).unwrap(); diff --git a/rs/moq-net/src/client.rs b/rs/moq-net/src/client.rs index 237b0bd620..0fa948cc44 100644 --- a/rs/moq-net/src/client.rs +++ b/rs/moq-net/src/client.rs @@ -749,37 +749,6 @@ mod tests { .expect("connect failed"); } - /// A peer that never delivers its announce count cannot stall the live - /// marker past its session: dropping the session lands the source. - #[tokio::test(start_paused = true)] - async fn a_dead_session_does_not_hold_the_live_marker() { - let gate = kio::Producer::new(true); - let transport = crate::lite::test_transport::SinkSession::gated_bi(gate.consume()) - .with_protocol(crate::version::ALPN_LITE_05); - - let origin = crate::origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let client = Client::new() - .with_versions([Version::Lite(lite::Version::Lite05)].into()) - .with_subscriber(origin.clone()); - let (session, driver) = client - .connect(tokio::time::Instant::now().into_std(), transport) - .await - .expect("connect failed"); - - let mut announced = origin.consume().announced(); - let mut next = std::pin::pin!(announced.next()); - assert!( - tokio::time::timeout(std::time::Duration::from_secs(5), next.as_mut()) - .await - .is_err(), - "live before the peer answered" - ); - - drop(driver); - drop(session); - assert!(matches!(next.await, Some(crate::announce::Event::Live))); - } - /// The client SETUP on the bidi control stream (the pre-draft-17 framing) carries the /// AUTHORITY next to the PATH. #[tokio::test(start_paused = true)] diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 856fca169b..d52c43181d 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -2232,8 +2232,7 @@ where /// The routes an announce cursor holds right now, without waiting for more. /// /// A route announced and retracted within the snapshot is left out, and a repeat - /// keeps its latest metadata, so each path appears once. The origin's live marker is - /// skipped: it says when the origin caught up, not what the peer should hear. + /// keeps its latest metadata, so each path appears once. fn snapshot(announced: &mut crate::announce::Consumer) -> Vec { let mut initial = std::collections::BTreeMap::new(); while let Some(event) = announced.try_next() { @@ -2244,7 +2243,6 @@ where crate::announce::Event::End(update) => { initial.remove(&update.prefix); } - crate::announce::Event::Live => {} } } initial.into_values().collect() @@ -2302,18 +2300,13 @@ where if let Poll::Ready(res) = target.poll_closed(&mut finished, self.version, &mut cx) { return Poll::Ready(NamespaceEvent::Closed(res)); } - // The origin's live marker means nothing to the peer here. - while let Poll::Ready(next) = announced.poll_next(waiter) { - match next { - Some(crate::announce::Event::Live) => continue, - Some(crate::announce::Event::Start(update) | crate::announce::Event::Update(update)) => { - return Poll::Ready(NamespaceEvent::Update(Some((update, true)))); + if let Poll::Ready(next) = announced.poll_next(waiter) { + return Poll::Ready(NamespaceEvent::Update(next.map(|event| match event { + crate::announce::Event::Start(update) | crate::announce::Event::Update(update) => { + (update, true) } - Some(crate::announce::Event::End(update)) => { - return Poll::Ready(NamespaceEvent::Update(Some((update, false)))); - } - None => return Poll::Ready(NamespaceEvent::Update(None)), - } + crate::announce::Event::End(update) => (update, false), + }))); } if retry.poll(waiter).is_ready() { return Poll::Ready(NamespaceEvent::Retry); diff --git a/rs/moq-net/src/ietf/session.rs b/rs/moq-net/src/ietf/session.rs index 03f5dc4153..bab483b931 100644 --- a/rs/moq-net/src/ietf/session.rs +++ b/rs/moq-net/src/ietf/session.rs @@ -256,7 +256,7 @@ where let mut pub_ns_run = std::pin::pin!(err_only(publisher.clone().run_publish_namespaces())); let mut sub_ns_run = std::pin::pin!(err_only(async { let mut prefixes = futures::stream::FuturesUnordered::new(); - for (prefix, replaying) in namespaces { + for prefix in namespaces { let mut sub_ns = sub_ns.clone(); let sub_ns_adapter = sub_ns_adapter.clone(); prefixes.push(async move { @@ -271,7 +271,7 @@ where } _ => Stream::open(&mut sub_ns_adapter.clone(), version).await?, }; - if let Err(err) = sub_ns.run_subscribe_namespace(stream, prefix, replaying).await { + if let Err(err) = sub_ns.run_subscribe_namespace(stream, prefix).await { // The peer breaking the protocol is fatal, and the driver // below turns this into the session close the draft wants. if is_protocol_violation(&err) { @@ -411,13 +411,13 @@ where let mut pub_ns_run = std::pin::pin!(err_only(publisher.clone().run_publish_namespaces())); let mut sub_ns_run = std::pin::pin!(err_only(async { let mut prefixes = futures::stream::FuturesUnordered::new(); - for (prefix, replaying) in namespaces { + for prefix in namespaces { let mut sub_ns = sub_ns.clone(); let sub_ns_session = sub_ns_session.clone(); prefixes.push(async move { let mut sub_ns_session = sub_ns_session; let stream = Stream::open(&mut sub_ns_session, version).await?; - if let Err(err) = sub_ns.run_subscribe_namespace(stream, prefix, replaying).await { + if let Err(err) = sub_ns.run_subscribe_namespace(stream, prefix).await { // The peer breaking the protocol is fatal, and the driver // below turns this into the session close the draft wants. if is_protocol_violation(&err) { diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index 44b6e5187f..0522e777cb 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -598,34 +598,8 @@ pub(super) struct Subscriber { /// /// Asked unconditionally: a peer with nothing to advertise answers with an empty set, /// which costs one stream. -/// -/// Each comes with the guard its stream holds until the peer's initial set has -/// landed. Taken when the session starts, before it is handed out, so no announce -/// cursor misses the replay. -pub(super) fn subscribe_prefixes(origin: &origin::Producer) -> Vec<(PathOwned, crate::model::Replaying)> { +pub(super) fn subscribe_prefixes(origin: &origin::Producer) -> Vec { crate::model::interest_prefixes(&origin.allowed()) - .into_iter() - .map(|prefix| (prefix.clone(), origin.replaying(prefix))) - .collect() -} - -/// How a SUBSCRIBE_NAMESPACE stream's initial set ends. -enum Landing { - /// MoQ Active Count: after this many more NAMESPACE messages. - Count(u64), - /// Once the stream goes quiet, which is all the base protocol offers. - Quiet(crate::model::Quiet), -} - -impl Landing { - /// Ready once the initial set has landed. - fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> { - match self { - Self::Count(0) => Poll::Ready(()), - Self::Count(_) => Poll::Pending, - Self::Quiet(quiet) => quiet.poll(waiter), - } - } } /// Resolve the subscription a data stream belongs to. @@ -866,7 +840,6 @@ where &mut self, mut stream: Stream, prefix: PathOwned, - replaying: crate::model::Replaying, ) -> Result<(), Error> { // A peer that sent GOAWAY told us to stop opening requests on this session, // announce-interest included (draft-19 sect 10.4). @@ -937,16 +910,13 @@ where tracing::debug!(%prefix, ?count, "subscribe_namespace ok"); - // MoQ Active Count says how many NAMESPACE messages come before we are caught up. - // Without it nothing on this wire marks the end, so the set has landed once the - // stream goes quiet. A count the negotiation did not promise, or a missing one it - // did, is the peer breaking the extension. - let landing = match (declared.active_count, count) { - (true, Some(count)) => Landing::Count(count), - (false, None) => Landing::Quiet(crate::model::Quiet::new(&self.runtime)), - _ => return Err(Error::ProtocolViolation), - }; - let mut landing = Some((replaying, landing)); + // MoQ Active Count says how many NAMESPACE messages make up the initial set; + // nothing here needs that boundary, so it is only checked. A count the + // negotiation did not promise, or a missing one it did, is the peer breaking + // the extension. + if declared.active_count != count.is_some() { + return Err(Error::ProtocolViolation); + } // The extension changes the NAMESPACE encoding, so we can't parse one until // the peer's SETUP says whether it negotiated. @@ -963,7 +933,7 @@ where // This is what moq-lite already does, where the equivalent map is a local whose // guards drop. let res = self - .run_namespace_entries(&mut stream, &prefix, &peer, &mut live, &mut landing) + .run_namespace_entries(&mut stream, &prefix, &peer, &mut live) .await; for path in live { let _ = self.stop_announce(path); @@ -982,31 +952,12 @@ where prefix: &PathOwned, peer: &cluster::Peer, live: &mut std::collections::HashSet, - // The replay guard and how it lands, until the initial set has. - landing: &mut Option<(crate::model::Replaying, Landing)>, ) -> Result<(), Error> { loop { - let next = { - let mut decode = std::pin::pin!(stream.reader.decode_maybe::()); - kio::wait(|waiter| { - // Land before decoding past the boundary, so no live update enters the - // origin ahead of the marker. - if let Some((_, how)) = landing - && how.poll(waiter).is_ready() - { - *landing = None; - } - waiter.poll_future(decode.as_mut()) - }) - .await - }; - let type_id: u64 = match next? { + let type_id: u64 = match stream.reader.decode_maybe().await? { Some(id) => id, None => break, // Stream closed }; - if let Some((_, Landing::Quiet(quiet))) = landing { - quiet.heard(); - } let size: u16 = stream.reader.decode().await?; let mut data = stream.reader.read_exact(size as usize).await?; @@ -1019,12 +970,6 @@ where if !data.is_empty() { return Err(Error::WrongSize); } - // Counted whether or not we keep it: the publisher cannot know we will - // drop a reflection. The guard only drops at the top of the loop, after - // this one is in the origin. - if let Some((_, Landing::Count(remaining))) = landing { - *remaining = remaining.saturating_sub(1); - } let path = prefix.join(&msg.suffix); let Some(advert) = self.route(msg.cluster.as_ref(), peer) else { // Looped back through us: forwarding it would extend the loop and @@ -4309,14 +4254,14 @@ mod tests { let mut namespaces = subscribe_prefixes(&subscriber.origin); assert_eq!( - namespaces.iter().map(|(prefix, _)| prefix.clone()).collect::>(), + namespaces, vec![crate::Path::new("cam").to_owned()], "one SUBSCRIBE_NAMESPACE per permitted prefix, relative to the root", ); - let (prefix, replaying) = namespaces.pop().unwrap(); + let prefix = namespaces.pop().unwrap(); let stream = Stream::open(&mut session.clone(), Version::Draft16).await.unwrap(); - let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, prefix, replaying)); + let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, prefix)); // Parks awaiting the peer's response; the request is already on the wire. assert!(futures::poll!(run.as_mut()).is_pending()); @@ -4387,10 +4332,10 @@ mod tests { Default::default(), ); - let (prefix, replaying) = subscribe_prefixes(&subscriber.origin).pop().expect("one prefix"); + let prefix = subscribe_prefixes(&subscriber.origin).pop().expect("one prefix"); let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap(); // Parks on the read after the scripted NAMESPACE is consumed. - let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, prefix, replaying)); + let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, prefix)); for _ in 0..100 { // The result is deliberately ignored: a regressed mount lands out of scope // and errors here, which the assertions below name far better than a poll @@ -4457,7 +4402,7 @@ mod tests { let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap(); let res = subscriber - .run_subscribe_namespace(stream, crate::Path::new("").to_owned(), subscriber.origin.replaying("")) + .run_subscribe_namespace(stream, crate::Path::new("").to_owned()) .await; assert!( matches!(res, Err(Error::ProtocolViolation)), @@ -5616,7 +5561,7 @@ mod tests { let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap(); subscriber - .run_subscribe_namespace(stream, crate::Path::new("").to_owned(), subscriber.origin.replaying("")) + .run_subscribe_namespace(stream, crate::Path::new("").to_owned()) .await .expect("a clean FIN is not an error"); settle().await; @@ -5692,9 +5637,7 @@ mod tests { ); let stream = Stream::open(&mut session.clone(), VERSION).await.unwrap(); - let replaying = subscriber.origin.replaying(""); - let mut run = - std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::new("").to_owned(), replaying)); + let mut run = std::pin::pin!(subscriber.run_subscribe_namespace(stream, crate::Path::new("").to_owned())); for _ in 0..100 { assert!( futures::poll!(run.as_mut()).is_pending(), diff --git a/rs/moq-net/src/lite/publisher.rs b/rs/moq-net/src/lite/publisher.rs index 861b82abf1..f3d50a5dfd 100644 --- a/rs/moq-net/src/lite/publisher.rs +++ b/rs/moq-net/src/lite/publisher.rs @@ -760,9 +760,6 @@ impl AnnounceRun { let (update, active) = match event { announce::Event::Start(update) | announce::Event::Update(update) => (update, true), announce::Event::End(update) => (update, false), - // The marker only says the origin caught up; the peer learns the - // initial set's end from the version's own framing. - announce::Event::Live => continue, }; let absolute = origin.absolute(&update.prefix); let suffix = update.prefix; @@ -798,9 +795,6 @@ impl AnnounceRun { let (update, active) = match event { announce::Event::Start(update) | announce::Event::Update(update) => (update, true), announce::Event::End(update) => (update, false), - // The marker only says the origin caught up; the peer learns the - // initial set's end from the version's own framing. - announce::Event::Live => continue, }; let absolute = origin.absolute(&update.prefix); let suffix = update.prefix; @@ -897,7 +891,6 @@ impl AnnounceRun { let (update, active) = match next { Some(announce::Event::Start(update) | announce::Event::Update(update)) => (update, true), Some(announce::Event::End(update)) => (update, false), - Some(announce::Event::Live) => continue, None => { // The buffer is empty (flushed at the loop top), so FIN now and // wait for the acknowledgement. diff --git a/rs/moq-net/src/lite/subscriber.rs b/rs/moq-net/src/lite/subscriber.rs index ea801d25c8..da74e04136 100644 --- a/rs/moq-net/src/lite/subscriber.rs +++ b/rs/moq-net/src/lite/subscriber.rs @@ -1194,20 +1194,6 @@ struct AnnouncePrefix { subscriber: Subscriber, prefix: PathOwned, state: PrefixState, - /// Held from the request until the peer's initial set has landed in the - /// origin, so announce cursors know when they have caught up. Taken at - /// construction, before the session is handed out, so no cursor misses it. - replaying: Option, -} - -/// How the peer's initial set ends on this version. -enum Landing { - /// Lite05+: after this many more announces, the count in ANNOUNCE_OK. - Count(u64), - /// Lite03/04 carry no boundary: once the stream goes quiet. - Quiet(crate::model::Quiet), - /// It has landed. Lite01/02 land it in ANNOUNCE_INIT, before the run. - Landed, } enum PrefixState { @@ -1221,8 +1207,6 @@ enum PrefixState { Cost { stream: Stream, responder_origin: Option, - /// Lite05+: the initial set's size, from ANNOUNCE_OK. - active: Option, }, /// Lite01/02: reading the ANNOUNCE_INIT set. ReadInit { stream: Stream, run: PrefixRun }, @@ -1243,32 +1227,17 @@ struct PrefixRun { // path, and lite-07 bases name it too. Tracked even for announces we drop // locally (reflected loops), since the sender doesn't know we dropped them. decoder: lite::AnnounceDecoder, - landing: Landing, } impl AnnouncePrefix { fn new(subscriber: Subscriber, prefix: PathOwned) -> Self { Self { - replaying: Some(subscriber.origin.replaying(&prefix)), subscriber, prefix, state: PrefixState::Open, } } - /// Drop the guard once the initial set has landed. - fn poll_landing(replaying: &mut Option, landing: &mut Landing, waiter: &kio::Waiter) { - let landed = match landing { - Landing::Count(remaining) => *remaining == 0, - Landing::Quiet(quiet) => quiet.poll(waiter).is_ready(), - Landing::Landed => return, - }; - if landed { - *landing = Landing::Landed; - *replaying = None; - } - } - fn poll(&mut self, waiter: &kio::Waiter) -> Poll> { let mut cx = waiter.context(); loop { @@ -1307,14 +1276,14 @@ impl AnnouncePrefix { false => PrefixState::Cost { stream, responder_origin: None, - active: None, }, }; } PrefixState::ReadOk { stream } => { // Lite05+: the publisher reports its own origin id, which we stamp onto // every received Announce's hop chain since it no longer does so itself. - // Its `active` count marks where the initial set ends. + // Its `active` count marks where the initial set ends; nothing here needs + // that boundary, so it is read and dropped. let ok = ready!(stream.reader.poll_decode::(&mut cx))?; // A peer may legally report id 0 (no identity). Keep it: the assigned // identity stays on `via` and is never forwarded as a hop. @@ -1325,7 +1294,6 @@ impl AnnouncePrefix { self.state = PrefixState::Cost { stream, responder_origin: Some(origin), - active: Some(ok.active), }; } PrefixState::Cost { .. } => { @@ -1333,23 +1301,16 @@ impl AnnouncePrefix { let PrefixState::Cost { stream, responder_origin, - active, } = std::mem::replace(&mut self.state, PrefixState::Open) else { unreachable!() }; - let landing = match (active, self.subscriber.version) { - (Some(active), _) => Landing::Count(active), - (None, Version::Lite01 | Version::Lite02) => Landing::Landed, - (None, _) => Landing::Quiet(crate::model::Quiet::new(&self.subscriber.runtime)), - }; let run = PrefixRun { responder_origin, link_cost, announced: Announced::default(), decoder: lite::AnnounceDecoder::default(), - landing, }; // Lite01/02 send the initial set as one ANNOUNCE_INIT message, so they @@ -1380,7 +1341,6 @@ impl AnnouncePrefix { else { unreachable!() }; - self.replaying = None; self.state = PrefixState::Run { stream, run }; } PrefixState::Run { stream, run } => { @@ -1392,19 +1352,9 @@ impl AnnouncePrefix { run.announced.drain(); } loop { - // Land before decoding past the boundary, so no live update - // enters the origin ahead of the marker. - Self::poll_landing(&mut self.replaying, &mut run.landing, waiter); match stream.reader.poll_decode_maybe::(&mut cx) { Poll::Ready(Ok(Some(announce))) => { - // The count is of ANNOUNCE_STARTs, not every message. - let start = matches!(announce, lite::AnnounceBroadcast::Active { .. }); self.subscriber.handle_announce(&self.prefix, announce, run)?; - match &mut run.landing { - Landing::Count(remaining) if start => *remaining = remaining.saturating_sub(1), - Landing::Quiet(quiet) => quiet.heard(), - Landing::Count(_) | Landing::Landed => {} - } } Poll::Ready(Ok(None)) => { // The publisher FINed: it has nothing (more) to announce for this @@ -3106,54 +3056,6 @@ mod tests { announced.withdraw(&path); cursor.assert_next_ended("room/host"); } - - /// ANNOUNCE_OK's count lands the initial set at exactly that many - /// ANNOUNCE_STARTs: an unknown message does not count toward it, and a live - /// update buffered right behind the set does not enter the origin before the - /// marker. - #[tokio::test(start_paused = true)] - async fn the_count_lands_at_the_last_initial_start() { - const VERSION: Version = Version::Lite06; - let start = |suffix| lite::AnnounceBroadcast::Active { - suffix: lite::PathRef::literal(Path::new(suffix)), - hops: lite::HopsRef::literal(crate::Hops::new()), - cost: crate::origin::Cost::default(), - }; - let mut script = Vec::new(); - lite::AnnounceOk { - origin: crate::Hop::new(9).unwrap(), - active: 1, - } - .encode(&mut script, VERSION) - .unwrap(); - // An unknown announce type with an empty body, which decodes as `Skipped`. - script.extend([0x3f, 0x00]); - start("a").encode(&mut script, VERSION).unwrap(); - start("b").encode(&mut script, VERSION).unwrap(); - - let origin = origin::Config::new(crate::Hop::new(1).unwrap()).produce(); - let consumer = origin.consume(); - let subscriber = Subscriber::new(SubscriberConfig { - runtime: crate::time::Clock::tokio(), - session: crate::lite::test_transport::ScriptedSession::new(script), - origin, - recv_bandwidth: None, - version: VERSION, - peer_setup: Default::default(), - cost: Some(1), - peer_hop: None, - going_away: Default::default(), - }); - let mut prefix = AnnouncePrefix::new(subscriber, Path::new("").to_owned()); - let mut cursor = consumer.announced(); - - let mut run = std::pin::pin!(kio::wait(|waiter| prefix.poll(waiter))); - assert!(futures::poll!(run.as_mut()).is_pending()); - - cursor.assert_next_active("a"); - cursor.assert_next_live(); - cursor.assert_next_active("b"); - } } /// The four wire fields a subscription's half-open range encodes to. diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index a2e6a3b414..d0300712d8 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -57,9 +57,6 @@ pub use origin_impl::{Consume, Hop, Hops, InvalidHop}; // The announce-interest prefixes a scope needs on a prefix-shaped wire. pub(crate) use origin_impl::interest_prefixes; -// Held by a session until the peer's initial announce set has landed. -pub(crate) use origin_impl::{Quiet, Replaying}; - // The advertise-only route guard, for tests shaping the route table. #[cfg(test)] pub(crate) use origin_impl::AnnounceProducer; diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index b8f3c848c3..714856925d 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -679,54 +679,9 @@ struct OriginConsumerState { /// Set by the origin's teardown: the cursor drains `pending`, then reports /// the end instead of parking forever on a table that can never fire again. ended: bool, - /// Progress toward the one [`AnnounceEvent::Live`] marker. - live: LiveState, -} - -/// Where a cursor stands on delivering its initial set. -enum LiveState { - /// This many remote sources pending at subscribe time are still replaying. - Waiting(usize), - /// Every source has landed: the marker follows once these prefixes, pending - /// when the last one landed, have been delivered (or cancelled). - Owed(BTreeSet), - /// The marker was delivered. - Done, -} - -impl Default for LiveState { - fn default() -> Self { - Self::Waiting(0) - } } impl OriginConsumerState { - /// One source the cursor was waiting on has landed its initial set. - fn landed(&mut self) { - if let LiveState::Waiting(count) = &mut self.live { - *count = count.saturating_sub(1); - if *count == 0 { - self.live = LiveState::Owed(self.pending.keys().cloned().collect()); - } - } - } - - /// `prefix` left the pending set (delivered or cancelled), so the marker no - /// longer waits on it. A prefix still pending (the announce trailing a - /// retraction) stays owed. - fn settled(&mut self, prefix: &PathOwned) { - if let LiveState::Owed(owed) = &mut self.live - && !self.pending.contains_key(prefix) - { - owed.remove(prefix); - } - } - - /// Whether the marker is due now. - fn live_due(&self) -> bool { - matches!(&self.live, LiveState::Owed(owed) if owed.is_empty()) - } - fn apply_announce(&mut self, prefix: PathOwned, meta: RouteMeta, captures: Option>) { let meta = (meta, captures); let new = match self.pending.remove(&prefix) { @@ -745,7 +700,7 @@ impl OriginConsumerState { match self.pending.remove(&prefix) { // The pending announce was never delivered and neither was any earlier // one, so the pair cancels entirely. - Some(PendingUpdate::Announce(_)) if !self.delivered.contains(&prefix) => self.settled(&prefix), + Some(PendingUpdate::Announce(_)) if !self.delivered.contains(&prefix) => {} // Either nothing is pending or the pending announce was a metadata // update on a delivered route; the consumer still owes a retraction. None | Some(PendingUpdate::Announce(_) | PendingUpdate::Unannounce(_)) => { @@ -799,14 +754,8 @@ impl OriginConsumerState { Err(wake) } - /// Deliver the [`AnnounceEvent::Live`] marker, once it is due. - fn take_live(&mut self) -> AnnounceEvent { - self.live = LiveState::Done; - AnnounceEvent::Live - } - /// Take the pending update at `prefix` for delivery. - fn take_prefix(&mut self, prefix: PathOwned) -> Option { + fn take_prefix(&mut self, prefix: PathOwned) -> AnnounceEvent { let ((meta, captures), kind): (_, fn(Announce) -> AnnounceEvent) = match self.pending.remove(&prefix).unwrap() { PendingUpdate::Announce(meta) => { // The consumer has seen this prefix before, so it is a metadata update. @@ -828,8 +777,7 @@ impl OriginConsumerState { (old, AnnounceEvent::End) } }; - self.settled(&prefix); - Some(kind(Announce { + kind(Announce { prefix, captures, route: Route { @@ -838,7 +786,7 @@ impl OriginConsumerState { via: Hop::UNKNOWN, source: meta.2, }, - })) + }) } } @@ -1352,10 +1300,6 @@ pub enum AnnounceEvent { Update(Announce), /// No route covers the prefix any more. Carries its last advertised route. End(Announce), - /// Every route live at subscribe time has been delivered; what follows is - /// live changes. Yielded once, and only after each remote source feeding the - /// origin at subscribe time has landed its initial set. - Live, } /// A route over a prefix, delivered by [`AnnounceConsumer`] inside an [`AnnounceEvent`]. @@ -1644,35 +1588,6 @@ impl Producer { ) } - /// Register a remote announce source replaying its initial set under - /// `prefix`, relative to this producer's root. - /// - /// Every announce cursor overlapping that prefix and registered while the - /// guard lives withholds its [`AnnounceEvent::Live`] marker until the guard - /// drops, so a session drops it once the peer's initial routes are in the - /// table (or the session dies). - pub(crate) fn replaying(&self, prefix: impl AsPath) -> Replaying { - // A subtree too complex to intersect waits on the whole scope instead. - let scope = Pattern::subtree(self.absolute(prefix).as_str()) - .ok() - .and_then(|subtree| self.scope.allowed.intersect(&subtree.into()).ok()) - .unwrap_or_else(|| self.scope.allowed.clone()); - let mut state = self.shared.lock(); - let id = state.next_replaying; - state.next_replaying += 1; - state.replaying.insert( - id, - ReplayingSource { - scope, - waiting: Vec::new(), - }, - ); - Replaying { - shared: self.shared.clone(), - id, - } - } - /// Advertise a route over `prefix` and serve the requests beneath it. /// /// A route is always a prefix: it claims `prefix` and every path beneath it @@ -3176,11 +3091,6 @@ struct OriginState { // a front dies with its watcher and a later request re-creates it. fronts: WeakCache, - // Remote announce sources still replaying their initial set, keyed by - // [`Replaying`] id. Empty outside a session's first round trip. - replaying: HashMap, - next_replaying: u64, - // Peers that withdrew a prefix while routes there still passed through them, // which are stale. See [`Dynamic::withdrawn`]. withdrawn: HashMap>, @@ -3190,14 +3100,6 @@ struct OriginState { closed: bool, } -/// A remote announce source still replaying its initial set. -struct ReplayingSource { - /// The absolute patterns the source may announce under. - scope: Patterns, - /// The cursors registered while it replayed, each owed its landing. - waiting: Vec, -} - impl OriginState { fn new(update_hold: Duration) -> Self { Self { @@ -3208,8 +3110,6 @@ impl OriginState { cursors: HashMap::new(), fronts: WeakCache::default(), withdrawn: HashMap::new(), - replaying: HashMap::new(), - next_replaying: 0, closed: false, } } @@ -3381,19 +3281,6 @@ impl OriginState { for p in &presented { Self::sync_cursor(&self.routes, &mut cursor, p); } - // The marker also waits on every source still replaying into this - // cursor's scope; with none, it follows the replay above. - let mut waiting = 0; - for source in self.replaying.values_mut() { - if source.scope.iter().any(|pattern| cursor.allowed.overlaps(pattern)) { - source.waiting.push(id); - waiting += 1; - } - } - if let Ok(mut state) = cursor.state.write() { - state.live = LiveState::Waiting(waiting + 1); - state.landed(); - } for head in &cursor.heads { self.routes.add_cursor(head, id); } @@ -3437,67 +3324,6 @@ impl OriginState { } } -/// A remote announce source still replaying its initial set, from -/// [`Producer::replaying`]. Drop it once the set has landed. -pub(crate) struct Replaying { - shared: kio::Shared, - id: u64, -} - -impl Drop for Replaying { - fn drop(&mut self) { - let mut state = self.shared.lock(); - let Some(source) = state.replaying.remove(&self.id) else { - return; - }; - for id in source.waiting { - // A cursor dropped since registering is simply gone. - if let Some(cursor) = state.cursors.get(&id) - && let Ok(mut cursor) = cursor.state.write() - { - cursor.landed(); - } - } - } -} - -/// When a [`Replaying`] source has landed, for wires that never say where the -/// initial set ends (lite-03/04, moq-transport without MoQ Active Count): once -/// its stream goes quiet. -/// -/// A peer writes its whole set back to back, so the first announcement gets a -/// round trip's grace and each one after it only has to beat its siblings. -pub(crate) struct Quiet { - deadline: crate::runtime::Deadline, - clock: Clock, -} - -impl Quiet { - /// How long the stream may stay silent before its first announcement. - const FIRST: Duration = Duration::from_millis(500); - /// How long the stream may stay silent between announcements. - const GAP: Duration = Duration::from_millis(30); - - /// Start counting from now. - pub(crate) fn new(clock: &Clock) -> Self { - Self { - deadline: crate::runtime::Deadline::after(clock, Self::FIRST), - clock: clock.clone(), - } - } - - /// An announcement arrived: the set is still landing. - pub(crate) fn heard(&mut self) { - let now = crate::runtime::Timers::now(&self.clock); - self.deadline.set(now.checked_add(Self::GAP)); - } - - /// Ready once the stream has gone quiet. - pub(crate) fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> { - self.deadline.poll(waiter) - } -} - /// One-shot result of a dynamic broadcast request. /// /// Stays `None` until a handler [`accept`](Request::accept)s (yielding the served @@ -4469,17 +4295,14 @@ impl AnnounceConsumer { AnnounceEvent::End(announce) => { self.guards.remove(&announce.prefix); } - AnnounceEvent::Live => {} } event } /// Returns the next route announcement, update, or retraction, its prefix - /// relative to this cursor's root, or the one [`AnnounceEvent::Live`] marker. + /// relative to this cursor's root. /// - /// The routes live at subscribe time come first, then the marker, then live - /// changes, so a caller listing what is live stops at the marker. A - /// retraction is only delivered for a previously announced prefix, and a + /// A retraction is only delivered for a previously announced prefix, and a /// repeated announcement for the same prefix is a metadata update. Returns /// None if the cursor is closed. The consumer is also a [`futures::Stream`] /// of the same events. @@ -4497,15 +4320,9 @@ impl AnnounceConsumer { let now = self.timers.try_now(); let hold = self.update_hold; let held = &mut self.held; - let mut live = false; let mut ready = None; let mut wake = None; let event = match self.state.poll(waiter, |state| { - // The marker goes out as soon as it is due, ahead of any update. - live = state.live_due(); - if live { - return Poll::Ready(()); - } if state.pending.is_empty() { return match state.ended { true => Poll::Ready(()), @@ -4524,8 +4341,7 @@ impl AnnounceConsumer { } }) { Poll::Ready(Ok(mut state)) => match ready { - _ if live => Some(state.take_live()), - Some(prefix) => state.take_prefix(prefix), + Some(prefix) => Some(state.take_prefix(prefix)), None => { // Ended by the origin's teardown, pending updates already // drained; close the channel so every closure signal agrees. @@ -4544,11 +4360,9 @@ impl AnnounceConsumer { let Some(event) = event else { return Poll::Ready(None); }; - if let AnnounceEvent::Start(announce) | AnnounceEvent::Update(announce) | AnnounceEvent::End(announce) = - &event - { - self.held.remove(&announce.prefix); - } + let (AnnounceEvent::Start(announce) | AnnounceEvent::Update(announce) | AnnounceEvent::End(announce)) = + &event; + self.held.remove(&announce.prefix); return Poll::Ready(Some(self.hand_out(event))); } } @@ -4560,11 +4374,6 @@ impl AnnounceConsumer { pub fn try_next(&mut self) -> Option { let now = self.timers.try_now(); let mut state = self.state.write().ok()?; - if state.live_due() { - let event = state.take_live(); - drop(state); - return Some(self.hand_out(event)); - } let prefix = match state.scan(&mut self.held, now, self.update_hold) { Ok(prefix) => prefix, Err(wake) => { @@ -4576,7 +4385,7 @@ impl AnnounceConsumer { } }; self.held.remove(&prefix); - let event = state.take_prefix(prefix)?; + let event = state.take_prefix(prefix); drop(state); Some(self.hand_out(event)) } @@ -4627,28 +4436,6 @@ use futures::FutureExt; #[cfg(test)] #[allow(missing_docs)] // test-only assertion helpers impl AnnounceConsumer { - /// The next route event without blocking, skipping the - /// [`AnnounceEvent::Live`] marker: these helpers pin routes, and the marker - /// has its own tests. - fn next_route_now(&mut self) -> Option> { - loop { - match self.next().now_or_never()? { - Some(AnnounceEvent::Live) => continue, - event => return Some(event), - } - } - } - - /// The `try_next` counterpart of [`Self::next_route_now`]. - fn try_next_route(&mut self) -> Option { - loop { - match self.try_next()? { - AnnounceEvent::Live => continue, - event => return Some(event), - } - } - } - /// The route of an active event at `expected`. fn active(event: AnnounceEvent, expected: &Path) -> Route { match event { @@ -4662,37 +4449,29 @@ impl AnnounceConsumer { /// The next update must be an active route at `expected`; returns it. pub fn assert_next_active(&mut self, expected: impl AsPath) -> Route { - let event = self.next_route_now().expect("next blocked").expect("no next"); + let event = self.next().now_or_never().expect("next blocked").expect("no next"); Self::active(event, &expected.as_path()) } /// The `try_next` counterpart of [`Self::assert_next_active`]. pub fn assert_try_next_active(&mut self, expected: impl AsPath) -> Route { - let event = self.try_next_route().expect("no next"); + let event = self.try_next().expect("no next"); Self::active(event, &expected.as_path()) } /// The next update must be a retraction at `expected`. pub fn assert_next_ended(&mut self, expected: impl AsPath) { - match self.next_route_now().expect("next blocked").expect("no next") { + match self.next().now_or_never().expect("next blocked").expect("no next") { AnnounceEvent::End(announce) => assert_eq!(announce.prefix, expected.as_path(), "wrong prefix"), other => panic!("should be a retraction: got {other:?}"), } } pub fn assert_next_wait(&mut self) { - if let Some(event) = self.next_route_now() { + if let Some(event) = self.next().now_or_never() { panic!("next should block: got {event:?}"); } } - - /// The next event must be the [`AnnounceEvent::Live`] marker. - pub fn assert_next_live(&mut self) { - match self.next().now_or_never() { - Some(Some(AnnounceEvent::Live)) => {} - other => panic!("expected the live marker: got {other:?}"), - } - } } /// Test-only construction shorthand: build the producer and spawn its driver on @@ -5176,87 +4955,14 @@ mod tests { } #[tokio::test] - async fn an_empty_origin_is_live_at_once() { - let producer = origin(1).produce(); - let mut announced = producer.consume().announced(); - announced.assert_next_live(); - announced.assert_next_wait(); - - // Later routes are live changes; the marker never repeats. - let _alice = producer.announce("alice", Route::default()).unwrap(); - announced.assert_next_active("alice"); - assert!(announced.next().now_or_never().is_none()); - } - - #[tokio::test] - async fn live_follows_the_replayed_routes() { - let producer = origin(1).produce(); - let _b = producer.announce("b", Route::default()).unwrap(); - let _c = producer.announce("c", Route::default()).unwrap(); - let mut announced = producer.consume().announced(); - - // A route arriving before the replay drains may come either side of the - // marker, but never pushes it past the replay. - let _a = producer.announce("a", Route::default()).unwrap(); - let mut seen = Vec::new(); - loop { - match announced.next().now_or_never().expect("blocked").expect("closed") { - AnnounceEvent::Live => break, - AnnounceEvent::Start(announce) => seen.push(announce.prefix.to_string()), - other => panic!("only announcements before the marker: got {other:?}"), - } - } - assert!( - seen.contains(&"b".to_string()) && seen.contains(&"c".to_string()), - "{seen:?}" - ); - } - - #[tokio::test] - async fn a_replayed_route_retracted_before_delivery_does_not_hold_live() { + async fn a_replayed_route_retracted_before_delivery_cancels() { let producer = origin(1).produce(); let alice = producer.announce("alice", Route::default()).unwrap(); let mut announced = producer.consume().announced(); drop(alice); - announced.assert_next_live(); announced.assert_next_wait(); } - #[tokio::test] - async fn a_replaying_source_withholds_live_until_it_lands() { - let producer = origin(1).produce(); - let room = producer.scope("", &scopes(&["room"])).unwrap(); - let replaying = room.replaying("room/alice"); - - let mut announced = producer.consume().announced(); - // Out of the source's scope: nothing to wait for. - let mut elsewhere = producer.consume().scope("", &scopes(&["other"])).unwrap().announced(); - elsewhere.assert_next_live(); - // In its scope but beside the prefix it replays: nothing to wait for either. - let mut bob = producer - .consume() - .scope("", &scopes(&["room/bob"])) - .unwrap() - .announced(); - bob.assert_next_live(); - - // Routes the source lands arrive before the marker. - let _alice = room.announce("room/alice", Route::default()).unwrap(); - announced.assert_next_active("room/alice"); - assert!( - announced.next().now_or_never().is_none(), - "live before the source landed" - ); - - drop(replaying); - announced.assert_next_live(); - - // A cursor registered after the source landed does not wait on it. - let mut later = producer.consume().announced(); - later.assert_next_active("room/alice"); - later.assert_next_live(); - } - #[tokio::test] async fn broadcast_announces_its_own_path() { let producer = origin(1).produce(); @@ -5655,19 +5361,19 @@ mod tests { .unwrap(); let mut announced = consumer.announced(); - let Some(Some(AnnounceEvent::Start(first))) = announced.next_route_now() else { + let Some(Some(AnnounceEvent::Start(first))) = announced.next().now_or_never() else { panic!("expected the announcement"); }; assert_eq!(first.prefix.as_str(), ""); assert_eq!(first.captures, Some(Vec::new())); drop(exact); - let Some(Some(AnnounceEvent::End(retracted))) = announced.next_route_now() else { + let Some(Some(AnnounceEvent::End(retracted))) = announced.next().now_or_never() else { panic!("expected the retraction"); }; assert_eq!(retracted.prefix.as_str(), ""); assert_eq!(retracted.captures, Some(Vec::new())); - let Some(Some(AnnounceEvent::Start(replacement))) = announced.next_route_now() else { + let Some(Some(AnnounceEvent::Start(replacement))) = announced.next().now_or_never() else { panic!("expected the replacement"); }; assert_eq!(replacement.prefix.as_str(), ""); @@ -6243,7 +5949,7 @@ mod tests { let second = producer.dynamic("live", Route::default().with_cost(1)).unwrap(); let mut announced = producer.consume().announced(); - let Some(Some(AnnounceEvent::Start(update))) = announced.next_route_now() else { + let Some(Some(AnnounceEvent::Start(update))) = announced.next().now_or_never() else { panic!("expected the announcement"); }; assert_eq!(update.prefix.as_str(), "live"); @@ -6251,7 +5957,7 @@ mod tests { announced.assert_next_wait(); drop(second); - let Some(Some(AnnounceEvent::Update(update))) = announced.next_route_now() else { + let Some(Some(AnnounceEvent::Update(update))) = announced.next().now_or_never() else { panic!("expected the reprice"); }; assert_eq!(update.prefix.as_str(), "live"); @@ -6444,7 +6150,6 @@ mod tests { panic!("expected the replayed route"); }; assert_eq!(update.prefix.as_str(), "live"); - assert!(matches!(next(), Some(Some(AnnounceEvent::Live)))); assert!(next().is_none()); drop(server); assert!(matches!(next(), Some(Some(AnnounceEvent::End(_))))); @@ -6661,11 +6366,11 @@ mod tests { let _far = peer.dynamic("room", route(&[9, 3, 4], 4)).unwrap(); drop(near); - assert!(announced.try_next_route().is_none(), "the update is held"); + assert!(announced.try_next().is_none(), "the update is held"); step(Duration::from_millis(1)).await; - assert!(announced.try_next_route().is_none(), "the update is still held"); + assert!(announced.try_next().is_none(), "the update is still held"); step(DEFAULT_UPDATE_HOLD).await; - match announced.try_next_route().expect("the hold passed") { + match announced.try_next().expect("the hold passed") { AnnounceEvent::Update(update) => assert_eq!(update.route.hops, hops(&[9, 3, 4])), other => panic!("should be an update: got {other:?}"), } @@ -6691,13 +6396,7 @@ mod tests { let start = tokio::time::Instant::now(); drop(near); drop(far); - let update = loop { - match announced.next().await.unwrap() { - AnnounceEvent::Live => continue, - event => break event, - } - }; - match update { + match announced.next().await.unwrap() { AnnounceEvent::Update(update) => assert_eq!(update.route.hops, hops(&[9, 5, 6, 7])), other => panic!("should be an update: got {other:?}"), } @@ -8655,7 +8354,7 @@ mod tests { let alice = producer.create_broadcast("room/alice/chat").unwrap(); alice.announce(Route::default()).unwrap(); - let Some(AnnounceEvent::Start(update)) = announced.try_next_route() else { + let Some(AnnounceEvent::Start(update)) = announced.try_next() else { panic!("expected alice's chat"); }; assert_eq!(update.prefix.as_str(), "room/alice/chat"); @@ -8666,7 +8365,7 @@ mod tests { announced.assert_next_wait(); let broad = producer.announce("room", Route::default()).unwrap(); - let Some(AnnounceEvent::Start(update)) = announced.try_next_route() else { + let Some(AnnounceEvent::Start(update)) = announced.try_next() else { panic!("expected the overlapping broad route"); }; assert_eq!(update.prefix.as_str(), "room"); @@ -8685,7 +8384,7 @@ mod tests { local.announce(Route::default()).unwrap(); let mut announced = producer.consume().announced(); - let Some(AnnounceEvent::Start(update)) = announced.try_next_route() else { + let Some(AnnounceEvent::Start(update)) = announced.try_next() else { panic!("expected one winning route"); }; assert_eq!(update.prefix.as_str(), "room/alice"); diff --git a/rs/moq-net/tests/announce_covering_route.rs b/rs/moq-net/tests/announce_covering_route.rs index cfd1669f09..31697b1e0c 100644 --- a/rs/moq-net/tests/announce_covering_route.rs +++ b/rs/moq-net/tests/announce_covering_route.rs @@ -24,8 +24,7 @@ fn produce_origin(hop: u64) -> origin::Producer { producer } -/// Drain every event the cursor has pending, as `kind prefix` lines, skipping the -/// live marker. +/// Drain every event the cursor has pending, as `kind prefix` lines. fn drain(announced: &mut moq_net::announce::Consumer) -> Vec { use moq_net::announce::Event; let mut seen = Vec::new(); @@ -34,7 +33,6 @@ fn drain(announced: &mut moq_net::announce::Consumer) -> Vec { Event::Start(announce) => ("Start", announce), Event::Update(announce) => ("Update", announce), Event::End(announce) => ("End", announce), - Event::Live => continue, }; seen.push(format!("{kind} {}", announce.prefix)); } diff --git a/rs/moq-net/tests/announce_to_serve.rs b/rs/moq-net/tests/announce_to_serve.rs index 224aad79d4..3820ce4e3e 100644 --- a/rs/moq-net/tests/announce_to_serve.rs +++ b/rs/moq-net/tests/announce_to_serve.rs @@ -45,7 +45,6 @@ fn drain(announced: &mut moq_net::announce::Consumer) -> Vec { Event::Start(announce) => ("Start", announce), Event::Update(announce) => ("Update", announce), Event::End(announce) => ("End", announce), - Event::Live => continue, }; seen.push(format!("{kind} {}", announce.prefix)); } diff --git a/rs/moq-net/tests/caught_up.rs b/rs/moq-net/tests/caught_up.rs deleted file mode 100644 index 9f597529a5..0000000000 --- a/rs/moq-net/tests/caught_up.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! An announce cursor on a session's origin yields `Live` once the peer's -//! initial set has landed, on every version. - -mod support; - -use std::time::Duration; - -use moq_net::{Hop, Version, announce, origin}; -use support::harness::{MockConnectOptions, connect_mock}; - -fn produce_origin(hop: Hop) -> origin::Producer { - let (producer, driver) = origin::Producer::new(origin::Config::new(hop)); - tokio::spawn(support::harness::run(driver)); - producer -} - -/// Versions whose wire says where the initial set ends: ANNOUNCE_INIT, ANNOUNCE_OK's -/// count, or REQUEST_OK's (MoQ Active Count, negotiated from draft-16). -const COUNTED: &[&str] = &[ - "moq-lite-01", - "moq-lite-02", - "moq-lite-05", - "moq-lite-06", - "moq-transport-16", - "moq-transport-19", -]; - -/// Versions that land the initial set once the stream goes quiet instead. -const QUIET: &[&str] = &["moq-lite-03", "moq-lite-04", "moq-transport-14", "moq-transport-15"]; - -/// The shortest a quiet stream can take to land: the gap after its last announcement. -const QUIET_GAP: Duration = Duration::from_millis(30); - -/// Connect a subscriber to a peer publishing `paths`, then read its cursor up to -/// the marker, returning the paths delivered before it and how long the marker took -/// after the session was up. -async fn caught_up(version: &str, paths: &[&str]) -> (Vec, Duration) { - let version: Version = version.parse().unwrap(); - let published = produce_origin(Hop::new(1).unwrap()); - let broadcasts: Vec<_> = paths - .iter() - .map(|path| { - let broadcast = published.create_broadcast(*path).unwrap(); - broadcast.announce(Default::default()).unwrap(); - broadcast - }) - .collect(); - - let subscribed = produce_origin(Hop::new(2).unwrap()); - let mut options = MockConnectOptions::new(version); - options.server_publish = Some(published.consume()); - options.client_subscribe = Some(subscribed.clone()); - let _pair = connect_mock(options).await; - let start = tokio::time::Instant::now(); - - let mut announced = subscribed.consume().announced(); - let mut live = Vec::new(); - let read = async { - loop { - match announced.next().await.expect("cursor closed") { - announce::Event::Live => break, - announce::Event::Start(update) => live.push(update.prefix.to_string()), - other => panic!("{version}: only announcements before the marker: got {other:?}"), - } - } - }; - tokio::time::timeout(Duration::from_secs(10), read) - .await - .unwrap_or_else(|_| panic!("{version}: never caught up")); - - let elapsed = start.elapsed(); - - drop(broadcasts); - live.sort(); - (live, elapsed) -} - -// Paused, so time only moves when every task waits on a timer: a counted set lands -// without one, and a quiet one has to wait out the gap. -#[tokio::test(start_paused = true)] -async fn the_marker_follows_the_whole_initial_set() { - for version in COUNTED.iter().chain(QUIET) { - let (live, elapsed) = caught_up(version, &["a", "b", "c"]).await; - assert_eq!(live, ["a", "b", "c"], "{version}"); - assert_eq!( - QUIET.contains(version), - elapsed >= QUIET_GAP, - "{version}: took {elapsed:?}" - ); - } -} - -#[tokio::test(start_paused = true)] -async fn an_empty_peer_is_caught_up() { - for version in COUNTED.iter().chain(QUIET) { - let (live, elapsed) = caught_up(version, &[]).await; - assert!(live.is_empty(), "{version}"); - assert_eq!( - QUIET.contains(version), - elapsed >= QUIET_GAP, - "{version}: took {elapsed:?}" - ); - } -} diff --git a/rs/moq-net/tests/legacy_reconnect.rs b/rs/moq-net/tests/legacy_reconnect.rs index f883b635b0..7847f9883f 100644 --- a/rs/moq-net/tests/legacy_reconnect.rs +++ b/rs/moq-net/tests/legacy_reconnect.rs @@ -36,7 +36,7 @@ async fn next_first_hop(announced: &mut announce::Consumer) -> Hop { loop { let update = match announced.next().await.expect("announce cursor ended") { announce::Event::Start(update) | announce::Event::Update(update) => update, - announce::Event::End(_) | announce::Event::Live => continue, + announce::Event::End(_) => continue, }; if update.prefix.as_str() != PATH { continue; diff --git a/rs/moq-net/tests/mesh_withdraw.rs b/rs/moq-net/tests/mesh_withdraw.rs index 92ba86756f..980dac5c83 100644 --- a/rs/moq-net/tests/mesh_withdraw.rs +++ b/rs/moq-net/tests/mesh_withdraw.rs @@ -49,7 +49,6 @@ fn watch(mut announced: announce::Consumer) -> mpsc::UnboundedReceiver<(String, announce::Event::Start(announce) => (Kind::Start, announce), announce::Event::Update(announce) => (Kind::Update, announce), announce::Event::End(announce) => (Kind::End, announce), - announce::Event::Live => continue, }; if tx.send((announce.prefix.to_string(), kind)).is_err() { break; diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index 30c5ff0b18..dc8d9372d4 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -1996,25 +1996,19 @@ mod tests { use super::*; use crate::Config as RelayConfig; - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } - /// The next announcement without blocking, skipping the caught-up marker. + /// The next announcement without blocking. fn try_next_announced(announced: &mut moq_net::announce::Consumer) -> Option { - loop { - return match announced.try_next()? { - moq_net::announce::Event::Start(route) => Some(route), - moq_net::announce::Event::Live => continue, - other => panic!("expected an announcement: got {other:?}"), - }; + match announced.try_next()? { + moq_net::announce::Event::Start(route) => Some(route), + other => panic!("expected an announcement: got {other:?}"), } } diff --git a/rs/moq-relay/src/internal.rs b/rs/moq-relay/src/internal.rs index 22889afde1..da6f201d32 100644 --- a/rs/moq-relay/src/internal.rs +++ b/rs/moq-relay/src/internal.rs @@ -695,14 +695,11 @@ fn render_uring(_out: &mut String, _workers: &[UringWorker]) {} mod tests { use super::*; - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/auth_lifetime.rs b/rs/moq-relay/tests/auth_lifetime.rs index 0c35d93acc..9163df0bf9 100644 --- a/rs/moq-relay/tests/auth_lifetime.rs +++ b/rs/moq-relay/tests/auth_lifetime.rs @@ -1121,13 +1121,10 @@ async fn a_relay_without_an_auth_source_is_decided_by_the_embedder() { .expect("relay exited with an error"); } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/cluster_unknown.rs b/rs/moq-relay/tests/cluster_unknown.rs index cf750a24f6..96f33fc0ad 100644 --- a/rs/moq-relay/tests/cluster_unknown.rs +++ b/rs/moq-relay/tests/cluster_unknown.rs @@ -365,13 +365,10 @@ async fn unknown_publisher_does_not_flap_across_a_lite04_cluster_triangle() { assert_unknown_publisher_stays_announced(Some(version), true).await; } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/drills.rs b/rs/moq-relay/tests/drills.rs index 6ba7488364..0f3f1e1d83 100644 --- a/rs/moq-relay/tests/drills.rs +++ b/rs/moq-relay/tests/drills.rs @@ -755,13 +755,10 @@ async fn no_publisher_never_delivers(lane: Lane) { lanes!(no_publisher_never_delivers); -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/embed.rs b/rs/moq-relay/tests/embed.rs index cc7055aae9..eb4026f3d2 100644 --- a/rs/moq-relay/tests/embed.rs +++ b/rs/moq-relay/tests/embed.rs @@ -478,13 +478,10 @@ fn embedded_cli_merges_only_relay_settings() { assert_eq!(parsed.relay.cluster.id, Some(9)); } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/goaway_cluster.rs b/rs/moq-relay/tests/goaway_cluster.rs index 2bacd26e84..132e9f572c 100644 --- a/rs/moq-relay/tests/goaway_cluster.rs +++ b/rs/moq-relay/tests/goaway_cluster.rs @@ -911,13 +911,10 @@ async fn goaway_handover_is_enforced_while_the_replacement_dial_hangs_inner() { drop(connection); } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/public_rules.rs b/rs/moq-relay/tests/public_rules.rs index cb7be07523..82ef2ea33b 100644 --- a/rs/moq-relay/tests/public_rules.rs +++ b/rs/moq-relay/tests/public_rules.rs @@ -100,13 +100,9 @@ async fn first_broadcast(url: url::Url) -> String { .expect("subscriber connect failed"); let update = tokio::time::timeout(TIMEOUT, async { - loop { - match announcements.next().await.expect("origin closed") { - moq_net::announce::Event::Start(update) => break update, - // The local origin is caught up before the session brings anything. - moq_net::announce::Event::Live => continue, - other => panic!("expected announce, got {other:?}"), - } + match announcements.next().await.expect("origin closed") { + moq_net::announce::Event::Start(update) => update, + other => panic!("expected announce, got {other:?}"), } }) .await diff --git a/rs/moq-relay/tests/runtime_uring.rs b/rs/moq-relay/tests/runtime_uring.rs index 7fbffe0edb..2b013656cd 100644 --- a/rs/moq-relay/tests/runtime_uring.rs +++ b/rs/moq-relay/tests/runtime_uring.rs @@ -639,13 +639,10 @@ async fn spawn_auth_server(policy: moq_auth::serve::Policy) -> url::Url { url } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/runtime_workers.rs b/rs/moq-relay/tests/runtime_workers.rs index 97ce387de3..5cf7b15997 100644 --- a/rs/moq-relay/tests/runtime_workers.rs +++ b/rs/moq-relay/tests/runtime_workers.rs @@ -144,13 +144,10 @@ async fn workers_serve_quic_and_share_one_origin() { let _ = running.await; } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-relay/tests/smoke.rs b/rs/moq-relay/tests/smoke.rs index 1f468466f3..b42540064c 100644 --- a/rs/moq-relay/tests/smoke.rs +++ b/rs/moq-relay/tests/smoke.rs @@ -1111,13 +1111,10 @@ async fn connect_once( Ok((client, connection)) } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-room/src/room.rs b/rs/moq-room/src/room.rs index e2a41636bd..a0986ff1d9 100644 --- a/rs/moq-room/src/room.rs +++ b/rs/moq-room/src/room.rs @@ -81,7 +81,6 @@ impl Room { let (update, active) = match ready!(self.announced.poll_next(waiter)) { Some(announce::Event::Start(update) | announce::Event::Update(update)) => (update, true), Some(announce::Event::End(update)) => (update, false), - Some(announce::Event::Live) => continue, None => return Poll::Ready(None), }; let path = update.prefix; diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index c9657a0d3b..9c63986030 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -75,14 +75,11 @@ pub use server::{Response, Server, whep, whip}; mod tests { use std::time::Duration; - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-stats/src/aggregate.rs b/rs/moq-stats/src/aggregate.rs index 50001c3ffa..71c49fac55 100644 --- a/rs/moq-stats/src/aggregate.rs +++ b/rs/moq-stats/src/aggregate.rs @@ -346,7 +346,6 @@ impl Merged { Poll::Ready(Some(moq_net::announce::Event::End(update))) => { changed |= self.apply_announce(update, false) } - Poll::Ready(Some(moq_net::announce::Event::Live)) => {} Poll::Ready(None) => return Poll::Ready(Ok(None)), Poll::Pending => break, } @@ -561,14 +560,11 @@ mod tests { producer } - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-stats/src/consume.rs b/rs/moq-stats/src/consume.rs index 068fa32211..baf0473b53 100644 --- a/rs/moq-stats/src/consume.rs +++ b/rs/moq-stats/src/consume.rs @@ -117,14 +117,11 @@ mod tests { producer } - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-stats/src/produce.rs b/rs/moq-stats/src/produce.rs index d54c8f36a5..f6475e103f 100644 --- a/rs/moq-stats/src/produce.rs +++ b/rs/moq-stats/src/produce.rs @@ -1160,14 +1160,11 @@ mod tests { producer } - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-tokio/examples/clock.rs b/rs/moq-tokio/examples/clock.rs index 020ca7e2ca..f5e73100ee 100644 --- a/rs/moq-tokio/examples/clock.rs +++ b/rs/moq-tokio/examples/clock.rs @@ -120,7 +120,6 @@ async fn main() -> anyhow::Result<()> { announce::Event::End(update) => { tracing::warn!(broadcast = %update.prefix, "broadcast is offline, waiting..."); } - announce::Event::Live => {} }, res = reconnect.closed() => return Ok(res?), // Drops the previous subscriber on each new announce. diff --git a/rs/moq-tokio/src/origin.rs b/rs/moq-tokio/src/origin.rs index 48ac3aac49..7ebd774d12 100644 --- a/rs/moq-tokio/src/origin.rs +++ b/rs/moq-tokio/src/origin.rs @@ -25,14 +25,11 @@ pub fn spawn_config(config: moq_net::origin::Config) -> moq_net::origin::Produce mod tests { use super::*; - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-tokio/src/server.rs b/rs/moq-tokio/src/server.rs index 1a360703b8..ad9d1d94a3 100644 --- a/rs/moq-tokio/src/server.rs +++ b/rs/moq-tokio/src/server.rs @@ -1620,14 +1620,11 @@ impl Request { mod tests { use super::*; - /// The next route and whether it is active, skipping the caught-up marker. + /// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-tokio/tests/backend.rs b/rs/moq-tokio/tests/backend.rs index 3338fbdaa5..4c66474e35 100644 --- a/rs/moq-tokio/tests/backend.rs +++ b/rs/moq-tokio/tests/backend.rs @@ -1351,13 +1351,10 @@ async fn noq_windows() { window_test("moqt").await; } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/rs/moq-tokio/tests/broadcast.rs b/rs/moq-tokio/tests/broadcast.rs index 70dc6ab3ca..00a00ba661 100644 --- a/rs/moq-tokio/tests/broadcast.rs +++ b/rs/moq-tokio/tests/broadcast.rs @@ -3828,13 +3828,10 @@ async fn abort_carries_its_code_to_the_peer() { server_handle.abort(); } -/// The next route and whether it is active, skipping the caught-up marker. +/// The next route and whether it is active. async fn next_update(announced: &mut moq_net::announce::Consumer) -> Option<(moq_net::announce::Announce, bool)> { - loop { - return match announced.next().await? { - moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), - moq_net::announce::Event::End(route) => Some((route, false)), - moq_net::announce::Event::Live => continue, - }; + match announced.next().await? { + moq_net::announce::Event::Start(route) | moq_net::announce::Event::Update(route) => Some((route, true)), + moq_net::announce::Event::End(route) => Some((route, false)), } } diff --git a/swift/README.md b/swift/README.md index 8bcbaa1ee4..8323726000 100644 --- a/swift/README.md +++ b/swift/README.md @@ -35,8 +35,7 @@ let session = try await client.connect(to: "https://relay.example.com") // origin between both sides. let announced = try session.consume.announced(prefix: "demos/", filter: "*/camera") for try await event in announced { - // `.live` follows the routes live at subscribe time; `.update` and - // `.end` report later changes. + // `.update` and `.end` report later changes. guard case .start(let announcement) = event else { continue } // Prefix stays origin-relative; captures reports what * matched. print("got broadcast \(announcement.prefix)") diff --git a/swift/Sources/Moq/Aliases.swift b/swift/Sources/Moq/Aliases.swift index 11e670dedf..8c63ccf75f 100644 --- a/swift/Sources/Moq/Aliases.swift +++ b/swift/Sources/Moq/Aliases.swift @@ -83,9 +83,8 @@ public typealias Route = MoqFFI.MoqRoute /// wildcard matched (`captures`, `nil` for a partial overlap), and the `route` /// serving it. Resolve a path with `OriginConsumer.requestBroadcast`. public typealias Announce = MoqFFI.MoqAnnounce -/// What an `AnnounceConsumer` yields: `.start`, `.update`, or `.end` -/// carrying an `Announce`, or `.live` once every route live at subscribe time -/// has been delivered. +/// What an `AnnounceConsumer` yields: `.start`, `.update`, or `.end`, each +/// carrying an `Announce`. public typealias AnnounceEvent = MoqFFI.MoqAnnounceEvent /// Per-subscription delivery preferences: priority, group ordering, latency /// budget, and group range. diff --git a/swift/Sources/Moq/Origin.swift b/swift/Sources/Moq/Origin.swift index 0357412b79..9bca84e20d 100644 --- a/swift/Sources/Moq/Origin.swift +++ b/swift/Sources/Moq/Origin.swift @@ -132,8 +132,7 @@ public final class OriginConsumer: Sendable { } /// A stream of announce events. Iterate directly: -/// `for try await event in announced { ... }`. A `.live` event follows the routes -/// live at subscribe time, so a loop can `break` there to list what is live. +/// `for try await event in announced { ... }`. /// The sequence ends when the origin closes; cancelling the consuming task /// cancels the subscription. public final class AnnounceConsumer: AsyncSequence, Sendable { diff --git a/swift/Tests/MoqTests/SmokeTests.swift b/swift/Tests/MoqTests/SmokeTests.swift index 55eb0e1dfc..e7961e6dca 100644 --- a/swift/Tests/MoqTests/SmokeTests.swift +++ b/swift/Tests/MoqTests/SmokeTests.swift @@ -2,15 +2,10 @@ import Foundation import XCTest @testable import Moq -/// The next announce event that is not `.live`, which lands wherever the backlog ends. +/// The next announce event. private func nextRoute(_ announced: AnnounceConsumer) async throws -> AnnounceEvent { - while true { - let next = try await announced.next() - let event = try XCTUnwrap(next, "announce stream ended") - if event != .live { - return event - } - } + let next = try await announced.next() + return try XCTUnwrap(next, "announce stream ended") } final class SmokeTests: XCTestCase { @@ -137,30 +132,6 @@ final class SmokeTests: XCTestCase { XCTAssertEqual(update.captures, ["alice"]) } - func testAnnouncedYieldsLiveOnceCaughtUp() async throws { - let origin = OriginProducer() - let consumer = origin.consume() - - let empty = try consumer.announced() - let first = try await empty.next() - XCTAssertEqual(first, .live) - empty.cancel() - - let broadcast = try origin.createBroadcast(path: "cam") - try broadcast.announce() - _ = try await consumer.announcedBroadcast(path: "cam").available() - - var listed: [String] = [] - for try await event in try consumer.announced() { - if case .start(let announce) = event { - listed.append(announce.prefix) - } else if event == .live { - break - } - } - XCTAssertEqual(listed, ["cam"]) - } - func testDynamicServesARequestUnderAPrefix() async throws { let origin = OriginProducer() let dynamic = try origin.dynamic(prefix: "live") From ae54f7e4aa989f3ed55eb547034019aeda73cb3d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 00:38:13 -0700 Subject: [PATCH 3/4] chore: format, and keep draining queued announce events in the C test Co-Authored-By: Claude Opus 5.5 --- js/net/src/ietf/subscriber.test.ts | 1 - js/net/src/integration.test.ts | 1 - js/net/src/lite/subscriber.test.ts | 1 - js/net/src/origin.test.ts | 1 - rs/moq-c/src/test.rs | 14 +++++++++++++- rs/moq-net/src/ietf/subscriber.rs | 4 +--- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/js/net/src/ietf/subscriber.test.ts b/js/net/src/ietf/subscriber.test.ts index 70df1d8305..fb0b80cce9 100644 --- a/js/net/src/ietf/subscriber.test.ts +++ b/js/net/src/ietf/subscriber.test.ts @@ -17,7 +17,6 @@ import { SubscribeNamespace, SubscribeNamespaceEntry, SubscribeNamespaceEntryDon import { Subscriber } from "./subscriber.ts"; import { ALPN, Version } from "./version.ts"; - const VERSION = Version.DRAFT_19; /** How long to wait for a stream before calling it absent. */ diff --git a/js/net/src/integration.test.ts b/js/net/src/integration.test.ts index 37bab7d093..d92f9d44f8 100644 --- a/js/net/src/integration.test.ts +++ b/js/net/src/integration.test.ts @@ -23,7 +23,6 @@ import type { Producer as TrackProducer } from "./track.ts"; import { withTimeout } from "./util/timeout.ts"; import { wireOf } from "./wire.ts"; - function publish(origin: OriginProducer, path: Path.Valid) { const broadcast = origin.createBroadcast(path); broadcast.announce(); diff --git a/js/net/src/lite/subscriber.test.ts b/js/net/src/lite/subscriber.test.ts index 9ccb5ba0fd..46e0f9472a 100644 --- a/js/net/src/lite/subscriber.test.ts +++ b/js/net/src/lite/subscriber.test.ts @@ -12,7 +12,6 @@ import { Subscriber } from "./subscriber.ts"; import { TrackInfo } from "./track.ts"; import { Version } from "./version.ts"; - test("closing the subscriber suppresses probe stream warnings", async () => { let readable!: ReadableStreamDefaultController; const quic = { diff --git a/js/net/src/origin.test.ts b/js/net/src/origin.test.ts index 141b464fa3..ff61f34144 100644 --- a/js/net/src/origin.test.ts +++ b/js/net/src/origin.test.ts @@ -8,7 +8,6 @@ import { Producer } from "./origin.ts"; import * as Path from "./path.ts"; import { wireOf } from "./wire.ts"; - function publish(origin: Producer, path: Path.Valid) { const broadcast = origin.createBroadcast(path); broadcast.announce(); diff --git a/rs/moq-c/src/test.rs b/rs/moq-c/src/test.rs index b348e97e3e..66a75439b3 100644 --- a/rs/moq-c/src/test.rs +++ b/rs/moq-c/src/test.rs @@ -128,6 +128,18 @@ impl Callback { let announced = id(self.recv()); (announced, announce_info(announced)) } + + /// Like [`recv_terminal`](Self::recv_terminal), but first frees any announce + /// events still queued ahead of it. + fn recv_announce_terminal(&self) -> i32 { + loop { + let code = self.recv(); + if code <= 0 { + return code; + } + assert_eq!(moq_origin_announced_free(id(code)), 0); + } + } } /// Read an announce event delivered to an `on_announce` callback. @@ -2089,7 +2101,7 @@ fn announced_hides_dot_paths_unless_asked() { assert_eq!(moq_origin_announced_free(announced), 0); assert_eq!(moq_origin_announced_cancel(task), 0); // Later events may be queued ahead of the terminal; free them. - assert_eq!(cb.recv_terminal(), 0); + assert_eq!(cb.recv_announce_terminal(), 0); } assert_eq!(moq_publish_close(stats), 0); diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index 0522e777cb..dc0863740f 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -932,9 +932,7 @@ where // // This is what moq-lite already does, where the equivalent map is a local whose // guards drop. - let res = self - .run_namespace_entries(&mut stream, &prefix, &peer, &mut live) - .await; + let res = self.run_namespace_entries(&mut stream, &prefix, &peer, &mut live).await; for path in live { let _ = self.stop_announce(path); } From 74ecba0cbbe482350146be50edc825b7b0de9d1e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 01:33:19 -0700 Subject: [PATCH 4/4] test(net): keep an unknown announce type in the lite run loop covered The deleted count-landing tests were the only ones feeding an unknown lite-06 announce type through the subscriber, so restore a trimmed version in Rust and JS. Also reword the Swift README's event comment. Co-Authored-By: Claude Opus 5.5 --- js/net/src/lite/subscriber.test.ts | 21 ++++++++++++++ rs/moq-net/src/lite/subscriber.rs | 45 ++++++++++++++++++++++++++++++ swift/README.md | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/js/net/src/lite/subscriber.test.ts b/js/net/src/lite/subscriber.test.ts index 46e0f9472a..a64db8548e 100644 --- a/js/net/src/lite/subscriber.test.ts +++ b/js/net/src/lite/subscriber.test.ts @@ -729,6 +729,27 @@ test("a draft-05 duplicate start is still a restart", async () => { subscriber.close(); }); +// Mirrors the Rust `an_unknown_announce_type_keeps_the_stream`. +test("an unknown announce type between starts keeps the stream", async () => { + const { subscriber, send, settle } = announceHarness(Version.DRAFT_06); + const announced = subscriber.announced(); + await settle(); + + const start = (suffix: string) => (w: Writer) => + encodeAnnounceBroadcast(w, { status: "active", suffix: Path.from(suffix), hops: [] }, Version.DRAFT_06); + await send((w) => new AnnounceOk(PEER, 2).encode(w, Version.DRAFT_06)); + await send(start("a")); + // An unknown announce type with an empty body, which decodes as skipped. + await send((w) => w.write(new Uint8Array([0x3f, 0x00]))); + await send(start("b")); + + expect(await announced.next()).toMatchObject({ prefix: Path.from("a"), kind: "start" }); + expect(await announced.next()).toMatchObject({ prefix: Path.from("b"), kind: "start" }); + + announced.close(); + subscriber.close(); +}); + interface FakeStream { inbound: ReadableStreamDefaultController; // Resolves once the subscriber waits on a read the test has not answered. diff --git a/rs/moq-net/src/lite/subscriber.rs b/rs/moq-net/src/lite/subscriber.rs index da74e04136..8ba76fba74 100644 --- a/rs/moq-net/src/lite/subscriber.rs +++ b/rs/moq-net/src/lite/subscriber.rs @@ -3056,6 +3056,51 @@ mod tests { announced.withdraw(&path); cursor.assert_next_ended("room/host"); } + + /// An unknown announce type between two starts is skipped without ending the + /// stream. + #[tokio::test(start_paused = true)] + async fn an_unknown_announce_type_keeps_the_stream() { + const VERSION: Version = Version::Lite06; + let start = |suffix| lite::AnnounceBroadcast::Active { + suffix: lite::PathRef::literal(Path::new(suffix)), + hops: lite::HopsRef::literal(crate::Hops::new()), + cost: crate::origin::Cost::default(), + }; + let mut script = Vec::new(); + lite::AnnounceOk { + origin: crate::Hop::new(9).unwrap(), + active: 2, + } + .encode(&mut script, VERSION) + .unwrap(); + start("a").encode(&mut script, VERSION).unwrap(); + // An unknown announce type with an empty body, which decodes as `Skipped`. + script.extend([0x3f, 0x00]); + start("b").encode(&mut script, VERSION).unwrap(); + + let origin = origin::Config::new(crate::Hop::new(1).unwrap()).produce(); + let consumer = origin.consume(); + let subscriber = Subscriber::new(SubscriberConfig { + runtime: crate::time::Clock::tokio(), + session: crate::lite::test_transport::ScriptedSession::new(script), + origin, + recv_bandwidth: None, + version: VERSION, + peer_setup: Default::default(), + cost: Some(1), + peer_hop: None, + going_away: Default::default(), + }); + let mut prefix = AnnouncePrefix::new(subscriber, Path::new("").to_owned()); + let mut cursor = consumer.announced(); + + let mut run = std::pin::pin!(kio::wait(|waiter| prefix.poll(waiter))); + assert!(futures::poll!(run.as_mut()).is_pending()); + + cursor.assert_next_active("a"); + cursor.assert_next_active("b"); + } } /// The four wire fields a subscription's half-open range encodes to. diff --git a/swift/README.md b/swift/README.md index 8323726000..14faa0918a 100644 --- a/swift/README.md +++ b/swift/README.md @@ -35,7 +35,7 @@ let session = try await client.connect(to: "https://relay.example.com") // origin between both sides. let announced = try session.consume.announced(prefix: "demos/", filter: "*/camera") for try await event in announced { - // `.update` and `.end` report later changes. + // `.update` reprices a route and `.end` retracts it. guard case .start(let announcement) = event else { continue } // Prefix stays origin-relative; captures reports what * matched. print("got broadcast \(announcement.prefix)")