@norskvideo/moq-net 0.1.8 → 0.2.1
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/publisher.js
CHANGED
|
@@ -1,31 +1,65 @@
|
|
|
1
1
|
/* @ts-self-types="./publisher.d.ts" */
|
|
2
|
-
import { Signal } from "@norskvideo/moq-signals";
|
|
3
|
-
import { error, reason } from "../error.js";
|
|
2
|
+
import { race, Signal } from "@norskvideo/moq-signals";
|
|
3
|
+
import { controlTimeout, error, reason, StreamCode, StreamError } from "../error.js";
|
|
4
|
+
import { hiddenBelow, hooks, presented } from "../internal.js";
|
|
4
5
|
import * as Path from "../path.js";
|
|
5
6
|
import { Writer } from "../stream.js";
|
|
6
|
-
import {
|
|
7
|
+
import { Milli, Timescale } from "../time.js";
|
|
8
|
+
import { TimeoutError, withTimeout } from "../util/timeout.js";
|
|
9
|
+
import * as Varint from "../varint.js";
|
|
10
|
+
import { wireOf } from "../wire.js";
|
|
7
11
|
import * as Cluster from "./cluster.js";
|
|
8
|
-
import {
|
|
12
|
+
import { requestReason, toRequestCode } from "./error.js";
|
|
13
|
+
import { FetchError, FetchHeader } from "./fetch.js";
|
|
9
14
|
import * as Filter from "./filter.js";
|
|
10
15
|
import { FetchFrame, Frame, Group as GroupMessage } from "./object.js";
|
|
11
|
-
import { fromWire } from "./priority.js";
|
|
16
|
+
import { fromWire, toWire } from "./priority.js";
|
|
12
17
|
import * as Properties from "./properties.js";
|
|
13
|
-
import { PublishDone } from "./publish.js";
|
|
14
|
-
import { PublishNamespace, PublishNamespaceDone, PublishNamespaceOk } from "./publish_namespace.js";
|
|
18
|
+
import { PublishDone, PublishDoneStatus } from "./publish.js";
|
|
19
|
+
import { PublishNamespace, PublishNamespaceDone, PublishNamespaceOk, PublishNamespaceUpdate, } from "./publish_namespace.js";
|
|
15
20
|
import { RequestError, RequestOk } from "./request.js";
|
|
16
21
|
import { SubscribeError, SubscribeOk } from "./subscribe.js";
|
|
17
22
|
import { SubscribeNamespaceEntry, SubscribeNamespaceEntryDone, SubscribeNamespaceOk, } from "./subscribe_namespace.js";
|
|
18
|
-
import { TrackStatus } from "./track.js";
|
|
19
23
|
import { Version } from "./version.js";
|
|
20
24
|
/** First wait before re-offering a namespace the peer refused or we couldn't open for. */
|
|
21
25
|
const RETRY_BASE = 100;
|
|
22
26
|
/** Ceiling on that wait. The loop retries for the life of the session, so it must not spin. */
|
|
23
27
|
const RETRY_MAX = 5000;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/** Cluster parameters for one snapshot: the advertised hops, then our own id last. */
|
|
29
|
+
function clusterFor(base, route) {
|
|
30
|
+
if (!base)
|
|
31
|
+
return undefined;
|
|
32
|
+
const self = base.hops.at(-1);
|
|
33
|
+
if (self === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
const hops = route.hops.at(-1) === self ? [...route.hops] : [...route.hops, self];
|
|
36
|
+
return { hops, cost: route.cost.warm };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What it takes to move the peer from `held` to `next`.
|
|
40
|
+
*
|
|
41
|
+
* - `"same"`: the peer would decode the same advertisement. The cold cost, and the whole
|
|
42
|
+
* route without Cluster, never reach the wire, so a change there sends nothing.
|
|
43
|
+
* - `"restart"`: nothing is held, nothing is wanted, or a different broadcast, which is
|
|
44
|
+
* withdrawn and advertised again.
|
|
45
|
+
* - Otherwise the same broadcast at a new hop chain or warm cost: the cluster parameters
|
|
46
|
+
* the peer holds and the ones it should, for the target to update.
|
|
47
|
+
*/
|
|
48
|
+
function change(base, held, next) {
|
|
49
|
+
if (held === undefined || next === undefined || held.identity !== next.identity)
|
|
50
|
+
return "restart";
|
|
51
|
+
const from = clusterFor(base, held.route);
|
|
52
|
+
const to = clusterFor(base, next.route);
|
|
53
|
+
if (from === undefined || to === undefined)
|
|
54
|
+
return from === to ? "same" : "restart";
|
|
55
|
+
if (from.cost === to.cost && sameHops(from.hops, to.hops))
|
|
56
|
+
return "same";
|
|
57
|
+
return { from, to };
|
|
58
|
+
}
|
|
59
|
+
/** Whether two hop paths list the same hops in the same order. */
|
|
60
|
+
function sameHops(a, b) {
|
|
61
|
+
return a.length === b.length && a.every((hop, i) => hop === b[i]);
|
|
62
|
+
}
|
|
29
63
|
/**
|
|
30
64
|
* How long one advertisement may take to be answered. Matches the Rust publisher, and the
|
|
31
65
|
* peer accepting the stream is only half the exchange: one it never answers on holds the
|
|
@@ -46,44 +80,32 @@ export class Publisher {
|
|
|
46
80
|
#quic;
|
|
47
81
|
#session;
|
|
48
82
|
#requiresSolicitation;
|
|
83
|
+
#hidden;
|
|
84
|
+
// The origin this session serves, borrowed: it outlives the session, and closing the
|
|
85
|
+
// session leaves its broadcasts alone. The namespaces are advertised with an unsolicited
|
|
86
|
+
// PUBLISH_NAMESPACE (see {@link runPublishNamespaces}), or on request if the peer asked
|
|
87
|
+
// for that (see {@link runSubscribeNamespace}).
|
|
88
|
+
#advertised;
|
|
89
|
+
#publish;
|
|
49
90
|
// What every advertisement carries on a session that negotiated the MoQ Cluster
|
|
50
91
|
// extension: a hop path holding our own id, so the peer can tell that what it hears
|
|
51
92
|
// back came from us. `undefined` when nothing negotiated it.
|
|
52
93
|
#advert;
|
|
53
|
-
// Our published broadcasts.
|
|
54
|
-
// It's a signal so we can live update any subscribe_namespace streams.
|
|
55
|
-
#broadcasts = new Signal(new Map());
|
|
56
94
|
/**
|
|
57
95
|
* Creates a new Publisher instance.
|
|
58
96
|
*
|
|
59
97
|
* @internal
|
|
60
98
|
*/
|
|
61
|
-
constructor({ quic, session, requiresSolicitation, cluster, }) {
|
|
99
|
+
constructor({ quic, session, publish, requiresSolicitation, hidden = false, cluster, }) {
|
|
62
100
|
this.#quic = quic;
|
|
63
101
|
this.#session = session;
|
|
102
|
+
const origin = publish && wireOf(publish);
|
|
103
|
+
this.#advertised = origin?.advertised ?? new Signal(new Map());
|
|
104
|
+
this.#publish = publish;
|
|
64
105
|
this.#requiresSolicitation = requiresSolicitation;
|
|
106
|
+
this.#hidden = hidden;
|
|
65
107
|
this.#advert = Cluster.advertise(cluster);
|
|
66
108
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Publishes a broadcast with any associated tracks.
|
|
69
|
-
* The namespace is advertised with an unsolicited PUBLISH_NAMESPACE, or on request
|
|
70
|
-
* if the peer asked for that (see {@link runPublishNamespaces}).
|
|
71
|
-
*/
|
|
72
|
-
publish(path, broadcast) {
|
|
73
|
-
this.#broadcasts.mutate((broadcasts) => {
|
|
74
|
-
if (!broadcasts)
|
|
75
|
-
throw new Error("closed");
|
|
76
|
-
broadcasts.set(path, broadcast);
|
|
77
|
-
});
|
|
78
|
-
// Remove the broadcast from the lookup when it's closed, unless the path was republished.
|
|
79
|
-
void broadcast.closed.then(() => {
|
|
80
|
-
this.#broadcasts.mutate((broadcasts) => {
|
|
81
|
-
if (broadcasts?.get(path) === broadcast) {
|
|
82
|
-
broadcasts.delete(path);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
109
|
/**
|
|
88
110
|
* Handles an incoming SUBSCRIBE request on a bidi stream.
|
|
89
111
|
* Owns the full lifecycle: sends response, serves track data, waits for close.
|
|
@@ -93,15 +115,43 @@ export class Publisher {
|
|
|
93
115
|
async runSubscribe(msg, stream) {
|
|
94
116
|
const version = this.#session.version;
|
|
95
117
|
const name = msg.trackNamespace;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
let broadcast;
|
|
119
|
+
let refusal;
|
|
120
|
+
// Legal requests we can't honor are refused one at a time. A subscription that
|
|
121
|
+
// forwards nothing is only useful to a subscriber that later turns forwarding on,
|
|
122
|
+
// and serving a Range Filter unfiltered would deliver objects it excluded.
|
|
123
|
+
const unsupported = !msg.forward
|
|
124
|
+
? "FORWARD=0 not supported"
|
|
125
|
+
: msg.rangeFilters
|
|
126
|
+
? "range filters not supported"
|
|
127
|
+
: undefined;
|
|
128
|
+
if (unsupported) {
|
|
129
|
+
refusal = { errorCode: toRequestCode("not_supported", "subscribe", version), reasonPhrase: unsupported };
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
try {
|
|
133
|
+
broadcast =
|
|
134
|
+
this.#publish && (wireOf(this.#publish).local(name) ?? (await wireOf(this.#publish).demand(name)));
|
|
135
|
+
if (!broadcast) {
|
|
136
|
+
refusal = {
|
|
137
|
+
errorCode: toRequestCode("does_not_exist", "subscribe", version),
|
|
138
|
+
reasonPhrase: "broadcast not found",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const e = error(err);
|
|
144
|
+
const condition = e instanceof StreamError && e.code === StreamCode.NotFound ? "does_not_exist" : "internal";
|
|
145
|
+
refusal = { errorCode: toRequestCode(condition, "subscribe", version), reasonPhrase: reason(e) };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (refusal) {
|
|
99
149
|
if (version === Version.DRAFT_14) {
|
|
100
150
|
await stream.writer.u53(SubscribeError.id);
|
|
101
151
|
const err = new SubscribeError({
|
|
102
152
|
requestId: msg.requestId,
|
|
103
|
-
errorCode:
|
|
104
|
-
reasonPhrase:
|
|
153
|
+
errorCode: refusal.errorCode,
|
|
154
|
+
reasonPhrase: refusal.reasonPhrase,
|
|
105
155
|
});
|
|
106
156
|
await err.encode(stream.writer, version);
|
|
107
157
|
}
|
|
@@ -109,21 +159,36 @@ export class Publisher {
|
|
|
109
159
|
await stream.writer.u53(RequestError.id);
|
|
110
160
|
const err = new RequestError({
|
|
111
161
|
requestId: version === Version.DRAFT_15 || version === Version.DRAFT_16 ? msg.requestId : undefined,
|
|
112
|
-
errorCode:
|
|
113
|
-
reasonPhrase:
|
|
162
|
+
errorCode: refusal.errorCode,
|
|
163
|
+
reasonPhrase: refusal.reasonPhrase,
|
|
114
164
|
});
|
|
115
165
|
await err.encode(stream.writer, version);
|
|
116
166
|
}
|
|
117
167
|
stream.close();
|
|
118
168
|
return;
|
|
119
169
|
}
|
|
170
|
+
if (!broadcast)
|
|
171
|
+
return;
|
|
120
172
|
const priority = fromWire(msg.subscriberPriority);
|
|
121
|
-
const track = broadcast.subscribe(msg.trackName, {
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
const track = wireOf(broadcast).subscribe(msg.trackName, {
|
|
174
|
+
priority,
|
|
175
|
+
// moq-transport has no subscriber latency parameter. Keep everything the
|
|
176
|
+
// producer retained and let the receiving subscriber enforce its own budget.
|
|
177
|
+
// Keep the sentinel encodable if this demand crosses a Lite hop before the
|
|
178
|
+
// producer's retention bound is known.
|
|
179
|
+
maxAge: Milli(Varint.MAX_U53),
|
|
180
|
+
});
|
|
124
181
|
let cache;
|
|
125
182
|
try {
|
|
126
|
-
|
|
183
|
+
// Declaring the timescale is what opts the track into timestamps; every object
|
|
184
|
+
// Timestamp below is in these units.
|
|
185
|
+
const info = await track.info();
|
|
186
|
+
const timescale = info.timescale;
|
|
187
|
+
// The model ranks higher-first, the IETF wire lower-first. Every group this
|
|
188
|
+
// subscription serves carries the same publisher priority, which is what lets a
|
|
189
|
+
// relay prefer catalog and audio over video when it has no subscriber preference
|
|
190
|
+
// to go on.
|
|
191
|
+
const publisherPriority = toWire(info.priority);
|
|
127
192
|
// The filter and any fill are relative to the live edge, so snapshot it once: the
|
|
128
193
|
// fill ends exactly where a Next Object subscription begins, which is what lets
|
|
129
194
|
// the draft's current-group join (Next Object plus a StartGroup=1 fill) cover the
|
|
@@ -133,21 +198,25 @@ export class Publisher {
|
|
|
133
198
|
// The wire request tells an upstream what we need; the cursor is what actually
|
|
134
199
|
// trims this subscriber, since the producer fans every cached group out to every
|
|
135
200
|
// sink regardless. An absent start joins at the latest group, which is what
|
|
136
|
-
// moq-lite means by joining a live track.
|
|
201
|
+
// moq-lite means by joining a live track. The range's end is the inclusive last
|
|
202
|
+
// group; the model's `endGroup` is exclusive.
|
|
137
203
|
track.update({
|
|
138
204
|
priority,
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
maxAge: Milli(Varint.MAX_U53),
|
|
206
|
+
groups: {
|
|
207
|
+
start: range.start ? { included: Number(range.start.group) } : undefined,
|
|
208
|
+
end: range.end ? { included: Number(range.end.group) } : undefined,
|
|
209
|
+
},
|
|
141
210
|
});
|
|
142
211
|
const startGroup = range.start ? Number(range.start.group) : track.latest();
|
|
143
212
|
if (startGroup !== undefined)
|
|
144
|
-
track.
|
|
213
|
+
track.setGroups({ start: { included: startGroup } });
|
|
145
214
|
// A fill reads the group cache through its own consumer, independent of the
|
|
146
215
|
// subscription's cursor. Forked from this subscriber rather than resolved through
|
|
147
216
|
// the broadcast again: a dynamic serve is one request per peer subscription, so
|
|
148
217
|
// asking the broadcast would mint a second producer nobody has accepted.
|
|
149
218
|
const fill = msg.fill && Filter.isDraft20(version) ? fillRange(msg.fill, msg.filter, edge.largest) : undefined;
|
|
150
|
-
cache = fill && fill.kind !== "empty" ? track.fork({ priority }) : undefined;
|
|
219
|
+
cache = fill && fill.kind !== "empty" ? track.fork({ priority, maxAge: Milli(Varint.MAX_U53) }) : undefined;
|
|
151
220
|
// Send SUBSCRIBE_OK
|
|
152
221
|
await stream.writer.u53(SubscribeOk.id);
|
|
153
222
|
const ok = new SubscribeOk({
|
|
@@ -156,13 +225,20 @@ export class Publisher {
|
|
|
156
225
|
: undefined,
|
|
157
226
|
trackAlias: msg.requestId,
|
|
158
227
|
// Required once the track has content; a fill-requesting subscriber sizes its
|
|
159
|
-
// backfill against this.
|
|
160
|
-
|
|
228
|
+
// backfill against this. Below draft-20 round it down to the start of its
|
|
229
|
+
// group, matching what the subscription serves there: a draft-20 subscriber
|
|
230
|
+
// recovers a group's head with a FILL, which we serve, while older drafts
|
|
231
|
+
// recover it with a joining FETCH, which we do not, so naming a mid-group
|
|
232
|
+
// start would strand them.
|
|
233
|
+
largest: edge.largest &&
|
|
234
|
+
(Filter.isDraft20(version)
|
|
235
|
+
? { groupId: edge.largest.group, objectId: edge.largest.object }
|
|
236
|
+
: { groupId: edge.largest.group, objectId: 0n }),
|
|
161
237
|
properties: msg.propertiesWanted
|
|
162
238
|
? // Declaring the timescale is what opts the track into timestamps; every
|
|
163
239
|
// object Timestamp below is in these units. We serve the newest group
|
|
164
240
|
// first, matching moq-lite.
|
|
165
|
-
{ timescale, groupOrder: Properties.DESCENDING }
|
|
241
|
+
{ timescale, priority: publisherPriority, groupOrder: Properties.DESCENDING }
|
|
166
242
|
: // INCLUDE_PROPERTIES=0. The block stays present but empty, which also means
|
|
167
243
|
// the track opts out of timestamps for this subscriber.
|
|
168
244
|
{},
|
|
@@ -183,13 +259,17 @@ export class Publisher {
|
|
|
183
259
|
},
|
|
184
260
|
// A reset is always the peer.
|
|
185
261
|
() => unsubscribe());
|
|
262
|
+
// Every group started, until its stream finishes or resets, and the data streams
|
|
263
|
+
// opened for PUBLISH_DONE to report.
|
|
264
|
+
const groups = new Set();
|
|
265
|
+
const streams = { opened: 0 };
|
|
186
266
|
// Serve track groups, racing with stream close (= Unsubscribe)
|
|
187
267
|
const serving = (async () => {
|
|
188
268
|
for (;;) {
|
|
189
269
|
const group = await track.recvGroup();
|
|
190
270
|
if (!group)
|
|
191
271
|
return;
|
|
192
|
-
// Past the filter's end. Dropped here rather than through `
|
|
272
|
+
// Past the filter's end. Dropped here rather than through `setGroups`, which
|
|
193
273
|
// parks a capped group instead: this range is fixed for the life of the
|
|
194
274
|
// subscription, so a group above it is never coming back in, and holding
|
|
195
275
|
// one keeps the loop from ever ending. A producer that publishes beyond
|
|
@@ -198,14 +278,18 @@ export class Publisher {
|
|
|
198
278
|
group.close();
|
|
199
279
|
continue;
|
|
200
280
|
}
|
|
201
|
-
|
|
281
|
+
const task = this.#runGroup({
|
|
202
282
|
requestId: msg.requestId,
|
|
203
283
|
group,
|
|
204
284
|
timescale,
|
|
285
|
+
publisherPriority,
|
|
205
286
|
stamped: msg.propertiesWanted,
|
|
206
287
|
slice: groupSlice(range, group.sequence),
|
|
207
288
|
unsubscribed,
|
|
289
|
+
streams,
|
|
208
290
|
});
|
|
291
|
+
groups.add(task);
|
|
292
|
+
void task.finally(() => groups.delete(task));
|
|
209
293
|
}
|
|
210
294
|
})();
|
|
211
295
|
// The fill (when one was requested) runs alongside on its own fetch stream; its
|
|
@@ -219,15 +303,35 @@ export class Publisher {
|
|
|
219
303
|
timescale,
|
|
220
304
|
stamped: msg.propertiesWanted,
|
|
221
305
|
unsubscribed,
|
|
306
|
+
streams,
|
|
222
307
|
})
|
|
223
308
|
: Promise.resolve();
|
|
224
309
|
let publishError;
|
|
310
|
+
let ended = false;
|
|
225
311
|
try {
|
|
226
|
-
|
|
312
|
+
const served = Symbol("served");
|
|
313
|
+
ended =
|
|
314
|
+
(await race([Promise.all([serving, filling]).then(() => served), stream.reader.closed])) === served;
|
|
227
315
|
}
|
|
228
316
|
catch (err) {
|
|
229
317
|
publishError = error(err);
|
|
230
318
|
}
|
|
319
|
+
// PUBLISH_DONE waits until every stream this subscription will open is closed, as
|
|
320
|
+
// the draft requires, so its count is final. The subscriber leaving cancels the
|
|
321
|
+
// ones still queued instead.
|
|
322
|
+
await race([Promise.all(groups), unsubscribed]);
|
|
323
|
+
// Draft 14 on has no end location in PUBLISH_DONE: an END_OF_TRACK object is what
|
|
324
|
+
// tells the subscriber where the track ended.
|
|
325
|
+
const final = track.final();
|
|
326
|
+
if (ended && !publishError && final !== undefined) {
|
|
327
|
+
await this.#runEndOfTrack({
|
|
328
|
+
requestId: msg.requestId,
|
|
329
|
+
final,
|
|
330
|
+
publisherPriority,
|
|
331
|
+
unsubscribed,
|
|
332
|
+
streams,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
231
335
|
console.debug(`publish done: broadcast=${name} track=${track.name}`);
|
|
232
336
|
if (publishError) {
|
|
233
337
|
console.warn(`publish error: broadcast=${name} track=${track.name} error=${reason(publishError)}`);
|
|
@@ -240,7 +344,8 @@ export class Publisher {
|
|
|
240
344
|
requestId: version === Version.DRAFT_14 || version === Version.DRAFT_15 || version === Version.DRAFT_16
|
|
241
345
|
? msg.requestId
|
|
242
346
|
: undefined,
|
|
243
|
-
statusCode: publishError ?
|
|
347
|
+
statusCode: publishError ? PublishDoneStatus.INTERNAL_ERROR : PublishDoneStatus.TRACK_ENDED,
|
|
348
|
+
streamCount: BigInt(streams.opened),
|
|
244
349
|
reasonPhrase: publishError ? "internal error" : "track ended",
|
|
245
350
|
});
|
|
246
351
|
await done.encode(stream.writer, version);
|
|
@@ -270,7 +375,7 @@ export class Publisher {
|
|
|
270
375
|
* Runs a group and sends its frames using ObjectStream (Subgroup delivery mode).
|
|
271
376
|
*/
|
|
272
377
|
async #runGroup(options) {
|
|
273
|
-
const { requestId, group, timescale, stamped, slice, unsubscribed } = options;
|
|
378
|
+
const { requestId, group, timescale, publisherPriority, stamped, slice, unsubscribed, streams } = options;
|
|
274
379
|
try {
|
|
275
380
|
// One stream per group is faster than a peer at its limit can retire them, so this
|
|
276
381
|
// is the one path that doesn't wait for a slot: the transport would serve the opens
|
|
@@ -285,11 +390,12 @@ export class Publisher {
|
|
|
285
390
|
group.close(new Error("no stream slot"));
|
|
286
391
|
return;
|
|
287
392
|
}
|
|
393
|
+
streams.opened += 1;
|
|
288
394
|
const header = new GroupMessage({
|
|
289
395
|
trackAlias: requestId,
|
|
290
396
|
groupId: group.sequence,
|
|
291
397
|
subGroupId: 0,
|
|
292
|
-
publisherPriority
|
|
398
|
+
publisherPriority,
|
|
293
399
|
flags: {
|
|
294
400
|
// The object properties carry the timestamp, so there is nothing to write
|
|
295
401
|
// when the track declared no units to read one in.
|
|
@@ -303,8 +409,8 @@ export class Publisher {
|
|
|
303
409
|
firstObject: slice.skip === 0,
|
|
304
410
|
},
|
|
305
411
|
});
|
|
306
|
-
await header.encode(stream, this.#session.version);
|
|
307
412
|
try {
|
|
413
|
+
await hooks.guardGroup(group, () => header.encode(stream, this.#session.version));
|
|
308
414
|
// The first written object goes on the wire as its absolute id, so a trimmed
|
|
309
415
|
// head shows the true numbering rather than a silently renumbered group.
|
|
310
416
|
let first = true;
|
|
@@ -318,20 +424,25 @@ export class Publisher {
|
|
|
318
424
|
// outside the requested range, so stop without waiting for the group's end.
|
|
319
425
|
if (slice.until !== undefined && next >= slice.until)
|
|
320
426
|
break;
|
|
321
|
-
// Reading from the filter's start drops the objects below it
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const frame = await Promise.race([group.readFrameSequence({ from: slice.skip }), stream.closed]);
|
|
326
|
-
if (!frame)
|
|
427
|
+
// Reading from the filter's start drops the objects below it: they are outside
|
|
428
|
+
// the requested range, so skipping them is not a gap.
|
|
429
|
+
const read = await race([hooks.readGroupFrame(group, slice.skip), stream.closed]);
|
|
430
|
+
if (!read)
|
|
327
431
|
break;
|
|
328
|
-
next =
|
|
329
|
-
if (slice.until !== undefined &&
|
|
432
|
+
next = read.sequence + 1;
|
|
433
|
+
if (slice.until !== undefined && read.sequence >= slice.until) {
|
|
434
|
+
read.complete();
|
|
330
435
|
break;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const obj = new Frame({ payload: read.frame.payload, timestamp: read.frame.timestamp });
|
|
439
|
+
const delta = first ? read.sequence : 0;
|
|
440
|
+
first = false;
|
|
441
|
+
await hooks.guardGroup(group, () => obj.encode(stream, header.flags, timescale, this.#session.version, delta));
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
read.complete();
|
|
445
|
+
}
|
|
335
446
|
}
|
|
336
447
|
stream.close();
|
|
337
448
|
}
|
|
@@ -343,6 +454,43 @@ export class Publisher {
|
|
|
343
454
|
group.close();
|
|
344
455
|
}
|
|
345
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Mark the track's end with an END_OF_TRACK object on its own stream, at object 0 of the
|
|
459
|
+
* group that will never exist.
|
|
460
|
+
*
|
|
461
|
+
* The last group's stream has usually finished before the track ends, so the marker cannot
|
|
462
|
+
* ride on it. A failure only costs the subscriber the early boundary.
|
|
463
|
+
*/
|
|
464
|
+
async #runEndOfTrack(options) {
|
|
465
|
+
const { requestId, final, publisherPriority, unsubscribed, streams } = options;
|
|
466
|
+
const version = this.#session.version;
|
|
467
|
+
const stream = await Writer.tryOpen(this.#quic, { cancel: unsubscribed, version }).catch(() => undefined);
|
|
468
|
+
if (!stream)
|
|
469
|
+
return;
|
|
470
|
+
streams.opened += 1;
|
|
471
|
+
try {
|
|
472
|
+
const header = new GroupMessage({
|
|
473
|
+
trackAlias: requestId,
|
|
474
|
+
groupId: final,
|
|
475
|
+
subGroupId: 0,
|
|
476
|
+
publisherPriority,
|
|
477
|
+
flags: {
|
|
478
|
+
hasExtensions: false,
|
|
479
|
+
hasSubgroup: false,
|
|
480
|
+
hasSubgroupObject: false,
|
|
481
|
+
hasEnd: false,
|
|
482
|
+
hasPriority: true,
|
|
483
|
+
firstObject: true,
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
await header.encode(stream, version);
|
|
487
|
+
await new Frame({ endOfTrack: true }).encode(stream, header.flags, Timescale.MILLI, version);
|
|
488
|
+
stream.close();
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
stream.reset(error(err));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
346
494
|
/**
|
|
347
495
|
* Serve a draft-20 fill on its own fetch stream: the requested range, read from the
|
|
348
496
|
* group cache, capped at the Largest Object snapshot.
|
|
@@ -352,7 +500,7 @@ export class Publisher {
|
|
|
352
500
|
* fill-failure signal. Nothing here touches the subscription either way.
|
|
353
501
|
*/
|
|
354
502
|
async #runFill(options) {
|
|
355
|
-
const { requestId, fill, cache, timescale, stamped, unsubscribed } = options;
|
|
503
|
+
const { requestId, fill, cache, timescale, stamped, unsubscribed, streams } = options;
|
|
356
504
|
const version = this.#session.version;
|
|
357
505
|
// Everything is inside the try so the cache fork is released on every path out,
|
|
358
506
|
// including a transport failure while opening the stream.
|
|
@@ -365,6 +513,7 @@ export class Publisher {
|
|
|
365
513
|
console.debug(`fill stream failed to open: fill=${requestId}`);
|
|
366
514
|
return;
|
|
367
515
|
}
|
|
516
|
+
streams.opened += 1;
|
|
368
517
|
await stream.u53(FetchHeader.type);
|
|
369
518
|
await new FetchHeader({ requestId }).encode(stream, version);
|
|
370
519
|
if (fill.kind !== "group") {
|
|
@@ -413,13 +562,8 @@ export class Publisher {
|
|
|
413
562
|
// subscription, so stop without waiting for the group's end.
|
|
414
563
|
if (fill.until !== undefined && next >= fill.until)
|
|
415
564
|
break;
|
|
416
|
-
// Reading from the fill's start drops everything below it
|
|
417
|
-
|
|
418
|
-
const frame = await Promise.race([
|
|
419
|
-
group.readFrameSequence({ from: Number(fill.skip) }),
|
|
420
|
-
stream.closed,
|
|
421
|
-
cancelled,
|
|
422
|
-
]);
|
|
565
|
+
// Reading from the fill's start drops everything below it; see the same read in #runGroup.
|
|
566
|
+
const frame = await race([group.readFrameSequence({ from: Number(fill.skip) }), stream.closed, cancelled]);
|
|
423
567
|
if (left)
|
|
424
568
|
throw new Error("unsubscribed before the fill finished");
|
|
425
569
|
if (!frame)
|
|
@@ -435,11 +579,12 @@ export class Publisher {
|
|
|
435
579
|
/**
|
|
436
580
|
* Handles an incoming SUBSCRIBE_NAMESPACE on a bidi stream.
|
|
437
581
|
*
|
|
438
|
-
* This carries the advertisements
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* broadcast
|
|
442
|
-
*
|
|
582
|
+
* This carries the advertisements when the peer asked to be told on request (MoQ
|
|
583
|
+
* Solicit); otherwise {@link runPublishNamespaces} has already announced everything
|
|
584
|
+
* visible and repeating it here would leave the peer holding two sources for one
|
|
585
|
+
* broadcast, so only the hidden namespaces it may see ride here (MoQ Hidden).
|
|
586
|
+
* Draft-16+ streams Namespace entries inline; draft-14/15 predate those messages, so
|
|
587
|
+
* each advertisement is a PUBLISH_NAMESPACE request of its own.
|
|
443
588
|
*
|
|
444
589
|
* @internal
|
|
445
590
|
*/
|
|
@@ -463,35 +608,38 @@ export class Publisher {
|
|
|
463
608
|
});
|
|
464
609
|
await ok.encode(stream.writer, version);
|
|
465
610
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return await this.#advertise(Path.join(prefix, suffix), requests, refused);
|
|
477
|
-
}
|
|
611
|
+
// Hidden namespaces are left out unless the peer opted in (MoQ Hidden). Unless the
|
|
612
|
+
// peer asked to be told only on request, it has already heard everything visible
|
|
613
|
+
// from the empty prefix unasked, so this stream carries only what that hid.
|
|
614
|
+
const carries = (covered) => (!this.#hidden || msg.hidden || !hiddenBelow(prefix, covered)) &&
|
|
615
|
+
(this.#requiresSolicitation || (this.#hidden && hiddenBelow(Path.empty(), covered)));
|
|
616
|
+
// Inline entries always land, and the receiver treats a repeated NAMESPACE as a
|
|
617
|
+
// replacement, so repricing one is just sending it again, a new original
|
|
618
|
+
// publisher included, as Rust does. Only a legacy PUBLISH_NAMESPACE request can
|
|
619
|
+
// be declined.
|
|
620
|
+
const namespace = async (suffix, snap) => {
|
|
478
621
|
await stream.writer.u53(SubscribeNamespaceEntry.id);
|
|
479
|
-
|
|
480
|
-
|
|
622
|
+
const cluster = clusterFor(this.#advert, snap.route);
|
|
623
|
+
await new SubscribeNamespaceEntry({ suffix, cluster }).encode(stream.writer, version);
|
|
624
|
+
return "held";
|
|
481
625
|
};
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
await stream.writer.u53(SubscribeNamespaceEntryDone.id);
|
|
488
|
-
await new SubscribeNamespaceEntryDone({ suffix }).encode(stream.writer, version);
|
|
626
|
+
const target = legacy
|
|
627
|
+
? {
|
|
628
|
+
advertise: (suffix, snap) => this.#advertise(Path.join(prefix, suffix), requests, clusterFor(this.#advert, snap.route)),
|
|
629
|
+
update: (suffix, _snap, reprice) => this.#update(Path.join(prefix, suffix), requests, reprice),
|
|
630
|
+
withdraw: (suffix) => this.#withdraw(Path.join(prefix, suffix), requests),
|
|
489
631
|
}
|
|
490
|
-
|
|
491
|
-
|
|
632
|
+
: {
|
|
633
|
+
advertise: namespace,
|
|
634
|
+
update: namespace,
|
|
635
|
+
withdraw: async (suffix) => {
|
|
636
|
+
await stream.writer.u53(SubscribeNamespaceEntryDone.id);
|
|
637
|
+
await new SubscribeNamespaceEntryDone({ suffix }).encode(stream.writer, version);
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
// Keyed by suffix, as the entries name them.
|
|
641
|
+
const ns = { active: new Map(), refused: new Map(), offered: new Map() };
|
|
492
642
|
let retry = 0;
|
|
493
|
-
// What the peer refused, and whether coming back is worth anything.
|
|
494
|
-
const refused = new Map();
|
|
495
643
|
for (;;) {
|
|
496
644
|
// Subscribe BEFORE reconciling, for the same reason as
|
|
497
645
|
// {@link runPublishNamespaces}: a publish landing while an advertisement
|
|
@@ -499,51 +647,19 @@ export class Publisher {
|
|
|
499
647
|
// TODO Make a better helper within Signals.
|
|
500
648
|
let dispose;
|
|
501
649
|
const changed = new Promise((resolve) => {
|
|
502
|
-
dispose = this.#
|
|
650
|
+
dispose = this.#advertised.changed(resolve);
|
|
503
651
|
});
|
|
504
|
-
const
|
|
505
|
-
if (!
|
|
652
|
+
const advertised = this.#advertised.peek();
|
|
653
|
+
if (!advertised) {
|
|
506
654
|
dispose();
|
|
507
655
|
break;
|
|
508
656
|
}
|
|
509
|
-
const
|
|
510
|
-
for (const name of broadcasts.keys()) {
|
|
511
|
-
const suffix = Path.stripPrefix(prefix, name);
|
|
512
|
-
if (suffix === null)
|
|
513
|
-
continue;
|
|
514
|
-
updated.add(suffix);
|
|
515
|
-
}
|
|
516
|
-
// A namespace that is gone takes its refusal with it, so re-announcing the
|
|
517
|
-
// path offers it again.
|
|
518
|
-
const live = new Set([...updated].map((suffix) => Path.join(prefix, suffix)));
|
|
519
|
-
for (const path of [...refused.keys()]) {
|
|
520
|
-
if (!live.has(path))
|
|
521
|
-
refused.delete(path);
|
|
522
|
-
}
|
|
523
|
-
// Track what the peer holds rather than what we attempted: a declined
|
|
524
|
-
// advertisement stays out of `held`, so the next turn retries it instead of
|
|
525
|
-
// believing the namespace is already up.
|
|
526
|
-
const held = new Set(active);
|
|
527
|
-
for (const added of updated.difference(active)) {
|
|
528
|
-
if (this.#offerable(Path.join(prefix, added), refused)) {
|
|
529
|
-
if (await advertise(added))
|
|
530
|
-
held.add(added);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
for (const removed of active.difference(updated)) {
|
|
534
|
-
await withdraw(removed);
|
|
535
|
-
held.delete(removed);
|
|
536
|
-
}
|
|
537
|
-
active = held;
|
|
538
|
-
// Whatever we wanted up and could not get up, as {@link runPublishNamespaces}
|
|
539
|
-
// does: only a legacy request can be declined, and nothing about the peer
|
|
540
|
-
// starting to answer raises a signal this loop is watching.
|
|
541
|
-
const outstanding = [...updated.difference(active)].some((suffix) => this.#pending(Path.join(prefix, suffix), refused));
|
|
657
|
+
const outstanding = await this.#reconcile(ns, presented(prefix, advertised, carries), target);
|
|
542
658
|
retry = outstanding ? Math.min(retry ? retry * 2 : RETRY_BASE, RETRY_MAX) : 0;
|
|
543
659
|
// Wait for the next change, or for the peer to unsubscribe.
|
|
544
660
|
const next = await (retry
|
|
545
|
-
?
|
|
546
|
-
:
|
|
661
|
+
? race([changed, stream.reader.closed, retryAfter(retry).then(() => advertised)])
|
|
662
|
+
: race([changed, stream.reader.closed]));
|
|
547
663
|
dispose();
|
|
548
664
|
if (!next)
|
|
549
665
|
break;
|
|
@@ -581,11 +697,22 @@ export class Publisher {
|
|
|
581
697
|
}
|
|
582
698
|
// The open PUBLISH_NAMESPACE request per advertised path.
|
|
583
699
|
const requests = new Map();
|
|
700
|
+
const target = {
|
|
701
|
+
advertise: (path, snap) => this.#advertise(path, requests, clusterFor(this.#advert, snap.route)),
|
|
702
|
+
update: (path, _snap, reprice) => this.#update(path, requests, reprice),
|
|
703
|
+
withdraw: (path) => this.#withdraw(path, requests),
|
|
704
|
+
};
|
|
705
|
+
// The origin outlives the session and its signal never ends, so this loop needs the
|
|
706
|
+
// session's own ending to stop: without it a closed connection leaves the loop parked
|
|
707
|
+
// on a shared origin forever, waking on someone else's publish to fail on a dead
|
|
708
|
+
// transport. `runSubscribeNamespace` gets this from the stream the peer asked on; an
|
|
709
|
+
// unsolicited loop has no stream of its own, so the session is what it watches.
|
|
710
|
+
const closed = this.#quic.closed.then(() => undefined, () => undefined);
|
|
711
|
+
let dispose;
|
|
584
712
|
try {
|
|
585
|
-
|
|
713
|
+
// Keyed by path.
|
|
714
|
+
const ns = { active: new Map(), refused: new Map(), offered: new Map() };
|
|
586
715
|
let retry = 0;
|
|
587
|
-
// What the peer refused, and whether coming back is worth anything.
|
|
588
|
-
const refused = new Map();
|
|
589
716
|
for (;;) {
|
|
590
717
|
// Subscribe BEFORE reconciling. Each advertisement below waits a round trip
|
|
591
718
|
// for the peer's reply, and a publish that lands in that window notifies
|
|
@@ -593,43 +720,28 @@ export class Publisher {
|
|
|
593
720
|
// through it and leave the namespace unadvertised until something unrelated
|
|
594
721
|
// changed.
|
|
595
722
|
// TODO Make a better helper within Signals.
|
|
596
|
-
let dispose;
|
|
597
723
|
const changed = new Promise((resolve) => {
|
|
598
|
-
dispose = this.#
|
|
724
|
+
dispose = this.#advertised.changed(resolve);
|
|
599
725
|
});
|
|
600
|
-
const
|
|
601
|
-
if (!
|
|
602
|
-
dispose();
|
|
726
|
+
const advertised = this.#advertised.peek();
|
|
727
|
+
if (!advertised) {
|
|
728
|
+
dispose?.();
|
|
603
729
|
break;
|
|
604
730
|
}
|
|
605
|
-
const updated = new
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
for (const added of updated.difference(active)) {
|
|
613
|
-
if (this.#offerable(added, refused)) {
|
|
614
|
-
await this.#advertise(added, requests, refused);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
for (const removed of active.difference(updated)) {
|
|
618
|
-
await this.#withdraw(removed, requests);
|
|
731
|
+
const updated = new Map();
|
|
732
|
+
for (const [covered, candidates] of advertised) {
|
|
733
|
+
// Only peers that declared MoQ Hidden filter unsolicited discovery.
|
|
734
|
+
if ((this.#hidden && hiddenBelow(Path.empty(), covered)) || candidates.length === 0)
|
|
735
|
+
continue;
|
|
736
|
+
updated.set(covered, candidates[0]);
|
|
619
737
|
}
|
|
620
|
-
|
|
621
|
-
// leaves no request behind, so it stays outstanding below.
|
|
622
|
-
active = new Set(requests.keys());
|
|
623
|
-
// Whatever we wanted up and could not get up. Stream credit freeing, a
|
|
624
|
-
// transient failure clearing, or the peer starting to answer raises no
|
|
625
|
-
// signal of its own, so the only way back is to ask again on a timer.
|
|
626
|
-
const outstanding = [...updated.difference(active)].some((path) => this.#pending(path, refused));
|
|
738
|
+
const outstanding = await this.#reconcile(ns, updated, target);
|
|
627
739
|
retry = outstanding ? Math.min(retry ? retry * 2 : RETRY_BASE, RETRY_MAX) : 0;
|
|
628
740
|
// Wait for the next change, which has already fired if one landed above.
|
|
629
741
|
const next = await (retry
|
|
630
|
-
?
|
|
631
|
-
: changed);
|
|
632
|
-
dispose();
|
|
742
|
+
? race([changed, closed, retryAfter(retry).then(() => advertised)])
|
|
743
|
+
: race([changed, closed]));
|
|
744
|
+
dispose?.();
|
|
633
745
|
if (!next)
|
|
634
746
|
break;
|
|
635
747
|
}
|
|
@@ -640,20 +752,86 @@ export class Publisher {
|
|
|
640
752
|
console.warn(`publish_namespace loop failed: ${reason(error(err))}`);
|
|
641
753
|
}
|
|
642
754
|
finally {
|
|
755
|
+
dispose?.();
|
|
643
756
|
// Close out every open PUBLISH_NAMESPACE request.
|
|
644
757
|
for (const path of [...requests.keys()]) {
|
|
645
758
|
await this.#withdraw(path, requests);
|
|
646
759
|
}
|
|
647
760
|
}
|
|
648
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Bring the peer's view in line with `updated`, one namespace at a time, and report
|
|
764
|
+
* whether anything it should hold is still missing and worth coming back for.
|
|
765
|
+
*
|
|
766
|
+
* Nothing the peer cannot see is sent, a reprice updates the namespace in place, and
|
|
767
|
+
* anything else is withdrawn first so a republish reads as withdraw-then-advertise.
|
|
768
|
+
* `ns` records what the peer holds rather than what we attempted: a declined
|
|
769
|
+
* advertisement or update stays out of it, so the next turn offers it fresh instead
|
|
770
|
+
* of believing it is already up.
|
|
771
|
+
*/
|
|
772
|
+
async #reconcile(ns, updated, target) {
|
|
773
|
+
// A namespace that is gone, or that a republish replaced, takes its refusal with
|
|
774
|
+
// it: the peer refused a broadcast, not a path forever, so a different one at the
|
|
775
|
+
// same path is offered again. The origin swaps the front in one mutation, so the
|
|
776
|
+
// path never leaves `updated` and only the front says they differ. Rust gets this
|
|
777
|
+
// by rebuilding the watched entry.
|
|
778
|
+
for (const key of [...ns.refused.keys()]) {
|
|
779
|
+
const snap = updated.get(key);
|
|
780
|
+
if (snap === undefined || ns.offered.get(key) !== snap.identity) {
|
|
781
|
+
ns.refused.delete(key);
|
|
782
|
+
ns.offered.delete(key);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const held = new Map(ns.active);
|
|
786
|
+
for (const [key, snap] of ns.active) {
|
|
787
|
+
if (change(this.#advert, snap, updated.get(key)) !== "restart")
|
|
788
|
+
continue;
|
|
789
|
+
await target.withdraw(key);
|
|
790
|
+
held.delete(key);
|
|
791
|
+
}
|
|
792
|
+
for (const [key, snap] of updated) {
|
|
793
|
+
const prev = held.get(key);
|
|
794
|
+
const diff = change(this.#advert, prev, snap);
|
|
795
|
+
if (diff === "same")
|
|
796
|
+
continue;
|
|
797
|
+
let answer;
|
|
798
|
+
if (prev !== undefined && diff !== "restart") {
|
|
799
|
+
answer = await target.update(key, snap, diff);
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
// A fresh offer waits for what a refusal asked for, whatever brought us back.
|
|
803
|
+
if (!this.#offerable(key, ns.refused))
|
|
804
|
+
continue;
|
|
805
|
+
answer = await target.advertise(key, snap);
|
|
806
|
+
}
|
|
807
|
+
// `offered` pairs with `refused`, so a held or withdrawn path leaves nothing behind.
|
|
808
|
+
if (answer === "held") {
|
|
809
|
+
held.set(key, snap);
|
|
810
|
+
ns.refused.delete(key);
|
|
811
|
+
ns.offered.delete(key);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
held.delete(key);
|
|
815
|
+
if (answer !== "dropped") {
|
|
816
|
+
ns.refused.set(key, answer);
|
|
817
|
+
ns.offered.set(key, snap.identity);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
ns.active = held;
|
|
822
|
+
// Whatever we wanted up and could not get up. Stream credit freeing, a transient
|
|
823
|
+
// failure clearing, or the peer starting to answer raises no signal of its own, so
|
|
824
|
+
// the only way back is to ask again on a timer.
|
|
825
|
+
return [...updated].some(([key, snap]) => change(this.#advert, held.get(key), snap) !== "same" && this.#pending(key, ns.refused));
|
|
826
|
+
}
|
|
649
827
|
/**
|
|
650
828
|
* Whether a namespace may be offered to the peer right now.
|
|
651
829
|
*
|
|
652
830
|
* A peer that asked never to be offered it again means it, whatever brought us back;
|
|
653
831
|
* one that named a minimum wait gets it, even when our own backoff comes round sooner.
|
|
654
832
|
*/
|
|
655
|
-
#offerable(
|
|
656
|
-
const entry = refused.get(
|
|
833
|
+
#offerable(key, refused) {
|
|
834
|
+
const entry = refused.get(key);
|
|
657
835
|
if (entry === undefined)
|
|
658
836
|
return true;
|
|
659
837
|
return entry !== "never" && Date.now() >= entry;
|
|
@@ -666,21 +844,31 @@ export class Publisher {
|
|
|
666
844
|
* gating the timer on offerable instead would disarm it for exactly the wait it is
|
|
667
845
|
* supposed to be counting. Only a refusal that forbids retrying ends it.
|
|
668
846
|
*/
|
|
669
|
-
#pending(
|
|
670
|
-
return refused.get(
|
|
847
|
+
#pending(key, refused) {
|
|
848
|
+
return refused.get(key) !== "never";
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* What a REQUEST_ERROR's retry interval, in milliseconds, says about re-offering the
|
|
852
|
+
* namespace.
|
|
853
|
+
*
|
|
854
|
+
* Draft-14/15 carry no such field, so a 0 there says nothing and our own backoff
|
|
855
|
+
* stands; everywhere else 0 means the peer does not want this again.
|
|
856
|
+
*/
|
|
857
|
+
#refusal(err) {
|
|
858
|
+
const version = this.#session.version;
|
|
859
|
+
if (version === Version.DRAFT_14 || version === Version.DRAFT_15)
|
|
860
|
+
return "dropped";
|
|
861
|
+
return err.retryInterval === 0n ? "never" : Date.now() + Number(err.retryInterval);
|
|
671
862
|
}
|
|
672
863
|
/**
|
|
673
864
|
* Advertise one namespace on its own PUBLISH_NAMESPACE request. A declined request
|
|
674
865
|
* is logged and skipped: a peer that wants none of this rejects each one and stays
|
|
675
866
|
* connected.
|
|
676
|
-
*
|
|
677
|
-
* `refused` records what a refusal said about coming back, so a peer that asked not to
|
|
678
|
-
* be offered a namespace again is not re-offered it by the retry above.
|
|
679
867
|
*/
|
|
680
|
-
async #advertise(path, requests,
|
|
868
|
+
async #advertise(path, requests, cluster = this.#advert) {
|
|
681
869
|
const requestId = await this.#session.nextRequestId();
|
|
682
870
|
if (requestId === undefined)
|
|
683
|
-
return
|
|
871
|
+
return "dropped";
|
|
684
872
|
// Opening is inside the try because it fails like any other advertisement does:
|
|
685
873
|
// a peer out of stream credit rejects here, and letting that escape would unwind
|
|
686
874
|
// the announce loop and take discovery down for the whole session, rather than
|
|
@@ -693,22 +881,18 @@ export class Publisher {
|
|
|
693
881
|
// advertisements, so a peer that takes the stream and answers nothing strands
|
|
694
882
|
// every publish and withdrawal behind it. Timing out costs this namespace a
|
|
695
883
|
// turn and leaves it outstanding, which the retry above re-offers.
|
|
696
|
-
await withTimeout((async () => {
|
|
884
|
+
const answer = await withTimeout((async () => {
|
|
697
885
|
await stream.writer.u53(PublishNamespace.id);
|
|
698
|
-
const msg = new PublishNamespace({ requestId, trackNamespace: path, cluster
|
|
886
|
+
const msg = new PublishNamespace({ requestId, trackNamespace: path, cluster });
|
|
699
887
|
await msg.encode(stream.writer, this.#session.version);
|
|
700
888
|
// Read response (RequestOk and PublishNamespaceOk share 0x07)
|
|
701
889
|
const respTypeId = await stream.reader.u53();
|
|
702
890
|
if (respTypeId === RequestError.id) {
|
|
703
|
-
// The peer named how long to stay away, in milliseconds. Draft-14/15
|
|
704
|
-
// carry no such field, so a 0 there says nothing and our own backoff
|
|
705
|
-
// stands; everywhere else 0 means it does not want this again.
|
|
706
891
|
const err = await RequestError.decode(stream.reader, this.#session.version);
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
throw new Error(`PublishNamespace rejected: ${err.errorCode} ${err.reasonPhrase}`);
|
|
892
|
+
const why = requestReason(err.errorCode, err.reasonPhrase, "publish_namespace", this.#session.version);
|
|
893
|
+
console.warn(`announce refused: broadcast=${path} error=${why}`);
|
|
894
|
+
stream.abort(new Error(`PublishNamespace rejected: ${why}`));
|
|
895
|
+
return this.#refusal(err);
|
|
712
896
|
}
|
|
713
897
|
if (respTypeId !== RequestOk.id) {
|
|
714
898
|
throw new Error(`PublishNamespace rejected: typeId=0x${respTypeId.toString(16)}`);
|
|
@@ -720,16 +904,78 @@ export class Publisher {
|
|
|
720
904
|
else {
|
|
721
905
|
await RequestOk.decode(stream.reader, this.#session.version);
|
|
722
906
|
}
|
|
907
|
+
return "held";
|
|
723
908
|
})(), ADVERTISE_TIMEOUT_MS, `advertisement timed out after ${ADVERTISE_TIMEOUT_MS}ms waiting for the peer's answer`);
|
|
724
|
-
|
|
725
|
-
|
|
909
|
+
if (answer === "held")
|
|
910
|
+
requests.set(path, { path, requestId, stream });
|
|
911
|
+
return answer;
|
|
726
912
|
}
|
|
727
913
|
catch (err) {
|
|
728
|
-
|
|
914
|
+
// The peer never answered the advertisement: a control timeout, not late content.
|
|
915
|
+
const e = err instanceof TimeoutError ? controlTimeout(err) : error(err);
|
|
729
916
|
console.warn(`announce failed: broadcast=${path} error=${reason(e)}`);
|
|
730
917
|
request?.abort(e);
|
|
731
|
-
return
|
|
918
|
+
return "dropped";
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Update a namespace the peer holds: REQUEST_UPDATE on the request that carries it,
|
|
923
|
+
* with only the parameters that changed, then its answer.
|
|
924
|
+
*
|
|
925
|
+
* Waiting for the answer keeps one update outstanding per stream, which satisfies any
|
|
926
|
+
* MAX_REQUEST_UPDATES the peer set without reading it; a change landing meanwhile waits
|
|
927
|
+
* for the next turn. The answer decides whether the peer still holds the namespace: a
|
|
928
|
+
* REQUEST_ERROR closes the peer's side and withdraws the advertisement, so ours is
|
|
929
|
+
* finished too, and an unanswered update is dropped abruptly, since a peer that ignored
|
|
930
|
+
* it cannot be assumed to hold either price. Either way the retry re-offers it fresh.
|
|
931
|
+
*
|
|
932
|
+
* A different original publisher is an update like any other: the receiver drains what
|
|
933
|
+
* it already serves from the old one and never splices the two.
|
|
934
|
+
*/
|
|
935
|
+
async #update(path, requests, { from, to }) {
|
|
936
|
+
const request = requests.get(path);
|
|
937
|
+
if (!request)
|
|
938
|
+
return "dropped";
|
|
939
|
+
const update = {
|
|
940
|
+
hops: sameHops(from.hops, to.hops) ? undefined : to.hops,
|
|
941
|
+
cost: from.cost === to.cost ? undefined : to.cost,
|
|
942
|
+
};
|
|
943
|
+
const version = this.#session.version;
|
|
944
|
+
let answer;
|
|
945
|
+
try {
|
|
946
|
+
const requestId = await this.#session.nextRequestId();
|
|
947
|
+
if (requestId === undefined)
|
|
948
|
+
throw new Error("no request ID for the update");
|
|
949
|
+
answer = await withTimeout((async () => {
|
|
950
|
+
const { writer, reader } = request.stream;
|
|
951
|
+
await writer.u53(PublishNamespaceUpdate.id);
|
|
952
|
+
await new PublishNamespaceUpdate({ requestId, update }).encode(writer, version);
|
|
953
|
+
const typeId = await reader.u53();
|
|
954
|
+
if (typeId === RequestError.id) {
|
|
955
|
+
const err = await RequestError.decode(reader, version);
|
|
956
|
+
const why = requestReason(err.errorCode, err.reasonPhrase, "publish_namespace", version);
|
|
957
|
+
console.warn(`publish_namespace update refused: broadcast=${path} error=${why}`);
|
|
958
|
+
return this.#refusal(err);
|
|
959
|
+
}
|
|
960
|
+
if (typeId !== RequestOk.id) {
|
|
961
|
+
throw new Error(`unexpected answer to the update: typeId=0x${typeId.toString(16)}`);
|
|
962
|
+
}
|
|
963
|
+
await RequestOk.decode(reader, version);
|
|
964
|
+
return "held";
|
|
965
|
+
})(), ADVERTISE_TIMEOUT_MS, `update timed out after ${ADVERTISE_TIMEOUT_MS}ms waiting for the peer's answer`);
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
const e = err instanceof TimeoutError ? controlTimeout(err) : error(err);
|
|
969
|
+
console.warn(`publish_namespace update failed: broadcast=${path} error=${reason(e)}`);
|
|
970
|
+
requests.delete(path);
|
|
971
|
+
request.stream.abort(e);
|
|
972
|
+
return "dropped";
|
|
732
973
|
}
|
|
974
|
+
// The peer already closed out a refused request, and its STOP_SENDING can reject the
|
|
975
|
+
// wait on our FIN. That is the withdrawal finishing, not a reason to end the loop.
|
|
976
|
+
if (answer !== "held")
|
|
977
|
+
await this.#withdraw(path, requests).catch(() => undefined);
|
|
978
|
+
return answer;
|
|
733
979
|
}
|
|
734
980
|
/**
|
|
735
981
|
* Close out a namespace's PUBLISH_NAMESPACE request with PUBLISH_NAMESPACE_DONE.
|
|
@@ -753,8 +999,14 @@ export class Publisher {
|
|
|
753
999
|
catch {
|
|
754
1000
|
// Stream might already be closed
|
|
755
1001
|
}
|
|
1002
|
+
request.stream.close();
|
|
1003
|
+
return;
|
|
756
1004
|
}
|
|
757
1005
|
request.stream.close();
|
|
1006
|
+
// Wait for transport acknowledgment before opening a replacement request.
|
|
1007
|
+
// This assumes the peer processes the withdrawal by then; FIN acknowledgment
|
|
1008
|
+
// does not itself acknowledge application processing across streams.
|
|
1009
|
+
await request.stream.writer.closed;
|
|
758
1010
|
}
|
|
759
1011
|
/**
|
|
760
1012
|
* Handles an incoming TRACK_STATUS_REQUEST on a bidi stream.
|
|
@@ -762,42 +1014,38 @@ export class Publisher {
|
|
|
762
1014
|
* @internal
|
|
763
1015
|
*/
|
|
764
1016
|
async runTrackStatusRequest(msg, stream) {
|
|
1017
|
+
// TRACK_STATUS_ERROR is 0x0f on draft-14.
|
|
1018
|
+
await this.#refuseUnsupported(stream, msg.requestId, "track_status", 0x0f, "TRACK_STATUS is not supported");
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Handles an incoming FETCH on a bidi stream. We serve none.
|
|
1022
|
+
*
|
|
1023
|
+
* @internal
|
|
1024
|
+
*/
|
|
1025
|
+
async runFetch(msg, stream) {
|
|
1026
|
+
await this.#refuseUnsupported(stream, msg.requestId, "fetch", FetchError.id, "FETCH is not supported");
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Refuse a request NOT_SUPPORTED. Draft-14 gives each request its own error message,
|
|
1030
|
+
* `errorId14`, with the SUBSCRIBE_ERROR body; later drafts use REQUEST_ERROR.
|
|
1031
|
+
*/
|
|
1032
|
+
async #refuseUnsupported(stream, requestId, kind, errorId14, reasonPhrase) {
|
|
765
1033
|
const version = this.#session.version;
|
|
1034
|
+
const errorCode = toRequestCode("not_supported", kind, version);
|
|
766
1035
|
if (version === Version.DRAFT_14) {
|
|
767
|
-
|
|
768
|
-
await stream.writer
|
|
769
|
-
const status = new TrackStatus({
|
|
770
|
-
trackNamespace: msg.trackNamespace,
|
|
771
|
-
trackName: msg.trackName,
|
|
772
|
-
statusCode: TrackStatus.STATUS_NOT_FOUND,
|
|
773
|
-
lastGroupId: 0n,
|
|
774
|
-
lastObjectId: 0n,
|
|
775
|
-
});
|
|
776
|
-
await status.encode(stream.writer, version);
|
|
1036
|
+
await stream.writer.u53(errorId14);
|
|
1037
|
+
await new SubscribeError({ requestId, errorCode, reasonPhrase }).encode(stream.writer, version);
|
|
777
1038
|
}
|
|
778
1039
|
else {
|
|
779
|
-
|
|
780
|
-
await
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1040
|
+
await stream.writer.u53(RequestError.id);
|
|
1041
|
+
await new RequestError({
|
|
1042
|
+
requestId: version === Version.DRAFT_15 || version === Version.DRAFT_16 ? requestId : undefined,
|
|
1043
|
+
errorCode,
|
|
1044
|
+
reasonPhrase,
|
|
1045
|
+
}).encode(stream.writer, version);
|
|
785
1046
|
}
|
|
786
1047
|
stream.close();
|
|
787
1048
|
}
|
|
788
|
-
/**
|
|
789
|
-
* Closes every published broadcast and stops accepting new ones.
|
|
790
|
-
*
|
|
791
|
-
* @internal
|
|
792
|
-
*/
|
|
793
|
-
close() {
|
|
794
|
-
this.#broadcasts.update((broadcasts) => {
|
|
795
|
-
for (const broadcast of broadcasts?.values() ?? []) {
|
|
796
|
-
broadcast.close();
|
|
797
|
-
}
|
|
798
|
-
return undefined;
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
1049
|
}
|
|
802
1050
|
/**
|
|
803
1051
|
* Take one group out of a cache view, without waiting.
|