@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.
Files changed (310) hide show
  1. package/README.md +1 -0
  2. package/announced.d.ts +111 -21
  3. package/announced.d.ts.map +1 -1
  4. package/announced.js +213 -40
  5. package/announced.js.map +1 -1
  6. package/broadcast.d.ts +80 -25
  7. package/broadcast.d.ts.map +1 -1
  8. package/broadcast.js +270 -49
  9. package/broadcast.js.map +1 -1
  10. package/connection/accept.d.ts +6 -0
  11. package/connection/accept.d.ts.map +1 -1
  12. package/connection/accept.js +42 -13
  13. package/connection/accept.js.map +1 -1
  14. package/connection/browser.d.ts +5 -0
  15. package/connection/browser.d.ts.map +1 -0
  16. package/connection/browser.js +31 -0
  17. package/connection/browser.js.map +1 -0
  18. package/connection/connect.d.ts +43 -1
  19. package/connection/connect.d.ts.map +1 -1
  20. package/connection/connect.js +118 -123
  21. package/connection/connect.js.map +1 -1
  22. package/connection/established.d.ts +43 -16
  23. package/connection/established.d.ts.map +1 -1
  24. package/connection/established.js.map +1 -1
  25. package/connection/handshake.d.ts +12 -1
  26. package/connection/handshake.d.ts.map +1 -1
  27. package/connection/handshake.js +30 -14
  28. package/connection/handshake.js.map +1 -1
  29. package/connection/index.d.ts +8 -0
  30. package/connection/index.d.ts.map +1 -1
  31. package/connection/index.js +6 -0
  32. package/connection/index.js.map +1 -1
  33. package/connection/reload.d.ts +66 -9
  34. package/connection/reload.d.ts.map +1 -1
  35. package/connection/reload.js +192 -66
  36. package/connection/reload.js.map +1 -1
  37. package/connection/stats.d.ts +92 -0
  38. package/connection/stats.d.ts.map +1 -0
  39. package/connection/stats.js +35 -0
  40. package/connection/stats.js.map +1 -0
  41. package/connection/transport.d.ts +10 -0
  42. package/connection/transport.d.ts.map +1 -0
  43. package/connection/transport.js +12 -0
  44. package/connection/transport.js.map +1 -0
  45. package/consume.d.ts +44 -0
  46. package/consume.d.ts.map +1 -0
  47. package/consume.js +60 -0
  48. package/consume.js.map +1 -0
  49. package/datagram.d.ts +23 -0
  50. package/datagram.d.ts.map +1 -0
  51. package/datagram.js +3 -0
  52. package/datagram.js.map +1 -0
  53. package/error.d.ts +76 -0
  54. package/error.d.ts.map +1 -0
  55. package/error.js +107 -0
  56. package/error.js.map +1 -0
  57. package/group.d.ts +143 -38
  58. package/group.d.ts.map +1 -1
  59. package/group.js +270 -68
  60. package/group.js.map +1 -1
  61. package/ietf/adapter.d.ts +2 -2
  62. package/ietf/adapter.d.ts.map +1 -1
  63. package/ietf/adapter.js +20 -10
  64. package/ietf/adapter.js.map +1 -1
  65. package/ietf/aliases.d.ts +73 -5
  66. package/ietf/aliases.d.ts.map +1 -1
  67. package/ietf/aliases.js +121 -14
  68. package/ietf/aliases.js.map +1 -1
  69. package/ietf/cluster.d.ts +141 -0
  70. package/ietf/cluster.d.ts.map +1 -0
  71. package/ietf/cluster.js +204 -0
  72. package/ietf/cluster.js.map +1 -0
  73. package/ietf/connection.d.ts +39 -6
  74. package/ietf/connection.d.ts.map +1 -1
  75. package/ietf/connection.js +73 -12
  76. package/ietf/connection.js.map +1 -1
  77. package/ietf/fetch.d.ts +17 -0
  78. package/ietf/fetch.d.ts.map +1 -1
  79. package/ietf/fetch.js +21 -0
  80. package/ietf/fetch.js.map +1 -1
  81. package/ietf/filter.d.ts +87 -0
  82. package/ietf/filter.d.ts.map +1 -0
  83. package/ietf/filter.js +292 -0
  84. package/ietf/filter.js.map +1 -0
  85. package/ietf/index.d.ts +2 -0
  86. package/ietf/index.d.ts.map +1 -1
  87. package/ietf/index.js +3 -0
  88. package/ietf/index.js.map +1 -1
  89. package/ietf/namespace.d.ts +6 -0
  90. package/ietf/namespace.d.ts.map +1 -1
  91. package/ietf/namespace.js +27 -2
  92. package/ietf/namespace.js.map +1 -1
  93. package/ietf/object.d.ts +54 -6
  94. package/ietf/object.d.ts.map +1 -1
  95. package/ietf/object.js +178 -25
  96. package/ietf/object.js.map +1 -1
  97. package/ietf/parameters.d.ts +35 -10
  98. package/ietf/parameters.d.ts.map +1 -1
  99. package/ietf/parameters.js +236 -45
  100. package/ietf/parameters.js.map +1 -1
  101. package/ietf/priority.d.ts +5 -0
  102. package/ietf/priority.d.ts.map +1 -0
  103. package/ietf/priority.js +10 -0
  104. package/ietf/priority.js.map +1 -0
  105. package/ietf/properties.d.ts +13 -2
  106. package/ietf/properties.d.ts.map +1 -1
  107. package/ietf/properties.js +65 -8
  108. package/ietf/properties.js.map +1 -1
  109. package/ietf/publish.d.ts.map +1 -1
  110. package/ietf/publish.js +13 -3
  111. package/ietf/publish.js.map +1 -1
  112. package/ietf/publish_namespace.d.ts +16 -2
  113. package/ietf/publish_namespace.d.ts.map +1 -1
  114. package/ietf/publish_namespace.js +25 -6
  115. package/ietf/publish_namespace.js.map +1 -1
  116. package/ietf/publisher.d.ts +40 -7
  117. package/ietf/publisher.d.ts.map +1 -1
  118. package/ietf/publisher.js +751 -134
  119. package/ietf/publisher.js.map +1 -1
  120. package/ietf/request.js +1 -1
  121. package/ietf/request.js.map +1 -1
  122. package/ietf/solicit.d.ts +41 -0
  123. package/ietf/solicit.d.ts.map +1 -0
  124. package/ietf/solicit.js +47 -0
  125. package/ietf/solicit.js.map +1 -0
  126. package/ietf/subscribe.d.ts +41 -2
  127. package/ietf/subscribe.d.ts.map +1 -1
  128. package/ietf/subscribe.js +103 -22
  129. package/ietf/subscribe.js.map +1 -1
  130. package/ietf/subscribe_namespace.d.ts +21 -7
  131. package/ietf/subscribe_namespace.d.ts.map +1 -1
  132. package/ietf/subscribe_namespace.js +31 -14
  133. package/ietf/subscribe_namespace.js.map +1 -1
  134. package/ietf/subscriber.d.ts +20 -7
  135. package/ietf/subscriber.d.ts.map +1 -1
  136. package/ietf/subscriber.js +512 -142
  137. package/ietf/subscriber.js.map +1 -1
  138. package/ietf/version.d.ts +7 -1
  139. package/ietf/version.d.ts.map +1 -1
  140. package/ietf/version.js +7 -0
  141. package/ietf/version.js.map +1 -1
  142. package/index.d.ts +10 -5
  143. package/index.d.ts.map +1 -1
  144. package/index.js +10 -5
  145. package/index.js.map +1 -1
  146. package/internal.d.ts +29 -0
  147. package/internal.d.ts.map +1 -0
  148. package/internal.js +8 -0
  149. package/internal.js.map +1 -0
  150. package/lite/announce.d.ts +62 -15
  151. package/lite/announce.d.ts.map +1 -1
  152. package/lite/announce.js +230 -94
  153. package/lite/announce.js.map +1 -1
  154. package/lite/connection.d.ts +69 -20
  155. package/lite/connection.d.ts.map +1 -1
  156. package/lite/connection.js +128 -81
  157. package/lite/connection.js.map +1 -1
  158. package/lite/datagram.d.ts +23 -0
  159. package/lite/datagram.d.ts.map +1 -0
  160. package/lite/datagram.js +57 -0
  161. package/lite/datagram.js.map +1 -0
  162. package/lite/datagram_stream.d.ts +8 -0
  163. package/lite/datagram_stream.d.ts.map +1 -0
  164. package/lite/datagram_stream.js +44 -0
  165. package/lite/datagram_stream.js.map +1 -0
  166. package/lite/fetch.d.ts.map +1 -1
  167. package/lite/fetch.js +2 -2
  168. package/lite/fetch.js.map +1 -1
  169. package/lite/index.d.ts +3 -0
  170. package/lite/index.d.ts.map +1 -1
  171. package/lite/index.js +3 -0
  172. package/lite/index.js.map +1 -1
  173. package/lite/priority.d.ts +67 -0
  174. package/lite/priority.d.ts.map +1 -0
  175. package/lite/priority.js +114 -0
  176. package/lite/priority.js.map +1 -0
  177. package/lite/probe.d.ts +18 -2
  178. package/lite/probe.d.ts.map +1 -1
  179. package/lite/probe.js +24 -22
  180. package/lite/probe.js.map +1 -1
  181. package/lite/publisher.d.ts +14 -7
  182. package/lite/publisher.d.ts.map +1 -1
  183. package/lite/publisher.js +406 -84
  184. package/lite/publisher.js.map +1 -1
  185. package/lite/setup.d.ts +83 -6
  186. package/lite/setup.d.ts.map +1 -1
  187. package/lite/setup.js +203 -31
  188. package/lite/setup.js.map +1 -1
  189. package/lite/stream.d.ts +12 -3
  190. package/lite/stream.d.ts.map +1 -1
  191. package/lite/stream.js +7 -4
  192. package/lite/stream.js.map +1 -1
  193. package/lite/subscribe.d.ts +37 -23
  194. package/lite/subscribe.d.ts.map +1 -1
  195. package/lite/subscribe.js +70 -55
  196. package/lite/subscribe.js.map +1 -1
  197. package/lite/subscriber.d.ts +29 -23
  198. package/lite/subscriber.d.ts.map +1 -1
  199. package/lite/subscriber.js +689 -112
  200. package/lite/subscriber.js.map +1 -1
  201. package/lite/track.d.ts +25 -13
  202. package/lite/track.d.ts.map +1 -1
  203. package/lite/track.js +60 -35
  204. package/lite/track.js.map +1 -1
  205. package/lite/version.d.ts +39 -4
  206. package/lite/version.d.ts.map +1 -1
  207. package/lite/version.js +139 -17
  208. package/lite/version.js.map +1 -1
  209. package/mock.d.ts +53 -3
  210. package/mock.d.ts.map +1 -1
  211. package/mock.js +110 -21
  212. package/mock.js.map +1 -1
  213. package/{lite/origin.d.ts → origin.d.ts} +25 -1
  214. package/origin.d.ts.map +1 -0
  215. package/{lite/origin.js → origin.js} +25 -1
  216. package/origin.js.map +1 -0
  217. package/package.json +6 -4
  218. package/path.d.ts +67 -0
  219. package/path.d.ts.map +1 -1
  220. package/path.js +128 -0
  221. package/path.js.map +1 -1
  222. package/stream.d.ts +94 -4
  223. package/stream.d.ts.map +1 -1
  224. package/stream.js +128 -17
  225. package/stream.js.map +1 -1
  226. package/time.d.ts +109 -86
  227. package/time.d.ts.map +1 -1
  228. package/time.js +80 -30
  229. package/time.js.map +1 -1
  230. package/track.d.ts +298 -47
  231. package/track.d.ts.map +1 -1
  232. package/track.js +721 -109
  233. package/track.js.map +1 -1
  234. package/util/timeout.d.ts +5 -0
  235. package/util/timeout.d.ts.map +1 -0
  236. package/util/timeout.js +18 -0
  237. package/util/timeout.js.map +1 -0
  238. package/util/utf8.d.ts +3 -0
  239. package/util/utf8.d.ts.map +1 -0
  240. package/util/utf8.js +7 -0
  241. package/util/utf8.js.map +1 -0
  242. package/varint.d.ts +14 -3
  243. package/varint.d.ts.map +1 -1
  244. package/varint.js +23 -12
  245. package/varint.js.map +1 -1
  246. package/zod.d.ts +5 -5
  247. package/zod.d.ts.map +1 -1
  248. package/zod.js.map +1 -1
  249. package/bandwidth.d.ts +0 -11
  250. package/bandwidth.d.ts.map +0 -1
  251. package/bandwidth.js +0 -7
  252. package/bandwidth.js.map +0 -1
  253. package/connection/cert.test.d.ts +0 -2
  254. package/connection/cert.test.d.ts.map +0 -1
  255. package/connection/cert.test.js +0 -26
  256. package/connection/cert.test.js.map +0 -1
  257. package/connection/reload.test.d.ts +0 -2
  258. package/connection/reload.test.d.ts.map +0 -1
  259. package/connection/reload.test.js +0 -39
  260. package/connection/reload.test.js.map +0 -1
  261. package/group.test.d.ts +0 -2
  262. package/group.test.d.ts.map +0 -1
  263. package/group.test.js +0 -66
  264. package/group.test.js.map +0 -1
  265. package/ietf/aliases.test.d.ts +0 -2
  266. package/ietf/aliases.test.d.ts.map +0 -1
  267. package/ietf/aliases.test.js +0 -30
  268. package/ietf/aliases.test.js.map +0 -1
  269. package/ietf/ietf.test.d.ts +0 -2
  270. package/ietf/ietf.test.d.ts.map +0 -1
  271. package/ietf/ietf.test.js +0 -873
  272. package/ietf/ietf.test.js.map +0 -1
  273. package/integration.test.d.ts +0 -2
  274. package/integration.test.d.ts.map +0 -1
  275. package/integration.test.js +0 -90
  276. package/integration.test.js.map +0 -1
  277. package/lite/origin.d.ts.map +0 -1
  278. package/lite/origin.js.map +0 -1
  279. package/lite/subscriber.test.d.ts +0 -2
  280. package/lite/subscriber.test.d.ts.map +0 -1
  281. package/lite/subscriber.test.js +0 -30
  282. package/lite/subscriber.test.js.map +0 -1
  283. package/lite/track.test.d.ts +0 -2
  284. package/lite/track.test.d.ts.map +0 -1
  285. package/lite/track.test.js +0 -53
  286. package/lite/track.test.js.map +0 -1
  287. package/path.test.d.ts +0 -2
  288. package/path.test.d.ts.map +0 -1
  289. package/path.test.js +0 -169
  290. package/path.test.js.map +0 -1
  291. package/stream.test.d.ts +0 -2
  292. package/stream.test.d.ts.map +0 -1
  293. package/stream.test.js +0 -210
  294. package/stream.test.js.map +0 -1
  295. package/track.test.d.ts +0 -2
  296. package/track.test.d.ts.map +0 -1
  297. package/track.test.js +0 -56
  298. package/track.test.js.map +0 -1
  299. package/util/error.d.ts +0 -3
  300. package/util/error.d.ts.map +0 -1
  301. package/util/error.js +0 -9
  302. package/util/error.js.map +0 -1
  303. package/util/index.d.ts +0 -2
  304. package/util/index.d.ts.map +0 -1
  305. package/util/index.js +0 -3
  306. package/util/index.js.map +0 -1
  307. package/varint.test.d.ts +0 -2
  308. package/varint.test.d.ts.map +0 -1
  309. package/varint.test.js +0 -106
  310. package/varint.test.js.map +0 -1
