@norskvideo/moq-net 0.1.3 → 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/group.js
CHANGED
|
@@ -1,135 +1,337 @@
|
|
|
1
1
|
/* @ts-self-types="./group.d.ts" */
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Group role handles: an ordered stream of frames within a track, delivered over one QUIC stream.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { Once, Signal } from "@norskvideo/moq-signals";
|
|
8
|
+
import { Timestamp } from "./time.js";
|
|
9
|
+
/** Maximum bytes of frames cached in a group before old frames are evicted from the front. */
|
|
10
|
+
export const MAX_GROUP_CACHE_BYTES = 32 * 1024 * 1024;
|
|
11
|
+
/** Maximum number of frames cached in a group before old frames are evicted from the front. */
|
|
12
|
+
export const MAX_GROUP_FRAMES = 1024;
|
|
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
|
+
}
|
|
23
|
+
/** Reactive backing state shared by the group producer and one consumer. */
|
|
3
24
|
class GroupState {
|
|
25
|
+
sequence;
|
|
4
26
|
frames = new Signal([]);
|
|
5
|
-
closed = new
|
|
27
|
+
closed = new Once();
|
|
6
28
|
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 and by an
|
|
30
|
+
// eviction alike: the sequence the next read returns.
|
|
31
|
+
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
|
+
cacheBytes = 0;
|
|
38
|
+
constructor(sequence) {
|
|
39
|
+
this.sequence = sequence;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function appendFrame(state, frame) {
|
|
43
|
+
if (state.closed.peek() !== undefined)
|
|
44
|
+
throw new Error("group is closed");
|
|
45
|
+
state.cacheBytes += frame.payload.byteLength;
|
|
46
|
+
state.frames.mutate((frames) => {
|
|
47
|
+
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
|
+
});
|
|
57
|
+
state.total.update((total) => total + 1);
|
|
7
58
|
}
|
|
8
|
-
/**
|
|
9
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The write side of an ordered stream of frames within a track.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export class Producer {
|
|
10
65
|
/** Sequence number of this group within its track. */
|
|
11
66
|
sequence;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
67
|
+
#state;
|
|
68
|
+
#mirrors;
|
|
69
|
+
// Whether any mirror reader is attached (see {@link used}). Fetch coalescing watches it to
|
|
70
|
+
// cancel an abandoned download; a group can stay open indefinitely (a catalog or JSON stream),
|
|
71
|
+
// so this is what stops a reader-less fetch instead of the stream ending on its own.
|
|
72
|
+
#used = new Signal(false);
|
|
17
73
|
constructor(sequence) {
|
|
74
|
+
this.#state = new GroupState(sequence);
|
|
18
75
|
this.sequence = sequence;
|
|
19
|
-
// Cache the closed promise to avoid recreating it every time.
|
|
20
|
-
this.closed = new Promise((resolve) => {
|
|
21
|
-
const dispose = this.#state.closed.subscribe((closed) => {
|
|
22
|
-
if (!closed)
|
|
23
|
-
return;
|
|
24
|
-
resolve(closed instanceof Error ? closed : undefined);
|
|
25
|
-
dispose();
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
76
|
}
|
|
29
77
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
78
|
+
* Settles once the group closes: `null` on a clean close, or the abort {@link Error}.
|
|
79
|
+
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
|
|
32
80
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
get closed() {
|
|
82
|
+
return this.#state.closed;
|
|
83
|
+
}
|
|
84
|
+
/** A read handle for this group. */
|
|
85
|
+
consume() {
|
|
86
|
+
return makeConsumer(this.#state);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create an independent read handle that receives every frame written here.
|
|
90
|
+
*
|
|
91
|
+
* Frames written so far are replayed synchronously; later writes and close are teed
|
|
92
|
+
* in as they happen.
|
|
93
|
+
*
|
|
94
|
+
* @internal Track fan-out and fetch coalescing only. Use {@link consume} instead.
|
|
95
|
+
*/
|
|
96
|
+
mirror() {
|
|
97
|
+
const dst = new GroupState(this.sequence);
|
|
98
|
+
for (const frame of this.#state.frames.peek())
|
|
99
|
+
appendFrame(dst, frame);
|
|
100
|
+
dst.start = this.#state.start;
|
|
101
|
+
dst.evicted = this.#state.evicted;
|
|
102
|
+
// The replay only covers what is still buffered, so the count has to come from the
|
|
103
|
+
// source: it is what names a frame's absolute sequence, and what a publisher reads
|
|
104
|
+
// to resolve the live edge.
|
|
105
|
+
dst.total.set(this.#state.total.peek());
|
|
106
|
+
const closed = this.#state.closed.peek();
|
|
107
|
+
if (closed !== undefined) {
|
|
108
|
+
dst.closed.set(closed);
|
|
109
|
+
return makeConsumer(dst);
|
|
110
|
+
}
|
|
111
|
+
this.#mirrors ??= new Set();
|
|
112
|
+
this.#mirrors.add(dst);
|
|
113
|
+
this.#used.set(true);
|
|
114
|
+
// Track this mirror's close eagerly (not just lazily on the next write) so demand drops the
|
|
115
|
+
// moment the last reader leaves, letting an abandoned fetch cancel even with no more frames.
|
|
116
|
+
const dispose = dst.closed.subscribe((c) => {
|
|
117
|
+
if (c === undefined)
|
|
118
|
+
return;
|
|
119
|
+
this.#mirrors?.delete(dst);
|
|
120
|
+
this.#used.set((this.#mirrors?.size ?? 0) > 0);
|
|
121
|
+
dispose();
|
|
38
122
|
});
|
|
39
|
-
|
|
123
|
+
return makeConsumer(dst);
|
|
40
124
|
}
|
|
41
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Whether any mirror reader is currently attached.
|
|
127
|
+
*
|
|
128
|
+
* Pairs with {@link unused}. Fetch coalescing watches it to cancel a download once every reader
|
|
129
|
+
* has gone: a group can stay open indefinitely (a catalog track, a JSON stream), so it can't
|
|
130
|
+
* rely on the stream ending on its own.
|
|
131
|
+
*
|
|
132
|
+
* @internal Track fan-out and fetch coalescing only.
|
|
133
|
+
*/
|
|
134
|
+
get used() {
|
|
135
|
+
return this.#used;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolves once no mirror reader remains (or the group closes).
|
|
139
|
+
*
|
|
140
|
+
* @internal Track fan-out and fetch coalescing only.
|
|
141
|
+
*/
|
|
142
|
+
async unused() {
|
|
143
|
+
while (this.#used.peek() && this.#state.closed.peek() === undefined) {
|
|
144
|
+
await Signal.race(this.#used, this.#state.closed);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Writes a frame to the group. */
|
|
148
|
+
writeFrame(frame) {
|
|
149
|
+
appendFrame(this.#state, frame);
|
|
150
|
+
if (this.#mirrors) {
|
|
151
|
+
for (const mirror of this.#mirrors) {
|
|
152
|
+
if (mirror.closed.peek() !== undefined)
|
|
153
|
+
this.#mirrors.delete(mirror);
|
|
154
|
+
else
|
|
155
|
+
appendFrame(mirror, frame);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Write a string as a single UTF-8 encoded frame, stamped with {@link Timestamp.now}. */
|
|
42
160
|
writeString(str) {
|
|
43
|
-
this.writeFrame(new TextEncoder().encode(str));
|
|
161
|
+
this.writeFrame({ payload: new TextEncoder().encode(str), timestamp: Timestamp.now() });
|
|
44
162
|
}
|
|
45
|
-
/** Write a value as a single JSON-encoded frame. */
|
|
163
|
+
/** Write a value as a single JSON-encoded frame, stamped with {@link Timestamp.now}. */
|
|
46
164
|
writeJson(json) {
|
|
47
165
|
this.writeString(JSON.stringify(json));
|
|
48
166
|
}
|
|
49
|
-
/** Write a boolean as a single one-byte frame. */
|
|
167
|
+
/** Write a boolean as a single one-byte frame, stamped with {@link Timestamp.now}. */
|
|
50
168
|
writeBool(bool) {
|
|
51
|
-
this.writeFrame(new Uint8Array([bool ? 1 : 0]));
|
|
169
|
+
this.writeFrame({ payload: new Uint8Array([bool ? 1 : 0]), timestamp: Timestamp.now() });
|
|
170
|
+
}
|
|
171
|
+
/** True once the group has been closed. */
|
|
172
|
+
get isClosed() {
|
|
173
|
+
return this.#state.closed.peek() !== undefined;
|
|
174
|
+
}
|
|
175
|
+
/** Closes the group, optionally with an error to abort readers. */
|
|
176
|
+
close(abort) {
|
|
177
|
+
if (this.#state.closed.peek() !== undefined)
|
|
178
|
+
return;
|
|
179
|
+
this.#state.closed.set(abort ?? null);
|
|
180
|
+
if (this.#mirrors) {
|
|
181
|
+
for (const mirror of this.#mirrors) {
|
|
182
|
+
if (mirror.closed.peek() === undefined)
|
|
183
|
+
mirror.closed.set(abort ?? null);
|
|
184
|
+
}
|
|
185
|
+
this.#mirrors.clear();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let makeConsumer;
|
|
190
|
+
/**
|
|
191
|
+
* The read side of an ordered stream of frames within a track.
|
|
192
|
+
*
|
|
193
|
+
* Created internally: obtain one from {@link Producer.consume} or a track subscriber's
|
|
194
|
+
* group reads.
|
|
195
|
+
*
|
|
196
|
+
* @public
|
|
197
|
+
*/
|
|
198
|
+
export class Consumer {
|
|
199
|
+
/** Sequence number of this group within its track. */
|
|
200
|
+
sequence;
|
|
201
|
+
#state;
|
|
202
|
+
constructor(state) {
|
|
203
|
+
this.#state = state;
|
|
204
|
+
this.sequence = state.sequence;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Settles once the group closes: `null` on a clean close, or the abort {@link Error}.
|
|
208
|
+
* Peek it synchronously (`undefined` while open), observe it reactively, or `await` it.
|
|
209
|
+
*/
|
|
210
|
+
get closed() {
|
|
211
|
+
return this.#state.closed;
|
|
212
|
+
}
|
|
213
|
+
static {
|
|
214
|
+
makeConsumer = (state) => new Consumer(state);
|
|
215
|
+
}
|
|
216
|
+
#readBufferedFrame() {
|
|
217
|
+
const frames = this.#state.frames.peek();
|
|
218
|
+
const frame = frames.shift();
|
|
219
|
+
if (!frame)
|
|
220
|
+
return undefined;
|
|
221
|
+
this.#state.cacheBytes -= frame.payload.byteLength;
|
|
222
|
+
return { sequence: this.#state.start++, frame };
|
|
52
223
|
}
|
|
53
224
|
/** True once no further frames can be read: the group has closed and every buffered frame is read. */
|
|
54
225
|
get done() {
|
|
55
|
-
return this.#state.frames.peek().length === 0 && this.#state.closed.peek() !==
|
|
226
|
+
return this.#state.frames.peek().length === 0 && this.#state.closed.peek() !== undefined;
|
|
227
|
+
}
|
|
228
|
+
/** True once the group has been closed, regardless of whether buffered frames remain unread. Synchronous complement to the {@link closed} promise. */
|
|
229
|
+
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;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* How many frames the group has held, including any already read or evicted.
|
|
238
|
+
*
|
|
239
|
+
* It is also the next frame's sequence number, so `frameCount - 1` names the newest
|
|
240
|
+
* frame written so far. A publisher snapshots it to resolve the track's live edge.
|
|
241
|
+
*/
|
|
242
|
+
get frameCount() {
|
|
243
|
+
return this.#state.total.peek();
|
|
56
244
|
}
|
|
57
245
|
/**
|
|
58
246
|
* Reads the next already-buffered frame without blocking.
|
|
247
|
+
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
59
248
|
*
|
|
60
|
-
* Returns `undefined` when nothing is buffered right now. That is
|
|
61
|
-
* check {@link done} to tell "no frame buffered yet"
|
|
62
|
-
* Drain a backlog by looping until this returns `undefined`, then branch on {@link done}: if not
|
|
63
|
-
* done, {@link readable} resolves when the next frame arrives.
|
|
249
|
+
* Returns `undefined` when nothing is buffered right now. That is not by itself
|
|
250
|
+
* end-of-group: check {@link done} to tell "no frame buffered yet" from "finished".
|
|
64
251
|
*/
|
|
65
252
|
tryReadFrame() {
|
|
66
|
-
|
|
253
|
+
const read = this.#readBufferedFrame();
|
|
254
|
+
return read?.frame;
|
|
67
255
|
}
|
|
68
256
|
/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */
|
|
69
257
|
tryReadFrameSequence() {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (frame === undefined)
|
|
258
|
+
const read = this.#readBufferedFrame();
|
|
259
|
+
if (!read)
|
|
73
260
|
return undefined;
|
|
74
|
-
return { sequence:
|
|
261
|
+
return { sequence: read.sequence, payload: read.frame.payload, timestamp: read.frame.timestamp };
|
|
75
262
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Resolves once {@link readFrame} would not block: a frame is buffered, or the group has closed.
|
|
78
|
-
* Always settles (never hangs), so on a finished group it resolves immediately; pair it with
|
|
79
|
-
* {@link done} to avoid re-waiting on a group that has nothing left.
|
|
80
|
-
*
|
|
81
|
-
* Lets a caller fold "this group has a frame" into a larger wait (e.g. racing it against a new
|
|
82
|
-
* group arriving) without touching the group's internal signals.
|
|
83
|
-
*/
|
|
263
|
+
/** Resolves once {@link readFrame} would not block. */
|
|
84
264
|
async readable() {
|
|
85
265
|
for (;;) {
|
|
86
266
|
if (this.#state.frames.peek().length > 0)
|
|
87
267
|
return;
|
|
88
|
-
if (this.#state.closed.peek())
|
|
268
|
+
if (this.#state.closed.peek() !== undefined)
|
|
89
269
|
return;
|
|
90
270
|
await Signal.race(this.#state.frames, this.#state.closed);
|
|
91
271
|
}
|
|
92
272
|
}
|
|
93
273
|
/**
|
|
94
274
|
* Reads the next frame from the group.
|
|
95
|
-
*
|
|
275
|
+
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
96
276
|
*/
|
|
97
277
|
async readFrame() {
|
|
98
|
-
|
|
278
|
+
for (;;) {
|
|
279
|
+
if (this.#state.evicted > 0)
|
|
280
|
+
throw new Lagged();
|
|
281
|
+
const read = this.#readBufferedFrame();
|
|
282
|
+
if (read)
|
|
283
|
+
return read.frame;
|
|
284
|
+
const closed = this.#state.closed.peek();
|
|
285
|
+
if (closed instanceof Error)
|
|
286
|
+
throw closed;
|
|
287
|
+
if (closed !== undefined)
|
|
288
|
+
return;
|
|
289
|
+
await Signal.race(this.#state.frames, this.#state.closed);
|
|
290
|
+
}
|
|
99
291
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Reads the next frame along with its sequence number within the group.
|
|
294
|
+
* Treat the returned frame bytes as read-only; they are shared with other consumers.
|
|
295
|
+
*/
|
|
296
|
+
async readFrameSequence(options) {
|
|
297
|
+
const from = options?.from ?? 0;
|
|
102
298
|
for (;;) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
299
|
+
if (this.#state.evicted > from)
|
|
300
|
+
throw new Lagged();
|
|
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
|
+
}
|
|
107
307
|
const closed = this.#state.closed.peek();
|
|
108
308
|
if (closed instanceof Error)
|
|
109
309
|
throw closed;
|
|
110
|
-
if (closed)
|
|
111
|
-
return
|
|
112
|
-
await this.
|
|
310
|
+
if (closed !== undefined)
|
|
311
|
+
return;
|
|
312
|
+
await Signal.race(this.#state.frames, this.#state.closed);
|
|
113
313
|
}
|
|
114
314
|
}
|
|
115
|
-
/** Reads the next frame and decodes
|
|
315
|
+
/** Reads the next frame and decodes its payload as a UTF-8 string. */
|
|
116
316
|
async readString() {
|
|
117
317
|
const frame = await this.readFrame();
|
|
118
|
-
return frame ? new TextDecoder().decode(frame) : undefined;
|
|
318
|
+
return frame ? new TextDecoder().decode(frame.payload) : undefined;
|
|
119
319
|
}
|
|
120
|
-
/** Reads the next frame and parses
|
|
320
|
+
/** Reads the next frame and parses its payload as JSON. */
|
|
121
321
|
async readJson() {
|
|
122
322
|
const frame = await this.readString();
|
|
123
323
|
return frame ? JSON.parse(frame) : undefined;
|
|
124
324
|
}
|
|
125
|
-
/** Reads the next frame and decodes
|
|
325
|
+
/** Reads the next frame and decodes its payload as a one-byte boolean. */
|
|
126
326
|
async readBool() {
|
|
127
327
|
const frame = await this.readFrame();
|
|
128
|
-
return frame ? frame[0] === 1 : undefined;
|
|
328
|
+
return frame ? frame.payload[0] === 1 : undefined;
|
|
129
329
|
}
|
|
130
|
-
/** Closes the group, optionally with an error to abort readers. */
|
|
330
|
+
/** Closes the group, optionally with an error to abort readers. Idempotent. */
|
|
131
331
|
close(abort) {
|
|
132
|
-
this.#state.closed.
|
|
332
|
+
if (this.#state.closed.peek() !== undefined)
|
|
333
|
+
return; // already closed
|
|
334
|
+
this.#state.closed.set(abort ?? null);
|
|
133
335
|
}
|
|
134
336
|
}
|
|
135
337
|
//# sourceMappingURL=group.js.map
|
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":["import { Signal } from \"@moq/signals\";\n\n/** Reactive backing state for a {@link Group}: buffered frames, a closed flag, and the running frame count. */\n/** A buffered frame: its payload plus any raw MOQ Object Properties bytes. */\ninterface BufferedFrame {\n\tdata: Uint8Array;\n\textensions?: Uint8Array;\n}\n\nclass GroupState {\n\tframes = new Signal<BufferedFrame[]>([]);\n\tclosed = new Signal<boolean | Error>(false);\n\ttotal = new Signal<number>(0); // The total number of frames in the group thus far\n}\n\n/** An ordered stream of frames within a track, delivered over a single QUIC stream. */\nexport class Group {\n\t/** Sequence number of this group within its track. */\n\treadonly sequence: number;\n\n\t// Reactive backing state, deliberately private: read through the read* methods so callers can't\n\t// poke the signals directly (and so the internal representation can change).\n\t#state = new GroupState();\n\n\t/** Resolves with the abort error (or undefined) once closed. */\n\treadonly closed: Promise<Error | undefined>;\n\n\tconstructor(sequence: number) {\n\t\tthis.sequence = sequence;\n\n\t\t// Cache the closed promise to avoid recreating it every time.\n\t\tthis.closed = new Promise((resolve) => {\n\t\t\tconst dispose = this.#state.closed.subscribe((closed) => {\n\t\t\t\tif (!closed) return;\n\t\t\t\tresolve(closed instanceof Error ? closed : undefined);\n\t\t\t\tdispose();\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Writes a frame to the group.\n\t * @param frame - The frame to write\n\t */\n\twriteFrame(frame: Uint8Array, extensions?: Uint8Array) {\n\t\tif (this.#state.closed.peek()) throw new Error(\"group is closed\");\n\n\t\tthis.#state.frames.mutate((frames) => {\n\t\t\tframes.push({ data: frame, extensions });\n\t\t});\n\n\t\tthis.#state.total.update((total) => total + 1);\n\t}\n\n\t/** Write a string as a single UTF-8 encoded frame. */\n\twriteString(str: string) {\n\t\tthis.writeFrame(new TextEncoder().encode(str));\n\t}\n\n\t/** Write a value as a single JSON-encoded frame. */\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. */\n\twriteBool(bool: boolean) {\n\t\tthis.writeFrame(new Uint8Array([bool ? 1 : 0]));\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() !== false;\n\t}\n\n\t/**\n\t * Reads the next already-buffered frame without blocking.\n\t *\n\t * Returns `undefined` when nothing is buffered right now. That is *not* by itself end-of-group:\n\t * check {@link done} to tell \"no frame buffered yet\" (more may arrive) from \"the group finished\".\n\t * Drain a backlog by looping until this returns `undefined`, then branch on {@link done}: if not\n\t * done, {@link readable} resolves when the next frame arrives.\n\t */\n\ttryReadFrame(): Uint8Array | undefined {\n\t\treturn this.tryReadFrameSequence()?.data;\n\t}\n\n\t/** Like {@link tryReadFrame} but also reports the frame's sequence number within the group. */\n\ttryReadFrameSequence(): { sequence: number; data: Uint8Array; extensions?: Uint8Array } | undefined {\n\t\tconst frames = this.#state.frames.peek();\n\t\tconst frame = frames.shift();\n\t\tif (frame === undefined) return undefined;\n\t\treturn { sequence: this.#state.total.peek() - frames.length - 1, data: frame.data, extensions: frame.extensions };\n\t}\n\n\t/**\n\t * Resolves once {@link readFrame} would not block: a frame is buffered, or the group has closed.\n\t * Always settles (never hangs), so on a finished group it resolves immediately; pair it with\n\t * {@link done} to avoid re-waiting on a group that has nothing left.\n\t *\n\t * Lets a caller fold \"this group has a frame\" into a larger wait (e.g. racing it against a new\n\t * group arriving) without touching the group's internal signals.\n\t */\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()) 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 * @returns A promise that resolves to the next frame or undefined\n\t */\n\tasync readFrame(): Promise<Uint8Array | undefined> {\n\t\treturn (await this.readFrameSequence())?.data;\n\t}\n\n\t/** Reads the next frame along with its sequence number within the group. */\n\tasync readFrameSequence(): Promise<{ sequence: number; data: Uint8Array; extensions?: Uint8Array } | undefined> {\n\t\tfor (;;) {\n\t\t\tconst next = this.tryReadFrameSequence();\n\t\t\tif (next) return next;\n\n\t\t\t// Drain buffered frames before observing the close, so a closed group still yields them.\n\t\t\tconst closed = this.#state.closed.peek();\n\t\t\tif (closed instanceof Error) throw closed;\n\t\t\tif (closed) return undefined;\n\n\t\t\tawait this.readable();\n\t\t}\n\t}\n\n\t/** Reads the next frame and decodes it 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) : undefined;\n\t}\n\n\t/** Reads the next frame and parses it 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 it as a one-byte boolean. */\n\tasync readBool(): Promise<boolean | undefined> {\n\t\tconst frame = await this.readFrame();\n\t\treturn frame ? frame[0] === 1 : undefined;\n\t}\n\n\t/** Closes the group, optionally with an error to abort readers. */\n\tclose(abort?: Error) {\n\t\tthis.#state.closed.set(abort ?? true);\n\t}\n}\n"]}
|
|
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"]}
|
package/ietf/adapter.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface Session {
|
|
|
19
19
|
export declare class NativeSession implements Session {
|
|
20
20
|
#private;
|
|
21
21
|
readonly version: IetfVersion;
|
|
22
|
-
constructor(quic: WebTransport, version: IetfVersion);
|
|
22
|
+
constructor(quic: WebTransport, version: IetfVersion, client: boolean);
|
|
23
23
|
openBi(): Promise<Stream>;
|
|
24
24
|
acceptBi(): Promise<Stream | undefined>;
|
|
25
25
|
nextRequestId(): Promise<bigint | undefined>;
|
|
@@ -36,7 +36,7 @@ export declare class NativeSession implements Session {
|
|
|
36
36
|
export declare class ControlStreamAdapter implements Session {
|
|
37
37
|
#private;
|
|
38
38
|
readonly version: IetfVersion;
|
|
39
|
-
constructor(quic: WebTransport, controlStream: Stream, version: IetfVersion, maxRequestId: bigint);
|
|
39
|
+
constructor(quic: WebTransport, controlStream: Stream, version: IetfVersion, maxRequestId: bigint, client: boolean);
|
|
40
40
|
/**
|
|
41
41
|
* Accept the next incoming virtual bidi stream.
|
|
42
42
|
* Blocks until a new request arrives on the control stream.
|
package/ietf/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;AAI3D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,cAAc,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC9B;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAK5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAIpE;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9B;IAEK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE5C;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIjD;IAED,kDAAkD;IAClD,KAAK,SAEJ;CACD;AAmBD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,OAAO;;IAQnD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IA+B9B,YACC,IAAI,EAAE,YAAY,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EAUf;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS5C;IAED;;;;;OAKG;IACH,MAAM,IAAI,MAAM,CAsDf;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpC;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjD;IAED;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAmDzB;IAuYD,KAAK,SA6BJ;CACD"}
|
package/ietf/adapter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* @ts-self-types="./adapter.d.ts" */
|
|
2
2
|
import { Mutex } from "async-mutex";
|
|
3
3
|
import { Reader, Stream } from "../stream.js";
|
|
4
|
+
import { decodeUtf8 } from "../util/utf8.js";
|
|
4
5
|
import * as Varint from "../varint.js";
|
|
5
6
|
import * as Namespace from "./namespace.js";
|
|
6
7
|
import { Version } from "./version.js";
|
|
@@ -10,14 +11,17 @@ import { Version } from "./version.js";
|
|
|
10
11
|
*/
|
|
11
12
|
export class NativeSession {
|
|
12
13
|
#quic;
|
|
13
|
-
|
|
14
|
+
// moq-transport reserves even request IDs for the client and odd for the server,
|
|
15
|
+
// so the two peers' ID spaces never overlap.
|
|
16
|
+
#requestId;
|
|
14
17
|
version;
|
|
15
|
-
constructor(quic, version) {
|
|
18
|
+
constructor(quic, version, client) {
|
|
16
19
|
this.#quic = quic;
|
|
17
20
|
this.version = version;
|
|
21
|
+
this.#requestId = client ? 0n : 1n;
|
|
18
22
|
}
|
|
19
23
|
async openBi() {
|
|
20
|
-
return Stream.open(this.#quic, this.version);
|
|
24
|
+
return Stream.open(this.#quic, { version: this.version });
|
|
21
25
|
}
|
|
22
26
|
async acceptBi() {
|
|
23
27
|
return Stream.accept(this.#quic, this.version);
|
|
@@ -69,12 +73,18 @@ export class ControlStreamAdapter {
|
|
|
69
73
|
// Incoming stream queue (for acceptBi)
|
|
70
74
|
#incomingQueue = [];
|
|
71
75
|
#incomingWaiters = [];
|
|
72
|
-
// Request ID flow control
|
|
73
|
-
|
|
76
|
+
// Request ID flow control. moq-transport reserves even request IDs for the
|
|
77
|
+
// client and odd for the server, so the two peers' ID spaces never overlap.
|
|
78
|
+
// This matters here because a single `#streams` map routes every request by
|
|
79
|
+
// ID: overlapping spaces would let an inbound request clobber the routing
|
|
80
|
+
// entry of an outbound one with the same number (e.g. an inbound
|
|
81
|
+
// PUBLISH_NAMESPACE stealing a pending SUBSCRIBE's slot, so SUBSCRIBE_OK is
|
|
82
|
+
// delivered to the wrong virtual stream).
|
|
83
|
+
#requestId;
|
|
74
84
|
#maxRequestId;
|
|
75
85
|
#maxRequestIdResolves = [];
|
|
76
86
|
#closed = false;
|
|
77
|
-
constructor(quic, controlStream, version, maxRequestId) {
|
|
87
|
+
constructor(quic, controlStream, version, maxRequestId, client) {
|
|
78
88
|
this.#quic = quic;
|
|
79
89
|
this.#reader = controlStream.reader;
|
|
80
90
|
this.#reader.version = version;
|
|
@@ -82,6 +92,7 @@ export class ControlStreamAdapter {
|
|
|
82
92
|
this.#writer.version = version;
|
|
83
93
|
this.version = version;
|
|
84
94
|
this.#maxRequestId = maxRequestId;
|
|
95
|
+
this.#requestId = client ? 0n : 1n;
|
|
85
96
|
}
|
|
86
97
|
/**
|
|
87
98
|
* Accept the next incoming virtual bidi stream.
|
|
@@ -155,7 +166,7 @@ export class ControlStreamAdapter {
|
|
|
155
166
|
* Open a real WebTransport bidi stream (for v16 SubscribeNamespace).
|
|
156
167
|
*/
|
|
157
168
|
async openNativeBi() {
|
|
158
|
-
return Stream.open(this.#quic, this.version);
|
|
169
|
+
return Stream.open(this.#quic, { version: this.version });
|
|
159
170
|
}
|
|
160
171
|
/**
|
|
161
172
|
* Allocate the next request ID, blocking if flow control limit reached.
|
|
@@ -369,16 +380,15 @@ export class ControlStreamAdapter {
|
|
|
369
380
|
* Parse a namespace from raw bytes and register it for reverse lookup.
|
|
370
381
|
*/
|
|
371
382
|
#parseAndRegisterNamespace(buf, requestId) {
|
|
372
|
-
const decoder = new TextDecoder();
|
|
373
383
|
const [partCount, afterCount] = Varint.decode(buf);
|
|
374
384
|
let cursor = afterCount;
|
|
375
385
|
const parts = [];
|
|
376
386
|
for (let i = 0; i < partCount; i++) {
|
|
377
387
|
const [len, afterLen] = Varint.decode(cursor);
|
|
378
|
-
parts.push(
|
|
388
|
+
parts.push(decodeUtf8(afterLen.subarray(0, len)));
|
|
379
389
|
cursor = afterLen.subarray(len);
|
|
380
390
|
}
|
|
381
|
-
const namespace =
|
|
391
|
+
const namespace = Namespace.fromTuple(parts);
|
|
382
392
|
this.#namespaces.set(namespace, requestId);
|
|
383
393
|
this.#namespacesByRequestId.set(requestId, namespace);
|
|
384
394
|
}
|