@norskvideo/moq-net 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/announce.d.ts +7 -0
- package/announce.d.ts.map +1 -0
- package/announce.js +8 -0
- package/announce.js.map +1 -0
- package/announced.d.ts +49 -91
- package/announced.d.ts.map +1 -1
- package/announced.js +21 -156
- package/announced.js.map +1 -1
- package/bandwidth.d.ts +163 -0
- package/bandwidth.d.ts.map +1 -0
- package/bandwidth.js +304 -0
- package/bandwidth.js.map +1 -0
- package/bandwidth_api.d.ts +7 -0
- package/bandwidth_api.d.ts.map +1 -0
- package/bandwidth_api.js +8 -0
- package/bandwidth_api.js.map +1 -0
- package/broadcast.d.ts +44 -35
- package/broadcast.d.ts.map +1 -1
- package/broadcast.js +104 -60
- package/broadcast.js.map +1 -1
- package/connection/accept.d.ts +16 -1
- package/connection/accept.d.ts.map +1 -1
- package/connection/accept.js +52 -28
- package/connection/accept.js.map +1 -1
- package/connection/browser.d.ts.map +1 -1
- package/connection/browser.js +9 -7
- package/connection/browser.js.map +1 -1
- package/connection/connect.d.ts +30 -6
- package/connection/connect.d.ts.map +1 -1
- package/connection/connect.js +110 -54
- package/connection/connect.js.map +1 -1
- package/connection/established.d.ts +17 -21
- package/connection/established.d.ts.map +1 -1
- package/connection/established.js.map +1 -1
- package/connection/forward.d.ts +2 -0
- package/connection/forward.d.ts.map +1 -0
- package/connection/forward.js +173 -0
- package/connection/forward.js.map +1 -0
- package/connection/handshake.d.ts +1 -0
- package/connection/handshake.d.ts.map +1 -1
- package/connection/handshake.js +5 -2
- package/connection/handshake.js.map +1 -1
- package/connection/index.d.ts +5 -5
- package/connection/index.d.ts.map +1 -1
- package/connection/index.js +4 -5
- package/connection/index.js.map +1 -1
- package/connection/pool.d.ts +186 -0
- package/connection/pool.d.ts.map +1 -0
- package/connection/pool.js +361 -0
- package/connection/pool.js.map +1 -0
- package/connection/reload.d.ts +14 -93
- package/connection/reload.d.ts.map +1 -1
- package/connection/reload.js +217 -81
- package/connection/reload.js.map +1 -1
- package/connection/stats.d.ts +3 -26
- package/connection/stats.d.ts.map +1 -1
- package/connection/stats.js.map +1 -1
- package/connection/transport.d.ts +0 -7
- package/connection/transport.d.ts.map +1 -1
- package/consume.d.ts +1 -43
- package/consume.d.ts.map +1 -1
- package/consume.js +1 -1
- package/consume.js.map +1 -1
- package/error.d.ts +180 -29
- package/error.d.ts.map +1 -1
- package/error.js +331 -16
- package/error.js.map +1 -1
- package/errors.d.ts +7 -0
- package/errors.d.ts.map +1 -0
- package/errors.js +8 -0
- package/errors.js.map +1 -0
- package/group.d.ts +9 -43
- package/group.d.ts.map +1 -1
- package/group.js +284 -69
- package/group.js.map +1 -1
- package/hop.d.ts +115 -0
- package/hop.d.ts.map +1 -0
- package/hop.js +119 -0
- package/hop.js.map +1 -0
- package/ietf/adapter.d.ts +5 -1
- package/ietf/adapter.d.ts.map +1 -1
- package/ietf/adapter.js +105 -60
- package/ietf/adapter.js.map +1 -1
- package/ietf/aliases.d.ts +1 -78
- package/ietf/aliases.d.ts.map +1 -1
- package/ietf/cluster.d.ts +9 -123
- package/ietf/cluster.d.ts.map +1 -1
- package/ietf/cluster.js +84 -44
- package/ietf/cluster.js.map +1 -1
- package/ietf/connection.d.ts +6 -72
- package/ietf/connection.d.ts.map +1 -1
- package/ietf/connection.js +62 -56
- package/ietf/connection.js.map +1 -1
- package/ietf/error.d.ts +11 -0
- package/ietf/error.d.ts.map +1 -0
- package/ietf/error.js +193 -0
- package/ietf/error.js.map +1 -0
- package/ietf/fetch.d.ts +7 -20
- package/ietf/fetch.d.ts.map +1 -1
- package/ietf/fetch.js +52 -22
- package/ietf/fetch.js.map +1 -1
- package/ietf/filter.d.ts +2 -0
- package/ietf/filter.d.ts.map +1 -1
- package/ietf/filter.js +10 -1
- package/ietf/filter.js.map +1 -1
- package/ietf/goaway.d.ts.map +1 -1
- package/ietf/goaway.js +22 -5
- package/ietf/goaway.js.map +1 -1
- package/ietf/hidden.d.ts +2 -0
- package/ietf/hidden.d.ts.map +1 -0
- package/ietf/hidden.js +30 -0
- package/ietf/hidden.js.map +1 -0
- package/ietf/index.d.ts +2 -0
- package/ietf/index.d.ts.map +1 -1
- package/ietf/index.js +2 -0
- package/ietf/index.js.map +1 -1
- package/ietf/object.d.ts +15 -8
- package/ietf/object.d.ts.map +1 -1
- package/ietf/object.js +51 -36
- package/ietf/object.js.map +1 -1
- package/ietf/parameters.d.ts +14 -2
- package/ietf/parameters.d.ts.map +1 -1
- package/ietf/parameters.js +97 -29
- package/ietf/parameters.js.map +1 -1
- package/ietf/properties.d.ts +1 -0
- package/ietf/properties.d.ts.map +1 -1
- package/ietf/properties.js +14 -0
- package/ietf/properties.js.map +1 -1
- package/ietf/publish.d.ts +19 -2
- package/ietf/publish.d.ts.map +1 -1
- package/ietf/publish.js +40 -6
- package/ietf/publish.js.map +1 -1
- package/ietf/publish_namespace.d.ts +25 -0
- package/ietf/publish_namespace.d.ts.map +1 -1
- package/ietf/publish_namespace.js +63 -0
- package/ietf/publish_namespace.js.map +1 -1
- package/ietf/publisher.d.ts +1 -82
- package/ietf/publisher.d.ts.map +1 -1
- package/ietf/publisher.js +483 -235
- package/ietf/publisher.js.map +1 -1
- package/ietf/solicit.d.ts +1 -40
- package/ietf/solicit.d.ts.map +1 -1
- package/ietf/subscribe.d.ts +8 -6
- package/ietf/subscribe.d.ts.map +1 -1
- package/ietf/subscribe.js +33 -27
- package/ietf/subscribe.js.map +1 -1
- package/ietf/subscribe_namespace.d.ts +8 -2
- package/ietf/subscribe_namespace.d.ts.map +1 -1
- package/ietf/subscribe_namespace.js +18 -8
- package/ietf/subscribe_namespace.js.map +1 -1
- package/ietf/subscriber.d.ts +1 -65
- package/ietf/subscriber.d.ts.map +1 -1
- package/ietf/subscriber.js +337 -123
- package/ietf/subscriber.js.map +1 -1
- package/ietf/token.d.ts +2 -0
- package/ietf/token.d.ts.map +1 -0
- package/ietf/token.js +99 -0
- package/ietf/token.js.map +1 -0
- package/ietf/track.d.ts +4 -0
- package/ietf/track.d.ts.map +1 -1
- package/ietf/track.js +6 -20
- package/ietf/track.js.map +1 -1
- package/ietf/version.d.ts +12 -1
- package/ietf/version.d.ts.map +1 -1
- package/ietf/version.js +13 -0
- package/ietf/version.js.map +1 -1
- package/index.d.ts +12 -7
- package/index.d.ts.map +1 -1
- package/index.js +10 -5
- package/index.js.map +1 -1
- package/internal.d.ts +115 -1
- package/internal.d.ts.map +1 -1
- package/internal.js +108 -0
- package/internal.js.map +1 -1
- package/lite/announce.d.ts +60 -10
- package/lite/announce.d.ts.map +1 -1
- package/lite/announce.js +176 -31
- package/lite/announce.js.map +1 -1
- package/lite/connection.d.ts +9 -59
- package/lite/connection.d.ts.map +1 -1
- package/lite/connection.js +34 -35
- package/lite/connection.js.map +1 -1
- package/lite/datagram.d.ts +3 -2
- package/lite/datagram.d.ts.map +1 -1
- package/lite/datagram.js +7 -8
- package/lite/datagram.js.map +1 -1
- package/lite/fetch.d.ts +15 -1
- package/lite/fetch.d.ts.map +1 -1
- package/lite/fetch.js +39 -7
- package/lite/fetch.js.map +1 -1
- package/lite/goaway.d.ts.map +1 -1
- package/lite/goaway.js +7 -1
- package/lite/goaway.js.map +1 -1
- package/lite/group.d.ts +30 -12
- package/lite/group.d.ts.map +1 -1
- package/lite/group.js +68 -26
- package/lite/group.js.map +1 -1
- package/lite/message.d.ts +2 -2
- package/lite/message.d.ts.map +1 -1
- package/lite/message.js +14 -5
- package/lite/message.js.map +1 -1
- package/lite/priority.d.ts +1 -61
- package/lite/priority.d.ts.map +1 -1
- package/lite/priority.js +4 -5
- package/lite/priority.js.map +1 -1
- package/lite/publisher.d.ts +1 -69
- package/lite/publisher.d.ts.map +1 -1
- package/lite/publisher.js +630 -262
- package/lite/publisher.js.map +1 -1
- package/lite/setup.d.ts +8 -8
- package/lite/setup.d.ts.map +1 -1
- package/lite/setup.js +33 -30
- package/lite/setup.js.map +1 -1
- package/lite/subscribe.d.ts +71 -17
- package/lite/subscribe.d.ts.map +1 -1
- package/lite/subscribe.js +205 -53
- package/lite/subscribe.js.map +1 -1
- package/lite/subscriber.d.ts +15 -56
- package/lite/subscriber.d.ts.map +1 -1
- package/lite/subscriber.js +451 -247
- package/lite/subscriber.js.map +1 -1
- package/lite/track.d.ts +4 -10
- package/lite/track.d.ts.map +1 -1
- package/lite/track.js +34 -29
- package/lite/track.js.map +1 -1
- package/lite/version.d.ts +42 -6
- package/lite/version.d.ts.map +1 -1
- package/lite/version.js +126 -10
- package/lite/version.js.map +1 -1
- package/origin.d.ts +256 -29
- package/origin.d.ts.map +1 -1
- package/origin.js +1427 -37
- package/origin.js.map +1 -1
- package/package.json +8 -3
- package/path.d.ts +25 -7
- package/path.d.ts.map +1 -1
- package/path.js +5 -3
- package/path.js.map +1 -1
- package/stream.d.ts +73 -14
- package/stream.d.ts.map +1 -1
- package/stream.js +372 -141
- package/stream.js.map +1 -1
- package/tail.d.ts +18 -0
- package/tail.d.ts.map +1 -0
- package/tail.js +167 -0
- package/tail.js.map +1 -0
- package/time.d.ts +15 -2
- package/time.d.ts.map +1 -1
- package/time.js +28 -9
- package/time.js.map +1 -1
- package/track.d.ts +211 -83
- package/track.d.ts.map +1 -1
- package/track.js +816 -205
- package/track.js.map +1 -1
- package/util/abort.d.ts +2 -0
- package/util/abort.d.ts.map +1 -0
- package/util/abort.js +20 -0
- package/util/abort.js.map +1 -0
- package/util/log.d.ts +5 -0
- package/util/log.d.ts.map +1 -0
- package/util/log.js +17 -0
- package/util/log.js.map +1 -0
- package/util/u64.d.ts +39 -0
- package/util/u64.d.ts.map +1 -0
- package/util/u64.js +83 -0
- package/util/u64.js.map +1 -0
- package/util/varint.d.ts +29 -0
- package/util/varint.d.ts.map +1 -0
- package/util/varint.js +198 -0
- package/util/varint.js.map +1 -0
- package/varint.d.ts +10 -6
- package/varint.d.ts.map +1 -1
- package/varint.js +40 -237
- package/varint.js.map +1 -1
- package/wire.d.ts +80 -0
- package/wire.d.ts.map +1 -0
- package/wire.js +32 -0
- package/wire.js.map +1 -0
- package/zod.d.ts +1 -1
- package/zod.d.ts.map +1 -1
- package/zod.js.map +1 -1
- package/mock.d.ts +0 -66
- package/mock.d.ts.map +0 -1
- package/mock.js +0 -243
- package/mock.js.map +0 -1
package/ietf/subscriber.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/* @ts-self-types="./subscriber.d.ts" */
|
|
2
|
+
import { race, Signal } from "@norskvideo/moq-signals";
|
|
2
3
|
import * as announce from "../announced.js";
|
|
3
4
|
import * as broadcast from "../broadcast.js";
|
|
4
5
|
import { BroadcastCache } from "../consume.js";
|
|
5
|
-
import { error, ProtocolViolation, reason } from "../error.js";
|
|
6
|
+
import { closeError, controlTimeout, error, ProtocolViolation, reason, sessionCause } from "../error.js";
|
|
6
7
|
import * as netGroup from "../group.js";
|
|
8
|
+
import { Cost, randomHop, routesEqual, stampHops, UNKNOWN_HOP } from "../hop.js";
|
|
9
|
+
import { hiddenBelow, hooks, scopeCaptures, scopeHead, scopeOverlaps } from "../internal.js";
|
|
7
10
|
import * as Path from "../path.js";
|
|
11
|
+
import { Tail } from "../tail.js";
|
|
8
12
|
import { Timestamp } from "../time.js";
|
|
9
|
-
import { withTimeout } from "../util/timeout.js";
|
|
13
|
+
import { TimeoutError, withTimeout } from "../util/timeout.js";
|
|
14
|
+
import { overrideBroadcastWire, wireOf } from "../wire.js";
|
|
10
15
|
import { DuplicateTrackAlias, RetiredTrackAlias, TrackAliases } from "./aliases.js";
|
|
11
16
|
import * as Cluster from "./cluster.js";
|
|
17
|
+
import { requestReason, toRequestCode } from "./error.js";
|
|
12
18
|
import { Frame } from "./object.js";
|
|
13
|
-
import { toWire } from "./priority.js";
|
|
14
|
-
import { PublishError } from "./publish.js";
|
|
15
|
-
import {
|
|
19
|
+
import { fromWire, toWire } from "./priority.js";
|
|
20
|
+
import { PublishDone, PublishError, publishDoneClean } from "./publish.js";
|
|
21
|
+
import { PublishNamespaceDone, PublishNamespaceError, PublishNamespaceOk, PublishNamespaceUpdate, } from "./publish_namespace.js";
|
|
16
22
|
import { RequestError, RequestOk } from "./request.js";
|
|
17
23
|
import { joinFilter, Subscribe, SubscribeError, SubscribeOk, Unsubscribe } from "./subscribe.js";
|
|
18
24
|
import { PublishBlocked, SubscribeNamespace, SubscribeNamespaceEntry, SubscribeNamespaceEntryDone, SubscribeNamespaceLegacy, SubscribeNamespaceOk, UnsubscribeNamespace, } from "./subscribe_namespace.js";
|
|
@@ -21,6 +27,10 @@ import { Version } from "./version.js";
|
|
|
21
27
|
// concurrent QUIC streams (Chrome ~100); past the cap openBi() silently
|
|
22
28
|
// blocks. The timeout turns that into a clear error.
|
|
23
29
|
const SUBSCRIBE_OK_TIMEOUT_MS = 10_000;
|
|
30
|
+
/** Whether a reader with `filter` sees an announcement at `path`. */
|
|
31
|
+
function sees(filter, path) {
|
|
32
|
+
return scopeOverlaps(filter.scope, path) && (filter.hidden || !hiddenBelow(filter.prefix, path));
|
|
33
|
+
}
|
|
24
34
|
/**
|
|
25
35
|
* Handles subscribing to broadcasts using moq-transport protocol.
|
|
26
36
|
* Uses the stream-per-request pattern (real bidi streams for v17, virtual for v14-v16).
|
|
@@ -29,6 +39,9 @@ const SUBSCRIBE_OK_TIMEOUT_MS = 10_000;
|
|
|
29
39
|
*/
|
|
30
40
|
export class Subscriber {
|
|
31
41
|
#session;
|
|
42
|
+
// The transport, so a request cut off by the session's close ends with the session's
|
|
43
|
+
// error. Optional for tests that drive a bare session.
|
|
44
|
+
#quic;
|
|
32
45
|
// The Hop IDs this session declared; see {@link Cluster}. What the peer declared is what
|
|
33
46
|
// says whether an advertisement carries a hop path, and ours is what a path looping back
|
|
34
47
|
// to us contains.
|
|
@@ -41,6 +54,9 @@ export class Subscriber {
|
|
|
41
54
|
#timescales = new Map();
|
|
42
55
|
// Dedup consumed broadcasts per path: repeat consume() calls share one subscription.
|
|
43
56
|
#consumes = new BroadcastCache();
|
|
57
|
+
// A random Hop ID of this connection's own, written as the first hop of any path that
|
|
58
|
+
// names no publisher, so a publisher that reconnects reads as a new one.
|
|
59
|
+
#stamp = randomHop();
|
|
44
60
|
// Paths with a legacy PUBLISH_NAMESPACE request in flight, reserved synchronously.
|
|
45
61
|
// The count below is only taken once the OK is written, and two requests that both
|
|
46
62
|
// got past the duplicate check before either attached would both take one.
|
|
@@ -53,16 +69,20 @@ export class Subscriber {
|
|
|
53
69
|
// Counting them is what keeps the second from duplicating the announce and the first
|
|
54
70
|
// to end from retracting what the other still holds.
|
|
55
71
|
#announced = new Map();
|
|
56
|
-
// Any consumers that want each new announcement.
|
|
57
|
-
#announcedConsumers = new
|
|
72
|
+
// Any consumers that want each new announcement, keyed by their local filter.
|
|
73
|
+
#announcedConsumers = new Map();
|
|
74
|
+
// Whether the peer understands the HIDDEN parameter (MoQ Hidden).
|
|
75
|
+
#hidden;
|
|
58
76
|
/**
|
|
59
77
|
* Creates a new Subscriber instance.
|
|
60
78
|
*
|
|
61
79
|
* @internal
|
|
62
80
|
*/
|
|
63
|
-
constructor({ session, cluster, }) {
|
|
81
|
+
constructor({ session, quic, cluster, hidden = false, }) {
|
|
64
82
|
this.#session = session;
|
|
83
|
+
this.#quic = quic;
|
|
65
84
|
this.#cluster = cluster;
|
|
85
|
+
this.#hidden = hidden;
|
|
66
86
|
}
|
|
67
87
|
/**
|
|
68
88
|
* Whether an advertisement is ours coming back: its hop path already ran through us, so
|
|
@@ -76,22 +96,47 @@ export class Subscriber {
|
|
|
76
96
|
return advert !== undefined && this.#cluster !== undefined && Cluster.loops(advert, this.#cluster.self);
|
|
77
97
|
}
|
|
78
98
|
/**
|
|
79
|
-
*
|
|
99
|
+
* The route an advertisement carries; one without a path is free. A path that names no
|
|
100
|
+
* publisher, or none at all, gets this connection's stamp in front of a 0.
|
|
101
|
+
*/
|
|
102
|
+
#route(advert) {
|
|
103
|
+
if (advert === undefined)
|
|
104
|
+
return { hops: [this.#stamp, UNKNOWN_HOP], cost: Cost.zero };
|
|
105
|
+
// A full chain, or a stamp colliding with an entry (a 1-in-2^53 draw), keeps the
|
|
106
|
+
// path as sent.
|
|
107
|
+
const hops = stampHops(advert.hops, this.#stamp) ?? [...advert.hops];
|
|
108
|
+
return { hops, cost: { warm: advert.cost, cold: advert.cost } };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Gets an announced reader matching `scope`. Paths are relative to the session,
|
|
112
|
+
* not the scope.
|
|
80
113
|
*
|
|
81
114
|
* The peer is asked with SUBSCRIBE_NAMESPACE regardless of what it declared, and an
|
|
82
115
|
* unsolicited PUBLISH_NAMESPACE lands here too, so a peer that only tells and one
|
|
83
116
|
* that only answers are both discovered.
|
|
117
|
+
*
|
|
118
|
+
* Hidden routes (a `.`-prefixed segment below the scope's head) are left out unless
|
|
119
|
+
* `options.hidden` opts in. The opt-in rides the SUBSCRIBE_NAMESPACE when the peer
|
|
120
|
+
* understands it (MoQ Hidden); the rule is also applied here, since an unsolicited
|
|
121
|
+
* PUBLISH_NAMESPACE or a peer that never heard of it hides nothing.
|
|
84
122
|
*/
|
|
85
|
-
announced(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
announced(scope = Path.Pattern.all(), options) {
|
|
124
|
+
// The wire speaks announce interest by prefix.
|
|
125
|
+
const prefix = scopeHead(scope);
|
|
126
|
+
const filter = { scope, prefix, hidden: options?.hidden ?? false };
|
|
127
|
+
const announced = new announce.Producer();
|
|
128
|
+
for (const [active, info] of this.#announced) {
|
|
129
|
+
if (!sees(filter, active))
|
|
90
130
|
continue;
|
|
91
|
-
announced.append({
|
|
131
|
+
announced.append({
|
|
132
|
+
prefix: active,
|
|
133
|
+
captures: scopeCaptures(scope, active),
|
|
134
|
+
kind: "announced",
|
|
135
|
+
route: info.route,
|
|
136
|
+
});
|
|
92
137
|
}
|
|
93
|
-
this.#announcedConsumers.
|
|
94
|
-
void this.#runAnnounced(announced, prefix).finally(() => {
|
|
138
|
+
this.#announcedConsumers.set(announced, filter);
|
|
139
|
+
void this.#runAnnounced(announced, prefix, filter.hidden && this.#hidden).finally(() => {
|
|
95
140
|
this.#announcedConsumers.delete(announced);
|
|
96
141
|
announced.close();
|
|
97
142
|
});
|
|
@@ -101,28 +146,53 @@ export class Subscriber {
|
|
|
101
146
|
* Record one more advertisement for a path, telling consumers only when it is the
|
|
102
147
|
* first. A second one is the same namespace said twice, not news.
|
|
103
148
|
*/
|
|
104
|
-
#attachAnnounce(path) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
#attachAnnounce(path, route) {
|
|
150
|
+
const existing = this.#announced.get(path);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.count += 1;
|
|
108
153
|
return;
|
|
154
|
+
}
|
|
155
|
+
this.#announced.set(path, { count: 1, route });
|
|
109
156
|
console.debug(`announced: broadcast=${path} active=true`);
|
|
110
|
-
for (const consumer of this.#announcedConsumers) {
|
|
111
|
-
|
|
112
|
-
if (suffix === null)
|
|
157
|
+
for (const [consumer, filter] of this.#announcedConsumers) {
|
|
158
|
+
if (!sees(filter, path))
|
|
113
159
|
continue;
|
|
114
|
-
|
|
160
|
+
const scope = filter.scope;
|
|
161
|
+
consumer.append({ prefix: path, captures: scopeCaptures(scope, path), kind: "announced", route });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Replace the stored route for a path that is already announced. A no-op when the
|
|
166
|
+
* hops and cost did not change; otherwise consumers hear `updated` so a forwarder
|
|
167
|
+
* can reprice without retracting.
|
|
168
|
+
*
|
|
169
|
+
* A new first hop is a new publisher: holders keep their broadcast to drain, but the
|
|
170
|
+
* next consume starts fresh rather than reusing the old publisher's cached track info.
|
|
171
|
+
*/
|
|
172
|
+
#updateAnnounce(path, route) {
|
|
173
|
+
const existing = this.#announced.get(path);
|
|
174
|
+
if (existing === undefined || routesEqual(existing.route, route))
|
|
175
|
+
return;
|
|
176
|
+
if (existing.route.hops[0] !== route.hops[0])
|
|
177
|
+
this.#consumes.evict(path);
|
|
178
|
+
existing.route = route;
|
|
179
|
+
console.debug(`announced: broadcast=${path} rerouted`);
|
|
180
|
+
for (const [consumer, filter] of this.#announcedConsumers) {
|
|
181
|
+
if (!sees(filter, path))
|
|
182
|
+
continue;
|
|
183
|
+
const scope = filter.scope;
|
|
184
|
+
consumer.append({ prefix: path, captures: scopeCaptures(scope, path), kind: "updated", route });
|
|
115
185
|
}
|
|
116
186
|
}
|
|
117
187
|
/**
|
|
118
188
|
* Drop one advertisement for a path, retracting it only once the last one goes.
|
|
119
189
|
*/
|
|
120
190
|
#detachAnnounce(path) {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
191
|
+
const existing = this.#announced.get(path);
|
|
192
|
+
if (existing === undefined)
|
|
123
193
|
return;
|
|
124
|
-
if (count > 1) {
|
|
125
|
-
|
|
194
|
+
if (existing.count > 1) {
|
|
195
|
+
existing.count -= 1;
|
|
126
196
|
return;
|
|
127
197
|
}
|
|
128
198
|
this.#announced.delete(path);
|
|
@@ -130,19 +200,24 @@ export class Subscriber {
|
|
|
130
200
|
// would otherwise hand the dead generation to whoever consumes the path next.
|
|
131
201
|
this.#consumes.evict(path);
|
|
132
202
|
console.debug(`announced: broadcast=${path} active=false`);
|
|
133
|
-
for (const consumer of this.#announcedConsumers) {
|
|
134
|
-
|
|
135
|
-
if (suffix === null)
|
|
203
|
+
for (const [consumer, filter] of this.#announcedConsumers) {
|
|
204
|
+
if (!sees(filter, path))
|
|
136
205
|
continue;
|
|
206
|
+
const scope = filter.scope;
|
|
137
207
|
try {
|
|
138
|
-
consumer.append({
|
|
208
|
+
consumer.append({
|
|
209
|
+
prefix: path,
|
|
210
|
+
captures: scopeCaptures(scope, path),
|
|
211
|
+
kind: "retracted",
|
|
212
|
+
route: existing.route,
|
|
213
|
+
});
|
|
139
214
|
}
|
|
140
215
|
catch {
|
|
141
216
|
// Consumer already closed, will be cleaned up
|
|
142
217
|
}
|
|
143
218
|
}
|
|
144
219
|
}
|
|
145
|
-
async #runAnnounced(announced, prefix) {
|
|
220
|
+
async #runAnnounced(announced, prefix, hidden) {
|
|
146
221
|
const version = this.#session.version;
|
|
147
222
|
// Suffixes live on this stream, so a repeat is recognized as an update to the
|
|
148
223
|
// advertisement rather than a second one, which would leak the count.
|
|
@@ -171,11 +246,11 @@ export class Subscriber {
|
|
|
171
246
|
version === Version.DRAFT_16 ||
|
|
172
247
|
version === Version.DRAFT_17) {
|
|
173
248
|
await stream.writer.u53(SubscribeNamespaceLegacy.id);
|
|
174
|
-
await new SubscribeNamespaceLegacy({ namespace: prefix, requestId }).encode(stream.writer, version);
|
|
249
|
+
await new SubscribeNamespaceLegacy({ namespace: prefix, requestId, hidden }).encode(stream.writer, version);
|
|
175
250
|
}
|
|
176
251
|
else {
|
|
177
252
|
await stream.writer.u53(SubscribeNamespace.id);
|
|
178
|
-
await new SubscribeNamespace({ namespace: prefix, requestId }).encode(stream.writer, version);
|
|
253
|
+
await new SubscribeNamespace({ namespace: prefix, requestId, hidden }).encode(stream.writer, version);
|
|
179
254
|
}
|
|
180
255
|
console.debug(`subscribe_namespace written: requestId=${requestId}`);
|
|
181
256
|
// Read response
|
|
@@ -212,10 +287,15 @@ export class Subscriber {
|
|
|
212
287
|
this.#detachAnnounce(path);
|
|
213
288
|
continue;
|
|
214
289
|
}
|
|
215
|
-
// A repeat
|
|
216
|
-
|
|
290
|
+
// A repeat replaces the advertisement in place: HOP_PATH / ROUTE_COST
|
|
291
|
+
// can change without a NAMESPACE_DONE. Only the first is a new path.
|
|
292
|
+
const route = this.#route(entry.cluster);
|
|
293
|
+
if (live.has(path)) {
|
|
294
|
+
this.#updateAnnounce(path, route);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
217
297
|
live.add(path);
|
|
218
|
-
this.#attachAnnounce(path);
|
|
298
|
+
this.#attachAnnounce(path, route);
|
|
219
299
|
}
|
|
220
300
|
}
|
|
221
301
|
else if (msgType === SubscribeNamespaceEntryDone.id) {
|
|
@@ -246,7 +326,7 @@ export class Subscriber {
|
|
|
246
326
|
this.#session.close();
|
|
247
327
|
});
|
|
248
328
|
// Wait for either the read loop or the announced to close
|
|
249
|
-
await
|
|
329
|
+
await race([readLoop, announced.closed]);
|
|
250
330
|
// For v14/v15: send UnsubscribeNamespace before closing
|
|
251
331
|
if (version === Version.DRAFT_14 || version === Version.DRAFT_15) {
|
|
252
332
|
try {
|
|
@@ -308,7 +388,7 @@ export class Subscriber {
|
|
|
308
388
|
const consumer = new ConsumeBroadcast();
|
|
309
389
|
void (async () => {
|
|
310
390
|
for (;;) {
|
|
311
|
-
const request = await consumer.requested();
|
|
391
|
+
const request = await wireOf(consumer).requested();
|
|
312
392
|
if (!request)
|
|
313
393
|
break;
|
|
314
394
|
void this.#runSubscribe(path, request);
|
|
@@ -316,28 +396,43 @@ export class Subscriber {
|
|
|
316
396
|
})();
|
|
317
397
|
return consumer;
|
|
318
398
|
}
|
|
399
|
+
// The adapter is gone. If the transport has already closed, that close is the
|
|
400
|
+
// error. A still-open transport, such as a GOAWAY drain, has no peer code yet,
|
|
401
|
+
// so this does not wait for it.
|
|
402
|
+
async #closedSession() {
|
|
403
|
+
const quic = this.#quic;
|
|
404
|
+
if (!quic)
|
|
405
|
+
return new Error("session closed");
|
|
406
|
+
return Promise.race([
|
|
407
|
+
closeError(quic),
|
|
408
|
+
new Promise((resolve) => queueMicrotask(() => resolve(new Error("session closed")))),
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
319
411
|
async #runSubscribe(broadcast, request) {
|
|
320
412
|
const requestId = await this.#session.nextRequestId();
|
|
321
413
|
if (requestId === undefined) {
|
|
322
|
-
request.reject(
|
|
414
|
+
request.reject(await this.#closedSession());
|
|
323
415
|
return;
|
|
324
416
|
}
|
|
325
417
|
console.debug(`subscribe start: id=${requestId} broadcast=${broadcast} track=${request.name}`);
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// incoming object streams are routed into.
|
|
331
|
-
const producer = request.accept({ ordered: false });
|
|
418
|
+
// Keep the request pending until SUBSCRIBE_OK supplies immutable track metadata.
|
|
419
|
+
// Group streams already wait on the alias, so early data stays behind this response.
|
|
420
|
+
const producer = hooks.pendingTrackProducer(request);
|
|
421
|
+
const subscription = { track: producer, tail: new Tail() };
|
|
332
422
|
// Open the stream and wait for SUBSCRIBE_OK under a timeout. State
|
|
333
423
|
// flows back via `state` so the timeout path can clean up the stream
|
|
334
424
|
// and any registration if setup eventually finishes.
|
|
335
425
|
const state = {};
|
|
336
|
-
const setup = this.#openSubscribe(state, broadcast, request,
|
|
426
|
+
const setup = this.#openSubscribe(state, broadcast, request, subscription, requestId);
|
|
337
427
|
// The publisher can be serving before it answers, so waiting only on the response
|
|
338
428
|
// would miss the local side going away and leave it serving a track nobody reads.
|
|
339
429
|
// Demand returning before we commit is not abandonment, matching the serving loop.
|
|
340
430
|
const waitAbandoned = async () => {
|
|
431
|
+
// An info-only lookup attaches no subscriber yet still waits on SUBSCRIBE_OK for
|
|
432
|
+
// the track info, so only demand that arrived and then left is abandonment.
|
|
433
|
+
while (!producer.used.peek() && producer.closed.peek() === undefined) {
|
|
434
|
+
await Signal.race(producer.used, producer.closed);
|
|
435
|
+
}
|
|
341
436
|
for (;;) {
|
|
342
437
|
await producer.unused();
|
|
343
438
|
if (producer.closed.peek() !== undefined || !producer.used.peek())
|
|
@@ -347,7 +442,7 @@ export class Subscriber {
|
|
|
347
442
|
let stream;
|
|
348
443
|
let trackAlias;
|
|
349
444
|
try {
|
|
350
|
-
const result = await
|
|
445
|
+
const result = await race([
|
|
351
446
|
withTimeout(setup, SUBSCRIBE_OK_TIMEOUT_MS, `subscribe timed out after ${SUBSCRIBE_OK_TIMEOUT_MS}ms waiting for SUBSCRIBE_OK (browser stream limit reached?)`),
|
|
352
447
|
waitAbandoned(),
|
|
353
448
|
]);
|
|
@@ -358,8 +453,9 @@ export class Subscriber {
|
|
|
358
453
|
console.debug(`subscribe ok: id=${requestId} broadcast=${broadcast} track=${request.name}`);
|
|
359
454
|
}
|
|
360
455
|
catch (err) {
|
|
361
|
-
|
|
362
|
-
|
|
456
|
+
// A control request that timed out is not late content, so it carries its own code.
|
|
457
|
+
const e = err instanceof TimeoutError ? controlTimeout(err) : await sessionCause(this.#quic, err);
|
|
458
|
+
request.reject(e);
|
|
363
459
|
console.warn(`subscribe error: id=${requestId} broadcast=${broadcast} track=${request.name} error=${reason(e)}`);
|
|
364
460
|
// Runs now for whatever is already open, and again if `setup` settles late.
|
|
365
461
|
// Retirement is repeated because a late SUBSCRIBE_OK can register an alias after
|
|
@@ -367,7 +463,7 @@ export class Subscriber {
|
|
|
367
463
|
let torn = false;
|
|
368
464
|
const cleanup = async (afterSetup) => {
|
|
369
465
|
state.cancelled = true;
|
|
370
|
-
if (state.registeredAlias !== undefined && this.#aliases.retire(state.registeredAlias,
|
|
466
|
+
if (state.registeredAlias !== undefined && this.#aliases.retire(state.registeredAlias, subscription)) {
|
|
371
467
|
this.#timescales.delete(state.registeredAlias);
|
|
372
468
|
}
|
|
373
469
|
if (!state.stream || torn)
|
|
@@ -402,10 +498,10 @@ export class Subscriber {
|
|
|
402
498
|
const publisherEnded = Symbol("publisher");
|
|
403
499
|
const localEnded = Symbol("local");
|
|
404
500
|
const idle = Symbol("idle");
|
|
405
|
-
// Terminal conditions settle at most once (
|
|
406
|
-
//
|
|
407
|
-
const done =
|
|
408
|
-
stream.
|
|
501
|
+
// Terminal conditions settle at most once (PublishDone, track close = local
|
|
502
|
+
// unsubscribe); race them once so the demand loop doesn't re-subscribe each pass.
|
|
503
|
+
const done = race([
|
|
504
|
+
this.#runPublishDone(stream, subscription).then(() => publisherEnded),
|
|
409
505
|
producer.closed.then(() => localEnded),
|
|
410
506
|
]);
|
|
411
507
|
// Serve until a terminal condition fires or the last local subscriber leaves. The unused
|
|
@@ -413,22 +509,26 @@ export class Subscriber {
|
|
|
413
509
|
// down resumes on the same stream.
|
|
414
510
|
let terminal = localEnded;
|
|
415
511
|
for (;;) {
|
|
416
|
-
const reason = await
|
|
512
|
+
const reason = await race([done, producer.unused().then(() => idle)]);
|
|
417
513
|
if (reason === idle && producer.closed.peek() === undefined && producer.used.peek())
|
|
418
514
|
continue;
|
|
419
515
|
terminal = reason;
|
|
420
516
|
break;
|
|
421
517
|
}
|
|
518
|
+
// Close before the cancellation is written, not after: awaiting the write first
|
|
519
|
+
// reopens the window the demand re-check above just closed, and a subscriber that
|
|
520
|
+
// returned during it would be closed by this line. The lite subscriber closes
|
|
521
|
+
// straight out of its loop for the same reason.
|
|
522
|
+
producer.close();
|
|
422
523
|
// The publisher already ended the request, so there is nothing to cancel. Sending
|
|
423
524
|
// UNSUBSCRIBE here would name a request it has already torn down.
|
|
424
525
|
if (terminal !== publisherEnded)
|
|
425
526
|
await this.#cancelSubscribe(stream, requestId);
|
|
426
|
-
producer.close();
|
|
427
527
|
stream.close();
|
|
428
528
|
console.debug(`subscribe close: id=${requestId} broadcast=${broadcast} track=${request.name}`);
|
|
429
529
|
}
|
|
430
530
|
catch (err) {
|
|
431
|
-
const e =
|
|
531
|
+
const e = await sessionCause(this.#quic, err);
|
|
432
532
|
producer.close(e);
|
|
433
533
|
stream.abort(e);
|
|
434
534
|
console.warn(`subscribe error: id=${requestId} broadcast=${broadcast} track=${request.name} error=${reason(e)}`);
|
|
@@ -436,10 +536,36 @@ export class Subscriber {
|
|
|
436
536
|
finally {
|
|
437
537
|
// Only the owner tears down the alias metadata: a later subscription may have
|
|
438
538
|
// reclaimed the alias and installed its own timescale.
|
|
439
|
-
if (this.#aliases.retire(trackAlias,
|
|
539
|
+
if (this.#aliases.retire(trackAlias, subscription))
|
|
440
540
|
this.#timescales.delete(trackAlias);
|
|
441
541
|
}
|
|
442
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Read the PUBLISH_DONE that ends a subscription, then wait for the data streams it counts.
|
|
545
|
+
*
|
|
546
|
+
* An error status aborts the track with it. A clean one leaves streams in flight, since
|
|
547
|
+
* QUIC does not order them, so wait until the Stream Count many have been read, or a
|
|
548
|
+
* bounded grace for the ones that never arrive (the draft says to use a timeout). The count
|
|
549
|
+
* is only a hint: a peer may send 0 regardless, so 0 waits out the grace. A request
|
|
550
|
+
* stream that FINs without PUBLISH_DONE is a protocol violation.
|
|
551
|
+
*/
|
|
552
|
+
async #runPublishDone(stream, subscription) {
|
|
553
|
+
const version = this.#session.version;
|
|
554
|
+
if (await stream.reader.done())
|
|
555
|
+
throw new ProtocolViolation("subscribe stream ended without PUBLISH_DONE");
|
|
556
|
+
const typeId = await stream.reader.u53();
|
|
557
|
+
if (typeId !== PublishDone.id) {
|
|
558
|
+
throw new ProtocolViolation(`unexpected message on a subscription: 0x${typeId.toString(16)}`);
|
|
559
|
+
}
|
|
560
|
+
const done = await PublishDone.decode(stream.reader, version);
|
|
561
|
+
if (!publishDoneClean(done.statusCode, version)) {
|
|
562
|
+
throw new Error(`publish done: status=0x${done.statusCode.toString(16)} reason=${done.reasonPhrase}`);
|
|
563
|
+
}
|
|
564
|
+
const count = done.streamCount;
|
|
565
|
+
const { tail, track } = subscription;
|
|
566
|
+
const complete = () => count > 0n && BigInt(tail.streams) >= count;
|
|
567
|
+
await tail.settle(complete, track.closed);
|
|
568
|
+
}
|
|
443
569
|
/**
|
|
444
570
|
* Tell the publisher to stop serving a subscription we are walking away from.
|
|
445
571
|
*
|
|
@@ -465,7 +591,7 @@ export class Subscriber {
|
|
|
465
591
|
// `state` is populated as soon as the stream opens and again when the
|
|
466
592
|
// trackAlias is registered, so the caller can clean both up on timeout
|
|
467
593
|
// even before this promise settles.
|
|
468
|
-
async #openSubscribe(state, broadcast, request,
|
|
594
|
+
async #openSubscribe(state, broadcast, request, subscription, requestId) {
|
|
469
595
|
const version = this.#session.version;
|
|
470
596
|
state.stream = await this.#session.openBi();
|
|
471
597
|
// The timeout can fire while the open is still in flight, in which case cleanup ran
|
|
@@ -503,7 +629,7 @@ export class Subscriber {
|
|
|
503
629
|
const err = version === Version.DRAFT_14
|
|
504
630
|
? await SubscribeError.decode(state.stream.reader, version)
|
|
505
631
|
: await RequestError.decode(state.stream.reader, version);
|
|
506
|
-
reasonPhrase =
|
|
632
|
+
reasonPhrase = requestReason(err.errorCode, err.reasonPhrase, "subscribe", version);
|
|
507
633
|
}
|
|
508
634
|
}
|
|
509
635
|
catch {
|
|
@@ -513,8 +639,11 @@ export class Subscriber {
|
|
|
513
639
|
throw new Error(`SUBSCRIBE error: ${reasonPhrase}`);
|
|
514
640
|
}
|
|
515
641
|
const ok = await SubscribeOk.decode(state.stream.reader, version);
|
|
642
|
+
if (state.cancelled)
|
|
643
|
+
throw new Error("subscribe cancelled before acceptance");
|
|
644
|
+
request.accept({ priority: fromWire(ok.properties.priority ?? 128) });
|
|
516
645
|
try {
|
|
517
|
-
this.#aliases.set(ok.trackAlias,
|
|
646
|
+
this.#aliases.set(ok.trackAlias, subscription, { broadcast, name: request.name });
|
|
518
647
|
const timescale = ok.properties.timescale;
|
|
519
648
|
if (timescale !== undefined) {
|
|
520
649
|
this.#timescales.set(ok.trackAlias, timescale);
|
|
@@ -548,7 +677,7 @@ export class Subscriber {
|
|
|
548
677
|
await stream.writer.u53(RequestError.id);
|
|
549
678
|
await new RequestError({
|
|
550
679
|
requestId: msg.requestId,
|
|
551
|
-
errorCode:
|
|
680
|
+
errorCode: toRequestCode("uninterested", "publish_namespace", version),
|
|
552
681
|
reasonPhrase: "route loops back",
|
|
553
682
|
}).encode(stream.writer, version);
|
|
554
683
|
stream.close();
|
|
@@ -563,11 +692,14 @@ export class Subscriber {
|
|
|
563
692
|
const legacy = version === Version.DRAFT_14 || version === Version.DRAFT_15;
|
|
564
693
|
if (legacy && (this.#announced.has(path) || this.#legacyRequests.has(path))) {
|
|
565
694
|
console.warn("duplicate PublishNamespace");
|
|
695
|
+
// No draft registers a code for a duplicate, and this refusal is a draft-14/15
|
|
696
|
+
// implementation limit, which is what INTERNAL_ERROR describes.
|
|
697
|
+
const errorCode = toRequestCode("internal", "publish_namespace", version);
|
|
566
698
|
if (version === Version.DRAFT_14) {
|
|
567
699
|
await stream.writer.u53(PublishNamespaceError.id);
|
|
568
700
|
await new PublishNamespaceError({
|
|
569
701
|
requestId: msg.requestId,
|
|
570
|
-
errorCode
|
|
702
|
+
errorCode,
|
|
571
703
|
reasonPhrase: "duplicate namespace",
|
|
572
704
|
}).encode(stream.writer, version);
|
|
573
705
|
}
|
|
@@ -575,7 +707,7 @@ export class Subscriber {
|
|
|
575
707
|
await stream.writer.u53(RequestError.id);
|
|
576
708
|
await new RequestError({
|
|
577
709
|
requestId: msg.requestId,
|
|
578
|
-
errorCode
|
|
710
|
+
errorCode,
|
|
579
711
|
reasonPhrase: "duplicate namespace",
|
|
580
712
|
}).encode(stream.writer, version);
|
|
581
713
|
}
|
|
@@ -608,10 +740,12 @@ export class Subscriber {
|
|
|
608
740
|
// Only now is the advertisement ours to announce, for the reason above: a
|
|
609
741
|
// consumer reacting with a SUBSCRIBE must not interleave with that OK.
|
|
610
742
|
attached = true;
|
|
611
|
-
this.#attachAnnounce(path);
|
|
612
|
-
// An advertisement is updated in place
|
|
613
|
-
//
|
|
614
|
-
//
|
|
743
|
+
this.#attachAnnounce(path, this.#route(msg.cluster));
|
|
744
|
+
// An advertisement is updated in place with REQUEST_UPDATE on the stream that
|
|
745
|
+
// already carries it, so read until the stream ends rather than waiting on the
|
|
746
|
+
// close. Nothing else would deliver a re-parented route. What the peer holds
|
|
747
|
+
// is kept current, since an update carries only what changed.
|
|
748
|
+
let held = msg.cluster;
|
|
615
749
|
const done = version === Version.DRAFT_16 || legacy;
|
|
616
750
|
for (;;) {
|
|
617
751
|
if (await stream.reader.done())
|
|
@@ -621,32 +755,47 @@ export class Subscriber {
|
|
|
621
755
|
await PublishNamespaceDone.decode(stream.reader, version);
|
|
622
756
|
break;
|
|
623
757
|
}
|
|
624
|
-
|
|
758
|
+
// A repeated PUBLISH_NAMESPACE lands here too: a second request on the
|
|
759
|
+
// stream is the base draft's duplicate request ID.
|
|
760
|
+
if (typeId !== PublishNamespaceUpdate.id) {
|
|
625
761
|
throw new ProtocolViolation(`unexpected message on publish_namespace stream: 0x${typeId.toString(16)}`);
|
|
626
762
|
}
|
|
627
|
-
const update = await
|
|
628
|
-
// The
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
763
|
+
const update = await PublishNamespaceUpdate.decode(stream.reader, version);
|
|
764
|
+
// The parameters exist only on a session that negotiated the extension;
|
|
765
|
+
// anywhere else they are the peer's violation.
|
|
766
|
+
if (held === undefined) {
|
|
767
|
+
if (update.update.hops !== undefined || update.update.cost !== undefined) {
|
|
768
|
+
throw new ProtocolViolation("cluster parameters on a session that negotiated none");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
// A different original publisher applies in place too, as it does inline.
|
|
773
|
+
held = Cluster.apply(held, update.update);
|
|
633
774
|
}
|
|
634
|
-
// A path that now runs through us is unusable, so give it back.
|
|
635
|
-
// reading: this stream is the
|
|
636
|
-
// clean path arrives here or
|
|
637
|
-
|
|
775
|
+
// A path that now runs through us is unusable, so give it back. The update
|
|
776
|
+
// itself is accepted, and reading continues: this stream is the
|
|
777
|
+
// advertisement's only channel, so a later clean path arrives here or
|
|
778
|
+
// nowhere.
|
|
779
|
+
if (this.#reflected(held)) {
|
|
638
780
|
if (attached) {
|
|
639
781
|
attached = false;
|
|
640
782
|
console.debug(`publish_namespace now loops back, detaching: broadcast=${path}`);
|
|
641
783
|
this.#detachAnnounce(path);
|
|
642
784
|
}
|
|
643
|
-
continue;
|
|
644
785
|
}
|
|
645
|
-
|
|
646
|
-
|
|
786
|
+
else if (!attached) {
|
|
787
|
+
// Re-attach: a clean path replaced the reflected one we detached from.
|
|
647
788
|
attached = true;
|
|
648
|
-
this.#attachAnnounce(path);
|
|
789
|
+
this.#attachAnnounce(path, this.#route(held));
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
this.#updateAnnounce(path, this.#route(held));
|
|
649
793
|
}
|
|
794
|
+
// Nothing here can fail to apply, so every update is acknowledged. A leaf
|
|
795
|
+
// routes nothing, so a repricing changes nothing it holds; consumers still
|
|
796
|
+
// hear the new route so a forwarder can reprice.
|
|
797
|
+
await stream.writer.u53(RequestOk.id);
|
|
798
|
+
await new RequestOk({}).encode(stream.writer, version);
|
|
650
799
|
}
|
|
651
800
|
}
|
|
652
801
|
finally {
|
|
@@ -667,17 +816,17 @@ export class Subscriber {
|
|
|
667
816
|
*/
|
|
668
817
|
async runPublish(msg, stream) {
|
|
669
818
|
const version = this.#session.version;
|
|
670
|
-
//
|
|
671
|
-
//
|
|
819
|
+
// We decline the method itself rather than this particular track, which would be
|
|
820
|
+
// UNINTERESTED.
|
|
672
821
|
//
|
|
673
822
|
// The alias the message carries is deliberately not recorded. Nothing will ever bind
|
|
674
823
|
// it, and a rejected request has no lifetime of ours to hang the cleanup on.
|
|
675
|
-
const
|
|
824
|
+
const errorCode = toRequestCode("not_supported", "publish", version);
|
|
676
825
|
if (version === Version.DRAFT_14) {
|
|
677
826
|
await stream.writer.u53(PublishError.id);
|
|
678
827
|
const err = new PublishError({
|
|
679
828
|
requestId: msg.requestId,
|
|
680
|
-
errorCode
|
|
829
|
+
errorCode,
|
|
681
830
|
reasonPhrase: "publish not supported",
|
|
682
831
|
});
|
|
683
832
|
await err.encode(stream.writer, version);
|
|
@@ -686,7 +835,7 @@ export class Subscriber {
|
|
|
686
835
|
await stream.writer.u53(RequestError.id);
|
|
687
836
|
const err = new RequestError({
|
|
688
837
|
requestId: version === Version.DRAFT_15 || version === Version.DRAFT_16 ? msg.requestId : undefined,
|
|
689
|
-
errorCode
|
|
838
|
+
errorCode,
|
|
690
839
|
reasonPhrase: "publish not supported",
|
|
691
840
|
});
|
|
692
841
|
await err.encode(stream.writer, version);
|
|
@@ -702,48 +851,114 @@ export class Subscriber {
|
|
|
702
851
|
if (group.subGroupId !== 0) {
|
|
703
852
|
throw new Error("subgroups are not supported");
|
|
704
853
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
854
|
+
let subscription;
|
|
855
|
+
try {
|
|
856
|
+
// The control message establishing this alias can arrive after the data stream.
|
|
857
|
+
subscription = await this.#aliases.get(group.trackAlias);
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
const e = error(err);
|
|
861
|
+
// Ours: we cancelled the subscription and the publisher has not stopped yet.
|
|
862
|
+
// Anything else on this alias is the publisher sending data for a track it never
|
|
863
|
+
// acknowledged, which is worth seeing.
|
|
864
|
+
if (e instanceof RetiredTrackAlias) {
|
|
865
|
+
console.debug(`dropping group for a cancelled subscription: alias=${group.trackAlias}`);
|
|
866
|
+
}
|
|
867
|
+
stream.stop(e);
|
|
718
868
|
return;
|
|
719
869
|
}
|
|
720
|
-
const
|
|
870
|
+
const { track, tail } = subscription;
|
|
871
|
+
// Every data stream counts toward PUBLISH_DONE's Stream Count, even one dropped below.
|
|
872
|
+
const read = tail.open(group.groupId);
|
|
873
|
+
// Created on the first object rather than the header: an END_OF_TRACK at object 0
|
|
874
|
+
// means the group does not exist at all.
|
|
875
|
+
let producer;
|
|
876
|
+
const open = () => {
|
|
877
|
+
if (!producer) {
|
|
878
|
+
// The publisher contradicted its own end, which no later group can repair.
|
|
879
|
+
if (subscription.end !== undefined && group.groupId >= subscription.end) {
|
|
880
|
+
throw new ProtocolViolation(`group ${group.groupId} is at or past the declared end ${subscription.end}`);
|
|
881
|
+
}
|
|
882
|
+
producer = new netGroup.Producer(group.groupId);
|
|
883
|
+
track.writeGroup(producer);
|
|
884
|
+
}
|
|
885
|
+
return producer;
|
|
886
|
+
};
|
|
721
887
|
try {
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
888
|
+
// FIRST_OBJECT clear says this stream starts partway through the group, which the
|
|
889
|
+
// draft lets a publisher do to answer a filter. Nothing above here can use it: the
|
|
890
|
+
// objects that would arrive are not decodable without the missing head, and a group
|
|
891
|
+
// is the unit an application resyncs on. Drop it and pick up at the next group, the
|
|
892
|
+
// same degradation as a publisher that no longer holds the head.
|
|
893
|
+
//
|
|
894
|
+
// This only saves reading a stream we would throw away. The bit is the publisher's
|
|
895
|
+
// claim, so what is enforced is the object ids themselves: `Frame.decode` holds every
|
|
896
|
+
// object to starting at 0 and incrementing by 1, whatever the header said and on the
|
|
897
|
+
// drafts that have no such bit to read.
|
|
898
|
+
if (!group.flags.firstObject) {
|
|
899
|
+
console.debug(`dropping a group with no head: alias=${group.trackAlias} group=${group.groupId}`);
|
|
900
|
+
stream.stop(new Error("a group must start at object 0"));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// The alias binds after SUBSCRIBE_OK commits the track property; an omitted
|
|
904
|
+
// header priority inherits it (draft-21 section 10.4).
|
|
905
|
+
if (!group.flags.hasPriority)
|
|
906
|
+
group.publisherPriority = toWire((await track.info()).priority);
|
|
907
|
+
const decode = (c) => Frame.decode(c, group.flags, this.#timescales.get(group.trackAlias));
|
|
725
908
|
for (;;) {
|
|
726
|
-
|
|
727
|
-
|
|
909
|
+
// Every object already buffered is written without an await, so the reader wakes
|
|
910
|
+
// once per batch rather than once per object. Only the group's own stream ends it:
|
|
911
|
+
// a track that closes first has already closed (or aborted) this group through its
|
|
912
|
+
// cache.
|
|
913
|
+
const frame = stream.tryDecode(decode) ??
|
|
914
|
+
(await (producer
|
|
915
|
+
? race([stream.decodeMaybe(decode), producer.closed])
|
|
916
|
+
: stream.decodeMaybe(decode)));
|
|
917
|
+
if (!frame || frame instanceof Error)
|
|
728
918
|
break;
|
|
729
|
-
|
|
919
|
+
if (frame.endOfTrack) {
|
|
920
|
+
// No object at or past this location exists: after the group's last object
|
|
921
|
+
// the track ends with it, and at object 0 it ends before it.
|
|
922
|
+
const end = producer ? group.groupId + 1 : group.groupId;
|
|
923
|
+
producer?.close();
|
|
924
|
+
try {
|
|
925
|
+
track.finishAt(end);
|
|
926
|
+
}
|
|
927
|
+
catch (err) {
|
|
928
|
+
throw new ProtocolViolation(`invalid END_OF_TRACK: ${reason(error(err))}`);
|
|
929
|
+
}
|
|
930
|
+
subscription.end ??= end;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
730
933
|
if (frame.payload === undefined)
|
|
731
934
|
break;
|
|
732
|
-
|
|
935
|
+
open().writeFrame({ payload: frame.payload, timestamp: frame.timestamp ?? Timestamp.now() });
|
|
733
936
|
}
|
|
734
|
-
|
|
937
|
+
// A group with no objects still exists.
|
|
938
|
+
open().close();
|
|
735
939
|
}
|
|
736
940
|
catch (err) {
|
|
737
941
|
const e = error(err);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
942
|
+
if (e instanceof ProtocolViolation) {
|
|
943
|
+
// The publisher broke the track's end, which no later group can repair.
|
|
944
|
+
producer?.close(e);
|
|
945
|
+
track.close(e);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
// A stream that fails before its first object still names a group, which the
|
|
949
|
+
// reader sees fail rather than silently go missing.
|
|
950
|
+
try {
|
|
951
|
+
open().close(e);
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
// The track has already closed or ended below this group.
|
|
955
|
+
}
|
|
743
956
|
}
|
|
744
|
-
producer.close(e);
|
|
745
957
|
stream.stop(e);
|
|
746
958
|
}
|
|
959
|
+
finally {
|
|
960
|
+
read();
|
|
961
|
+
}
|
|
747
962
|
}
|
|
748
963
|
}
|
|
749
964
|
/**
|
|
@@ -752,16 +967,15 @@ export class Subscriber {
|
|
|
752
967
|
* group fetch, so `track.Consumer.fetchGroup()` is rejected.
|
|
753
968
|
*/
|
|
754
969
|
class ConsumeBroadcast extends broadcast.Consumer {
|
|
755
|
-
// biome-ignore lint/complexity/noUselessConstructor: widens the protected base constructor to public
|
|
756
970
|
constructor(state) {
|
|
757
971
|
super(state);
|
|
972
|
+
overrideBroadcastWire(this, {
|
|
973
|
+
fetchGroup: () => Promise.reject(new Error("fetch group is not supported for moq-transport")),
|
|
974
|
+
});
|
|
758
975
|
}
|
|
759
976
|
// Preserve the subclass when the consume cache shares this broadcast across callers.
|
|
760
977
|
clone() {
|
|
761
978
|
return new ConsumeBroadcast(this.shareState());
|
|
762
979
|
}
|
|
763
|
-
fetchGroup() {
|
|
764
|
-
return Promise.reject(new Error("fetch group is not supported for moq-transport"));
|
|
765
|
-
}
|
|
766
980
|
}
|
|
767
981
|
//# sourceMappingURL=subscriber.js.map
|