package/group.js CHANGED
@@ -1,135 +1,337 @@
1
1
  /* @ts-self-types="./group.d.ts" */
2
- import { Signal } from "@norskvideo/moq-signals";
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 Signal(false);
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
- /** An ordered stream of frames within a track, delivered over a single QUIC stream. */
9
- export class Group {
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
- // Reactive backing state, deliberately private: read through the read* methods so callers can't
13
- // poke the signals directly (and so the internal representation can change).
14
- #state = new GroupState();
15
- /** Resolves with the abort error (or undefined) once closed. */
16
- closed;
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
- * Writes a frame to the group.
31
- * @param frame - The frame to write
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
- writeFrame(frame, extensions) {
34
- if (this.#state.closed.peek())
35
- throw new Error("group is closed");
36
- this.#state.frames.mutate((frames) => {
37
- frames.push({ data: frame, extensions });
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
- this.#state.total.update((total) => total + 1);
123
+ return makeConsumer(dst);
40
124
  }
41
- /** Write a string as a single UTF-8 encoded frame. */
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() !== false;
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 *not* by itself end-of-group:
61
- * check {@link done} to tell "no frame buffered yet" (more may arrive) from "the group finished".
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
- return this.tryReadFrameSequence()?.data;
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 frames = this.#state.frames.peek();
71
- const frame = frames.shift();
72
- if (frame === undefined)
258
+ const read = this.#readBufferedFrame();
259
+ if (!read)
73
260
  return undefined;
74
- return { sequence: this.#state.total.peek() - frames.length - 1, data: frame.data, extensions: frame.extensions };
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
- * @returns A promise that resolves to the next frame or undefined
275
+ * Treat the returned frame bytes as read-only; they are shared with other consumers.
96
276
  */
97
277
  async readFrame() {
98
- return (await this.readFrameSequence())?.data;
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
- /** Reads the next frame along with its sequence number within the group. */
101
- async readFrameSequence() {
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
- const next = this.tryReadFrameSequence();
104
- if (next)
105
- return next;
106
- // Drain buffered frames before observing the close, so a closed group still yields them.
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 undefined;
112
- await this.readable();
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 it as a UTF-8 string. */
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 it as JSON. */
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 it as a one-byte boolean. */
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.set(abort ?? true);
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.
@@ -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;AAG3D,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;;IAG5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAGnD;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;IAyB9B,YAAY,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAQhG;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;IAwYD,KAAK,SA6BJ;CACD"}
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
- #requestId = 0n;
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
- #requestId = 0n;
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(decoder.decode(afterLen.subarray(0, len)));
388
+ parts.push(decodeUtf8(afterLen.subarray(0, len)));
379
389
  cursor = afterLen.subarray(len);
380
390
  }
381
- const namespace = parts.join("/");
391
+ const namespace = Namespace.fromTuple(parts);
382
392
  this.#namespaces.set(namespace, requestId);
383
393
  this.#namespacesByRequestId.set(requestId, namespace);
384
394
  }