@norskvideo/moq-net 0.1.0

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 (274) hide show
  1. package/README.md +70 -0
  2. package/announced.d.ts +45 -0
  3. package/announced.d.ts.map +1 -0
  4. package/announced.js +70 -0
  5. package/announced.js.map +1 -0
  6. package/bandwidth.d.ts +11 -0
  7. package/bandwidth.d.ts.map +1 -0
  8. package/bandwidth.js +7 -0
  9. package/bandwidth.js.map +1 -0
  10. package/broadcast.d.ts +39 -0
  11. package/broadcast.d.ts.map +1 -0
  12. package/broadcast.js +76 -0
  13. package/broadcast.js.map +1 -0
  14. package/connection/accept.d.ts +15 -0
  15. package/connection/accept.d.ts.map +1 -0
  16. package/connection/accept.js +137 -0
  17. package/connection/accept.js.map +1 -0
  18. package/connection/cert.test.d.ts +2 -0
  19. package/connection/cert.test.d.ts.map +1 -0
  20. package/connection/cert.test.js +26 -0
  21. package/connection/cert.test.js.map +1 -0
  22. package/connection/connect.d.ts +37 -0
  23. package/connection/connect.d.ts.map +1 -0
  24. package/connection/connect.js +348 -0
  25. package/connection/connect.js.map +1 -0
  26. package/connection/established.d.ts +30 -0
  27. package/connection/established.d.ts.map +1 -0
  28. package/connection/established.js +3 -0
  29. package/connection/established.js.map +1 -0
  30. package/connection/handshake.d.ts +10 -0
  31. package/connection/handshake.d.ts.map +1 -0
  32. package/connection/handshake.js +48 -0
  33. package/connection/handshake.js.map +1 -0
  34. package/connection/index.d.ts +5 -0
  35. package/connection/index.d.ts.map +1 -0
  36. package/connection/index.js +6 -0
  37. package/connection/index.js.map +1 -0
  38. package/connection/reload.d.ts +47 -0
  39. package/connection/reload.d.ts.map +1 -0
  40. package/connection/reload.js +137 -0
  41. package/connection/reload.js.map +1 -0
  42. package/group.d.ts +66 -0
  43. package/group.d.ts.map +1 -0
  44. package/group.js +135 -0
  45. package/group.js.map +1 -0
  46. package/group.test.d.ts +2 -0
  47. package/group.test.d.ts.map +1 -0
  48. package/group.test.js +66 -0
  49. package/group.test.js.map +1 -0
  50. package/ietf/adapter.d.ts +65 -0
  51. package/ietf/adapter.d.ts.map +1 -0
  52. package/ietf/adapter.js +626 -0
  53. package/ietf/adapter.js.map +1 -0
  54. package/ietf/connection.d.ts +65 -0
  55. package/ietf/connection.d.ts.map +1 -0
  56. package/ietf/connection.js +248 -0
  57. package/ietf/connection.js.map +1 -0
  58. package/ietf/control.d.ts +135 -0
  59. package/ietf/control.d.ts.map +1 -0
  60. package/ietf/control.js +242 -0
  61. package/ietf/control.js.map +1 -0
  62. package/ietf/fetch.d.ts +64 -0
  63. package/ietf/fetch.d.ts.map +1 -0
  64. package/ietf/fetch.js +100 -0
  65. package/ietf/fetch.js.map +1 -0
  66. package/ietf/goaway.d.ts +15 -0
  67. package/ietf/goaway.d.ts.map +1 -0
  68. package/ietf/goaway.js +34 -0
  69. package/ietf/goaway.js.map +1 -0
  70. package/ietf/ietf.test.d.ts +2 -0
  71. package/ietf/ietf.test.d.ts.map +1 -0
  72. package/ietf/ietf.test.js +873 -0
  73. package/ietf/ietf.test.js.map +1 -0
  74. package/ietf/index.d.ts +18 -0
  75. package/ietf/index.d.ts.map +1 -0
  76. package/ietf/index.js +19 -0
  77. package/ietf/index.js.map +1 -0
  78. package/ietf/message.d.ts +4 -0
  79. package/ietf/message.d.ts.map +1 -0
  80. package/ietf/message.js +54 -0
  81. package/ietf/message.js.map +1 -0
  82. package/ietf/namespace.d.ts +5 -0
  83. package/ietf/namespace.d.ts.map +1 -0
  84. package/ietf/namespace.js +27 -0
  85. package/ietf/namespace.js.map +1 -0
  86. package/ietf/object.d.ts +40 -0
  87. package/ietf/object.d.ts.map +1 -0
  88. package/ietf/object.js +160 -0
  89. package/ietf/object.js.map +1 -0
  90. package/ietf/parameters.d.ts +56 -0
  91. package/ietf/parameters.d.ts.map +1 -0
  92. package/ietf/parameters.js +337 -0
  93. package/ietf/parameters.js.map +1 -0
  94. package/ietf/properties.d.ts +4 -0
  95. package/ietf/properties.d.ts.map +1 -0
  96. package/ietf/properties.js +33 -0
  97. package/ietf/properties.js.map +1 -0
  98. package/ietf/publish.d.ts +68 -0
  99. package/ietf/publish.d.ts.map +1 -0
  100. package/ietf/publish.js +192 -0
  101. package/ietf/publish.js.map +1 -0
  102. package/ietf/publish_namespace.d.ts +68 -0
  103. package/ietf/publish_namespace.d.ts.map +1 -0
  104. package/ietf/publish_namespace.js +176 -0
  105. package/ietf/publish_namespace.js.map +1 -0
  106. package/ietf/publisher.d.ts +50 -0
  107. package/ietf/publisher.d.ts.map +1 -0
  108. package/ietf/publisher.js +330 -0
  109. package/ietf/publisher.js.map +1 -0
  110. package/ietf/request.d.ts +52 -0
  111. package/ietf/request.d.ts.map +1 -0
  112. package/ietf/request.js +119 -0
  113. package/ietf/request.js.map +1 -0
  114. package/ietf/setup.d.ts +38 -0
  115. package/ietf/setup.d.ts.map +1 -0
  116. package/ietf/setup.js +129 -0
  117. package/ietf/setup.js.map +1 -0
  118. package/ietf/subscribe.d.ts +66 -0
  119. package/ietf/subscribe.d.ts.map +1 -0
  120. package/ietf/subscribe.js +276 -0
  121. package/ietf/subscribe.js.map +1 -0
  122. package/ietf/subscribe_namespace.d.ts +101 -0
  123. package/ietf/subscribe_namespace.d.ts.map +1 -0
  124. package/ietf/subscribe_namespace.js +256 -0
  125. package/ietf/subscribe_namespace.js.map +1 -0
  126. package/ietf/subscriber.d.ts +53 -0
  127. package/ietf/subscriber.d.ts.map +1 -0
  128. package/ietf/subscriber.js +404 -0
  129. package/ietf/subscriber.js.map +1 -0
  130. package/ietf/track.d.ts +40 -0
  131. package/ietf/track.d.ts.map +1 -0
  132. package/ietf/track.js +108 -0
  133. package/ietf/track.js.map +1 -0
  134. package/ietf/version.d.ts +56 -0
  135. package/ietf/version.d.ts.map +1 -0
  136. package/ietf/version.js +63 -0
  137. package/ietf/version.js.map +1 -0
  138. package/index.d.ts +22 -0
  139. package/index.d.ts.map +1 -0
  140. package/index.js +23 -0
  141. package/index.js.map +1 -0
  142. package/integration.test.d.ts +2 -0
  143. package/integration.test.d.ts.map +1 -0
  144. package/integration.test.js +90 -0
  145. package/integration.test.js.map +1 -0
  146. package/lite/announce.d.ts +35 -0
  147. package/lite/announce.d.ts.map +1 -0
  148. package/lite/announce.js +166 -0
  149. package/lite/announce.js.map +1 -0
  150. package/lite/connection.d.ts +47 -0
  151. package/lite/connection.d.ts.map +1 -0
  152. package/lite/connection.js +247 -0
  153. package/lite/connection.js.map +1 -0
  154. package/lite/fetch.d.ts +14 -0
  155. package/lite/fetch.d.ts.map +1 -0
  156. package/lite/fetch.js +47 -0
  157. package/lite/fetch.js.map +1 -0
  158. package/lite/goaway.d.ts +10 -0
  159. package/lite/goaway.d.ts.map +1 -0
  160. package/lite/goaway.js +37 -0
  161. package/lite/goaway.js.map +1 -0
  162. package/lite/group.d.ts +18 -0
  163. package/lite/group.d.ts.map +1 -0
  164. package/lite/group.js +46 -0
  165. package/lite/group.js.map +1 -0
  166. package/lite/index.d.ts +11 -0
  167. package/lite/index.d.ts.map +1 -0
  168. package/lite/index.js +12 -0
  169. package/lite/index.js.map +1 -0
  170. package/lite/message.d.ts +5 -0
  171. package/lite/message.d.ts.map +1 -0
  172. package/lite/message.js +51 -0
  173. package/lite/message.js.map +1 -0
  174. package/lite/origin.d.ts +25 -0
  175. package/lite/origin.d.ts.map +1 -0
  176. package/lite/origin.js +35 -0
  177. package/lite/origin.js.map +1 -0
  178. package/lite/probe.d.ts +12 -0
  179. package/lite/probe.d.ts.map +1 -0
  180. package/lite/probe.js +60 -0
  181. package/lite/probe.js.map +1 -0
  182. package/lite/publisher.d.ts +63 -0
  183. package/lite/publisher.d.ts.map +1 -0
  184. package/lite/publisher.js +350 -0
  185. package/lite/publisher.js.map +1 -0
  186. package/lite/session.d.ts +36 -0
  187. package/lite/session.d.ts.map +1 -0
  188. package/lite/session.js +130 -0
  189. package/lite/session.js.map +1 -0
  190. package/lite/setup.d.ts +18 -0
  191. package/lite/setup.d.ts.map +1 -0
  192. package/lite/setup.js +55 -0
  193. package/lite/setup.js.map +1 -0
  194. package/lite/stream.d.ts +23 -0
  195. package/lite/stream.d.ts.map +1 -0
  196. package/lite/stream.js +20 -0
  197. package/lite/stream.js.map +1 -0
  198. package/lite/subscribe.d.ts +111 -0
  199. package/lite/subscribe.d.ts.map +1 -0
  200. package/lite/subscribe.js +341 -0
  201. package/lite/subscribe.js.map +1 -0
  202. package/lite/subscriber.d.ts +80 -0
  203. package/lite/subscriber.d.ts.map +1 -0
  204. package/lite/subscriber.js +316 -0
  205. package/lite/subscriber.js.map +1 -0
  206. package/lite/track.d.ts +38 -0
  207. package/lite/track.d.ts.map +1 -0
  208. package/lite/track.js +81 -0
  209. package/lite/track.js.map +1 -0
  210. package/lite/track.test.d.ts +2 -0
  211. package/lite/track.test.d.ts.map +1 -0
  212. package/lite/track.test.js +53 -0
  213. package/lite/track.test.js.map +1 -0
  214. package/lite/version.d.ts +16 -0
  215. package/lite/version.d.ts.map +1 -0
  216. package/lite/version.js +41 -0
  217. package/lite/version.js.map +1 -0
  218. package/mock.d.ts +16 -0
  219. package/mock.d.ts.map +1 -0
  220. package/mock.js +154 -0
  221. package/mock.js.map +1 -0
  222. package/package.json +27 -0
  223. package/path.d.ts +111 -0
  224. package/path.d.ts.map +1 -0
  225. package/path.js +133 -0
  226. package/path.js.map +1 -0
  227. package/path.test.d.ts +2 -0
  228. package/path.test.d.ts.map +1 -0
  229. package/path.test.js +169 -0
  230. package/path.test.js.map +1 -0
  231. package/stream.d.ts +57 -0
  232. package/stream.d.ts.map +1 -0
  233. package/stream.js +326 -0
  234. package/stream.js.map +1 -0
  235. package/stream.test.d.ts +2 -0
  236. package/stream.test.d.ts.map +1 -0
  237. package/stream.test.js +210 -0
  238. package/stream.test.js.map +1 -0
  239. package/time.d.ts +109 -0
  240. package/time.d.ts.map +1 -0
  241. package/time.js +94 -0
  242. package/time.js.map +1 -0
  243. package/track.d.ts +85 -0
  244. package/track.d.ts.map +1 -0
  245. package/track.js +227 -0
  246. package/track.js.map +1 -0
  247. package/track.test.d.ts +2 -0
  248. package/track.test.d.ts.map +1 -0
  249. package/track.test.js +56 -0
  250. package/track.test.js.map +1 -0
  251. package/util/error.d.ts +3 -0
  252. package/util/error.d.ts.map +1 -0
  253. package/util/error.js +9 -0
  254. package/util/error.js.map +1 -0
  255. package/util/hex.d.ts +3 -0
  256. package/util/hex.d.ts.map +1 -0
  257. package/util/hex.js +16 -0
  258. package/util/hex.js.map +1 -0
  259. package/util/index.d.ts +2 -0
  260. package/util/index.d.ts.map +1 -0
  261. package/util/index.js +3 -0
  262. package/util/index.js.map +1 -0
  263. package/varint.d.ts +36 -0
  264. package/varint.d.ts.map +1 -0
  265. package/varint.js +288 -0
  266. package/varint.js.map +1 -0
  267. package/varint.test.d.ts +2 -0
  268. package/varint.test.d.ts.map +1 -0
  269. package/varint.test.js +106 -0
  270. package/varint.test.js.map +1 -0
  271. package/zod.d.ts +13 -0
  272. package/zod.d.ts.map +1 -0
  273. package/zod.js +19 -0
  274. package/zod.js.map +1 -0
