@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/group.js
CHANGED
|
@@ -5,36 +5,27 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { Once, Signal } from "@norskvideo/moq-signals";
|
|
8
|
+
import { FrameTooLarge, GroupTooLarge, TooFarBehind } from "./error.js";
|
|
9
|
+
import { hooks } from "./internal.js";
|
|
8
10
|
import { Timestamp } from "./time.js";
|
|
9
|
-
/** Maximum bytes of frames
|
|
11
|
+
/** Maximum bytes of frames in a group. A write that would exceed this aborts with {@link GroupTooLarge}. */
|
|
10
12
|
export const MAX_GROUP_CACHE_BYTES = 32 * 1024 * 1024;
|
|
11
|
-
/** Maximum number of frames
|
|
12
|
-
export const MAX_GROUP_FRAMES =
|
|
13
|
-
/**
|
|
14
|
-
* Thrown by a frame read when the reader fell behind the group's eviction window: frames
|
|
15
|
-
* it had not yet read were dropped to stay under the cache cap, so the stream has a gap.
|
|
16
|
-
*/
|
|
17
|
-
export class Lagged extends Error {
|
|
18
|
-
constructor() {
|
|
19
|
-
super("lagged: frames were evicted before being read");
|
|
20
|
-
this.name = "Lagged";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
13
|
+
/** Maximum number of frames in a group. 8192 is the largest legal group; the 8193rd write is {@link GroupTooLarge}. */
|
|
14
|
+
export const MAX_GROUP_FRAMES = 8192;
|
|
23
15
|
/** Reactive backing state shared by the group producer and one consumer. */
|
|
24
16
|
class GroupState {
|
|
25
17
|
sequence;
|
|
26
18
|
frames = new Signal([]);
|
|
27
19
|
closed = new Once();
|
|
28
20
|
total = new Signal(0); // The total number of frames in the group thus far
|
|
29
|
-
// Absolute sequence of the frame at the front of `frames`, advanced by a read
|
|
30
|
-
//
|
|
21
|
+
// Absolute sequence of the frame at the front of `frames`, advanced by a read: the
|
|
22
|
+
// sequence the next read returns.
|
|
31
23
|
start = 0;
|
|
32
|
-
// One past the newest frame the cache cap evicted before it could be read, or 0 when
|
|
33
|
-
// none was. A read that wanted a frame below it has a gap, so it throws Lagged rather
|
|
34
|
-
// than skipping silently; a read that starts above it never asked for the missing
|
|
35
|
-
// frames, so it proceeds.
|
|
36
|
-
evicted = 0;
|
|
37
24
|
cacheBytes = 0;
|
|
25
|
+
// The first frame's timestamp, retained after reads.
|
|
26
|
+
timestamp;
|
|
27
|
+
// The newest frame's timestamp: where a reader that has taken every frame sits.
|
|
28
|
+
latest;
|
|
38
29
|
constructor(sequence) {
|
|
39
30
|
this.sequence = sequence;
|
|
40
31
|
}
|
|
@@ -42,17 +33,11 @@ class GroupState {
|
|
|
42
33
|
function appendFrame(state, frame) {
|
|
43
34
|
if (state.closed.peek() !== undefined)
|
|
44
35
|
throw new Error("group is closed");
|
|
36
|
+
state.timestamp ??= frame.timestamp;
|
|
37
|
+
state.latest = frame.timestamp;
|
|
45
38
|
state.cacheBytes += frame.payload.byteLength;
|
|
46
39
|
state.frames.mutate((frames) => {
|
|
47
40
|
frames.push(frame);
|
|
48
|
-
while (frames.length > MAX_GROUP_FRAMES || state.cacheBytes > MAX_GROUP_CACHE_BYTES) {
|
|
49
|
-
const evicted = frames.shift();
|
|
50
|
-
if (!evicted)
|
|
51
|
-
break;
|
|
52
|
-
state.cacheBytes -= evicted.payload.byteLength;
|
|
53
|
-
state.start++;
|
|
54
|
-
state.evicted = state.start;
|
|
55
|
-
}
|
|
56
41
|
});
|
|
57
42
|
state.total.update((total) => total + 1);
|
|
58
43
|
}
|
|
@@ -70,10 +55,21 @@ export class Producer {
|
|
|
70
55
|
// cancel an abandoned download; a group can stay open indefinitely (a catalog or JSON stream),
|
|
71
56
|
// so this is what stops a reader-less fetch instead of the stream ending on its own.
|
|
72
57
|
#used = new Signal(false);
|
|
58
|
+
// When the group was created or last written. Retention ages a group from this,
|
|
59
|
+
// not from its creation, so a group still being filled is never reclaimed.
|
|
60
|
+
#activity = performance.now();
|
|
73
61
|
constructor(sequence) {
|
|
74
62
|
this.#state = new GroupState(sequence);
|
|
75
63
|
this.sequence = sequence;
|
|
76
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* When the group was created or last written, in `performance.now()` milliseconds.
|
|
67
|
+
*
|
|
68
|
+
* @internal Track retention only.
|
|
69
|
+
*/
|
|
70
|
+
get activity() {
|
|
71
|
+
return this.#activity;
|
|
72
|
+
}
|
|
77
73
|
/**
|
|
78
74
|
* Settles once the group closes: `null` on a clean close, or the abort {@link Error}.
|
|
79
75
|
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
|
|
@@ -95,10 +91,11 @@ export class Producer {
|
|
|
95
91
|
*/
|
|
96
92
|
mirror() {
|
|
97
93
|
const dst = new GroupState(this.sequence);
|
|
94
|
+
dst.timestamp = this.#state.timestamp;
|
|
95
|
+
dst.latest = this.#state.latest;
|
|
98
96
|
for (const frame of this.#state.frames.peek())
|
|
99
97
|
appendFrame(dst, frame);
|
|
100
98
|
dst.start = this.#state.start;
|
|
101
|
-
dst.evicted = this.#state.evicted;
|
|
102
99
|
// The replay only covers what is still buffered, so the count has to come from the
|
|
103
100
|
// source: it is what names a frame's absolute sequence, and what a publisher reads
|
|
104
101
|
// to resolve the live edge.
|
|
@@ -146,6 +143,30 @@ export class Producer {
|
|
|
146
143
|
}
|
|
147
144
|
/** Writes a frame to the group. */
|
|
148
145
|
writeFrame(frame) {
|
|
146
|
+
// Closed first: a write after a clean close at the cap would otherwise take the
|
|
147
|
+
// overflow path, wipe committed frames, and leave readers with an empty success.
|
|
148
|
+
if (this.#state.closed.peek() !== undefined)
|
|
149
|
+
throw new Error("group is closed");
|
|
150
|
+
// A frame past the cache cap would make the group exceed its budget by itself, so
|
|
151
|
+
// accepting it would report success for a write nothing can ever read. Rust rejects
|
|
152
|
+
// it up front with `Error::FrameTooLarge`; do the same rather than silently dropping it.
|
|
153
|
+
if (frame.payload.byteLength > MAX_GROUP_CACHE_BYTES)
|
|
154
|
+
throw new FrameTooLarge();
|
|
155
|
+
if (this.#state.total.peek() >= MAX_GROUP_FRAMES ||
|
|
156
|
+
this.#state.cacheBytes + frame.payload.byteLength > MAX_GROUP_CACHE_BYTES) {
|
|
157
|
+
const err = new GroupTooLarge();
|
|
158
|
+
this.#state.frames.set([]);
|
|
159
|
+
this.#state.cacheBytes = 0;
|
|
160
|
+
if (this.#mirrors) {
|
|
161
|
+
for (const mirror of this.#mirrors) {
|
|
162
|
+
mirror.frames.set([]);
|
|
163
|
+
mirror.cacheBytes = 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.close(err);
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
this.#activity = performance.now();
|
|
149
170
|
appendFrame(this.#state, frame);
|
|
150
171
|
if (this.#mirrors) {
|
|
151
172
|
for (const mirror of this.#mirrors) {
|
|
@@ -187,6 +208,65 @@ export class Producer {
|
|
|
187
208
|
}
|
|
188
209
|
}
|
|
189
210
|
let makeConsumer;
|
|
211
|
+
// A consumer can finish from its own expiry verdict or from the shared group state. Keep
|
|
212
|
+
// one stable public handle while letting peek() observe either source synchronously, before
|
|
213
|
+
// signal subscribers run in the next microtask.
|
|
214
|
+
class CombinedClosed {
|
|
215
|
+
#preferred;
|
|
216
|
+
#source;
|
|
217
|
+
#sourceCleanReady;
|
|
218
|
+
#value = new Once();
|
|
219
|
+
#dispose;
|
|
220
|
+
constructor(preferred, source, sourceCleanReady) {
|
|
221
|
+
this.#preferred = preferred;
|
|
222
|
+
this.#source = source;
|
|
223
|
+
this.#sourceCleanReady = sourceCleanReady;
|
|
224
|
+
if (this.#sync())
|
|
225
|
+
return;
|
|
226
|
+
const dispose = [preferred.changed(() => this.#sync()), source.changed(() => this.#sync())];
|
|
227
|
+
this.#dispose = () => {
|
|
228
|
+
for (const close of dispose)
|
|
229
|
+
close();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
#sync() {
|
|
233
|
+
if (this.#value.peek() !== undefined)
|
|
234
|
+
return true;
|
|
235
|
+
const preferred = this.#preferred.peek();
|
|
236
|
+
const source = this.#source.peek();
|
|
237
|
+
const value = preferred !== undefined
|
|
238
|
+
? preferred
|
|
239
|
+
: source instanceof Error || this.#sourceCleanReady()
|
|
240
|
+
? source
|
|
241
|
+
: undefined;
|
|
242
|
+
if (value === undefined)
|
|
243
|
+
return false;
|
|
244
|
+
this.#value.set(value);
|
|
245
|
+
this.#dispose?.();
|
|
246
|
+
this.#dispose = undefined;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
refresh() {
|
|
250
|
+
this.#sync();
|
|
251
|
+
}
|
|
252
|
+
peek() {
|
|
253
|
+
this.#sync();
|
|
254
|
+
return this.#value.peek();
|
|
255
|
+
}
|
|
256
|
+
changed(fn) {
|
|
257
|
+
this.#sync();
|
|
258
|
+
return fn ? this.#value.changed(fn) : this.#value.changed();
|
|
259
|
+
}
|
|
260
|
+
subscribe(fn) {
|
|
261
|
+
this.#sync();
|
|
262
|
+
return this.#value.subscribe(fn);
|
|
263
|
+
}
|
|
264
|
+
// biome-ignore lint/suspicious/noThenProperty: CombinedClosed is intentionally awaitable.
|
|
265
|
+
then(onFulfilled, onRejected) {
|
|
266
|
+
this.#sync();
|
|
267
|
+
return this.#value.then(onFulfilled, onRejected);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
190
270
|
/**
|
|
191
271
|
* The read side of an ordered stream of frames within a track.
|
|
192
272
|
*
|
|
@@ -199,42 +279,160 @@ export class Consumer {
|
|
|
199
279
|
/** Sequence number of this group within its track. */
|
|
200
280
|
sequence;
|
|
201
281
|
#state;
|
|
282
|
+
#expiry;
|
|
283
|
+
// Sticky verdicts, set once. `#ended` is a drained group the budget gave up on,
|
|
284
|
+
// which is indistinguishable from one that ended: nothing was lost, so it reads as
|
|
285
|
+
// the end of the group. `#terminal` is a failure, including a budget that gave up
|
|
286
|
+
// while content was still unread.
|
|
287
|
+
#ended = false;
|
|
288
|
+
#terminal;
|
|
289
|
+
#verdict = new Once();
|
|
290
|
+
#closed;
|
|
291
|
+
#pendingFrames = 0;
|
|
202
292
|
constructor(state) {
|
|
203
293
|
this.#state = state;
|
|
204
294
|
this.sequence = state.sequence;
|
|
205
295
|
}
|
|
206
296
|
/**
|
|
207
|
-
* Settles
|
|
297
|
+
* Settles when this consumer reaches a terminal state: `null` after a clean close has no
|
|
298
|
+
* unread or in-flight frames, or the terminal {@link Error}.
|
|
208
299
|
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
|
|
209
300
|
*/
|
|
210
301
|
get closed() {
|
|
211
|
-
|
|
302
|
+
this.#closed ??= new CombinedClosed(this.#verdict, this.#state.closed, () => this.#state.frames.peek().length === 0 && this.#pendingFrames === 0);
|
|
303
|
+
return this.#closed;
|
|
212
304
|
}
|
|
213
305
|
static {
|
|
214
306
|
makeConsumer = (state) => new Consumer(state);
|
|
307
|
+
hooks.groupTimestamp = (group) => group.#state.timestamp;
|
|
308
|
+
hooks.groupLatest = (group) => group.#state.latest;
|
|
309
|
+
hooks.expireGroup = (group, expiry) => {
|
|
310
|
+
group.#expiry = expiry;
|
|
311
|
+
};
|
|
312
|
+
hooks.guardGroup = (group, operation) => group.#guard(operation);
|
|
313
|
+
hooks.readGroupFrame = (group, from) => group.#readFramePosition(true, from);
|
|
314
|
+
hooks.evictGroup = (group) => {
|
|
315
|
+
group.#evict();
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Evaluate the drift budget for a read that has nothing buffered and is about to
|
|
319
|
+
// wait. A group with frames in hand is always drained: the budget bounds a group
|
|
320
|
+
// that has stalled while the live edge moved on, not a reader slower than the wire.
|
|
321
|
+
// Returns true if a verdict was just reached, so the caller re-checks.
|
|
322
|
+
//
|
|
323
|
+
// `unread` is for a caller that still holds content the reader has not seen (a
|
|
324
|
+
// publisher part-way through writing a frame): giving up there is a truncation, so
|
|
325
|
+
// it fails rather than ending.
|
|
326
|
+
#expire(unread = false) {
|
|
327
|
+
if (this.#terminal || this.#ended)
|
|
328
|
+
return false;
|
|
329
|
+
if (this.#state.closed.peek() instanceof Error)
|
|
330
|
+
return false;
|
|
331
|
+
if (!this.#expiry?.expired())
|
|
332
|
+
return false;
|
|
333
|
+
if (unread) {
|
|
334
|
+
this.#terminal = new Error("group exceeded the subscription max age budget");
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.#ended = true;
|
|
338
|
+
}
|
|
339
|
+
if (this.#verdict.peek() === undefined)
|
|
340
|
+
this.#verdict.set(this.#terminal ?? null);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
// Takes a thunk rather than a started promise, so a group already past its budget
|
|
344
|
+
// never begins the write: an abandoned write would reject later with no handler.
|
|
345
|
+
#guard(operation) {
|
|
346
|
+
this.#expire(true);
|
|
347
|
+
if (this.#terminal)
|
|
348
|
+
return Promise.reject(this.#terminal);
|
|
349
|
+
const expiry = this.#expiry;
|
|
350
|
+
if (!expiry)
|
|
351
|
+
return operation();
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
let settled = false;
|
|
354
|
+
const disposes = [];
|
|
355
|
+
const finish = (fn) => {
|
|
356
|
+
if (settled)
|
|
357
|
+
return;
|
|
358
|
+
settled = true;
|
|
359
|
+
for (const dispose of disposes)
|
|
360
|
+
dispose();
|
|
361
|
+
fn();
|
|
362
|
+
};
|
|
363
|
+
const check = () => {
|
|
364
|
+
this.#expire(true);
|
|
365
|
+
if (this.#terminal) {
|
|
366
|
+
const error = this.#terminal;
|
|
367
|
+
finish(() => reject(error));
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
for (const changed of expiry.changed)
|
|
371
|
+
disposes.push(changed.subscribe(check));
|
|
372
|
+
operation().then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
373
|
+
check();
|
|
374
|
+
});
|
|
215
375
|
}
|
|
216
|
-
#
|
|
376
|
+
#evict() {
|
|
217
377
|
const frames = this.#state.frames.peek();
|
|
218
|
-
|
|
219
|
-
|
|
378
|
+
if (frames.length > 0) {
|
|
379
|
+
// Unread content is going away either way. If the budget had already given up
|
|
380
|
+
// on this group that is the more specific reason, so ask it first; otherwise
|
|
381
|
+
// the reader simply lagged behind the cache.
|
|
382
|
+
this.#expire(true);
|
|
383
|
+
if (!this.#terminal && !this.#ended) {
|
|
384
|
+
this.#terminal = new TooFarBehind();
|
|
385
|
+
if (this.#verdict.peek() === undefined)
|
|
386
|
+
this.#verdict.set(this.#terminal);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (frames.length > 0) {
|
|
390
|
+
this.#state.start += frames.length;
|
|
391
|
+
}
|
|
392
|
+
this.#state.cacheBytes = 0;
|
|
393
|
+
this.#state.frames.set([]);
|
|
394
|
+
}
|
|
395
|
+
async #changed() {
|
|
396
|
+
if (!this.#expiry) {
|
|
397
|
+
await Signal.race(this.#state.frames, this.#state.closed);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
await Signal.race(this.#state.frames, this.#state.closed, ...this.#expiry.changed);
|
|
401
|
+
}
|
|
402
|
+
#readBufferedFrame(pending = false) {
|
|
403
|
+
const frames = this.#state.frames.peek();
|
|
404
|
+
const buffered = frames.shift();
|
|
405
|
+
if (!buffered)
|
|
220
406
|
return undefined;
|
|
221
|
-
this.#state.
|
|
222
|
-
|
|
407
|
+
const sequence = this.#state.start++;
|
|
408
|
+
if (pending)
|
|
409
|
+
this.#pendingFrames++;
|
|
410
|
+
let completed = false;
|
|
411
|
+
return {
|
|
412
|
+
sequence,
|
|
413
|
+
frame: buffered,
|
|
414
|
+
complete: () => {
|
|
415
|
+
if (completed)
|
|
416
|
+
return;
|
|
417
|
+
completed = true;
|
|
418
|
+
if (pending)
|
|
419
|
+
this.#pendingFrames--;
|
|
420
|
+
this.#closed?.refresh();
|
|
421
|
+
},
|
|
422
|
+
};
|
|
223
423
|
}
|
|
224
424
|
/** True once no further frames can be read: the group has closed and every buffered frame is read. */
|
|
225
425
|
get done() {
|
|
226
|
-
return
|
|
426
|
+
return (this.#terminal !== undefined ||
|
|
427
|
+
this.#ended ||
|
|
428
|
+
(this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== undefined));
|
|
227
429
|
}
|
|
228
430
|
/** True once the group has been closed, regardless of whether buffered frames remain unread. Synchronous complement to the {@link closed} promise. */
|
|
229
431
|
get isClosed() {
|
|
230
|
-
return this.#state.closed.peek() !== undefined;
|
|
231
|
-
}
|
|
232
|
-
/** True if frames were evicted from the front of this group before being read. */
|
|
233
|
-
get skipped() {
|
|
234
|
-
return this.#state.evicted > 0;
|
|
432
|
+
return this.#terminal !== undefined || this.#ended || this.#state.closed.peek() !== undefined;
|
|
235
433
|
}
|
|
236
434
|
/**
|
|
237
|
-
* How many frames the group has held, including any already read
|
|
435
|
+
* How many frames the group has held, including any already read.
|
|
238
436
|
*
|
|
239
437
|
* It is also the next frame's sequence number, so `frameCount - 1` names the newest
|
|
240
438
|
* frame written so far. A publisher snapshots it to resolve the track's live edge.
|
|
@@ -250,24 +448,36 @@ export class Consumer {
|
|
|
250
448
|
* end-of-group: check {@link done} to tell "no frame buffered yet" from "finished".
|
|
251
449
|
*/
|
|
252
450
|
tryReadFrame() {
|
|
451
|
+
if (this.#terminal || this.#ended)
|
|
452
|
+
return undefined;
|
|
253
453
|
const read = this.#readBufferedFrame();
|
|
454
|
+
read?.complete();
|
|
254
455
|
return read?.frame;
|
|
255
456
|
}
|
|
256
457
|
/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */
|
|
257
458
|
tryReadFrameSequence() {
|
|
459
|
+
if (this.#terminal || this.#ended)
|
|
460
|
+
return undefined;
|
|
258
461
|
const read = this.#readBufferedFrame();
|
|
259
462
|
if (!read)
|
|
260
463
|
return undefined;
|
|
464
|
+
read.complete();
|
|
261
465
|
return { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };
|
|
262
466
|
}
|
|
263
467
|
/** Resolves once {@link readFrame} would not block. */
|
|
264
468
|
async readable() {
|
|
265
469
|
for (;;) {
|
|
470
|
+
if (this.#terminal || this.#ended)
|
|
471
|
+
return;
|
|
266
472
|
if (this.#state.frames.peek().length > 0)
|
|
267
473
|
return;
|
|
268
|
-
if (this.#state.closed.peek() !== undefined)
|
|
474
|
+
if (this.#state.closed.peek() !== undefined) {
|
|
475
|
+
this.#closed?.refresh();
|
|
269
476
|
return;
|
|
270
|
-
|
|
477
|
+
}
|
|
478
|
+
if (this.#expire())
|
|
479
|
+
return;
|
|
480
|
+
await this.#changed();
|
|
271
481
|
}
|
|
272
482
|
}
|
|
273
483
|
/**
|
|
@@ -275,18 +485,36 @@ export class Consumer {
|
|
|
275
485
|
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
276
486
|
*/
|
|
277
487
|
async readFrame() {
|
|
488
|
+
return (await this.#readFramePosition())?.frame;
|
|
489
|
+
}
|
|
490
|
+
async #readFramePosition(pending = false, from = 0) {
|
|
278
491
|
for (;;) {
|
|
279
|
-
if (this.#
|
|
280
|
-
throw
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
492
|
+
if (this.#terminal)
|
|
493
|
+
throw this.#terminal;
|
|
494
|
+
if (this.#ended)
|
|
495
|
+
return;
|
|
496
|
+
const read = this.#readBufferedFrame(pending);
|
|
497
|
+
if (read) {
|
|
498
|
+
if (read.sequence < from) {
|
|
499
|
+
read.complete();
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (!pending)
|
|
503
|
+
read.complete();
|
|
504
|
+
return read;
|
|
505
|
+
}
|
|
284
506
|
const closed = this.#state.closed.peek();
|
|
285
507
|
if (closed instanceof Error)
|
|
286
508
|
throw closed;
|
|
287
|
-
if (closed !== undefined)
|
|
509
|
+
if (closed !== undefined) {
|
|
510
|
+
this.#closed?.refresh();
|
|
288
511
|
return;
|
|
289
|
-
|
|
512
|
+
}
|
|
513
|
+
// Nothing buffered and the group is still open: this wait is the stall the
|
|
514
|
+
// drift budget bounds, and the only place it applies.
|
|
515
|
+
if (this.#expire())
|
|
516
|
+
continue;
|
|
517
|
+
await this.#changed();
|
|
290
518
|
}
|
|
291
519
|
}
|
|
292
520
|
/**
|
|
@@ -294,23 +522,10 @@ export class Consumer {
|
|
|
294
522
|
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
295
523
|
*/
|
|
296
524
|
async readFrameSequence(options) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const read = this.#readBufferedFrame();
|
|
302
|
-
if (read) {
|
|
303
|
-
if (read.sequence < from)
|
|
304
|
-
continue;
|
|
305
|
-
return { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };
|
|
306
|
-
}
|
|
307
|
-
const closed = this.#state.closed.peek();
|
|
308
|
-
if (closed instanceof Error)
|
|
309
|
-
throw closed;
|
|
310
|
-
if (closed !== undefined)
|
|
311
|
-
return;
|
|
312
|
-
await Signal.race(this.#state.frames, this.#state.closed);
|
|
313
|
-
}
|
|
525
|
+
const read = await this.#readFramePosition(false, options?.from ?? 0);
|
|
526
|
+
if (!read)
|
|
527
|
+
return undefined;
|
|
528
|
+
return { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };
|
|
314
529
|
}
|
|
315
530
|
/** Reads the next frame and decodes its payload as a UTF-8 string. */
|
|
316
531
|
async readString() {
|
package/group.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group.js","sourceRoot":"","sources":["../src/group.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Group role handles: an ordered stream of frames within a track, delivered over one QUIC stream.\n *\n * @module\n */\nimport { type GetPromise, type Getter, Once, Signal } from \"@moq/signals\";\nimport { Timestamp } from \"./time.ts\";\n\n/** Maximum bytes of frames cached in a group before old frames are evicted from the front. */\nexport const MAX_GROUP_CACHE_BYTES = 32 * 1024 * 1024;\n\n/** Maximum number of frames cached in a group before old frames are evicted from the front. */\nexport const MAX_GROUP_FRAMES = 1024;\n\n/**\n * A frame buffered in a group: its presentation {@link Timestamp} and payload bytes.\n *\n * The timestamp carries its own scale, so a track can pick its units; the wire layer\n * converts it into the track's negotiated timescale.\n */\nexport interface Frame {\n\t/** The frame payload. */\n\tpayload: Uint8Array;\n\t/**\n\t * Presentation timestamp. Required: for a payload with no presentation time of its own\n\t * (a JSON catalog, control state) pass {@link Timestamp.now} explicitly.\n\t */\n\ttimestamp: Timestamp;\n}\n\n/** Options for a sequence-aware frame read. */\nexport interface ReadOptions {\n\t/**\n\t * The lowest sequence number the caller wants; defaults to the whole group.\n\t *\n\t * Frames below it are discarded instead of returned, and frames evicted below it are not\n\t * a gap: the read resumes at the next retained frame rather than throwing {@link Lagged}.\n\t * An eviction at or above it still throws, because the caller asked for that frame.\n\t */\n\tfrom?: number;\n}\n\n/** Immutable group metadata. */\nexport interface Info {\n\t/** Sequence number of this group within its track. */\n\tsequence: number;\n}\n\n/**\n * Thrown by a frame read when the reader fell behind the group's eviction window: frames\n * it had not yet read were dropped to stay under the cache cap, so the stream has a gap.\n */\nexport class Lagged extends Error {\n\tconstructor() {\n\t\tsuper(\"lagged: frames were evicted before being read\");\n\t\tthis.name = \"Lagged\";\n\t}\n}\n\n/** Reactive backing state shared by the group producer and one consumer. */\nclass GroupState {\n\treadonly sequence: number;\n\tframes = new Signal<Frame[]>([]);\n\tclosed = new Once<Error | null>();\n\ttotal = new Signal<number>(0); // The total number of frames in the group thus far\n\n\t// Absolute sequence of the frame at the front of `frames`, advanced by a read and by an\n\t// eviction alike: the sequence the next read returns.\n\tstart = 0;\n\n\t// One past the newest frame the cache cap evicted before it could be read, or 0 when\n\t// none was. A read that wanted a frame below it has a gap, so it throws Lagged rather\n\t// than skipping silently; a read that starts above it never asked for the missing\n\t// frames, so it proceeds.\n\tevicted = 0;\n\n\tcacheBytes = 0;\n\n\tconstructor(sequence: number) {\n\t\tthis.sequence = sequence;\n\t}\n}\n\nfunction appendFrame(state: GroupState, frame: Frame) {\n\tif (state.closed.peek() !== undefined) throw new Error(\"group is closed\");\n\n\tstate.cacheBytes += frame.payload.byteLength;\n\tstate.frames.mutate((frames) => {\n\t\tframes.push(frame);\n\n\t\twhile (frames.length > MAX_GROUP_FRAMES || state.cacheBytes > MAX_GROUP_CACHE_BYTES) {\n\t\t\tconst evicted = frames.shift();\n\t\t\tif (!evicted) break;\n\t\t\tstate.cacheBytes -= evicted.payload.byteLength;\n\t\t\tstate.start++;\n\t\t\tstate.evicted = state.start;\n\t\t}\n\t});\n\n\tstate.total.update((total) => total + 1);\n}\n\n/**\n * The write side of an ordered stream of frames within a track.\n *\n * @public\n */\nexport class Producer {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t#state: GroupState;\n\t#mirrors?: Set<GroupState>;\n\n\t// Whether any mirror reader is attached (see {@link used}). Fetch coalescing watches it to\n\t// cancel an abandoned download; a group can stay open indefinitely (a catalog or JSON stream),\n\t// so this is what stops a reader-less fetch instead of the stream ending on its own.\n\t#used = new Signal<boolean>(false);\n\n\tconstructor(sequence: number) {\n\t\tthis.#state = new GroupState(sequence);\n\t\tthis.sequence = sequence;\n\t}\n\n\t/**\n\t * Settles once the group closes: `null` on a clean close, or the abort {@link Error}.\n\t * Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.\n\t */\n\tget closed(): GetPromise<Error | null> {\n\t\treturn this.#state.closed;\n\t}\n\n\t/** A read handle for this group. */\n\tconsume(): Consumer {\n\t\treturn makeConsumer(this.#state);\n\t}\n\n\t/**\n\t * Create an independent read handle that receives every frame written here.\n\t *\n\t * Frames written so far are replayed synchronously; later writes and close are teed\n\t * in as they happen.\n\t *\n\t * @internal Track fan-out and fetch coalescing only. Use {@link consume} instead.\n\t */\n\tmirror(): Consumer {\n\t\tconst dst = new GroupState(this.sequence);\n\t\tfor (const frame of this.#state.frames.peek()) appendFrame(dst, frame);\n\t\tdst.start = this.#state.start;\n\t\tdst.evicted = this.#state.evicted;\n\t\t// The replay only covers what is still buffered, so the count has to come from the\n\t\t// source: it is what names a frame's absolute sequence, and what a publisher reads\n\t\t// to resolve the live edge.\n\t\tdst.total.set(this.#state.total.peek());\n\n\t\tconst closed = this.#state.closed.peek();\n\t\tif (closed !== undefined) {\n\t\t\tdst.closed.set(closed);\n\t\t\treturn makeConsumer(dst);\n\t\t}\n\n\t\tthis.#mirrors ??= new Set();\n\t\tthis.#mirrors.add(dst);\n\t\tthis.#used.set(true);\n\n\t\t// Track this mirror's close eagerly (not just lazily on the next write) so demand drops the\n\t\t// moment the last reader leaves, letting an abandoned fetch cancel even with no more frames.\n\t\tconst dispose = dst.closed.subscribe((c) => {\n\t\t\tif (c === undefined) return;\n\t\t\tthis.#mirrors?.delete(dst);\n\t\t\tthis.#used.set((this.#mirrors?.size ?? 0) > 0);\n\t\t\tdispose();\n\t\t});\n\n\t\treturn makeConsumer(dst);\n\t}\n\n\t/**\n\t * Whether any mirror reader is currently attached.\n\t *\n\t * Pairs with {@link unused}. Fetch coalescing watches it to cancel a download once every reader\n\t * has gone: a group can stay open indefinitely (a catalog track, a JSON stream), so it can't\n\t * rely on the stream ending on its own.\n\t *\n\t * @internal Track fan-out and fetch coalescing only.\n\t */\n\tget used(): Getter<boolean> {\n\t\treturn this.#used;\n\t}\n\n\t/**\n\t * Resolves once no mirror reader remains (or the group closes).\n\t *\n\t * @internal Track fan-out and fetch coalescing only.\n\t */\n\tasync unused(): Promise<void> {\n\t\twhile (this.#used.peek() && this.#state.closed.peek() === undefined) {\n\t\t\tawait Signal.race(this.#used, this.#state.closed);\n\t\t}\n\t}\n\n\t/** Writes a frame to the group. */\n\twriteFrame(frame: Frame) {\n\t\tappendFrame(this.#state, frame);\n\n\t\tif (this.#mirrors) {\n\t\t\tfor (const mirror of this.#mirrors) {\n\t\t\t\tif (mirror.closed.peek() !== undefined) this.#mirrors.delete(mirror);\n\t\t\t\telse appendFrame(mirror, frame);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Write a string as a single UTF-8 encoded frame, stamped with {@link Timestamp.now}. */\n\twriteString(str: string) {\n\t\tthis.writeFrame({ payload: new TextEncoder().encode(str), timestamp: Timestamp.now() });\n\t}\n\n\t/** Write a value as a single JSON-encoded frame, stamped with {@link Timestamp.now}. */\n\twriteJson(json: unknown) {\n\t\tthis.writeString(JSON.stringify(json));\n\t}\n\n\t/** Write a boolean as a single one-byte frame, stamped with {@link Timestamp.now}. */\n\twriteBool(bool: boolean) {\n\t\tthis.writeFrame({ payload: new Uint8Array([bool ? 1 : 0]), timestamp: Timestamp.now() });\n\t}\n\n\t/** True once the group has been closed. */\n\tget isClosed(): boolean {\n\t\treturn this.#state.closed.peek() !== undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. */\n\tclose(abort?: Error) {\n\t\tif (this.#state.closed.peek() !== undefined) return;\n\t\tthis.#state.closed.set(abort ?? null);\n\n\t\tif (this.#mirrors) {\n\t\t\tfor (const mirror of this.#mirrors) {\n\t\t\t\tif (mirror.closed.peek() === undefined) mirror.closed.set(abort ?? null);\n\t\t\t}\n\t\t\tthis.#mirrors.clear();\n\t\t}\n\t}\n}\n\nlet makeConsumer: (state: GroupState) => Consumer;\n\n/**\n * The read side of an ordered stream of frames within a track.\n *\n * Created internally: obtain one from {@link Producer.consume} or a track subscriber's\n * group reads.\n *\n * @public\n */\nexport class Consumer {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t#state: GroupState;\n\n\tprivate constructor(state: GroupState) {\n\t\tthis.#state = state;\n\t\tthis.sequence = state.sequence;\n\t}\n\n\t/**\n\t * Settles once the group closes: `null` on a clean close, or the abort {@link Error}.\n\t * Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.\n\t */\n\tget closed(): GetPromise<Error | null> {\n\t\treturn this.#state.closed;\n\t}\n\n\tstatic {\n\t\tmakeConsumer = (state) => new Consumer(state);\n\t}\n\n\t#readBufferedFrame(): { sequence: number; frame: Frame } | undefined {\n\t\tconst frames = this.#state.frames.peek();\n\t\tconst frame = frames.shift();\n\t\tif (!frame) return undefined;\n\n\t\tthis.#state.cacheBytes -= frame.payload.byteLength;\n\t\treturn { sequence: this.#state.start++, frame };\n\t}\n\n\t/** True once no further frames can be read: the group has closed and every buffered frame is read. */\n\tget done(): boolean {\n\t\treturn this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== undefined;\n\t}\n\n\t/** True once the group has been closed, regardless of whether buffered frames remain unread. Synchronous complement to the {@link closed} promise. */\n\tget isClosed(): boolean {\n\t\treturn this.#state.closed.peek() !== undefined;\n\t}\n\n\t/** True if frames were evicted from the front of this group before being read. */\n\tget skipped(): boolean {\n\t\treturn this.#state.evicted > 0;\n\t}\n\n\t/**\n\t * How many frames the group has held, including any already read or evicted.\n\t *\n\t * It is also the next frame's sequence number, so `frameCount - 1` names the newest\n\t * frame written so far. A publisher snapshots it to resolve the track's live edge.\n\t */\n\tget frameCount(): number {\n\t\treturn this.#state.total.peek();\n\t}\n\n\t/**\n\t * Reads the next already-buffered frame without blocking.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t *\n\t * Returns `undefined` when nothing is buffered right now. That is not by itself\n\t * end-of-group: check {@link done} to tell \"no frame buffered yet\" from \"finished\".\n\t */\n\ttryReadFrame(): Frame | undefined {\n\t\tconst read = this.#readBufferedFrame();\n\t\treturn read?.frame;\n\t}\n\n\t/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */\n\ttryReadFrameSequence(): ({ sequence: number } & Frame) | undefined {\n\t\tconst read = this.#readBufferedFrame();\n\t\tif (!read) return undefined;\n\t\treturn { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };\n\t}\n\n\t/** Resolves once {@link readFrame} would not block. */\n\tasync readable(): Promise<void> {\n\t\tfor (;;) {\n\t\t\tif (this.#state.frames.peek().length > 0) return;\n\t\t\tif (this.#state.closed.peek() !== undefined) return;\n\t\t\tawait Signal.race(this.#state.frames, this.#state.closed);\n\t\t}\n\t}\n\n\t/**\n\t * Reads the next frame from the group.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t */\n\tasync readFrame(): Promise<Frame | undefined> {\n\t\tfor (;;) {\n\t\t\tif (this.#state.evicted > 0) throw new Lagged();\n\n\t\t\tconst read = this.#readBufferedFrame();\n\t\t\tif (read) return read.frame;\n\n\t\t\tconst closed = this.#state.closed.peek();\n\t\t\tif (closed instanceof Error) throw closed;\n\t\t\tif (closed !== undefined) return;\n\n\t\t\tawait Signal.race(this.#state.frames, this.#state.closed);\n\t\t}\n\t}\n\n\t/**\n\t * Reads the next frame along with its sequence number within the group.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t */\n\tasync readFrameSequence(options?: ReadOptions): Promise<({ sequence: number } & Frame) | undefined> {\n\t\tconst from = options?.from ?? 0;\n\n\t\tfor (;;) {\n\t\t\tif (this.#state.evicted > from) throw new Lagged();\n\n\t\t\tconst read = this.#readBufferedFrame();\n\t\t\tif (read) {\n\t\t\t\tif (read.sequence < from) continue;\n\t\t\t\treturn { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };\n\t\t\t}\n\n\t\t\tconst closed = this.#state.closed.peek();\n\t\t\tif (closed instanceof Error) throw closed;\n\t\t\tif (closed !== undefined) return;\n\n\t\t\tawait Signal.race(this.#state.frames, this.#state.closed);\n\t\t}\n\t}\n\n\t/** Reads the next frame and decodes its payload as a UTF-8 string. */\n\tasync readString(): Promise<string | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? new TextDecoder().decode(frame.payload) : undefined;\n\t}\n\n\t/** Reads the next frame and parses its payload as JSON. */\n\tasync readJson(): Promise<unknown | undefined> {\n\t\tconst frame = await this.readString();\n\t\treturn frame ? JSON.parse(frame) : undefined;\n\t}\n\n\t/** Reads the next frame and decodes its payload as a one-byte boolean. */\n\tasync readBool(): Promise<boolean | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? frame.payload[0] === 1 : undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. Idempotent. */\n\tclose(abort?: Error) {\n\t\tif (this.#state.closed.peek() !== undefined) return; // already closed\n\t\tthis.#state.closed.set(abort ?? null);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"group.js","sourceRoot":"","sources":["../src/group.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAA8C,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,KAAK,EAAuB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,4GAA4G;AAC5G,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD,uHAAuH;AACvH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAmCrC,4EAA4E;AAC5E,MAAM,UAAU;IACN,QAAQ,CAAS;IAC1B,MAAM,GAAG,IAAI,MAAM,CAAU,EAAE,CAAC,CAAC;IACjC,MAAM,GAAG,IAAI,IAAI,EAAgB,CAAC;IAClC,KAAK,GAAG,IAAI,MAAM,CAAS,CAAC,CAAC,CAAC,CAAC,mDAAmD;IAElF,mFAAmF;IACnF,kCAAkC;IAClC,KAAK,GAAG,CAAC,CAAC;IAEV,UAAU,GAAG,CAAC,CAAC;IACf,qDAAqD;IACrD,SAAS,CAAa;IACtB,gFAAgF;IAChF,MAAM,CAAa;IAEnB,YAAY,QAAgB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;CACD;AAED,SAAS,WAAW,CAAC,KAAiB,EAAE,KAAY;IACnD,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAE1E,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,CAAC;IACpC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IAC/B,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;IAC7C,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;QAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,QAAQ;IACpB,sDAAsD;IAC7C,QAAQ,CAAS;IAE1B,MAAM,CAAa;IACnB,QAAQ,CAAmB;IAE3B,2FAA2F;IAC3F,+FAA+F;IAC/F,qFAAqF;IACrF,KAAK,GAAG,IAAI,MAAM,CAAU,KAAK,CAAC,CAAC;IAEnC,gFAAgF;IAChF,2EAA2E;IAC3E,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAE9B,YAAY,QAAgB;QAC3B,IAAI,CAAC,MAAM,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,IAAI,QAAQ;QACX,OAAO,IAAI,CAAC,SAAS,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,oCAAoC;IACpC,OAAO;QACN,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IAED;;;;;;;OAOG;IACH,MAAM;QACL,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;QACtC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QAChC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;YAAE,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QAC9B,mFAAmF;QACnF,mFAAmF;QACnF,4BAA4B;QAC5B,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAExC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACvB,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAED,IAAI,CAAC,QAAQ,KAAK,IAAI,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAErB,4FAA4F;QAC5F,6FAA6F;QAC7F,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE;YAC1C,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO;YAC5B,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/C,OAAO,EAAE,CAAC;QACX,CAAC,CAAC,CAAC;QAEH,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,IAAI;QACP,OAAO,IAAI,CAAC,KAAK,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrE,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;IACF,CAAC;IAED,mCAAmC;IACnC,UAAU,CAAC,KAAY;QACtB,gFAAgF;QAChF,iFAAiF;QACjF,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAChF,kFAAkF;QAClF,oFAAoF;QACpF,yFAAyF;QACzF,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,GAAG,qBAAqB;YAAE,MAAM,IAAI,aAAa,EAAE,CAAC;QAChF,IACC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,gBAAgB;YAC5C,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,GAAG,qBAAqB,EACxE,CAAC;YACF,MAAM,GAAG,GAAG,IAAI,aAAa,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC;YAC3B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACnB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACpC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBACtB,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC;gBACvB,CAAC;YACF,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAChB,MAAM,GAAG,CAAC;QACX,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QACnC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAEhC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;oBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;;oBAChE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACjC,CAAC;QACF,CAAC;IACF,CAAC;IAED,0FAA0F;IAC1F,WAAW,CAAC,GAAW;QACtB,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,wFAAwF;IACxF,SAAS,CAAC,IAAa;QACtB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,CAAC;IAED,sFAAsF;IACtF,SAAS,CAAC,IAAa;QACtB,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC;IAED,2CAA2C;IAC3C,IAAI,QAAQ;QACX,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS,CAAC;IAChD,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,KAAa;QAClB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO;QACpD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;QAEtC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;oBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;IACF,CAAC;CACD;AAED,IAAI,YAA6C,CAAC;AAElD,yFAAyF;AACzF,4FAA4F;AAC5F,gDAAgD;AAChD,MAAM,cAAc;IACnB,UAAU,CAAqB;IAC/B,OAAO,CAAqB;IAC5B,iBAAiB,CAAgB;IACjC,MAAM,GAAG,IAAI,IAAI,EAAgB,CAAC;IAClC,QAAQ,CAAW;IAEnB,YAAY,SAA6B,EAAE,MAA0B,EAAE,gBAA+B;QACrG,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;QAC1C,IAAI,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO;QAEzB,MAAM,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC5F,IAAI,CAAC,QAAQ,GAAG,GAAG,EAAE;YACpB,KAAK,MAAM,KAAK,IAAI,OAAO;gBAAE,KAAK,EAAE,CAAC;QACtC,CAAC,CAAC;IACH,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QAClD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,KAAK,GACV,SAAS,KAAK,SAAS;YACtB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM,YAAY,KAAK,IAAI,IAAI,CAAC,iBAAiB,EAAE;gBACpD,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,SAAS,CAAC;QACf,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAEtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvB,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC1B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO;QACN,IAAI,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED,IAAI;QACH,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;IAID,OAAO,CAAC,EAA8C;QACrD,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IAC7D,CAAC;IAED,SAAS,CAAC,EAA6C;QACtD,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,0FAA0F;IAC1F,IAAI,CACH,WAAoE,EACpE,UAA+D;QAE/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAClD,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,QAAQ;IACpB,sDAAsD;IAC7C,QAAQ,CAAS;IAE1B,MAAM,CAAa;IACnB,OAAO,CAAmE;IAC1E,gFAAgF;IAChF,mFAAmF;IACnF,kFAAkF;IAClF,kCAAkC;IAClC,MAAM,GAAG,KAAK,CAAC;IACf,SAAS,CAAS;IAClB,QAAQ,GAAG,IAAI,IAAI,EAAgB,CAAC;IACpC,OAAO,CAAkB;IACzB,cAAc,GAAG,CAAC,CAAC;IAEnB,YAAoB,KAAiB;QACpC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAChC,CAAC;IAED;;;;OAIG;IACH,IAAI,MAAM;QACT,IAAI,CAAC,OAAO,KAAK,IAAI,cAAc,CAClC,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,MAAM,CAAC,MAAM,EAClB,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,KAAK,CAAC,CACzE,CAAC;QACF,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED;QACC,YAAY,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC9C,KAAK,CAAC,cAAc,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;QACzD,KAAK,CAAC,WAAW,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;QACnD,KAAK,CAAC,WAAW,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACrC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACxB,CAAC,CAAC;QACF,KAAK,CAAC,UAAU,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACjE,KAAK,CAAC,cAAc,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7E,KAAK,CAAC,UAAU,GAAG,CAAC,KAAK,EAAE,EAAE;YAC5B,KAAK,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,iFAAiF;IACjF,oFAAoF;IACpF,uEAAuE;IACvE,EAAE;IACF,+EAA+E;IAC/E,mFAAmF;IACnF,+BAA+B;IAC/B,OAAO,CAAC,MAAM,GAAG,KAAK;QACrB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,KAAK;YAAE,OAAO,KAAK,CAAC;QAC7D,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE;YAAE,OAAO,KAAK,CAAC;QAE3C,IAAI,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,GAAG,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAC9E,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;QAClF,OAAO,IAAI,CAAC;IACb,CAAC;IAED,kFAAkF;IAClF,iFAAiF;IACjF,MAAM,CAAI,SAA2B;QACpC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,EAAE,CAAC;QAEhC,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACzC,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,MAAM,QAAQ,GAAc,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,CAAC,EAAc,EAAE,EAAE;gBACjC,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,KAAK,MAAM,OAAO,IAAI,QAAQ;oBAAE,OAAO,EAAE,CAAC;gBAC1C,EAAE,EAAE,CAAC;YACN,CAAC,CAAC;YACF,MAAM,KAAK,GAAG,GAAG,EAAE;gBAClB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACnB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;oBACpB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;oBAC7B,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC7B,CAAC;YACF,CAAC,CAAC;YAEF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,OAAO;gBAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9E,SAAS,EAAE,CAAC,IAAI,CACf,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EACvC,CAAC,KAAc,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC/C,CAAC;YACF,KAAK,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,MAAM;QACL,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,8EAA8E;YAC9E,6EAA6E;YAC7E,6CAA6C;YAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACrC,IAAI,CAAC,SAAS,GAAG,IAAI,YAAY,EAAE,CAAC;gBACpC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,SAAS;oBAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3E,CAAC;QACF,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,QAAQ;QACb,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1D,OAAO;QACR,CAAC;QACD,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpF,CAAC;IAED,kBAAkB,CAAC,OAAO,GAAG,KAAK;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,IAAI,CAAC,QAAQ;YAAE,OAAO,SAAS,CAAC;QAEhC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACrC,IAAI,OAAO;YAAE,IAAI,CAAC,cAAc,EAAE,CAAC;QACnC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,OAAO;YACN,QAAQ;YACR,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,GAAG,EAAE;gBACd,IAAI,SAAS;oBAAE,OAAO;gBACtB,SAAS,GAAG,IAAI,CAAC;gBACjB,IAAI,OAAO;oBAAE,IAAI,CAAC,cAAc,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;YACzB,CAAC;SACD,CAAC;IACH,CAAC;IAED,sGAAsG;IACtG,IAAI,IAAI;QACP,OAAO,CACN,IAAI,CAAC,SAAS,KAAK,SAAS;YAC5B,IAAI,CAAC,MAAM;YACX,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS,CAAC,CACnF,CAAC;IACH,CAAC;IAED,sJAAsJ;IACtJ,IAAI,QAAQ;QACX,OAAO,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS,CAAC;IAC/F,CAAC;IAED;;;;;OAKG;IACH,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC;IAED;;;;;;OAMG;IACH,YAAY;QACX,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvC,IAAI,EAAE,QAAQ,EAAE,CAAC;QACjB,OAAO,IAAI,EAAE,KAAK,CAAC;IACpB,CAAC;IAED,+FAA+F;IAC/F,oBAAoB;QACnB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC;QAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;IAClG,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,QAAQ;QACb,SAAS,CAAC;YACT,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO;YAC1C,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO;YACjD,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;gBAC7C,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;gBACxB,OAAO;YACR,CAAC;YACD,IAAI,IAAI,CAAC,OAAO,EAAE;gBAAE,OAAO;YAC3B,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACvB,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS;QACd,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC,EAAE,KAAK,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,OAAO,GAAG,KAAK,EAAE,IAAI,GAAG,CAAC;QACjD,SAAS,CAAC;YACT,IAAI,IAAI,CAAC,SAAS;gBAAE,MAAM,IAAI,CAAC,SAAS,CAAC;YACzC,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO;YAExB,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;YAC9C,IAAI,IAAI,EAAE,CAAC;gBACV,IAAI,IAAI,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC;oBAC1B,IAAI,CAAC,QAAQ,EAAE,CAAC;oBAChB,SAAS;gBACV,CAAC;gBACD,IAAI,CAAC,OAAO;oBAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9B,OAAO,IAAI,CAAC;YACb,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,MAAM,YAAY,KAAK;gBAAE,MAAM,MAAM,CAAC;YAC1C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC1B,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;gBACxB,OAAO;YACR,CAAC;YAED,2EAA2E;YAC3E,sDAAsD;YACtD,IAAI,IAAI,CAAC,OAAO,EAAE;gBAAE,SAAS;YAC7B,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACvB,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,iBAAiB,CAAC,OAAqB;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC;QAC5B,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;IAClG,CAAC;IAED,sEAAsE;IACtE,KAAK,CAAC,UAAU;QACf,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,QAAQ;QACb,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,QAAQ;QACb,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,KAAa;QAClB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO,CAAC,iBAAiB;QACtE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACvC,CAAC;CACD","sourcesContent":["/**\n * Group role handles: an ordered stream of frames within a track, delivered over one QUIC stream.\n *\n * @module\n */\nimport { type Dispose, type GetPromise, type Getter, Once, Signal } from \"@moq/signals\";\nimport { FrameTooLarge, GroupTooLarge, TooFarBehind } from \"./error.ts\";\nimport { hooks, type ReadGroupFrame } from \"./internal.ts\";\nimport { Timestamp } from \"./time.ts\";\n\n/** Maximum bytes of frames in a group. A write that would exceed this aborts with {@link GroupTooLarge}. */\nexport const MAX_GROUP_CACHE_BYTES = 32 * 1024 * 1024;\n\n/** Maximum number of frames in a group. 8192 is the largest legal group; the 8193rd write is {@link GroupTooLarge}. */\nexport const MAX_GROUP_FRAMES = 8192;\n\n/**\n * A frame buffered in a group: its presentation {@link Timestamp} and payload bytes.\n *\n * The timestamp carries its own scale, so a track can pick its units; the wire layer\n * converts it into the track's negotiated timescale.\n */\nexport interface Frame {\n\t/** The frame payload. */\n\tpayload: Uint8Array;\n\t/**\n\t * Presentation timestamp. Required: for a payload with no presentation time of its own\n\t * (a JSON catalog, control state) pass {@link Timestamp.now} explicitly.\n\t */\n\ttimestamp: Timestamp;\n}\n\n/** Options for a sequence-aware frame read. */\nexport interface ReadOptions {\n\t/**\n\t * The lowest sequence number the caller wants; defaults to the whole group.\n\t *\n\t * Frames below it are discarded instead of returned. The group still starts at frame 0,\n\t * so this is a reader filter, not a missing prefix.\n\t */\n\tfrom?: number;\n}\n\n/** Immutable group metadata. */\nexport interface Info {\n\t/** Sequence number of this group within its track. */\n\tsequence: number;\n}\n\n/** Reactive backing state shared by the group producer and one consumer. */\nclass GroupState {\n\treadonly sequence: number;\n\tframes = new Signal<Frame[]>([]);\n\tclosed = new Once<Error | null>();\n\ttotal = new Signal<number>(0); // The total number of frames in the group thus far\n\n\t// Absolute sequence of the frame at the front of `frames`, advanced by a read: the\n\t// sequence the next read returns.\n\tstart = 0;\n\n\tcacheBytes = 0;\n\t// The first frame's timestamp, retained after reads.\n\ttimestamp?: Timestamp;\n\t// The newest frame's timestamp: where a reader that has taken every frame sits.\n\tlatest?: Timestamp;\n\n\tconstructor(sequence: number) {\n\t\tthis.sequence = sequence;\n\t}\n}\n\nfunction appendFrame(state: GroupState, frame: Frame) {\n\tif (state.closed.peek() !== undefined) throw new Error(\"group is closed\");\n\n\tstate.timestamp ??= frame.timestamp;\n\tstate.latest = frame.timestamp;\n\tstate.cacheBytes += frame.payload.byteLength;\n\tstate.frames.mutate((frames) => {\n\t\tframes.push(frame);\n\t});\n\n\tstate.total.update((total) => total + 1);\n}\n\n/**\n * The write side of an ordered stream of frames within a track.\n *\n * @public\n */\nexport class Producer {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t#state: GroupState;\n\t#mirrors?: Set<GroupState>;\n\n\t// Whether any mirror reader is attached (see {@link used}). Fetch coalescing watches it to\n\t// cancel an abandoned download; a group can stay open indefinitely (a catalog or JSON stream),\n\t// so this is what stops a reader-less fetch instead of the stream ending on its own.\n\t#used = new Signal<boolean>(false);\n\n\t// When the group was created or last written. Retention ages a group from this,\n\t// not from its creation, so a group still being filled is never reclaimed.\n\t#activity = performance.now();\n\n\tconstructor(sequence: number) {\n\t\tthis.#state = new GroupState(sequence);\n\t\tthis.sequence = sequence;\n\t}\n\n\t/**\n\t * When the group was created or last written, in `performance.now()` milliseconds.\n\t *\n\t * @internal Track retention only.\n\t */\n\tget activity(): number {\n\t\treturn this.#activity;\n\t}\n\n\t/**\n\t * Settles once the group closes: `null` on a clean close, or the abort {@link Error}.\n\t * Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.\n\t */\n\tget closed(): GetPromise<Error | null> {\n\t\treturn this.#state.closed;\n\t}\n\n\t/** A read handle for this group. */\n\tconsume(): Consumer {\n\t\treturn makeConsumer(this.#state);\n\t}\n\n\t/**\n\t * Create an independent read handle that receives every frame written here.\n\t *\n\t * Frames written so far are replayed synchronously; later writes and close are teed\n\t * in as they happen.\n\t *\n\t * @internal Track fan-out and fetch coalescing only. Use {@link consume} instead.\n\t */\n\tmirror(): Consumer {\n\t\tconst dst = new GroupState(this.sequence);\n\t\tdst.timestamp = this.#state.timestamp;\n\t\tdst.latest = this.#state.latest;\n\t\tfor (const frame of this.#state.frames.peek()) appendFrame(dst, frame);\n\t\tdst.start = this.#state.start;\n\t\t// The replay only covers what is still buffered, so the count has to come from the\n\t\t// source: it is what names a frame's absolute sequence, and what a publisher reads\n\t\t// to resolve the live edge.\n\t\tdst.total.set(this.#state.total.peek());\n\n\t\tconst closed = this.#state.closed.peek();\n\t\tif (closed !== undefined) {\n\t\t\tdst.closed.set(closed);\n\t\t\treturn makeConsumer(dst);\n\t\t}\n\n\t\tthis.#mirrors ??= new Set();\n\t\tthis.#mirrors.add(dst);\n\t\tthis.#used.set(true);\n\n\t\t// Track this mirror's close eagerly (not just lazily on the next write) so demand drops the\n\t\t// moment the last reader leaves, letting an abandoned fetch cancel even with no more frames.\n\t\tconst dispose = dst.closed.subscribe((c) => {\n\t\t\tif (c === undefined) return;\n\t\t\tthis.#mirrors?.delete(dst);\n\t\t\tthis.#used.set((this.#mirrors?.size ?? 0) > 0);\n\t\t\tdispose();\n\t\t});\n\n\t\treturn makeConsumer(dst);\n\t}\n\n\t/**\n\t * Whether any mirror reader is currently attached.\n\t *\n\t * Pairs with {@link unused}. Fetch coalescing watches it to cancel a download once every reader\n\t * has gone: a group can stay open indefinitely (a catalog track, a JSON stream), so it can't\n\t * rely on the stream ending on its own.\n\t *\n\t * @internal Track fan-out and fetch coalescing only.\n\t */\n\tget used(): Getter<boolean> {\n\t\treturn this.#used;\n\t}\n\n\t/**\n\t * Resolves once no mirror reader remains (or the group closes).\n\t *\n\t * @internal Track fan-out and fetch coalescing only.\n\t */\n\tasync unused(): Promise<void> {\n\t\twhile (this.#used.peek() && this.#state.closed.peek() === undefined) {\n\t\t\tawait Signal.race(this.#used, this.#state.closed);\n\t\t}\n\t}\n\n\t/** Writes a frame to the group. */\n\twriteFrame(frame: Frame) {\n\t\t// Closed first: a write after a clean close at the cap would otherwise take the\n\t\t// overflow path, wipe committed frames, and leave readers with an empty success.\n\t\tif (this.#state.closed.peek() !== undefined) throw new Error(\"group is closed\");\n\t\t// A frame past the cache cap would make the group exceed its budget by itself, so\n\t\t// accepting it would report success for a write nothing can ever read. Rust rejects\n\t\t// it up front with `Error::FrameTooLarge`; do the same rather than silently dropping it.\n\t\tif (frame.payload.byteLength > MAX_GROUP_CACHE_BYTES) throw new FrameTooLarge();\n\t\tif (\n\t\t\tthis.#state.total.peek() >= MAX_GROUP_FRAMES ||\n\t\t\tthis.#state.cacheBytes + frame.payload.byteLength > MAX_GROUP_CACHE_BYTES\n\t\t) {\n\t\t\tconst err = new GroupTooLarge();\n\t\t\tthis.#state.frames.set([]);\n\t\t\tthis.#state.cacheBytes = 0;\n\t\t\tif (this.#mirrors) {\n\t\t\t\tfor (const mirror of this.#mirrors) {\n\t\t\t\t\tmirror.frames.set([]);\n\t\t\t\t\tmirror.cacheBytes = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.close(err);\n\t\t\tthrow err;\n\t\t}\n\n\t\tthis.#activity = performance.now();\n\t\tappendFrame(this.#state, frame);\n\n\t\tif (this.#mirrors) {\n\t\t\tfor (const mirror of this.#mirrors) {\n\t\t\t\tif (mirror.closed.peek() !== undefined) this.#mirrors.delete(mirror);\n\t\t\t\telse appendFrame(mirror, frame);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Write a string as a single UTF-8 encoded frame, stamped with {@link Timestamp.now}. */\n\twriteString(str: string) {\n\t\tthis.writeFrame({ payload: new TextEncoder().encode(str), timestamp: Timestamp.now() });\n\t}\n\n\t/** Write a value as a single JSON-encoded frame, stamped with {@link Timestamp.now}. */\n\twriteJson(json: unknown) {\n\t\tthis.writeString(JSON.stringify(json));\n\t}\n\n\t/** Write a boolean as a single one-byte frame, stamped with {@link Timestamp.now}. */\n\twriteBool(bool: boolean) {\n\t\tthis.writeFrame({ payload: new Uint8Array([bool ? 1 : 0]), timestamp: Timestamp.now() });\n\t}\n\n\t/** True once the group has been closed. */\n\tget isClosed(): boolean {\n\t\treturn this.#state.closed.peek() !== undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. */\n\tclose(abort?: Error) {\n\t\tif (this.#state.closed.peek() !== undefined) return;\n\t\tthis.#state.closed.set(abort ?? null);\n\n\t\tif (this.#mirrors) {\n\t\t\tfor (const mirror of this.#mirrors) {\n\t\t\t\tif (mirror.closed.peek() === undefined) mirror.closed.set(abort ?? null);\n\t\t\t}\n\t\t\tthis.#mirrors.clear();\n\t\t}\n\t}\n}\n\nlet makeConsumer: (state: GroupState) => Consumer;\n\n// A consumer can finish from its own expiry verdict or from the shared group state. Keep\n// one stable public handle while letting peek() observe either source synchronously, before\n// signal subscribers run in the next microtask.\nclass CombinedClosed implements GetPromise<Error | null> {\n\t#preferred: Once<Error | null>;\n\t#source: Once<Error | null>;\n\t#sourceCleanReady: () => boolean;\n\t#value = new Once<Error | null>();\n\t#dispose?: Dispose;\n\n\tconstructor(preferred: Once<Error | null>, source: Once<Error | null>, sourceCleanReady: () => boolean) {\n\t\tthis.#preferred = preferred;\n\t\tthis.#source = source;\n\t\tthis.#sourceCleanReady = sourceCleanReady;\n\t\tif (this.#sync()) return;\n\n\t\tconst dispose = [preferred.changed(() => this.#sync()), source.changed(() => this.#sync())];\n\t\tthis.#dispose = () => {\n\t\t\tfor (const close of dispose) close();\n\t\t};\n\t}\n\n\t#sync(): boolean {\n\t\tif (this.#value.peek() !== undefined) return true;\n\t\tconst preferred = this.#preferred.peek();\n\t\tconst source = this.#source.peek();\n\t\tconst value =\n\t\t\tpreferred !== undefined\n\t\t\t\t? preferred\n\t\t\t\t: source instanceof Error || this.#sourceCleanReady()\n\t\t\t\t\t? source\n\t\t\t\t\t: undefined;\n\t\tif (value === undefined) return false;\n\n\t\tthis.#value.set(value);\n\t\tthis.#dispose?.();\n\t\tthis.#dispose = undefined;\n\t\treturn true;\n\t}\n\n\trefresh() {\n\t\tthis.#sync();\n\t}\n\n\tpeek(): Error | null | undefined {\n\t\tthis.#sync();\n\t\treturn this.#value.peek();\n\t}\n\n\tchanged(): Promise<Error | null | undefined>;\n\tchanged(fn: (value: Error | null | undefined) => void): Dispose;\n\tchanged(fn?: (value: Error | null | undefined) => void): Promise<Error | null | undefined> | Dispose {\n\t\tthis.#sync();\n\t\treturn fn ? this.#value.changed(fn) : this.#value.changed();\n\t}\n\n\tsubscribe(fn: (value: Error | null | undefined) => void): Dispose {\n\t\tthis.#sync();\n\t\treturn this.#value.subscribe(fn);\n\t}\n\n\t// biome-ignore lint/suspicious/noThenProperty: CombinedClosed is intentionally awaitable.\n\tthen<R1 = Error | null, R2 = never>(\n\t\tonFulfilled?: ((value: Error | null) => R1 | PromiseLike<R1>) | null,\n\t\tonRejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,\n\t): PromiseLike<R1 | R2> {\n\t\tthis.#sync();\n\t\treturn this.#value.then(onFulfilled, onRejected);\n\t}\n}\n\n/**\n * The read side of an ordered stream of frames within a track.\n *\n * Created internally: obtain one from {@link Producer.consume} or a track subscriber's\n * group reads.\n *\n * @public\n */\nexport class Consumer {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t#state: GroupState;\n\t#expiry?: { expired: () => boolean; changed: readonly Getter<unknown>[] };\n\t// Sticky verdicts, set once. `#ended` is a drained group the budget gave up on,\n\t// which is indistinguishable from one that ended: nothing was lost, so it reads as\n\t// the end of the group. `#terminal` is a failure, including a budget that gave up\n\t// while content was still unread.\n\t#ended = false;\n\t#terminal?: Error;\n\t#verdict = new Once<Error | null>();\n\t#closed?: CombinedClosed;\n\t#pendingFrames = 0;\n\n\tprivate constructor(state: GroupState) {\n\t\tthis.#state = state;\n\t\tthis.sequence = state.sequence;\n\t}\n\n\t/**\n\t * Settles when this consumer reaches a terminal state: `null` after a clean close has no\n\t * unread or in-flight frames, or the terminal {@link Error}.\n\t * Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.\n\t */\n\tget closed(): GetPromise<Error | null> {\n\t\tthis.#closed ??= new CombinedClosed(\n\t\t\tthis.#verdict,\n\t\t\tthis.#state.closed,\n\t\t\t() => this.#state.frames.peek().length === 0 && this.#pendingFrames === 0,\n\t\t);\n\t\treturn this.#closed;\n\t}\n\n\tstatic {\n\t\tmakeConsumer = (state) => new Consumer(state);\n\t\thooks.groupTimestamp = (group) => group.#state.timestamp;\n\t\thooks.groupLatest = (group) => group.#state.latest;\n\t\thooks.expireGroup = (group, expiry) => {\n\t\t\tgroup.#expiry = expiry;\n\t\t};\n\t\thooks.guardGroup = (group, operation) => group.#guard(operation);\n\t\thooks.readGroupFrame = (group, from) => group.#readFramePosition(true, from);\n\t\thooks.evictGroup = (group) => {\n\t\t\tgroup.#evict();\n\t\t};\n\t}\n\n\t// Evaluate the drift budget for a read that has nothing buffered and is about to\n\t// wait. A group with frames in hand is always drained: the budget bounds a group\n\t// that has stalled while the live edge moved on, not a reader slower than the wire.\n\t// Returns true if a verdict was just reached, so the caller re-checks.\n\t//\n\t// `unread` is for a caller that still holds content the reader has not seen (a\n\t// publisher part-way through writing a frame): giving up there is a truncation, so\n\t// it fails rather than ending.\n\t#expire(unread = false): boolean {\n\t\tif (this.#terminal || this.#ended) return false;\n\t\tif (this.#state.closed.peek() instanceof Error) return false;\n\t\tif (!this.#expiry?.expired()) return false;\n\n\t\tif (unread) {\n\t\t\tthis.#terminal = new Error(\"group exceeded the subscription max age budget\");\n\t\t} else {\n\t\t\tthis.#ended = true;\n\t\t}\n\t\tif (this.#verdict.peek() === undefined) this.#verdict.set(this.#terminal ?? null);\n\t\treturn true;\n\t}\n\n\t// Takes a thunk rather than a started promise, so a group already past its budget\n\t// never begins the write: an abandoned write would reject later with no handler.\n\t#guard<T>(operation: () => Promise<T>): Promise<T> {\n\t\tthis.#expire(true);\n\t\tif (this.#terminal) return Promise.reject(this.#terminal);\n\t\tconst expiry = this.#expiry;\n\t\tif (!expiry) return operation();\n\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\tlet settled = false;\n\t\t\tconst disposes: Dispose[] = [];\n\t\t\tconst finish = (fn: () => void) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tfor (const dispose of disposes) dispose();\n\t\t\t\tfn();\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tthis.#expire(true);\n\t\t\t\tif (this.#terminal) {\n\t\t\t\t\tconst error = this.#terminal;\n\t\t\t\t\tfinish(() => reject(error));\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tfor (const changed of expiry.changed) disposes.push(changed.subscribe(check));\n\t\t\toperation().then(\n\t\t\t\t(value) => finish(() => resolve(value)),\n\t\t\t\t(error: unknown) => finish(() => reject(error)),\n\t\t\t);\n\t\t\tcheck();\n\t\t});\n\t}\n\n\t#evict() {\n\t\tconst frames = this.#state.frames.peek();\n\t\tif (frames.length > 0) {\n\t\t\t// Unread content is going away either way. If the budget had already given up\n\t\t\t// on this group that is the more specific reason, so ask it first; otherwise\n\t\t\t// the reader simply lagged behind the cache.\n\t\t\tthis.#expire(true);\n\t\t\tif (!this.#terminal && !this.#ended) {\n\t\t\t\tthis.#terminal = new TooFarBehind();\n\t\t\t\tif (this.#verdict.peek() === undefined) this.#verdict.set(this.#terminal);\n\t\t\t}\n\t\t}\n\t\tif (frames.length > 0) {\n\t\t\tthis.#state.start += frames.length;\n\t\t}\n\t\tthis.#state.cacheBytes = 0;\n\t\tthis.#state.frames.set([]);\n\t}\n\n\tasync #changed(): Promise<void> {\n\t\tif (!this.#expiry) {\n\t\t\tawait Signal.race(this.#state.frames, this.#state.closed);\n\t\t\treturn;\n\t\t}\n\t\tawait Signal.race(this.#state.frames, this.#state.closed, ...this.#expiry.changed);\n\t}\n\n\t#readBufferedFrame(pending = false): ReadGroupFrame | undefined {\n\t\tconst frames = this.#state.frames.peek();\n\t\tconst buffered = frames.shift();\n\t\tif (!buffered) return undefined;\n\n\t\tconst sequence = this.#state.start++;\n\t\tif (pending) this.#pendingFrames++;\n\t\tlet completed = false;\n\t\treturn {\n\t\t\tsequence,\n\t\t\tframe: buffered,\n\t\t\tcomplete: () => {\n\t\t\t\tif (completed) return;\n\t\t\t\tcompleted = true;\n\t\t\t\tif (pending) this.#pendingFrames--;\n\t\t\t\tthis.#closed?.refresh();\n\t\t\t},\n\t\t};\n\t}\n\n\t/** True once no further frames can be read: the group has closed and every buffered frame is read. */\n\tget done(): boolean {\n\t\treturn (\n\t\t\tthis.#terminal !== undefined ||\n\t\t\tthis.#ended ||\n\t\t\t(this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== undefined)\n\t\t);\n\t}\n\n\t/** True once the group has been closed, regardless of whether buffered frames remain unread. Synchronous complement to the {@link closed} promise. */\n\tget isClosed(): boolean {\n\t\treturn this.#terminal !== undefined || this.#ended || this.#state.closed.peek() !== undefined;\n\t}\n\n\t/**\n\t * How many frames the group has held, including any already read.\n\t *\n\t * It is also the next frame's sequence number, so `frameCount - 1` names the newest\n\t * frame written so far. A publisher snapshots it to resolve the track's live edge.\n\t */\n\tget frameCount(): number {\n\t\treturn this.#state.total.peek();\n\t}\n\n\t/**\n\t * Reads the next already-buffered frame without blocking.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t *\n\t * Returns `undefined` when nothing is buffered right now. That is not by itself\n\t * end-of-group: check {@link done} to tell \"no frame buffered yet\" from \"finished\".\n\t */\n\ttryReadFrame(): Frame | undefined {\n\t\tif (this.#terminal || this.#ended) return undefined;\n\t\tconst read = this.#readBufferedFrame();\n\t\tread?.complete();\n\t\treturn read?.frame;\n\t}\n\n\t/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */\n\ttryReadFrameSequence(): ({ sequence: number } & Frame) | undefined {\n\t\tif (this.#terminal || this.#ended) return undefined;\n\t\tconst read = this.#readBufferedFrame();\n\t\tif (!read) return undefined;\n\t\tread.complete();\n\t\treturn { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };\n\t}\n\n\t/** Resolves once {@link readFrame} would not block. */\n\tasync readable(): Promise<void> {\n\t\tfor (;;) {\n\t\t\tif (this.#terminal || this.#ended) return;\n\t\t\tif (this.#state.frames.peek().length > 0) return;\n\t\t\tif (this.#state.closed.peek() !== undefined) {\n\t\t\t\tthis.#closed?.refresh();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.#expire()) return;\n\t\t\tawait this.#changed();\n\t\t}\n\t}\n\n\t/**\n\t * Reads the next frame from the group.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t */\n\tasync readFrame(): Promise<Frame | undefined> {\n\t\treturn (await this.#readFramePosition())?.frame;\n\t}\n\n\tasync #readFramePosition(pending = false, from = 0): Promise<ReadGroupFrame | undefined> {\n\t\tfor (;;) {\n\t\t\tif (this.#terminal) throw this.#terminal;\n\t\t\tif (this.#ended) return;\n\n\t\t\tconst read = this.#readBufferedFrame(pending);\n\t\t\tif (read) {\n\t\t\t\tif (read.sequence < from) {\n\t\t\t\t\tread.complete();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!pending) read.complete();\n\t\t\t\treturn read;\n\t\t\t}\n\n\t\t\tconst closed = this.#state.closed.peek();\n\t\t\tif (closed instanceof Error) throw closed;\n\t\t\tif (closed !== undefined) {\n\t\t\t\tthis.#closed?.refresh();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Nothing buffered and the group is still open: this wait is the stall the\n\t\t\t// drift budget bounds, and the only place it applies.\n\t\t\tif (this.#expire()) continue;\n\t\t\tawait this.#changed();\n\t\t}\n\t}\n\n\t/**\n\t * Reads the next frame along with its sequence number within the group.\n\t * Treat the returned frame bytes as read-only; they are shared with other consumers.\n\t */\n\tasync readFrameSequence(options?: ReadOptions): Promise<({ sequence: number } & Frame) | undefined> {\n\t\tconst read = await this.#readFramePosition(false, options?.from ?? 0);\n\t\tif (!read) return undefined;\n\t\treturn { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };\n\t}\n\n\t/** Reads the next frame and decodes its payload as a UTF-8 string. */\n\tasync readString(): Promise<string | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? new TextDecoder().decode(frame.payload) : undefined;\n\t}\n\n\t/** Reads the next frame and parses its payload as JSON. */\n\tasync readJson(): Promise<unknown | undefined> {\n\t\tconst frame = await this.readString();\n\t\treturn frame ? JSON.parse(frame) : undefined;\n\t}\n\n\t/** Reads the next frame and decodes its payload as a one-byte boolean. */\n\tasync readBool(): Promise<boolean | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? frame.payload[0] === 1 : undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. Idempotent. */\n\tclose(abort?: Error) {\n\t\tif (this.#state.closed.peek() !== undefined) return; // already closed\n\t\tthis.#state.closed.set(abort ?? null);\n\t}\n}\n"]}
|