@norskvideo/moq-net 0.1.2 → 0.1.4
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 +1 -0
- package/announced.d.ts +111 -21
- package/announced.d.ts.map +1 -1
- package/announced.js +213 -40
- package/announced.js.map +1 -1
- package/broadcast.d.ts +80 -25
- package/broadcast.d.ts.map +1 -1
- package/broadcast.js +270 -49
- package/broadcast.js.map +1 -1
- package/connection/accept.d.ts +6 -0
- package/connection/accept.d.ts.map +1 -1
- package/connection/accept.js +42 -13
- package/connection/accept.js.map +1 -1
- package/connection/browser.d.ts +5 -0
- package/connection/browser.d.ts.map +1 -0
- package/connection/browser.js +31 -0
- package/connection/browser.js.map +1 -0
- package/connection/connect.d.ts +43 -1
- package/connection/connect.d.ts.map +1 -1
- package/connection/connect.js +118 -123
- package/connection/connect.js.map +1 -1
- package/connection/established.d.ts +43 -16
- package/connection/established.d.ts.map +1 -1
- package/connection/established.js.map +1 -1
- package/connection/handshake.d.ts +12 -1
- package/connection/handshake.d.ts.map +1 -1
- package/connection/handshake.js +30 -14
- package/connection/handshake.js.map +1 -1
- package/connection/index.d.ts +8 -0
- package/connection/index.d.ts.map +1 -1
- package/connection/index.js +6 -0
- package/connection/index.js.map +1 -1
- package/connection/reload.d.ts +66 -9
- package/connection/reload.d.ts.map +1 -1
- package/connection/reload.js +192 -66
- package/connection/reload.js.map +1 -1
- package/connection/stats.d.ts +92 -0
- package/connection/stats.d.ts.map +1 -0
- package/connection/stats.js +35 -0
- package/connection/stats.js.map +1 -0
- package/connection/transport.d.ts +10 -0
- package/connection/transport.d.ts.map +1 -0
- package/connection/transport.js +12 -0
- package/connection/transport.js.map +1 -0
- package/consume.d.ts +44 -0
- package/consume.d.ts.map +1 -0
- package/consume.js +60 -0
- package/consume.js.map +1 -0
- package/datagram.d.ts +23 -0
- package/datagram.d.ts.map +1 -0
- package/datagram.js +3 -0
- package/datagram.js.map +1 -0
- package/error.d.ts +76 -0
- package/error.d.ts.map +1 -0
- package/error.js +107 -0
- package/error.js.map +1 -0
- package/group.d.ts +143 -38
- package/group.d.ts.map +1 -1
- package/group.js +270 -68
- package/group.js.map +1 -1
- package/ietf/adapter.d.ts +2 -2
- package/ietf/adapter.d.ts.map +1 -1
- package/ietf/adapter.js +20 -10
- package/ietf/adapter.js.map +1 -1
- package/ietf/aliases.d.ts +73 -5
- package/ietf/aliases.d.ts.map +1 -1
- package/ietf/aliases.js +121 -14
- package/ietf/aliases.js.map +1 -1
- package/ietf/cluster.d.ts +141 -0
- package/ietf/cluster.d.ts.map +1 -0
- package/ietf/cluster.js +204 -0
- package/ietf/cluster.js.map +1 -0
- package/ietf/connection.d.ts +39 -6
- package/ietf/connection.d.ts.map +1 -1
- package/ietf/connection.js +73 -12
- package/ietf/connection.js.map +1 -1
- package/ietf/fetch.d.ts +17 -0
- package/ietf/fetch.d.ts.map +1 -1
- package/ietf/fetch.js +21 -0
- package/ietf/fetch.js.map +1 -1
- package/ietf/filter.d.ts +87 -0
- package/ietf/filter.d.ts.map +1 -0
- package/ietf/filter.js +292 -0
- package/ietf/filter.js.map +1 -0
- package/ietf/index.d.ts +2 -0
- package/ietf/index.d.ts.map +1 -1
- package/ietf/index.js +3 -0
- package/ietf/index.js.map +1 -1
- package/ietf/namespace.d.ts +6 -0
- package/ietf/namespace.d.ts.map +1 -1
- package/ietf/namespace.js +27 -2
- package/ietf/namespace.js.map +1 -1
- package/ietf/object.d.ts +54 -6
- package/ietf/object.d.ts.map +1 -1
- package/ietf/object.js +178 -25
- package/ietf/object.js.map +1 -1
- package/ietf/parameters.d.ts +35 -10
- package/ietf/parameters.d.ts.map +1 -1
- package/ietf/parameters.js +236 -45
- package/ietf/parameters.js.map +1 -1
- package/ietf/priority.d.ts +5 -0
- package/ietf/priority.d.ts.map +1 -0
- package/ietf/priority.js +10 -0
- package/ietf/priority.js.map +1 -0
- package/ietf/properties.d.ts +13 -2
- package/ietf/properties.d.ts.map +1 -1
- package/ietf/properties.js +65 -8
- package/ietf/properties.js.map +1 -1
- package/ietf/publish.d.ts.map +1 -1
- package/ietf/publish.js +13 -3
- package/ietf/publish.js.map +1 -1
- package/ietf/publish_namespace.d.ts +16 -2
- package/ietf/publish_namespace.d.ts.map +1 -1
- package/ietf/publish_namespace.js +25 -6
- package/ietf/publish_namespace.js.map +1 -1
- package/ietf/publisher.d.ts +40 -7
- package/ietf/publisher.d.ts.map +1 -1
- package/ietf/publisher.js +751 -134
- package/ietf/publisher.js.map +1 -1
- package/ietf/request.js +1 -1
- package/ietf/request.js.map +1 -1
- package/ietf/solicit.d.ts +41 -0
- package/ietf/solicit.d.ts.map +1 -0
- package/ietf/solicit.js +47 -0
- package/ietf/solicit.js.map +1 -0
- package/ietf/subscribe.d.ts +41 -2
- package/ietf/subscribe.d.ts.map +1 -1
- package/ietf/subscribe.js +103 -22
- package/ietf/subscribe.js.map +1 -1
- package/ietf/subscribe_namespace.d.ts +21 -7
- package/ietf/subscribe_namespace.d.ts.map +1 -1
- package/ietf/subscribe_namespace.js +31 -14
- package/ietf/subscribe_namespace.js.map +1 -1
- package/ietf/subscriber.d.ts +20 -7
- package/ietf/subscriber.d.ts.map +1 -1
- package/ietf/subscriber.js +512 -142
- package/ietf/subscriber.js.map +1 -1
- package/ietf/version.d.ts +7 -1
- package/ietf/version.d.ts.map +1 -1
- package/ietf/version.js +7 -0
- package/ietf/version.js.map +1 -1
- package/index.d.ts +10 -5
- package/index.d.ts.map +1 -1
- package/index.js +10 -5
- package/index.js.map +1 -1
- package/internal.d.ts +29 -0
- package/internal.d.ts.map +1 -0
- package/internal.js +8 -0
- package/internal.js.map +1 -0
- package/lite/announce.d.ts +62 -15
- package/lite/announce.d.ts.map +1 -1
- package/lite/announce.js +230 -94
- package/lite/announce.js.map +1 -1
- package/lite/connection.d.ts +69 -20
- package/lite/connection.d.ts.map +1 -1
- package/lite/connection.js +128 -81
- package/lite/connection.js.map +1 -1
- package/lite/datagram.d.ts +23 -0
- package/lite/datagram.d.ts.map +1 -0
- package/lite/datagram.js +57 -0
- package/lite/datagram.js.map +1 -0
- package/lite/datagram_stream.d.ts +8 -0
- package/lite/datagram_stream.d.ts.map +1 -0
- package/lite/datagram_stream.js +44 -0
- package/lite/datagram_stream.js.map +1 -0
- package/lite/fetch.d.ts.map +1 -1
- package/lite/fetch.js +2 -2
- package/lite/fetch.js.map +1 -1
- package/lite/index.d.ts +3 -0
- package/lite/index.d.ts.map +1 -1
- package/lite/index.js +3 -0
- package/lite/index.js.map +1 -1
- package/lite/priority.d.ts +67 -0
- package/lite/priority.d.ts.map +1 -0
- package/lite/priority.js +114 -0
- package/lite/priority.js.map +1 -0
- package/lite/probe.d.ts +18 -2
- package/lite/probe.d.ts.map +1 -1
- package/lite/probe.js +24 -22
- package/lite/probe.js.map +1 -1
- package/lite/publisher.d.ts +14 -7
- package/lite/publisher.d.ts.map +1 -1
- package/lite/publisher.js +406 -84
- package/lite/publisher.js.map +1 -1
- package/lite/setup.d.ts +83 -6
- package/lite/setup.d.ts.map +1 -1
- package/lite/setup.js +203 -31
- package/lite/setup.js.map +1 -1
- package/lite/stream.d.ts +12 -3
- package/lite/stream.d.ts.map +1 -1
- package/lite/stream.js +7 -4
- package/lite/stream.js.map +1 -1
- package/lite/subscribe.d.ts +37 -23
- package/lite/subscribe.d.ts.map +1 -1
- package/lite/subscribe.js +70 -55
- package/lite/subscribe.js.map +1 -1
- package/lite/subscriber.d.ts +29 -23
- package/lite/subscriber.d.ts.map +1 -1
- package/lite/subscriber.js +689 -112
- package/lite/subscriber.js.map +1 -1
- package/lite/track.d.ts +25 -13
- package/lite/track.d.ts.map +1 -1
- package/lite/track.js +60 -35
- package/lite/track.js.map +1 -1
- package/lite/version.d.ts +39 -4
- package/lite/version.d.ts.map +1 -1
- package/lite/version.js +139 -17
- package/lite/version.js.map +1 -1
- package/mock.d.ts +53 -3
- package/mock.d.ts.map +1 -1
- package/mock.js +110 -21
- package/mock.js.map +1 -1
- package/{lite/origin.d.ts → origin.d.ts} +25 -1
- package/origin.d.ts.map +1 -0
- package/{lite/origin.js → origin.js} +25 -1
- package/origin.js.map +1 -0
- package/package.json +6 -4
- package/path.d.ts +67 -0
- package/path.d.ts.map +1 -1
- package/path.js +128 -0
- package/path.js.map +1 -1
- package/stream.d.ts +94 -4
- package/stream.d.ts.map +1 -1
- package/stream.js +128 -17
- package/stream.js.map +1 -1
- package/time.d.ts +109 -86
- package/time.d.ts.map +1 -1
- package/time.js +80 -30
- package/time.js.map +1 -1
- package/track.d.ts +298 -47
- package/track.d.ts.map +1 -1
- package/track.js +721 -109
- package/track.js.map +1 -1
- package/util/timeout.d.ts +5 -0
- package/util/timeout.d.ts.map +1 -0
- package/util/timeout.js +18 -0
- package/util/timeout.js.map +1 -0
- package/util/utf8.d.ts +3 -0
- package/util/utf8.d.ts.map +1 -0
- package/util/utf8.js +7 -0
- package/util/utf8.js.map +1 -0
- package/varint.d.ts +14 -3
- package/varint.d.ts.map +1 -1
- package/varint.js +23 -12
- package/varint.js.map +1 -1
- package/zod.d.ts +5 -5
- package/zod.d.ts.map +1 -1
- package/zod.js.map +1 -1
- package/bandwidth.d.ts +0 -11
- package/bandwidth.d.ts.map +0 -1
- package/bandwidth.js +0 -7
- package/bandwidth.js.map +0 -1
- package/connection/cert.test.d.ts +0 -2
- package/connection/cert.test.d.ts.map +0 -1
- package/connection/cert.test.js +0 -26
- package/connection/cert.test.js.map +0 -1
- package/connection/reload.test.d.ts +0 -2
- package/connection/reload.test.d.ts.map +0 -1
- package/connection/reload.test.js +0 -39
- package/connection/reload.test.js.map +0 -1
- package/group.test.d.ts +0 -2
- package/group.test.d.ts.map +0 -1
- package/group.test.js +0 -66
- package/group.test.js.map +0 -1
- package/ietf/aliases.test.d.ts +0 -2
- package/ietf/aliases.test.d.ts.map +0 -1
- package/ietf/aliases.test.js +0 -30
- package/ietf/aliases.test.js.map +0 -1
- package/ietf/ietf.test.d.ts +0 -2
- package/ietf/ietf.test.d.ts.map +0 -1
- package/ietf/ietf.test.js +0 -873
- package/ietf/ietf.test.js.map +0 -1
- package/integration.test.d.ts +0 -2
- package/integration.test.d.ts.map +0 -1
- package/integration.test.js +0 -90
- package/integration.test.js.map +0 -1
- package/lite/origin.d.ts.map +0 -1
- package/lite/origin.js.map +0 -1
- package/lite/subscriber.test.d.ts +0 -2
- package/lite/subscriber.test.d.ts.map +0 -1
- package/lite/subscriber.test.js +0 -30
- package/lite/subscriber.test.js.map +0 -1
- package/lite/track.test.d.ts +0 -2
- package/lite/track.test.d.ts.map +0 -1
- package/lite/track.test.js +0 -53
- package/lite/track.test.js.map +0 -1
- package/path.test.d.ts +0 -2
- package/path.test.d.ts.map +0 -1
- package/path.test.js +0 -169
- package/path.test.js.map +0 -1
- package/stream.test.d.ts +0 -2
- package/stream.test.d.ts.map +0 -1
- package/stream.test.js +0 -210
- package/stream.test.js.map +0 -1
- package/track.test.d.ts +0 -2
- package/track.test.d.ts.map +0 -1
- package/track.test.js +0 -56
- package/track.test.js.map +0 -1
- package/util/error.d.ts +0 -3
- package/util/error.d.ts.map +0 -1
- package/util/error.js +0 -9
- package/util/error.js.map +0 -1
- package/util/index.d.ts +0 -2
- package/util/index.d.ts.map +0 -1
- package/util/index.js +0 -3
- package/util/index.js.map +0 -1
- package/varint.test.d.ts +0 -2
- package/varint.test.d.ts.map +0 -1
- package/varint.test.js +0 -106
- package/varint.test.js.map +0 -1
package/track.js
CHANGED
|
@@ -1,69 +1,491 @@
|
|
|
1
1
|
/* @ts-self-types="./track.d.ts" */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Track role handles: a live stream of groups (and best-effort datagrams) within a broadcast.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { Once, Signal } from "@norskvideo/moq-signals";
|
|
8
|
+
import { Producer as GroupProducer, Lagged } from "./group.js";
|
|
9
|
+
import { hooks } from "./internal.js";
|
|
10
|
+
import { Timescale } from "./time.js";
|
|
11
|
+
/** Default {@link Info.latencyMax} window (milliseconds) when the publisher does not set one. */
|
|
12
|
+
export const DEFAULT_LATENCY_MAX_MS = 5000;
|
|
13
|
+
/**
|
|
14
|
+
* How long (milliseconds) a datagram stays in the per-subscriber buffer before it is dropped.
|
|
15
|
+
*
|
|
16
|
+
* Datagrams are a best-effort send buffer, not a replay cache (unlike groups): only the last few
|
|
17
|
+
* tens of milliseconds are kept, so a consumer that stalls loses stale datagrams instead of
|
|
18
|
+
* replaying them. Mirrors the Rust `MAX_DATAGRAM_AGE`.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_DATAGRAM_AGE_MS = 50;
|
|
21
|
+
/**
|
|
22
|
+
* Sanity cap on a datagram payload: the QUIC DATAGRAM frame ceiling. The real limit is
|
|
23
|
+
* per-hop (the negotiated transport datagram size minus a small header) and oversize
|
|
24
|
+
* datagrams are dropped there; a payload above this cap could never fit anywhere.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_DATAGRAM_BYTES = 65535;
|
|
27
|
+
/** Fill in any unset {@link Info} fields with their defaults. */
|
|
28
|
+
export function infoDefaults(info = {}) {
|
|
29
|
+
return {
|
|
30
|
+
timescale: info.timescale ?? Timescale.MILLI,
|
|
31
|
+
latencyMax: info.latencyMax ?? DEFAULT_LATENCY_MAX_MS,
|
|
32
|
+
priority: info.priority ?? 0,
|
|
33
|
+
ordered: info.ordered ?? false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Materialize the defaults at the model boundary so every layer observes a complete
|
|
37
|
+
// subscription rather than interpreting an omitted field differently.
|
|
38
|
+
function subscriptionDefaults(subscription = {}) {
|
|
39
|
+
return {
|
|
40
|
+
priority: subscription.priority ?? 0,
|
|
41
|
+
ordered: subscription.ordered ?? false,
|
|
42
|
+
latencyMax: subscription.latencyMax ?? 0,
|
|
43
|
+
startGroup: subscription.startGroup,
|
|
44
|
+
endGroup: subscription.endGroup,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Aggregate the preferences of every live subscriber, matching Rust's Subscription::poll_combined.
|
|
48
|
+
function combineSubscriptions(states) {
|
|
49
|
+
let combined;
|
|
50
|
+
for (const state of states) {
|
|
51
|
+
const subscription = state.update.peek();
|
|
52
|
+
if (!subscription)
|
|
53
|
+
continue;
|
|
54
|
+
if (!combined) {
|
|
55
|
+
combined = { ...subscription };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
combined.priority = Math.max(combined.priority ?? 0, subscription.priority ?? 0);
|
|
59
|
+
combined.ordered = (combined.ordered ?? false) && (subscription.ordered ?? false);
|
|
60
|
+
combined.latencyMax = Math.max(combined.latencyMax ?? 0, subscription.latencyMax ?? 0);
|
|
61
|
+
if (subscription.startGroup !== undefined) {
|
|
62
|
+
combined.startGroup =
|
|
63
|
+
combined.startGroup === undefined
|
|
64
|
+
? subscription.startGroup
|
|
65
|
+
: Math.min(combined.startGroup, subscription.startGroup);
|
|
66
|
+
}
|
|
67
|
+
if (combined.endGroup === undefined || subscription.endGroup === undefined) {
|
|
68
|
+
combined.endGroup = undefined;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
combined.endGroup = Math.max(combined.endGroup, subscription.endGroup);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return combined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A request for a track the peer wants, yielded by `Broadcast.Producer.requested`.
|
|
78
|
+
*
|
|
79
|
+
* Created internally by the broadcast when a subscription (or info lookup) needs a track
|
|
80
|
+
* served; answer it with {@link accept} or {@link reject}.
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export class Request {
|
|
85
|
+
/** The requested track name. */
|
|
86
|
+
name;
|
|
87
|
+
#producer;
|
|
88
|
+
#sequences;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
this.name = options.name;
|
|
91
|
+
this.#producer = options.producer;
|
|
92
|
+
this.#sequences = options.sequences;
|
|
93
|
+
}
|
|
94
|
+
static {
|
|
95
|
+
hooks.makeRequest = (options) => new Request(options);
|
|
96
|
+
}
|
|
97
|
+
/** The aggregate subscription requested for this track. */
|
|
98
|
+
get subscription() {
|
|
99
|
+
return this.#producer.subscription.peek() ?? subscriptionDefaults();
|
|
100
|
+
}
|
|
101
|
+
/** The subscriber's priority for this track. */
|
|
102
|
+
get priority() {
|
|
103
|
+
return this.subscription.priority ?? 0;
|
|
104
|
+
}
|
|
105
|
+
/** Accept the request, committing the track's immutable {@link Info}. */
|
|
106
|
+
accept(info = {}) {
|
|
107
|
+
bindProducer(this.name, this.#producer, this.#sequences);
|
|
108
|
+
return this.#producer.accept(info);
|
|
109
|
+
}
|
|
110
|
+
/** Reject the request, closing the track optionally with an error. */
|
|
111
|
+
reject(err) {
|
|
112
|
+
this.#producer.close(err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A lazy handle to a track on a consumed broadcast.
|
|
117
|
+
*
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
export class Consumer {
|
|
121
|
+
/** The track name. */
|
|
122
|
+
name;
|
|
123
|
+
#broadcast;
|
|
124
|
+
constructor(name, broadcast) {
|
|
125
|
+
this.name = name;
|
|
126
|
+
this.#broadcast = broadcast;
|
|
127
|
+
}
|
|
128
|
+
/** Open a live subscription to the track. */
|
|
129
|
+
subscribe(options) {
|
|
130
|
+
return this.#broadcast.subscribe(this.name, options);
|
|
131
|
+
}
|
|
132
|
+
/** Fetch the track's immutable publisher properties without subscribing. */
|
|
133
|
+
info() {
|
|
134
|
+
return this.#broadcast.resolveTrackInfo(this.name);
|
|
135
|
+
}
|
|
136
|
+
/** Fetch a single group by sequence without holding a live subscription. */
|
|
137
|
+
fetchGroup(sequence, options) {
|
|
138
|
+
return this.#broadcast.fetchGroup(this.name, sequence, options);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// The shared state behind a Producer / Subscriber pair. Package-internal
|
|
142
|
+
// wiring, unexported so it never appears in the published type declarations.
|
|
143
|
+
class TrackState {
|
|
144
|
+
/** The producer fanning into this sink, so a subscriber can mint a sibling of itself. */
|
|
145
|
+
producer;
|
|
6
146
|
groups = new Signal([]);
|
|
7
|
-
|
|
8
|
-
|
|
147
|
+
/** Best-effort datagram channel, parallel to {@link groups}; an age-evicted send buffer per subscriber. */
|
|
148
|
+
datagrams = new Signal([]);
|
|
149
|
+
latest;
|
|
150
|
+
closed = new Once();
|
|
151
|
+
update;
|
|
152
|
+
/** Resolved once the producer commits the immutable properties. */
|
|
153
|
+
info = new Signal(undefined);
|
|
154
|
+
constructor(subscription) {
|
|
155
|
+
this.update = new Signal(subscription === undefined ? undefined : subscriptionDefaults(subscription));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Settle a track state's closed Once. Idempotent: Once.set throws on a second settle, and a
|
|
159
|
+
// Producer closing its sinks races the Subscriber closing itself (the sink is only removed from
|
|
160
|
+
// #sinks a microtask later, via the closed subscription below).
|
|
161
|
+
function closeTrackState(state, abort) {
|
|
162
|
+
if (state.closed.peek() !== undefined)
|
|
163
|
+
return false;
|
|
164
|
+
state.closed.set(abort ?? null);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// Resolve the track's immutable publisher properties, or reject if it closes first.
|
|
168
|
+
// On a producer this resolves once info is committed (at accept time); on a consumer
|
|
169
|
+
// once the wire layer commits the TRACK_INFO it received (lite-05+) or defaults (older
|
|
170
|
+
// drafts), so awaiting it never yields a placeholder.
|
|
171
|
+
async function resolveInfo(state) {
|
|
172
|
+
for (;;) {
|
|
173
|
+
const info = state.info.peek();
|
|
174
|
+
if (info)
|
|
175
|
+
return info;
|
|
176
|
+
const closed = state.closed.peek();
|
|
177
|
+
if (closed instanceof Error)
|
|
178
|
+
throw closed;
|
|
179
|
+
if (closed !== undefined)
|
|
180
|
+
throw new Error("track closed before info was known");
|
|
181
|
+
await Signal.race(state.info, state.closed);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function bindProducer(name, producer, sequences) {
|
|
185
|
+
let shared = sequences.get(name);
|
|
186
|
+
if (!shared) {
|
|
187
|
+
shared = { next: 0 };
|
|
188
|
+
sequences.set(name, shared);
|
|
189
|
+
}
|
|
190
|
+
bindProducerSequence(producer, shared);
|
|
9
191
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
192
|
+
let bindProducerSequence;
|
|
193
|
+
// Constructs a Subscriber from within this module without exposing a public
|
|
194
|
+
// constructor that would leak the unexported TrackState. Assigned in the class's
|
|
195
|
+
// static block.
|
|
196
|
+
let makeSubscriber;
|
|
197
|
+
/**
|
|
198
|
+
* The write side of a track, mirroring the Rust `Producer`.
|
|
199
|
+
*
|
|
200
|
+
* A producer is a fan-out source: every {@link subscribe} (including each wire
|
|
201
|
+
* subscription the publisher serves from it) gets an independent
|
|
202
|
+
* {@link Subscriber} that receives a full copy of the groups, each with its own
|
|
203
|
+
* read cursor. Groups are mirrored into every live subscriber and retained for the
|
|
204
|
+
* track's `latencyMax` window so a late subscriber replays the recent groups.
|
|
205
|
+
*
|
|
206
|
+
* Obtained from {@link Request.accept} (the wire asks the application for a track to
|
|
207
|
+
* serve) or constructed directly for an in-process track.
|
|
208
|
+
*/
|
|
209
|
+
export class Producer {
|
|
210
|
+
/** The track name. */
|
|
13
211
|
name;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
closed
|
|
212
|
+
// The producer's own state is the source of truth (info/closed); subscribers
|
|
213
|
+
// read mirrored sinks, never this state directly.
|
|
214
|
+
#state = new TrackState();
|
|
215
|
+
#sequence = { next: 0 };
|
|
216
|
+
// Recently written source groups, retained for replay to late subscribers and
|
|
217
|
+
// pruned once closed and older than the cache window. Each entry tracks the mirror
|
|
218
|
+
// it handed to every sink so eviction can drop them too: otherwise a slow consumer
|
|
219
|
+
// that never reads would pin old groups (and their frame bytes) forever.
|
|
220
|
+
#cache = [];
|
|
221
|
+
// One independent downstream state per live subscriber.
|
|
222
|
+
#sinks = new Set();
|
|
223
|
+
// Whether any subscriber is currently attached. Exposed as {@link used}; the consumer wire
|
|
224
|
+
// watches it to tear down an idle upstream, and a publisher can watch it for on-demand capture.
|
|
225
|
+
#used = new Signal(false);
|
|
20
226
|
constructor(name) {
|
|
21
227
|
this.name = name;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
228
|
+
}
|
|
229
|
+
static {
|
|
230
|
+
bindProducerSequence = (producer, sequence) => {
|
|
231
|
+
producer.#sequence = sequence;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolve this track's immutable publisher properties, committed at accept time.
|
|
236
|
+
* Rejects if the track is closed before the properties are known.
|
|
237
|
+
*/
|
|
238
|
+
info() {
|
|
239
|
+
return resolveInfo(this.#state);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Settles once the track closes: `null` on a clean close, or the abort {@link Error}.
|
|
243
|
+
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
|
|
244
|
+
*/
|
|
245
|
+
get closed() {
|
|
246
|
+
return this.#state.closed;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The aggregate subscription across live subscribers, or `undefined` when there are none.
|
|
250
|
+
* The wire layer watches this to emit SUBSCRIBE_UPDATE.
|
|
251
|
+
*/
|
|
252
|
+
get subscription() {
|
|
253
|
+
return this.#state.update;
|
|
254
|
+
}
|
|
255
|
+
/** Commit the immutable publisher properties, resolving {@link info}. Returns `this`. */
|
|
256
|
+
accept(info = {}) {
|
|
257
|
+
const resolved = infoDefaults(info);
|
|
258
|
+
this.#state.info.set(resolved);
|
|
259
|
+
// Propagate to any sink handed out before accept (the on-demand path).
|
|
260
|
+
for (const sink of this.#sinks)
|
|
261
|
+
sink.info.set(resolved);
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
/** An independent {@link Subscriber} receiving a full copy of this track's groups. */
|
|
265
|
+
subscribe(options = {}) {
|
|
266
|
+
const sink = new TrackState(options);
|
|
267
|
+
this.#addSink(sink);
|
|
268
|
+
return makeSubscriber(this.name, sink);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Whether the track currently has any subscribers.
|
|
272
|
+
*
|
|
273
|
+
* Watch it (`effect.get` / `.peek()`) to drive on-demand work: a publisher can start and stop
|
|
274
|
+
* capture with demand, and the consumer wire watches it to tear an idle upstream subscription
|
|
275
|
+
* down instead of downloading to nobody. Pairs with {@link unused}. Mirrors the Rust `Demand`.
|
|
276
|
+
*/
|
|
277
|
+
get used() {
|
|
278
|
+
return this.#used;
|
|
279
|
+
}
|
|
280
|
+
/** Resolves once the track has no subscribers (or has closed). Await it to react to demand ending. */
|
|
281
|
+
async unused() {
|
|
282
|
+
while (this.#used.peek() && this.#state.closed.peek() === undefined) {
|
|
283
|
+
await Signal.race(this.#used, this.#state.closed);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Register a downstream sink: seed its info, replay the retained window, and (while
|
|
287
|
+
// the track is open) mirror future groups into it. A late subscriber to a closed
|
|
288
|
+
// track still drains the buffered groups before seeing the end.
|
|
289
|
+
#addSink(sink) {
|
|
290
|
+
sink.producer = this;
|
|
291
|
+
const info = this.#state.info.peek();
|
|
292
|
+
if (info)
|
|
293
|
+
sink.info.set(info);
|
|
294
|
+
const closed = this.#state.closed.peek();
|
|
295
|
+
if (closed === undefined) {
|
|
296
|
+
this.#sinks.add(sink);
|
|
297
|
+
this.#used.set(true);
|
|
298
|
+
// Forward subscription updates from the sink's Subscriber to the producer's own
|
|
299
|
+
// state, which the wire layer (or the serving application) watches.
|
|
300
|
+
const forward = sink.update.subscribe(() => this.#updateSubscription());
|
|
301
|
+
this.#updateSubscription();
|
|
302
|
+
// Drop the sink once its consumer goes away, closing its mirrors so source
|
|
303
|
+
// groups stop teeing into them, so a long-lived producer doesn't leak. This
|
|
304
|
+
// covers mirrors already handed out via recvGroup (no longer in sink.groups)
|
|
305
|
+
// by closing them through the cache's per-sink tracking.
|
|
306
|
+
const dispose = sink.closed.subscribe((c) => {
|
|
307
|
+
if (c === undefined)
|
|
25
308
|
return;
|
|
26
|
-
|
|
309
|
+
const abort = c instanceof Error ? c : undefined;
|
|
310
|
+
forward();
|
|
311
|
+
this.#sinks.delete(sink);
|
|
312
|
+
this.#updateSubscription();
|
|
313
|
+
for (const entry of this.#cache) {
|
|
314
|
+
const mirror = entry.mirrors.get(sink);
|
|
315
|
+
if (mirror) {
|
|
316
|
+
mirror.close(abort);
|
|
317
|
+
entry.mirrors.delete(sink);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const group of sink.groups.peek())
|
|
321
|
+
group.close(abort);
|
|
27
322
|
dispose();
|
|
323
|
+
// Update demand: once the last subscriber leaves, the consumer wire (watching
|
|
324
|
+
// {@link unused}) tears the upstream down instead of downloading to nobody.
|
|
325
|
+
this.#used.set(this.#sinks.size > 0);
|
|
28
326
|
});
|
|
327
|
+
}
|
|
328
|
+
this.#prune();
|
|
329
|
+
for (const entry of this.#cache)
|
|
330
|
+
this.#mirror(entry, sink);
|
|
331
|
+
if (closed !== undefined)
|
|
332
|
+
closeTrackState(sink, closed instanceof Error ? closed : undefined);
|
|
333
|
+
}
|
|
334
|
+
// Recompute from every live sink because an update or close can narrow as well as widen
|
|
335
|
+
// the aggregate. The wire layer observes this signal and emits SUBSCRIBE_UPDATE.
|
|
336
|
+
#updateSubscription() {
|
|
337
|
+
this.#state.update.set(combineSubscriptions(this.#sinks));
|
|
338
|
+
}
|
|
339
|
+
// Mirror a cached source group into a sink. The mirror fills synchronously as the
|
|
340
|
+
// source is written and keeps its own read cursor; frame bytes are shared by
|
|
341
|
+
// reference. Tracked on the entry so eviction can drop it from the sink.
|
|
342
|
+
#mirror(entry, sink) {
|
|
343
|
+
const dst = entry.group.mirror();
|
|
344
|
+
entry.mirrors.set(sink, dst);
|
|
345
|
+
sink.latest = Math.max(sink.latest ?? 0, dst.sequence);
|
|
346
|
+
sink.groups.mutate((groups) => {
|
|
347
|
+
groups.push(dst);
|
|
348
|
+
groups.sort((a, b) => a.sequence - b.sequence);
|
|
29
349
|
});
|
|
30
350
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
351
|
+
// Drop a cached group's mirror from every sink so no consumer can pin it.
|
|
352
|
+
#evict(entry) {
|
|
353
|
+
for (const [sink, mirror] of entry.mirrors) {
|
|
354
|
+
sink.groups.mutate((groups) => {
|
|
355
|
+
const i = groups.indexOf(mirror);
|
|
356
|
+
if (i >= 0)
|
|
357
|
+
groups.splice(i, 1);
|
|
358
|
+
});
|
|
359
|
+
mirror.close();
|
|
360
|
+
}
|
|
361
|
+
entry.mirrors.clear();
|
|
362
|
+
}
|
|
363
|
+
// Evict cached groups that are closed and older than the cache window.
|
|
364
|
+
#prune() {
|
|
365
|
+
const latencyMaxMs = this.#state.info.peek()?.latencyMax ?? DEFAULT_LATENCY_MAX_MS;
|
|
366
|
+
const cutoff = Date.now() - latencyMaxMs;
|
|
367
|
+
const retained = [];
|
|
368
|
+
for (const entry of this.#cache) {
|
|
369
|
+
if (entry.time > cutoff || entry.group.closed.peek() === undefined) {
|
|
370
|
+
retained.push(entry);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
this.#evict(entry);
|
|
374
|
+
}
|
|
375
|
+
this.#cache = retained;
|
|
376
|
+
}
|
|
377
|
+
// Retain a source group and fan it out to every live sink.
|
|
378
|
+
#publish(group) {
|
|
379
|
+
const entry = { group, time: Date.now(), mirrors: new Map() };
|
|
380
|
+
this.#cache.push(entry);
|
|
381
|
+
this.#prune();
|
|
382
|
+
for (const sink of this.#sinks)
|
|
383
|
+
this.#mirror(entry, sink);
|
|
384
|
+
}
|
|
385
|
+
/** Append a new group with the next sequence number. */
|
|
35
386
|
appendGroup() {
|
|
36
|
-
if (this
|
|
387
|
+
if (this.#state.closed.peek() !== undefined)
|
|
37
388
|
throw new Error("track is closed");
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
groups.sort((a, b) => a.sequence - b.sequence);
|
|
43
|
-
});
|
|
389
|
+
const sequence = this.#sequence;
|
|
390
|
+
const group = new GroupProducer(sequence.next);
|
|
391
|
+
sequence.next = group.sequence + 1;
|
|
392
|
+
this.#publish(group);
|
|
44
393
|
return group;
|
|
45
394
|
}
|
|
46
395
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
396
|
+
* Insert an existing group into the track.
|
|
397
|
+
*
|
|
398
|
+
* Throws on a sequence that is still cached: a live duplicate would fan out to every
|
|
399
|
+
* subscriber twice. An aborted incarnation is evicted so a fresh group can serve the
|
|
400
|
+
* sequence again. Best effort (mirrors Rust): nothing remembers a sequence whose cache
|
|
401
|
+
* entry is already gone, so a long-evicted sequence is accepted as new.
|
|
49
402
|
*/
|
|
50
403
|
writeGroup(group) {
|
|
51
|
-
if (this
|
|
404
|
+
if (this.#state.closed.peek() !== undefined)
|
|
52
405
|
throw new Error("track is closed");
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
406
|
+
const existing = this.#cache.findIndex((entry) => entry.group.sequence === group.sequence);
|
|
407
|
+
if (existing >= 0) {
|
|
408
|
+
const entry = this.#cache[existing];
|
|
409
|
+
if (!(entry.group.closed.peek() instanceof Error)) {
|
|
410
|
+
throw new Error(`duplicate group: sequence=${group.sequence}`);
|
|
411
|
+
}
|
|
412
|
+
this.#evict(entry);
|
|
413
|
+
this.#cache.splice(existing, 1);
|
|
56
414
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
415
|
+
// Only advance the shared counter upward (for appendGroup auto-increment).
|
|
416
|
+
const sequence = this.#sequence;
|
|
417
|
+
if (group.sequence >= sequence.next) {
|
|
418
|
+
sequence.next = group.sequence + 1;
|
|
419
|
+
}
|
|
420
|
+
this.#publish(group);
|
|
421
|
+
}
|
|
422
|
+
// Fan a datagram out to every live subscriber, dropping the oldest once the ring is full.
|
|
423
|
+
// Late subscribers do NOT replay old datagrams (best-effort, unlike the group cache).
|
|
424
|
+
#publishDatagram(datagram) {
|
|
425
|
+
const now = performance.now();
|
|
426
|
+
for (const sink of this.#sinks) {
|
|
427
|
+
sink.datagrams.mutate((list) => {
|
|
428
|
+
list.push({ datagram, time: now });
|
|
429
|
+
// Drop anything older than the send-buffer window.
|
|
430
|
+
while (list.length > 0 && now - list[0].time > MAX_DATAGRAM_AGE_MS)
|
|
431
|
+
list.shift();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Append a datagram with the next sequence number, returning the assigned sequence.
|
|
437
|
+
*
|
|
438
|
+
* A datagram is delivered best-effort over a single QUIC datagram, parallel to the track's
|
|
439
|
+
* groups but drawing from the same sequence namespace (interleaving with {@link appendGroup}
|
|
440
|
+
* never reuses a number). The payload must fit the negotiated transport datagram size minus
|
|
441
|
+
* a small header; an oversize payload is dropped at each hop (there is no group fallback), so
|
|
442
|
+
* keep datagram payloads small (e.g. a single audio frame). Datagrams are never delivered
|
|
443
|
+
* over IETF moq-transport or stream-only transports (the WebSocket fallback). A payload over
|
|
444
|
+
* 65535 bytes (the QUIC datagram frame ceiling) throws. An origin publisher uses this; a
|
|
445
|
+
* relay preserving upstream numbering uses {@link writeDatagram}.
|
|
446
|
+
*/
|
|
447
|
+
appendDatagram(timestamp, payload) {
|
|
448
|
+
if (this.#state.closed.peek() !== undefined)
|
|
449
|
+
throw new Error("track is closed");
|
|
450
|
+
if (payload.byteLength > MAX_DATAGRAM_BYTES)
|
|
451
|
+
throw new Error("datagram payload too large");
|
|
452
|
+
const counter = this.#sequence;
|
|
453
|
+
const sequence = counter.next;
|
|
454
|
+
counter.next = sequence + 1;
|
|
455
|
+
this.#publishDatagram({ sequence, timestamp, payload });
|
|
456
|
+
return sequence;
|
|
61
457
|
}
|
|
62
458
|
/**
|
|
63
|
-
*
|
|
459
|
+
* Write a datagram with an explicit sequence number.
|
|
64
460
|
*
|
|
65
|
-
*
|
|
461
|
+
* Preserves the supplied sequence (advancing the shared counter if needed) so a relay can
|
|
462
|
+
* forward a datagram without renumbering it. The size limits of {@link appendDatagram}
|
|
463
|
+
* apply. Most origin publishers want {@link appendDatagram} instead.
|
|
66
464
|
*/
|
|
465
|
+
writeDatagram(datagram) {
|
|
466
|
+
if (this.#state.closed.peek() !== undefined)
|
|
467
|
+
throw new Error("track is closed");
|
|
468
|
+
if (datagram.payload.byteLength > MAX_DATAGRAM_BYTES)
|
|
469
|
+
throw new Error("datagram payload too large");
|
|
470
|
+
const sequence = this.#sequence;
|
|
471
|
+
if (datagram.sequence >= sequence.next) {
|
|
472
|
+
sequence.next = datagram.sequence + 1;
|
|
473
|
+
}
|
|
474
|
+
this.#publishDatagram(datagram);
|
|
475
|
+
}
|
|
476
|
+
/** Close the track and every subscriber, mirroring the abort to their groups. Idempotent. */
|
|
477
|
+
close(abort) {
|
|
478
|
+
closeTrackState(this.#state, abort);
|
|
479
|
+
for (const { group } of this.#cache)
|
|
480
|
+
group.close(abort);
|
|
481
|
+
for (const sink of this.#sinks) {
|
|
482
|
+
for (const group of sink.groups.peek())
|
|
483
|
+
group.close(abort);
|
|
484
|
+
closeTrackState(sink, abort);
|
|
485
|
+
}
|
|
486
|
+
this.#sinks.clear();
|
|
487
|
+
}
|
|
488
|
+
/** Append a frame as its own single-frame group. */
|
|
67
489
|
writeFrame(frame) {
|
|
68
490
|
const group = this.appendGroup();
|
|
69
491
|
group.writeFrame(frame);
|
|
@@ -87,101 +509,299 @@ export class Track {
|
|
|
87
509
|
group.writeBool(bool);
|
|
88
510
|
group.close();
|
|
89
511
|
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* The read side of a live track subscription, mirroring the Rust `Subscriber`.
|
|
515
|
+
*
|
|
516
|
+
* Obtained from `Broadcast.Consumer.subscribe` / `Track.Consumer.subscribe`, or from
|
|
517
|
+
* {@link Producer.subscribe} for an in-process track. Reads the groups a
|
|
518
|
+
* {@link Producer} on the same underlying state writes.
|
|
519
|
+
*/
|
|
520
|
+
export class Subscriber {
|
|
521
|
+
/** The track name. */
|
|
522
|
+
name;
|
|
523
|
+
#state;
|
|
524
|
+
#nextSequence = 0;
|
|
525
|
+
#cursor = new Signal({ start: 0 });
|
|
526
|
+
constructor(name, state) {
|
|
527
|
+
this.name = name;
|
|
528
|
+
this.#state = state;
|
|
529
|
+
}
|
|
530
|
+
static {
|
|
531
|
+
makeSubscriber = (name, state) => new Subscriber(name, state);
|
|
532
|
+
}
|
|
90
533
|
/**
|
|
91
|
-
*
|
|
534
|
+
* Resolve this track's immutable publisher properties.
|
|
92
535
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
536
|
+
* Resolves once the wire layer commits the TRACK_INFO it received (lite-05+) or
|
|
537
|
+
* defaults (older drafts), so awaiting it never yields a placeholder. Rejects if
|
|
538
|
+
* the track is closed before the properties are known (e.g. a rejected subscription).
|
|
539
|
+
*/
|
|
540
|
+
info() {
|
|
541
|
+
return resolveInfo(this.#state);
|
|
542
|
+
}
|
|
543
|
+
/** Settles once the track closes; see {@link Producer.closed}. */
|
|
544
|
+
get closed() {
|
|
545
|
+
return this.#state.closed;
|
|
546
|
+
}
|
|
547
|
+
/** This subscriber's current options, including defaults and the last {@link update}. */
|
|
548
|
+
get subscription() {
|
|
549
|
+
return this.#state.update;
|
|
550
|
+
}
|
|
551
|
+
/** Return the latest group sequence observed on this track, if any. */
|
|
552
|
+
latest() {
|
|
553
|
+
return this.#state.latest;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The newest frame this track has produced, or `undefined` while it has none.
|
|
557
|
+
*
|
|
558
|
+
* Read from the groups buffered for this subscriber, so nothing is consumed. A newest
|
|
559
|
+
* group that has no frames yet falls back to the newest older group that does. It
|
|
560
|
+
* reads as `undefined` once the newest group has been evicted or received: what is
|
|
561
|
+
* left can no longer say where the edge is.
|
|
562
|
+
*/
|
|
563
|
+
largest() {
|
|
564
|
+
const latest = this.#state.latest;
|
|
565
|
+
if (latest === undefined)
|
|
566
|
+
return undefined;
|
|
567
|
+
const groups = this.#state.groups.peek();
|
|
568
|
+
const newest = groups[groups.length - 1];
|
|
569
|
+
if (!newest || newest.sequence !== latest)
|
|
570
|
+
return undefined;
|
|
571
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
572
|
+
const count = groups[i].frameCount;
|
|
573
|
+
if (count > 0)
|
|
574
|
+
return { group: groups[i].sequence, frame: count - 1 };
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* An independent subscriber to the same track, with its own cursor and its own replay of
|
|
580
|
+
* the retained window.
|
|
581
|
+
*
|
|
582
|
+
* Reads here do not consume anything this one would deliver, which is what lets a
|
|
583
|
+
* publisher read the cache alongside the cursor it is serving from. Resolving the track
|
|
584
|
+
* again through the broadcast would not do: a dynamic serve is one request per peer
|
|
585
|
+
* subscription, so that mints a second producer the application has to answer separately.
|
|
586
|
+
*
|
|
587
|
+
* @internal Wire layers only.
|
|
588
|
+
*/
|
|
589
|
+
fork(options) {
|
|
590
|
+
const producer = this.#state.producer;
|
|
591
|
+
if (!producer)
|
|
592
|
+
throw new Error("track has no producer to fork from");
|
|
593
|
+
return producer.subscribe(options);
|
|
594
|
+
}
|
|
595
|
+
/** Start this subscriber's local read cursor at `sequence`, without changing its wire request. */
|
|
596
|
+
startAt(sequence) {
|
|
597
|
+
this.#cursor.update((cursor) => ({ ...cursor, start: sequence }));
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Cap {@link nextGroup} and {@link recvGroup} at `sequence` inclusively, or omit it to
|
|
601
|
+
* remove the cap. Groups above the cap remain buffered and become readable if the cap
|
|
602
|
+
* is raised. This local cursor does not change the subscription's wire request.
|
|
603
|
+
*/
|
|
604
|
+
endAt(sequence) {
|
|
605
|
+
this.#cursor.update((cursor) => ({ ...cursor, end: sequence }));
|
|
606
|
+
}
|
|
607
|
+
/** Close the track (optionally with an error), closing any pending groups. Idempotent. */
|
|
608
|
+
close(abort) {
|
|
609
|
+
// Settle if we're first (the producer may already have); either way drop anything
|
|
610
|
+
// still buffered. Groups parked at the endAt cap deliberately outlive a clean
|
|
611
|
+
// producer close, so the subscriber leaving is what must release them: closing
|
|
612
|
+
// and clearing wakes a pending read to observe the end instead of hanging.
|
|
613
|
+
closeTrackState(this.#state, abort);
|
|
614
|
+
this.#state.groups.mutate((groups) => {
|
|
615
|
+
for (const group of groups)
|
|
616
|
+
group.close(abort);
|
|
617
|
+
groups.length = 0;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Receive every group on this track exactly once, as it becomes available.
|
|
622
|
+
*
|
|
623
|
+
* Groups may arrive out of order or with gaps due to network conditions; unlike
|
|
624
|
+
* {@link nextGroup}, one that arrives after a newer group was already returned is
|
|
625
|
+
* still delivered. When several groups are buffered, the lowest sequence is
|
|
626
|
+
* returned first.
|
|
627
|
+
*
|
|
628
|
+
* Honors the floor set by {@link startAt} and the cap set by {@link endAt}: a group
|
|
629
|
+
* beyond the cap stays buffered (not dropped) and is offered once the cap rises, even
|
|
630
|
+
* after a clean close, without blocking in-range groups that arrive behind it.
|
|
96
631
|
*/
|
|
97
632
|
async recvGroup() {
|
|
98
633
|
for (;;) {
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
const closed = this.state.closed.peek();
|
|
634
|
+
const group = this.tryRecvGroup();
|
|
635
|
+
if (group)
|
|
636
|
+
return group;
|
|
637
|
+
const closed = this.#state.closed.peek();
|
|
104
638
|
if (closed instanceof Error)
|
|
105
639
|
throw closed;
|
|
106
|
-
if
|
|
640
|
+
// A group beyond the cap outlives a clean close: it becomes deliverable if
|
|
641
|
+
// the cap rises, so the track isn't over while any are held.
|
|
642
|
+
if (closed !== undefined && !this.#state.groups.peek()[0])
|
|
107
643
|
return undefined;
|
|
108
|
-
await Signal.race(this
|
|
644
|
+
await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
|
|
109
645
|
}
|
|
110
646
|
}
|
|
111
647
|
/**
|
|
112
|
-
*
|
|
648
|
+
* Take the next buffered group without blocking, honoring the same cursor bounds as
|
|
649
|
+
* {@link recvGroup}.
|
|
650
|
+
*
|
|
651
|
+
* Returns `undefined` when nothing is deliverable right now, which is not by itself the
|
|
652
|
+
* end of the track: a group may still arrive, or one may be parked beyond the
|
|
653
|
+
* {@link endAt} cap. Use it to drain what the retained window already holds, where
|
|
654
|
+
* waiting for a sequence nothing will republish would park forever.
|
|
113
655
|
*/
|
|
114
|
-
|
|
115
|
-
|
|
656
|
+
tryRecvGroup() {
|
|
657
|
+
const groups = this.#state.groups.peek();
|
|
658
|
+
const { start, end } = this.#cursor.peek();
|
|
659
|
+
while (groups.length > 0 && groups[0].sequence < start)
|
|
660
|
+
groups.shift()?.close();
|
|
661
|
+
// The buffer is sequence-sorted, so an in-range group that arrives behind a
|
|
662
|
+
// beyond-cap one sorts in front of it and is never blocked by it.
|
|
663
|
+
const group = groups[0];
|
|
664
|
+
if (group && (end === undefined || group.sequence <= end)) {
|
|
665
|
+
groups.shift();
|
|
666
|
+
return group;
|
|
667
|
+
}
|
|
668
|
+
return undefined;
|
|
116
669
|
}
|
|
117
670
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* Late arrivals (with a sequence number at or below the last one returned) are silently skipped.
|
|
671
|
+
* Receive the next datagram in arrival order.
|
|
121
672
|
*
|
|
122
|
-
*
|
|
673
|
+
* Datagrams are a separate best-effort channel from groups (see
|
|
674
|
+
* {@link Producer.appendDatagram}); they share only the sequence namespace. A consumer
|
|
675
|
+
* that falls too far behind silently loses the oldest datagrams. Read this alongside
|
|
676
|
+
* {@link recvGroup} (e.g. in a separate loop) to receive both channels concurrently. Returning
|
|
677
|
+
* a datagram advances {@link nextGroup} past that sequence.
|
|
123
678
|
*/
|
|
124
|
-
async
|
|
679
|
+
async recvDatagram() {
|
|
125
680
|
for (;;) {
|
|
126
|
-
const
|
|
127
|
-
|
|
681
|
+
const datagrams = this.#state.datagrams.peek();
|
|
682
|
+
// Evict datagrams older than the send-buffer window (also enforced on write), so a
|
|
683
|
+
// reader that stalled skips stale datagrams instead of replaying them.
|
|
684
|
+
const cutoff = performance.now() - MAX_DATAGRAM_AGE_MS;
|
|
685
|
+
while (datagrams.length > 0 && datagrams[0].time < cutoff)
|
|
686
|
+
datagrams.shift();
|
|
687
|
+
if (datagrams.length > 0) {
|
|
688
|
+
const datagram = datagrams.shift()?.datagram;
|
|
689
|
+
if (datagram) {
|
|
690
|
+
this.#nextSequence = Math.max(this.#nextSequence, datagram.sequence + 1);
|
|
691
|
+
}
|
|
692
|
+
return datagram;
|
|
693
|
+
}
|
|
694
|
+
const closed = this.#state.closed.peek();
|
|
695
|
+
if (closed instanceof Error)
|
|
696
|
+
throw closed;
|
|
697
|
+
if (closed !== undefined)
|
|
128
698
|
return undefined;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
699
|
+
await Signal.race(this.#state.datagrams, this.#state.closed);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Return the next group with a strictly-greater sequence number than the last returned.
|
|
704
|
+
*
|
|
705
|
+
* Late arrivals (sequence at or below the last returned) are silently skipped.
|
|
706
|
+
* Use {@link recvGroup} to see every group in arrival order instead.
|
|
707
|
+
*/
|
|
708
|
+
async nextGroup() {
|
|
709
|
+
for (;;) {
|
|
710
|
+
const groups = this.#state.groups.peek();
|
|
711
|
+
const cursor = this.#cursor.peek();
|
|
712
|
+
const start = Math.max(cursor.start, this.#nextSequence);
|
|
713
|
+
while (groups.length > 0 && groups[0].sequence < start)
|
|
714
|
+
groups.shift()?.close();
|
|
715
|
+
const group = groups[0];
|
|
716
|
+
if (group && (cursor.end === undefined || group.sequence <= cursor.end)) {
|
|
717
|
+
groups.shift();
|
|
718
|
+
this.#nextSequence = group.sequence + 1;
|
|
719
|
+
return group;
|
|
132
720
|
}
|
|
133
|
-
|
|
134
|
-
|
|
721
|
+
const closed = this.#state.closed.peek();
|
|
722
|
+
if (closed instanceof Error)
|
|
723
|
+
throw closed;
|
|
724
|
+
if (closed !== undefined && !group)
|
|
725
|
+
return undefined;
|
|
726
|
+
await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
|
|
135
727
|
}
|
|
136
728
|
}
|
|
137
|
-
/**
|
|
729
|
+
/**
|
|
730
|
+
* Reads the next frame across all groups, discarding older groups.
|
|
731
|
+
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
732
|
+
*/
|
|
138
733
|
async readFrame() {
|
|
139
|
-
|
|
734
|
+
const next = await this.readFrameSequence();
|
|
735
|
+
return next ? { payload: next.payload, timestamp: next.timestamp } : undefined;
|
|
140
736
|
}
|
|
141
|
-
/**
|
|
737
|
+
/**
|
|
738
|
+
* Reads the next frame along with its group and frame sequence numbers.
|
|
739
|
+
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
740
|
+
*/
|
|
142
741
|
async readFrameSequence() {
|
|
143
742
|
for (;;) {
|
|
144
|
-
const groups = this
|
|
145
|
-
|
|
743
|
+
const groups = this.#state.groups.peek();
|
|
744
|
+
const { start } = this.#cursor.peek();
|
|
745
|
+
while (groups.length > 0 && groups[0].sequence < start)
|
|
746
|
+
groups.shift()?.close();
|
|
747
|
+
// Drain older groups first, dropping each once empty.
|
|
146
748
|
while (groups.length > 1) {
|
|
749
|
+
if (groups[0].skipped) {
|
|
750
|
+
// The reader fell behind this group's eviction window. Drop it and
|
|
751
|
+
// signal the gap; the next read resyncs from the following group.
|
|
752
|
+
groups.shift()?.close();
|
|
753
|
+
throw new Lagged();
|
|
754
|
+
}
|
|
147
755
|
const next = groups[0].tryReadFrameSequence();
|
|
148
756
|
if (next) {
|
|
149
|
-
return {
|
|
757
|
+
return {
|
|
758
|
+
group: groups[0].sequence,
|
|
759
|
+
frame: next.sequence,
|
|
760
|
+
payload: next.payload,
|
|
761
|
+
timestamp: next.timestamp,
|
|
762
|
+
};
|
|
150
763
|
}
|
|
151
|
-
// Skip this old group
|
|
152
764
|
groups.shift()?.close();
|
|
153
765
|
}
|
|
154
|
-
// If there's no groups, wait for a new one.
|
|
155
766
|
if (groups.length === 0) {
|
|
156
|
-
const closed = this
|
|
767
|
+
const closed = this.#state.closed.peek();
|
|
157
768
|
if (closed instanceof Error)
|
|
158
769
|
throw closed;
|
|
159
|
-
if (closed)
|
|
770
|
+
if (closed !== undefined)
|
|
160
771
|
return undefined;
|
|
161
|
-
await Signal.race(this
|
|
772
|
+
await Signal.race(this.#state.groups, this.#cursor, this.#state.closed);
|
|
162
773
|
continue;
|
|
163
774
|
}
|
|
164
|
-
// If there's a group, wait for a frame.
|
|
165
775
|
const group = groups[0];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
776
|
+
if (group.skipped) {
|
|
777
|
+
// Fell behind this group's eviction window. Drop it and signal the gap;
|
|
778
|
+
// the next read resyncs from the following group.
|
|
779
|
+
groups.shift()?.close();
|
|
780
|
+
throw new Lagged();
|
|
169
781
|
}
|
|
170
|
-
|
|
171
|
-
|
|
782
|
+
const next = group.tryReadFrameSequence();
|
|
783
|
+
if (next)
|
|
784
|
+
return {
|
|
785
|
+
group: group.sequence,
|
|
786
|
+
frame: next.sequence,
|
|
787
|
+
payload: next.payload,
|
|
788
|
+
timestamp: next.timestamp,
|
|
789
|
+
};
|
|
790
|
+
const closed = this.#state.closed.peek();
|
|
172
791
|
if (closed instanceof Error)
|
|
173
792
|
throw closed;
|
|
174
|
-
if (closed)
|
|
793
|
+
if (closed !== undefined)
|
|
175
794
|
return undefined;
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
795
|
+
// A finished (drained + closed) group has nothing left: drop it and loop, rather than
|
|
796
|
+
// busy-waiting on its already-resolved readable() (which would livelock and starve the
|
|
797
|
+
// macrotask that delivers the next group).
|
|
179
798
|
if (group.done) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
else {
|
|
183
|
-
await Promise.race([Signal.race(this.state.groups, this.state.closed), group.readable()]);
|
|
799
|
+
groups.shift()?.close();
|
|
800
|
+
continue;
|
|
184
801
|
}
|
|
802
|
+
// Lone open group with nothing buffered yet: wait for a frame on it, a new group, or
|
|
803
|
+
// the track closing.
|
|
804
|
+
await Promise.race([Signal.race(this.#state.groups, this.#cursor, this.#state.closed), group.readable()]);
|
|
185
805
|
}
|
|
186
806
|
}
|
|
187
807
|
/** Reads the next frame and decodes it as a UTF-8 string. */
|
|
@@ -189,7 +809,7 @@ export class Track {
|
|
|
189
809
|
const next = await this.readFrame();
|
|
190
810
|
if (!next)
|
|
191
811
|
return undefined;
|
|
192
|
-
return new TextDecoder().decode(next);
|
|
812
|
+
return new TextDecoder().decode(next.payload);
|
|
193
813
|
}
|
|
194
814
|
/** Reads the next frame and parses it as JSON. */
|
|
195
815
|
async readJson() {
|
|
@@ -203,25 +823,17 @@ export class Track {
|
|
|
203
823
|
const next = await this.readFrame();
|
|
204
824
|
if (!next)
|
|
205
825
|
return undefined;
|
|
206
|
-
|
|
826
|
+
const payload = next.payload;
|
|
827
|
+
if (payload.byteLength !== 1 || !(payload[0] === 0 || payload[0] === 1))
|
|
207
828
|
throw new Error("invalid bool frame");
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Update the subscription priority. Triggers a SUBSCRIBE_UPDATE
|
|
212
|
-
* to the publisher when used on a subscribed track.
|
|
213
|
-
*/
|
|
214
|
-
updatePriority(priority) {
|
|
215
|
-
this.state.priority.set(priority, true);
|
|
829
|
+
return payload[0] === 1;
|
|
216
830
|
}
|
|
217
831
|
/**
|
|
218
|
-
*
|
|
832
|
+
* Update this subscription's options (e.g. priority), triggering a SUBSCRIBE_UPDATE to the
|
|
833
|
+
* publisher. Mirrors the Rust `Subscriber::update`.
|
|
219
834
|
*/
|
|
220
|
-
|
|
221
|
-
this
|
|
222
|
-
for (const group of this.state.groups.peek()) {
|
|
223
|
-
group.close(abort);
|
|
224
|
-
}
|
|
835
|
+
update(options) {
|
|
836
|
+
this.#state.update.set(subscriptionDefaults(options));
|
|
225
837
|
}
|
|
226
838
|
}
|
|
227
839
|
//# sourceMappingURL=track.js.map
|