package/track.js ADDED
@@ -0,0 +1,227 @@
1
+ /* @ts-self-types="./track.d.ts" */
2
+ import { Signal } from "@norskvideo/moq-signals";
3
+ import { Group } from "./group.js";
4
+ /** Reactive backing state for a {@link Track}: buffered groups, a closed flag, and the subscription priority. */
5
+ export class TrackState {
6
+ groups = new Signal([]);
7
+ closed = new Signal(false);
8
+ priority = new Signal(undefined);
9
+ }
10
+ /** A live stream of groups within a broadcast, identified by name. */
11
+ export class Track {
12
+ /** Name of this track within its broadcast. */
13
+ name;
14
+ /** Reactive backing state. */
15
+ state = new TrackState();
16
+ #next;
17
+ #nextSequence = 0;
18
+ /** Resolves with the abort error (or undefined) once closed. */
19
+ closed;
20
+ constructor(name) {
21
+ this.name = name;
22
+ this.closed = new Promise((resolve) => {
23
+ const dispose = this.state.closed.subscribe((closed) => {
24
+ if (!closed)
25
+ return;
26
+ resolve(closed instanceof Error ? closed : undefined);
27
+ dispose();
28
+ });
29
+ });
30
+ }
31
+ /**
32
+ * Appends a new group to the track.
33
+ * @returns A GroupProducer for the new group
34
+ */
35
+ appendGroup() {
36
+ if (this.state.closed.peek())
37
+ throw new Error("track is closed");
38
+ const group = new Group(this.#next ?? 0);
39
+ this.#next = group.sequence + 1;
40
+ this.state.groups.mutate((groups) => {
41
+ groups.push(group);
42
+ groups.sort((a, b) => a.sequence - b.sequence);
43
+ });
44
+ return group;
45
+ }
46
+ /**
47
+ * Inserts an existing group into the track.
48
+ * @param group - The group to insert
49
+ */
50
+ writeGroup(group) {
51
+ if (this.state.closed.peek())
52
+ throw new Error("track is closed");
53
+ // Only advance #next upward (for appendGroup auto-increment).
54
+ if (group.sequence >= (this.#next ?? 0)) {
55
+ this.#next = group.sequence + 1;
56
+ }
57
+ this.state.groups.mutate((groups) => {
58
+ groups.push(group);
59
+ groups.sort((a, b) => a.sequence - b.sequence);
60
+ });
61
+ }
62
+ /**
63
+ * Appends a frame to the track in its own group.
64
+ *
65
+ * @param frame - The frame to append
66
+ */
67
+ writeFrame(frame) {
68
+ const group = this.appendGroup();
69
+ group.writeFrame(frame);
70
+ group.close();
71
+ }
72
+ /** Appends a string to the track as its own single-frame group. */
73
+ writeString(str) {
74
+ const group = this.appendGroup();
75
+ group.writeString(str);
76
+ group.close();
77
+ }
78
+ /** Appends a JSON value to the track as its own single-frame group. */
79
+ writeJson(json) {
80
+ const group = this.appendGroup();
81
+ group.writeJson(json);
82
+ group.close();
83
+ }
84
+ /** Appends a boolean to the track as its own single-frame group. */
85
+ writeBool(bool) {
86
+ const group = this.appendGroup();
87
+ group.writeBool(bool);
88
+ group.close();
89
+ }
90
+ /**
91
+ * Receive the next group available on this track, in arrival order.
92
+ *
93
+ * Groups may arrive out of order or with gaps due to network conditions.
94
+ * Use {@link nextGroupOrdered} if you need groups in sequence order,
95
+ * skipping those that arrive too late.
96
+ */
97
+ async recvGroup() {
98
+ for (;;) {
99
+ const groups = this.state.groups.peek();
100
+ if (groups.length > 0) {
101
+ return groups.shift();
102
+ }
103
+ const closed = this.state.closed.peek();
104
+ if (closed instanceof Error)
105
+ throw closed;
106
+ if (closed)
107
+ return undefined;
108
+ await Signal.race(this.state.groups, this.state.closed);
109
+ }
110
+ }
111
+ /**
112
+ * @deprecated Use {@link recvGroup} for arrival order, or {@link nextGroupOrdered} for sequence order.
113
+ */
114
+ async nextGroup() {
115
+ return this.recvGroup();
116
+ }
117
+ /**
118
+ * Return the next group with a strictly-greater sequence number than the last returned.
119
+ *
120
+ * Late arrivals (with a sequence number at or below the last one returned) are silently skipped.
121
+ *
122
+ * NOTE: This will be renamed to `nextGroup` in the next major version.
123
+ */
124
+ async nextGroupOrdered() {
125
+ for (;;) {
126
+ const group = await this.recvGroup();
127
+ if (!group)
128
+ return undefined;
129
+ if (group.sequence < this.#nextSequence) {
130
+ group.close();
131
+ continue;
132
+ }
133
+ this.#nextSequence = group.sequence + 1;
134
+ return group;
135
+ }
136
+ }
137
+ /** Reads the next frame across all groups, discarding older groups. */
138
+ async readFrame() {
139
+ return (await this.readFrameSequence())?.data;
140
+ }
141
+ /** Reads the next frame along with its group and frame sequence numbers. */
142
+ async readFrameSequence() {
143
+ for (;;) {
144
+ const groups = this.state.groups.peek();
145
+ // Discard old groups.
146
+ while (groups.length > 1) {
147
+ const next = groups[0].tryReadFrameSequence();
148
+ if (next) {
149
+ return { group: groups[0].sequence, frame: next.sequence, data: next.data };
150
+ }
151
+ // Skip this old group
152
+ groups.shift()?.close();
153
+ }
154
+ // If there's no groups, wait for a new one.
155
+ if (groups.length === 0) {
156
+ const closed = this.state.closed.peek();
157
+ if (closed instanceof Error)
158
+ throw closed;
159
+ if (closed)
160
+ return undefined;
161
+ await Signal.race(this.state.groups, this.state.closed);
162
+ continue;
163
+ }
164
+ // If there's a group, wait for a frame.
165
+ const group = groups[0];
166
+ const next = group.tryReadFrameSequence();
167
+ if (next) {
168
+ return { group: group.sequence, frame: next.sequence, data: next.data };
169
+ }
170
+ // If the track is closed, return undefined.
171
+ const closed = this.state.closed.peek();
172
+ if (closed instanceof Error)
173
+ throw closed;
174
+ if (closed)
175
+ return undefined;
176
+ // Wake on a new group or the track closing. Only fold in the current group's readability
177
+ // while it can still deliver frames: a finished group's readable() resolves immediately, so
178
+ // racing it would busy-loop until a newer group arrives instead of waiting.
179
+ if (group.done) {
180
+ await Signal.race(this.state.groups, this.state.closed);
181
+ }
182
+ else {
183
+ await Promise.race([Signal.race(this.state.groups, this.state.closed), group.readable()]);
184
+ }
185
+ }
186
+ }
187
+ /** Reads the next frame and decodes it as a UTF-8 string. */
188
+ async readString() {
189
+ const next = await this.readFrame();
190
+ if (!next)
191
+ return undefined;
192
+ return new TextDecoder().decode(next);
193
+ }
194
+ /** Reads the next frame and parses it as JSON. */
195
+ async readJson() {
196
+ const next = await this.readString();
197
+ if (!next)
198
+ return undefined;
199
+ return JSON.parse(next);
200
+ }
201
+ /** Reads the next frame and decodes it as a one-byte boolean, throwing on a malformed frame. */
202
+ async readBool() {
203
+ const next = await this.readFrame();
204
+ if (!next)
205
+ return undefined;
206
+ if (next.byteLength !== 1 || !(next[0] === 0 || next[0] === 1))
207
+ throw new Error("invalid bool frame");
208
+ return next[0] === 1;
209
+ }
210
+ /**
211
+ * Update the subscription priority. Triggers a SUBSCRIBE_UPDATE
212
+ * to the publisher when used on a subscribed track.
213
+ */
214
+ updatePriority(priority) {
215
+ this.state.priority.set(priority, true);
216
+ }
217
+ /**
218
+ * Closes the publisher and all associated groups.
219
+ */
220
+ close(abort) {
221
+ this.state.closed.set(abort ?? true);
222
+ for (const group of this.state.groups.peek()) {
223
+ group.close(abort);
224
+ }
225
+ }
226
+ }
227
+ //# sourceMappingURL=track.js.map
package/track.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.js","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"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","sourcesContent":["import { Signal } from \"@moq/signals\";\nimport { Group } from \"./group.ts\";\n\n/** Reactive backing state for a {@link Track}: buffered groups, a closed flag, and the subscription priority. */\nexport class TrackState {\n\tgroups = new Signal<Group[]>([]);\n\tclosed = new Signal<boolean | Error>(false);\n\tpriority = new Signal<number | undefined>(undefined);\n}\n\n/** A live stream of groups within a broadcast, identified by name. */\nexport class Track {\n\t/** Name of this track within its broadcast. */\n\treadonly name: string;\n\n\t/** Reactive backing state. */\n\tstate = new TrackState();\n\t#next?: number;\n\t#nextSequence = 0;\n\n\t/** Resolves with the abort error (or undefined) once closed. */\n\treadonly closed: Promise<Error | undefined>;\n\n\tconstructor(name: string) {\n\t\tthis.name = name;\n\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 * Appends a new group to the track.\n\t * @returns A GroupProducer for the new group\n\t */\n\tappendGroup(): Group {\n\t\tif (this.state.closed.peek()) throw new Error(\"track is closed\");\n\n\t\tconst group = new Group(this.#next ?? 0);\n\n\t\tthis.#next = group.sequence + 1;\n\t\tthis.state.groups.mutate((groups) => {\n\t\t\tgroups.push(group);\n\t\t\tgroups.sort((a, b) => a.sequence - b.sequence);\n\t\t});\n\n\t\treturn group;\n\t}\n\n\t/**\n\t * Inserts an existing group into the track.\n\t * @param group - The group to insert\n\t */\n\twriteGroup(group: Group) {\n\t\tif (this.state.closed.peek()) throw new Error(\"track is closed\");\n\n\t\t// Only advance #next upward (for appendGroup auto-increment).\n\t\tif (group.sequence >= (this.#next ?? 0)) {\n\t\t\tthis.#next = group.sequence + 1;\n\t\t}\n\n\t\tthis.state.groups.mutate((groups) => {\n\t\t\tgroups.push(group);\n\t\t\tgroups.sort((a, b) => a.sequence - b.sequence);\n\t\t});\n\t}\n\n\t/**\n\t * Appends a frame to the track in its own group.\n\t *\n\t * @param frame - The frame to append\n\t */\n\twriteFrame(frame: Uint8Array) {\n\t\tconst group = this.appendGroup();\n\t\tgroup.writeFrame(frame);\n\t\tgroup.close();\n\t}\n\n\t/** Appends a string to the track as its own single-frame group. */\n\twriteString(str: string) {\n\t\tconst group = this.appendGroup();\n\t\tgroup.writeString(str);\n\t\tgroup.close();\n\t}\n\n\t/** Appends a JSON value to the track as its own single-frame group. */\n\twriteJson(json: unknown) {\n\t\tconst group = this.appendGroup();\n\t\tgroup.writeJson(json);\n\t\tgroup.close();\n\t}\n\n\t/** Appends a boolean to the track as its own single-frame group. */\n\twriteBool(bool: boolean) {\n\t\tconst group = this.appendGroup();\n\t\tgroup.writeBool(bool);\n\t\tgroup.close();\n\t}\n\n\t/**\n\t * Receive the next group available on this track, in arrival order.\n\t *\n\t * Groups may arrive out of order or with gaps due to network conditions.\n\t * Use {@link nextGroupOrdered} if you need groups in sequence order,\n\t * skipping those that arrive too late.\n\t */\n\tasync recvGroup(): Promise<Group | undefined> {\n\t\tfor (;;) {\n\t\t\tconst groups = this.state.groups.peek();\n\t\t\tif (groups.length > 0) {\n\t\t\t\treturn groups.shift();\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) return undefined;\n\n\t\t\tawait Signal.race(this.state.groups, this.state.closed);\n\t\t}\n\t}\n\n\t/**\n\t * @deprecated Use {@link recvGroup} for arrival order, or {@link nextGroupOrdered} for sequence order.\n\t */\n\tasync nextGroup(): Promise<Group | undefined> {\n\t\treturn this.recvGroup();\n\t}\n\n\t/**\n\t * Return the next group with a strictly-greater sequence number than the last returned.\n\t *\n\t * Late arrivals (with a sequence number at or below the last one returned) are silently skipped.\n\t *\n\t * NOTE: This will be renamed to `nextGroup` in the next major version.\n\t */\n\tasync nextGroupOrdered(): Promise<Group | undefined> {\n\t\tfor (;;) {\n\t\t\tconst group = await this.recvGroup();\n\t\t\tif (!group) return undefined;\n\t\t\tif (group.sequence < this.#nextSequence) {\n\t\t\t\tgroup.close();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.#nextSequence = group.sequence + 1;\n\t\t\treturn group;\n\t\t}\n\t}\n\n\t/** Reads the next frame across all groups, discarding older groups. */\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 group and frame sequence numbers. */\n\tasync readFrameSequence(): Promise<{ group: number; frame: number; data: Uint8Array } | undefined> {\n\t\tfor (;;) {\n\t\t\tconst groups = this.state.groups.peek();\n\n\t\t\t// Discard old groups.\n\t\t\twhile (groups.length > 1) {\n\t\t\t\tconst next = groups[0].tryReadFrameSequence();\n\t\t\t\tif (next) {\n\t\t\t\t\treturn { group: groups[0].sequence, frame: next.sequence, data: next.data };\n\t\t\t\t}\n\n\t\t\t\t// Skip this old group\n\t\t\t\tgroups.shift()?.close();\n\t\t\t}\n\n\t\t\t// If there's no groups, wait for a new one.\n\t\t\tif (groups.length === 0) {\n\t\t\t\tconst closed = this.state.closed.peek();\n\t\t\t\tif (closed instanceof Error) throw closed;\n\t\t\t\tif (closed) return undefined;\n\n\t\t\t\tawait Signal.race(this.state.groups, this.state.closed);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If there's a group, wait for a frame.\n\t\t\tconst group = groups[0];\n\t\t\tconst next = group.tryReadFrameSequence();\n\t\t\tif (next) {\n\t\t\t\treturn { group: group.sequence, frame: next.sequence, data: next.data };\n\t\t\t}\n\n\t\t\t// If the track is closed, return undefined.\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\t// Wake on a new group or the track closing. Only fold in the current group's readability\n\t\t\t// while it can still deliver frames: a finished group's readable() resolves immediately, so\n\t\t\t// racing it would busy-loop until a newer group arrives instead of waiting.\n\t\t\tif (group.done) {\n\t\t\t\tawait Signal.race(this.state.groups, this.state.closed);\n\t\t\t} else {\n\t\t\t\tawait Promise.race([Signal.race(this.state.groups, this.state.closed), group.readable()]);\n\t\t\t}\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 next = await this.readFrame();\n\t\tif (!next) return undefined;\n\t\treturn new TextDecoder().decode(next);\n\t}\n\n\t/** Reads the next frame and parses it as JSON. */\n\tasync readJson(): Promise<unknown | undefined> {\n\t\tconst next = await this.readString();\n\t\tif (!next) return undefined;\n\t\treturn JSON.parse(next);\n\t}\n\n\t/** Reads the next frame and decodes it as a one-byte boolean, throwing on a malformed frame. */\n\tasync readBool(): Promise<boolean | undefined> {\n\t\tconst next = await this.readFrame();\n\t\tif (!next) return undefined;\n\t\tif (next.byteLength !== 1 || !(next[0] === 0 || next[0] === 1)) throw new Error(\"invalid bool frame\");\n\t\treturn next[0] === 1;\n\t}\n\n\t/**\n\t * Update the subscription priority. Triggers a SUBSCRIBE_UPDATE\n\t * to the publisher when used on a subscribed track.\n\t */\n\tupdatePriority(priority: number) {\n\t\tthis.state.priority.set(priority, true);\n\t}\n\n\t/**\n\t * Closes the publisher and all associated groups.\n\t */\n\tclose(abort?: Error) {\n\t\tthis.state.closed.set(abort ?? true);\n\n\t\tfor (const group of this.state.groups.peek()) {\n\t\t\tgroup.close(abort);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=track.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.test.d.ts","sourceRoot":"","sources":["../src/track.test.ts"],"names":[],"mappings":""}
package/track.test.js ADDED
@@ -0,0 +1,56 @@
1
+ /* @ts-self-types="./track.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { Group } from "./group.js";
4
+ import { Track } from "./track.js";
5
+ test("nextGroupOrdered skips late arrivals", async () => {
6
+ const track = new Track("test");
7
+ track.writeGroup(new Group(5));
8
+ const first = await track.nextGroupOrdered();
9
+ expect(first?.sequence).toBe(5);
10
+ // Late arrivals with sequence <= last returned are skipped.
11
+ track.writeGroup(new Group(3));
12
+ track.writeGroup(new Group(4));
13
+ track.writeGroup(new Group(7));
14
+ const next = await track.nextGroupOrdered();
15
+ expect(next?.sequence).toBe(7);
16
+ });
17
+ test("nextGroupOrdered returns buffered groups in sequence", async () => {
18
+ const track = new Track("test");
19
+ track.writeGroup(new Group(3));
20
+ track.writeGroup(new Group(5));
21
+ expect((await track.nextGroupOrdered())?.sequence).toBe(3);
22
+ expect((await track.nextGroupOrdered())?.sequence).toBe(5);
23
+ });
24
+ test("recvGroup after nextGroupOrdered still returns late arrivals", async () => {
25
+ const track = new Track("test");
26
+ track.writeGroup(new Group(5));
27
+ // Ordered returns seq 5, advancing its cursor.
28
+ const ordered = await track.nextGroupOrdered();
29
+ expect(ordered?.sequence).toBe(5);
30
+ // recvGroup is independent of the ordered cursor: a late seq 3 still surfaces.
31
+ track.writeGroup(new Group(3));
32
+ const recv = await track.recvGroup();
33
+ expect(recv?.sequence).toBe(3);
34
+ });
35
+ test("nextGroupOrdered returns undefined when track closes", async () => {
36
+ const track = new Track("test");
37
+ track.close();
38
+ expect(await track.nextGroupOrdered()).toBeUndefined();
39
+ });
40
+ test("readFrame does not livelock when a sole group finishes before the next arrives", async () => {
41
+ const track = new Track("test");
42
+ // A group is appended then finished empty while the track stays open. A finished group's
43
+ // readable() resolves immediately, so the reader must not busy-wait on it (which would starve the
44
+ // macrotask queue and never observe the next group).
45
+ const g0 = track.appendGroup();
46
+ g0.close();
47
+ // The next group arrives via a macrotask; if the reader livelocks on microtasks it never runs.
48
+ setTimeout(() => {
49
+ const g1 = track.appendGroup();
50
+ g1.writeString("hello");
51
+ g1.close();
52
+ track.close();
53
+ }, 10);
54
+ expect(await track.readString()).toBe("hello");
55
+ }, 2000);
56
+ //# sourceMappingURL=track.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.test.js","sourceRoot":"","sources":["../src/track.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Group } from \"./group.ts\";\nimport { Track } from \"./track.ts\";\n\ntest(\"nextGroupOrdered skips late arrivals\", async () => {\n\tconst track = new Track(\"test\");\n\n\ttrack.writeGroup(new Group(5));\n\n\tconst first = await track.nextGroupOrdered();\n\texpect(first?.sequence).toBe(5);\n\n\t// Late arrivals with sequence <= last returned are skipped.\n\ttrack.writeGroup(new Group(3));\n\ttrack.writeGroup(new Group(4));\n\ttrack.writeGroup(new Group(7));\n\n\tconst next = await track.nextGroupOrdered();\n\texpect(next?.sequence).toBe(7);\n});\n\ntest(\"nextGroupOrdered returns buffered groups in sequence\", async () => {\n\tconst track = new Track(\"test\");\n\n\ttrack.writeGroup(new Group(3));\n\ttrack.writeGroup(new Group(5));\n\n\texpect((await track.nextGroupOrdered())?.sequence).toBe(3);\n\texpect((await track.nextGroupOrdered())?.sequence).toBe(5);\n});\n\ntest(\"recvGroup after nextGroupOrdered still returns late arrivals\", async () => {\n\tconst track = new Track(\"test\");\n\n\ttrack.writeGroup(new Group(5));\n\n\t// Ordered returns seq 5, advancing its cursor.\n\tconst ordered = await track.nextGroupOrdered();\n\texpect(ordered?.sequence).toBe(5);\n\n\t// recvGroup is independent of the ordered cursor: a late seq 3 still surfaces.\n\ttrack.writeGroup(new Group(3));\n\tconst recv = await track.recvGroup();\n\texpect(recv?.sequence).toBe(3);\n});\n\ntest(\"nextGroupOrdered returns undefined when track closes\", async () => {\n\tconst track = new Track(\"test\");\n\ttrack.close();\n\texpect(await track.nextGroupOrdered()).toBeUndefined();\n});\n\ntest(\"readFrame does not livelock when a sole group finishes before the next arrives\", async () => {\n\tconst track = new Track(\"test\");\n\n\t// A group is appended then finished empty while the track stays open. A finished group's\n\t// readable() resolves immediately, so the reader must not busy-wait on it (which would starve the\n\t// macrotask queue and never observe the next group).\n\tconst g0 = track.appendGroup();\n\tg0.close();\n\n\t// The next group arrives via a macrotask; if the reader livelocks on microtasks it never runs.\n\tsetTimeout(() => {\n\t\tconst g1 = track.appendGroup();\n\t\tg1.writeString(\"hello\");\n\t\tg1.close();\n\t\ttrack.close();\n\t}, 10);\n\n\texpect(await track.readString()).toBe(\"hello\");\n}, 2000);\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function error(err: unknown): Error;
2
+ export declare function unreachable(value: never): never;
3
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/util/error.ts"],"names":[],"mappings":"AACA,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAEzC;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAE/C"}
package/util/error.js ADDED
@@ -0,0 +1,9 @@
1
+ /* @ts-self-types="./error.d.ts" */
2
+ // I hate javascript.
3
+ export function error(err) {
4
+ return err instanceof Error ? err : new Error(String(err));
5
+ }
6
+ export function unreachable(value) {
7
+ throw new Error(`unreachable: ${value}`);
8
+ }
9
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/util/error.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,MAAM,UAAU,KAAK,CAAC,GAAY;IACjC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAY;IACvC,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,EAAE,CAAC,CAAC;AAC1C,CAAC","sourcesContent":["// I hate javascript.\nexport function error(err: unknown): Error {\n\treturn err instanceof Error ? err : new Error(String(err));\n}\n\nexport function unreachable(value: never): never {\n\tthrow new Error(`unreachable: ${value}`);\n}\n"]}
package/util/hex.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function toBytes(hex: string): Uint8Array<ArrayBuffer>;
2
+ export declare function fromBytes(bytes: Uint8Array): string;
3
+ //# sourceMappingURL=hex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hex.d.ts","sourceRoot":"","sources":["../../src/util/hex.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAY5D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEnD"}
package/util/hex.js ADDED
@@ -0,0 +1,16 @@
1
+ /* @ts-self-types="./hex.d.ts" */
2
+ export function toBytes(hex) {
3
+ hex = hex.startsWith("0x") ? hex.slice(2) : hex;
4
+ if (hex.length % 2) {
5
+ throw new Error("invalid hex string length");
6
+ }
7
+ const matches = hex.match(/.{2}/g);
8
+ if (!matches) {
9
+ throw new Error("invalid hex string format");
10
+ }
11
+ return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
12
+ }
13
+ export function fromBytes(bytes) {
14
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ //# sourceMappingURL=hex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hex.js","sourceRoot":"","sources":["../../src/util/hex.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,OAAO,CAAC,GAAW;IAClC,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAChD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAiB;IAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACjF,CAAC","sourcesContent":["export function toBytes(hex: string): Uint8Array<ArrayBuffer> {\n\thex = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n\tif (hex.length % 2) {\n\t\tthrow new Error(\"invalid hex string length\");\n\t}\n\n\tconst matches = hex.match(/.{2}/g);\n\tif (!matches) {\n\t\tthrow new Error(\"invalid hex string format\");\n\t}\n\n\treturn new Uint8Array(matches.map((byte) => parseInt(byte, 16)));\n}\n\nexport function fromBytes(bytes: Uint8Array): string {\n\treturn Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export * from "./error";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/util/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
package/util/index.js ADDED
@@ -0,0 +1,3 @@
1
+ /* @ts-self-types="./index.d.ts" */
2
+ export * from "./error.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/util/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./error.ts\";\n"]}
package/varint.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /** Largest value that fits in a 1-byte varint (6 bits). */
2
+ export declare const MAX_U6: number;
3
+ /** Largest value that fits in a 2-byte varint (14 bits). */
4
+ export declare const MAX_U14: number;
5
+ /** Largest value that fits in a 4-byte varint (30 bits). */
6
+ export declare const MAX_U30: number;
7
+ /** Largest value representable without precision loss (`Number.MAX_SAFE_INTEGER`, 53 bits). */
8
+ export declare const MAX_U53: number;
9
+ /** Number of bytes needed to encode a value in the leading-ones varint format. */
10
+ export declare function sizeLeadingOnes(v: number | bigint): number;
11
+ /** Encode a value in leading-ones varint format into the provided buffer, returning the written subarray. */
12
+ export declare function encodeLeadingOnesTo(dst: ArrayBuffer, v: number | bigint): Uint8Array;
13
+ /** Encode a value in leading-ones varint format into a freshly allocated buffer. */
14
+ export declare function encodeLeadingOnes(v: number | bigint): Uint8Array;
15
+ /** Decode a leading-ones varint, returning the value and the remaining buffer. */
16
+ export declare function decodeLeadingOnes(buf: Uint8Array): [bigint, Uint8Array];
17
+ /**
18
+ * Returns the number of bytes needed to encode a value as a varint.
19
+ */
20
+ export declare function size(v: number): number;
21
+ /**
22
+ * Encodes a number or bigint into a scratch buffer.
23
+ * Used by stream.ts to avoid allocations.
24
+ */
25
+ export declare function encodeTo(dst: ArrayBuffer, v: number | bigint): Uint8Array;
26
+ /**
27
+ * Encodes a number as a QUIC variable-length integer.
28
+ * Returns a new Uint8Array containing the encoded bytes.
29
+ */
30
+ export declare function encode(v: number): Uint8Array;
31
+ /**
32
+ * Decodes a QUIC variable-length integer from a buffer.
33
+ * Returns a tuple of [value, remaining buffer].
34
+ */
35
+ export declare function decode(buf: Uint8Array): [number, Uint8Array];
36
+ //# sourceMappingURL=varint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"varint.d.ts","sourceRoot":"","sources":["../src/varint.ts"],"names":[],"mappings":"AAGA,2DAA2D;AAC3D,eAAO,MAAM,MAAM,QAAa,CAAC;AACjC,4DAA4D;AAC5D,eAAO,MAAM,OAAO,QAAc,CAAC;AACnC,4DAA4D;AAC5D,eAAO,MAAM,OAAO,QAAc,CAAC;AACnC,+FAA+F;AAC/F,eAAO,MAAM,OAAO,QAA0B,CAAC;AAgB/C,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAY1D;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAkDpF;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAEhE;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAmFvE;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMtC;AA6BD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAmBzE;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAE5C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CA8B5D"}