@moqtap/codec 0.10.0 → 0.11.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 (213) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/auth-redaction-DsdLq4Qs.d.cts +102 -0
  3. package/dist/auth-redaction-DsdLq4Qs.d.ts +102 -0
  4. package/dist/{chunk-RYKQXBG6.cjs → chunk-27K5BUAG.cjs} +481 -517
  5. package/dist/{chunk-WCPENYNE.js → chunk-3RNQMGKA.js} +359 -390
  6. package/dist/{chunk-TSUZ6GJV.js → chunk-5AWIXOLB.js} +438 -469
  7. package/dist/{chunk-4XLYCT5Q.cjs → chunk-5IMHJIAZ.cjs} +52 -2
  8. package/dist/{chunk-KTNERCTZ.cjs → chunk-5JGZCKJO.cjs} +488 -518
  9. package/dist/{chunk-WCTMPDUX.cjs → chunk-73K4OSAN.cjs} +401 -432
  10. package/dist/{chunk-N2V7Z37J.js → chunk-7VQKRGRD.js} +19 -3
  11. package/dist/{chunk-BIBMDTU5.cjs → chunk-A3UCQOIB.cjs} +486 -517
  12. package/dist/{chunk-K2B3OVFK.js → chunk-A7PDMVXM.js} +24 -3
  13. package/dist/{chunk-VZHL2U2N.cjs → chunk-A7VQO6YC.cjs} +54 -38
  14. package/dist/{chunk-AQ445VV7.cjs → chunk-ACQHZLRZ.cjs} +66 -46
  15. package/dist/{chunk-L5DRACXS.js → chunk-BBICOMW6.js} +25 -3
  16. package/dist/{chunk-T4VWPKDG.cjs → chunk-BNIUVOCJ.cjs} +529 -599
  17. package/dist/{chunk-UKKDOOUO.cjs → chunk-CNC6ANS4.cjs} +58 -37
  18. package/dist/{chunk-OZRK73NQ.js → chunk-FDLQSWHK.js} +433 -469
  19. package/dist/{chunk-IJAF6PAY.js → chunk-FFNOE4UR.js} +19 -3
  20. package/dist/{chunk-CZNW3R5E.cjs → chunk-GMENT63T.cjs} +54 -38
  21. package/dist/{chunk-XCRCAUGI.cjs → chunk-HM7BLRVP.cjs} +291 -297
  22. package/dist/{chunk-XHAP2YST.js → chunk-HZONHFNO.js} +19 -3
  23. package/dist/{chunk-SDJTSMOW.cjs → chunk-ILYABWDK.cjs} +60 -38
  24. package/dist/{chunk-IDV2JYJ6.cjs → chunk-KRSFTZVO.cjs} +56 -40
  25. package/dist/{chunk-RIIA4ALQ.cjs → chunk-KSPH6KMH.cjs} +54 -38
  26. package/dist/{chunk-OUNN4IQB.cjs → chunk-KTY4LAUV.cjs} +482 -497
  27. package/dist/{chunk-RZTLQAKF.js → chunk-OJNLJT5D.js} +0 -1
  28. package/dist/{chunk-FT3HDNIB.js → chunk-PJK2ZS3I.js} +467 -537
  29. package/dist/{chunk-G53WYUXL.js → chunk-PSMZKHOC.js} +441 -456
  30. package/dist/{chunk-7RE6KSIY.cjs → chunk-Q5JQGHWY.cjs} +0 -1
  31. package/dist/{chunk-BX2N7TZM.js → chunk-SQTUQY5E.js} +31 -11
  32. package/dist/{chunk-CF5DDD3Q.js → chunk-WCRLAKDW.js} +239 -245
  33. package/dist/{chunk-KYVTM5VI.js → chunk-WL2YUEXH.js} +19 -3
  34. package/dist/{chunk-KGDVC4O6.js → chunk-YCNIVZS7.js} +439 -469
  35. package/dist/{chunk-KLPWJU6W.js → chunk-YRZQVNZO.js} +50 -0
  36. package/dist/{codec-CFWu5KVT.d.cts → codec-76xzrLTL.d.cts} +21 -2
  37. package/dist/{codec-gl4_QEva.d.ts → codec-8p40WdLg.d.ts} +20 -1
  38. package/dist/{codec-Dl_QIXMz.d.ts → codec-BN2Bu0KI.d.ts} +20 -1
  39. package/dist/{codec-Bux_Q40X.d.ts → codec-Bb6GKTrv.d.ts} +21 -2
  40. package/dist/{codec-EvybrBuY.d.cts → codec-BgpfHoAd.d.cts} +20 -1
  41. package/dist/{codec-alVA27Uc.d.ts → codec-BkheTXO7.d.ts} +20 -1
  42. package/dist/{codec-Czejcmxr.d.cts → codec-BwVDkokh.d.cts} +21 -2
  43. package/dist/{codec-BrTi4K7r.d.cts → codec-C5s1eBJX.d.cts} +21 -2
  44. package/dist/codec-CM5yd6n3.d.ts +38 -0
  45. package/dist/{codec-CWnf8mn_.d.cts → codec-Cb8HC3XV.d.cts} +21 -2
  46. package/dist/codec-Clu_skx8.d.cts +38 -0
  47. package/dist/{codec-DzkdW4kO.d.cts → codec-CpOpemR8.d.cts} +20 -1
  48. package/dist/{codec-CXCjya_G.d.cts → codec-Cshdl9Tt.d.cts} +20 -1
  49. package/dist/{codec-f7vxeVIS.d.cts → codec-DELe00w_.d.cts} +21 -2
  50. package/dist/{codec-DNmiG2e0.d.cts → codec-DHzKAUCa.d.cts} +20 -1
  51. package/dist/{codec--CVeqGQv.d.ts → codec-DdVBtvx7.d.ts} +20 -1
  52. package/dist/{codec-CBdOIfMY.d.ts → codec-DlxGuYMv.d.ts} +21 -2
  53. package/dist/{codec-BNy5syCU.d.ts → codec-DxtgPxJS.d.ts} +20 -1
  54. package/dist/{codec-guzqtfMk.d.cts → codec-GAY4lybi.d.cts} +20 -1
  55. package/dist/{codec-Cfo_z5ET.d.cts → codec-I8u2pfS4.d.cts} +20 -1
  56. package/dist/{codec-BxQTybUX.d.cts → codec-XLrnreeu.d.cts} +21 -2
  57. package/dist/{codec-DOmAmFkI.d.ts → codec-c0YEwwWn.d.ts} +20 -1
  58. package/dist/{codec-DaXjRrOV.d.ts → codec-ejDu-va6.d.ts} +21 -2
  59. package/dist/{codec-C3mdsGx3.d.ts → codec-kaBUzdpV.d.ts} +21 -2
  60. package/dist/{codec-CwZ6OCPP.d.ts → codec-mVYqkIUF.d.ts} +21 -2
  61. package/dist/{codec-08ZOr5wI.d.ts → codec-qSbO5nOa.d.ts} +21 -2
  62. package/dist/{codec-Cm3D261p.d.ts → codec-yYKGOipj.d.ts} +20 -1
  63. package/dist/{codec-Bd60SGKH.d.cts → codec-zfzg9uiy.d.cts} +20 -1
  64. package/dist/draft07.cjs +48 -4
  65. package/dist/draft07.d.cts +42 -2
  66. package/dist/draft07.d.ts +42 -2
  67. package/dist/draft07.js +49 -5
  68. package/dist/draft08.cjs +76 -3
  69. package/dist/draft08.d.cts +70 -2
  70. package/dist/draft08.d.ts +70 -2
  71. package/dist/draft08.js +77 -4
  72. package/dist/draft09.cjs +76 -3
  73. package/dist/draft09.d.cts +70 -2
  74. package/dist/draft09.d.ts +70 -2
  75. package/dist/draft09.js +77 -4
  76. package/dist/draft10.cjs +76 -3
  77. package/dist/draft10.d.cts +70 -2
  78. package/dist/draft10.d.ts +70 -2
  79. package/dist/draft10.js +77 -4
  80. package/dist/draft11.cjs +104 -3
  81. package/dist/draft11.d.cts +98 -2
  82. package/dist/draft11.d.ts +98 -2
  83. package/dist/draft11.js +105 -4
  84. package/dist/draft12.cjs +113 -3
  85. package/dist/draft12.d.cts +107 -2
  86. package/dist/draft12.d.ts +107 -2
  87. package/dist/draft12.js +114 -4
  88. package/dist/draft13.cjs +105 -3
  89. package/dist/draft13.d.cts +99 -2
  90. package/dist/draft13.d.ts +99 -2
  91. package/dist/draft13.js +106 -4
  92. package/dist/draft14.cjs +107 -3
  93. package/dist/draft14.d.cts +101 -28
  94. package/dist/draft14.d.ts +101 -28
  95. package/dist/draft14.js +108 -4
  96. package/dist/draft15-session.d.cts +1 -1
  97. package/dist/draft15-session.d.ts +1 -1
  98. package/dist/draft15.cjs +69 -3
  99. package/dist/draft15.d.cts +65 -22
  100. package/dist/draft15.d.ts +65 -22
  101. package/dist/draft15.js +70 -4
  102. package/dist/draft16-session.cjs +2 -2
  103. package/dist/draft16-session.d.cts +1 -1
  104. package/dist/draft16-session.d.ts +1 -1
  105. package/dist/draft16-session.js +1 -1
  106. package/dist/draft16.cjs +76 -8
  107. package/dist/draft16.d.cts +67 -13
  108. package/dist/draft16.d.ts +67 -13
  109. package/dist/draft16.js +73 -5
  110. package/dist/draft17-session.d.cts +1 -1
  111. package/dist/draft17-session.d.ts +1 -1
  112. package/dist/draft17.cjs +77 -3
  113. package/dist/draft17.d.cts +73 -4
  114. package/dist/draft17.d.ts +73 -4
  115. package/dist/draft17.js +78 -4
  116. package/dist/draft18-session.d.cts +1 -1
  117. package/dist/draft18-session.d.ts +1 -1
  118. package/dist/draft18.cjs +8 -3
  119. package/dist/draft18.d.cts +4 -3
  120. package/dist/draft18.d.ts +4 -3
  121. package/dist/draft18.js +9 -4
  122. package/dist/draft19-session.d.cts +1 -1
  123. package/dist/draft19-session.d.ts +1 -1
  124. package/dist/draft19.cjs +11 -4
  125. package/dist/draft19.d.cts +4 -3
  126. package/dist/draft19.d.ts +4 -3
  127. package/dist/draft19.js +12 -5
  128. package/dist/draft20-session.d.cts +1 -1
  129. package/dist/draft20-session.d.ts +1 -1
  130. package/dist/draft20.cjs +8 -3
  131. package/dist/draft20.d.cts +4 -3
  132. package/dist/draft20.d.ts +4 -3
  133. package/dist/draft20.js +9 -4
  134. package/dist/index.cjs +30 -30
  135. package/dist/index.d.cts +21 -20
  136. package/dist/index.d.ts +21 -20
  137. package/dist/index.js +15 -15
  138. package/dist/session.cjs +2 -2
  139. package/dist/session.d.cts +6 -6
  140. package/dist/session.d.ts +6 -6
  141. package/dist/session.js +1 -1
  142. package/dist/{types-C1rxh-id.d.cts → types-Btp416jt.d.cts} +8 -0
  143. package/dist/{types-C1rxh-id.d.ts → types-Btp416jt.d.ts} +8 -0
  144. package/dist/{types-CZTzbIzQ.d.cts → types-BxTL1NXw.d.cts} +7 -0
  145. package/dist/{types-CZTzbIzQ.d.ts → types-BxTL1NXw.d.ts} +7 -0
  146. package/dist/{types-pShrs3fN.d.cts → types-CuibHYcf.d.cts} +7 -0
  147. package/dist/{types-pShrs3fN.d.ts → types-CuibHYcf.d.ts} +7 -0
  148. package/dist/{types-CY-QWxd-.d.cts → types-CwN7AEoL.d.cts} +13 -1
  149. package/dist/{types-CY-QWxd-.d.ts → types-CwN7AEoL.d.ts} +13 -1
  150. package/dist/{types-D4bmW8rY.d.cts → types-DAnalt5K.d.cts} +7 -0
  151. package/dist/{types-D4bmW8rY.d.ts → types-DAnalt5K.d.ts} +7 -0
  152. package/dist/{types-Bg86GGVZ.d.cts → types-DxXNP32l.d.cts} +8 -0
  153. package/dist/{types-Bg86GGVZ.d.ts → types-DxXNP32l.d.ts} +8 -0
  154. package/package.json +3 -3
  155. package/src/core/auth-redaction.ts +192 -0
  156. package/src/drafts/draft07/codec.ts +40 -0
  157. package/src/drafts/draft07/error-codes.ts +44 -0
  158. package/src/drafts/draft07/index.ts +15 -0
  159. package/src/drafts/draft08/codec.ts +40 -0
  160. package/src/drafts/draft08/error-codes.ts +76 -0
  161. package/src/drafts/draft08/index.ts +21 -0
  162. package/src/drafts/draft09/codec.ts +40 -0
  163. package/src/drafts/draft09/error-codes.ts +76 -0
  164. package/src/drafts/draft09/index.ts +21 -0
  165. package/src/drafts/draft10/codec.ts +40 -0
  166. package/src/drafts/draft10/error-codes.ts +76 -0
  167. package/src/drafts/draft10/index.ts +21 -0
  168. package/src/drafts/draft11/codec.ts +60 -0
  169. package/src/drafts/draft11/error-codes.ts +106 -0
  170. package/src/drafts/draft11/index.ts +23 -0
  171. package/src/drafts/draft12/codec.ts +61 -0
  172. package/src/drafts/draft12/error-codes.ts +116 -0
  173. package/src/drafts/draft12/index.ts +25 -0
  174. package/src/drafts/draft13/codec.ts +69 -11
  175. package/src/drafts/draft13/error-codes.ts +107 -0
  176. package/src/drafts/draft13/index.ts +23 -0
  177. package/src/drafts/draft13/types.ts +1 -1
  178. package/src/drafts/draft14/codec.ts +65 -4
  179. package/src/drafts/draft14/data-streams.ts +331 -259
  180. package/src/drafts/draft14/error-codes.ts +109 -0
  181. package/src/drafts/draft14/index.ts +23 -0
  182. package/src/drafts/draft15/codec.ts +65 -8
  183. package/src/drafts/draft15/data-streams.ts +489 -444
  184. package/src/drafts/draft15/error-codes.ts +66 -0
  185. package/src/drafts/draft15/index.ts +15 -0
  186. package/src/drafts/draft15/types.ts +7 -0
  187. package/src/drafts/draft16/codec.ts +146 -52
  188. package/src/drafts/draft16/data-streams.ts +513 -442
  189. package/src/drafts/draft16/error-codes.ts +68 -0
  190. package/src/drafts/draft16/index.ts +15 -0
  191. package/src/drafts/draft16/messages.ts +4 -4
  192. package/src/drafts/draft16/session-fsm.ts +0 -1
  193. package/src/drafts/draft16/types.ts +22 -8
  194. package/src/drafts/draft17/codec.ts +73 -12
  195. package/src/drafts/draft17/data-streams.ts +567 -488
  196. package/src/drafts/draft17/index.ts +15 -0
  197. package/src/drafts/draft17/types.ts +7 -0
  198. package/src/drafts/draft18/codec.ts +66 -9
  199. package/src/drafts/draft18/data-streams.ts +570 -482
  200. package/src/drafts/draft18/index.ts +3 -0
  201. package/src/drafts/draft18/types.ts +16 -8
  202. package/src/drafts/draft19/codec.ts +70 -10
  203. package/src/drafts/draft19/data-streams.ts +570 -482
  204. package/src/drafts/draft19/error-codes.ts +3 -1
  205. package/src/drafts/draft19/index.ts +3 -0
  206. package/src/drafts/draft19/messages.ts +3 -1
  207. package/src/drafts/draft19/types.ts +22 -11
  208. package/src/drafts/draft20/codec.ts +90 -23
  209. package/src/drafts/draft20/data-streams.ts +610 -563
  210. package/src/drafts/draft20/index.ts +3 -0
  211. package/src/drafts/draft20/types.ts +14 -2
  212. package/dist/codec-BJ7BHuiZ.d.ts +0 -18
  213. package/dist/codec-C6Mx4bwz.d.cts +0 -18
@@ -216,107 +216,163 @@ export function encodeFetchStream(stream: FetchStream): Uint8Array {
216
216
  return w.finish()
217
217
  }
218
218
 
219
- export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
220
- try {
221
- const r = new BufferReader(bytes)
222
- const streamType = Number(r.readVarInt())
219
+ /**
220
+ * The SUBGROUP_HEADER fields, read once for both decoders.
221
+ *
222
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` used to parse this
223
+ * header separately and they drifted: the incremental one dropped the header
224
+ * flags, reported no Type Flags, and left the Subgroup ID at zero under the
225
+ * mode that derives it from the first Object. One reader means there is
226
+ * nothing left to keep in sync.
227
+ */
228
+ interface SubgroupHeaderFields {
229
+ readonly streamType: number
230
+ readonly propertiesPresent: boolean
231
+ readonly subgroupIsFirstObjId: boolean
232
+ readonly endOfGroup: boolean
233
+ readonly trackAlias: bigint
234
+ readonly groupId: bigint
235
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
236
+ readonly subgroupId: bigint
237
+ readonly publisherPriority: number
238
+ }
223
239
 
224
- if (
225
- !((streamType >= 0x10 && streamType <= 0x1d) || (streamType >= 0x30 && streamType <= 0x3d)) ||
226
- (streamType & 0x06) === 0x06 // reserved SUBGROUP_ID_MODE (bits 2:1 = 0b11)
227
- ) {
228
- return {
229
- ok: false,
230
- error: new DecodeError(
231
- 'CONSTRAINT_VIOLATION',
232
- `Expected subgroup stream type 0x10-0x15/0x18-0x1D/0x30-0x35/0x38-0x3D, got 0x${streamType.toString(16)}`,
233
- 0,
234
- ),
235
- }
236
- }
240
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
241
+ const streamType = Number(r.readVarInt())
242
+ if (
243
+ !((streamType >= 0x10 && streamType <= 0x1d) || (streamType >= 0x30 && streamType <= 0x3d)) ||
244
+ (streamType & 0x06) === 0x06 // reserved SUBGROUP_ID_MODE (bits 2:1 = 0b11)
245
+ ) {
246
+ throw new DecodeError(
247
+ 'CONSTRAINT_VIOLATION',
248
+ `Expected subgroup stream type 0x10-0x15/0x18-0x1D/0x30-0x35/0x38-0x3D, got 0x${streamType.toString(16)}`,
249
+ 0,
250
+ )
251
+ }
237
252
 
238
- const propertiesPresent = (streamType & 0x01) !== 0
239
- const hasSubgroupField = (streamType & 0x04) !== 0
240
- const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
241
- const hasPriority = streamType < 0x30
242
- const endOfGroup = (streamType & 0x08) !== 0
253
+ const propertiesPresent = (streamType & 0x01) !== 0
254
+ const hasSubgroupField = (streamType & 0x04) !== 0
255
+ const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
256
+ const hasPriority = streamType < 0x30
257
+ const endOfGroup = (streamType & 0x08) !== 0
243
258
 
244
- const trackAlias = r.readVarInt()
245
- const groupId = r.readVarInt()
259
+ const trackAlias = r.readVarInt()
260
+ const groupId = r.readVarInt()
246
261
 
247
- let subgroupId = 0n
248
- if (hasSubgroupField) {
249
- subgroupId = r.readVarInt()
250
- }
262
+ let subgroupId = 0n
263
+ if (hasSubgroupField) {
264
+ subgroupId = r.readVarInt()
265
+ }
251
266
 
252
- let publisherPriority = 128
253
- if (hasPriority) {
254
- publisherPriority = r.readUint8()
267
+ let publisherPriority = 128
268
+ if (hasPriority) {
269
+ publisherPriority = r.readUint8()
270
+ }
271
+
272
+ return {
273
+ streamType,
274
+ propertiesPresent,
275
+ subgroupIsFirstObjId,
276
+ endOfGroup,
277
+ trackAlias,
278
+ groupId,
279
+ subgroupId,
280
+ publisherPriority,
281
+ }
282
+ }
283
+
284
+ /**
285
+ * One Object off a subgroup stream, for both decoders.
286
+ *
287
+ * `base` is the absolute offset within the stream that `r.offset === 0`
288
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
289
+ * stream and passes 0; the incremental decoder reads from a window over its
290
+ * own buffer and passes that window's position. That is what makes
291
+ * `byteOffset` mean the same thing on both paths, instead of being hardcoded
292
+ * to zero on one of them.
293
+ *
294
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
295
+ * incremental decoder reads as "wait for more bytes" rather than as an error.
296
+ */
297
+ function readSubgroupObject(
298
+ r: BufferReader,
299
+ propertiesPresent: boolean,
300
+ isFirst: boolean,
301
+ prevObjectId: bigint,
302
+ base: number,
303
+ ): ObjectPayload {
304
+ const byteOffset = base + r.offset
305
+ const delta = r.readVarInt()
306
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
307
+
308
+ const extensionData = new Uint8Array(0)
309
+ let objectProperties: Record<string, bigint> | undefined
310
+ if (propertiesPresent) {
311
+ const propsLen = Number(r.readVarInt())
312
+ if (propsLen > 0) {
313
+ objectProperties = decodeObjectProperties(r, propsLen)
255
314
  }
315
+ }
316
+
317
+ const payloadLength = Number(r.readVarInt())
318
+ let payload: Uint8Array
319
+ let status: bigint | undefined
320
+ let payloadByteOffset: number
321
+ if (payloadLength === 0) {
322
+ // The Object Status field is sent only when the Payload Length is zero.
323
+ // Skipping it leaves the varint to be read as the next Object's Object ID
324
+ // Delta, which desynchronises the rest of the stream silently.
325
+ status = r.readVarInt()
326
+ payloadByteOffset = base + r.offset
327
+ payload = new Uint8Array(0)
328
+ } else {
329
+ payloadByteOffset = base + r.offset
330
+ payload = r.readBytesView(payloadLength)
331
+ }
332
+
333
+ const obj: ObjectPayload = {
334
+ type: 'object',
335
+ byteOffset,
336
+ payloadByteOffset,
337
+ objectId,
338
+ payloadLength,
339
+ payload,
340
+ extensionData,
341
+ }
342
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
343
+ if (objectProperties !== undefined)
344
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
345
+ return obj
346
+ }
347
+
348
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
349
+ try {
350
+ const r = new BufferReader(bytes)
351
+ const h = readSubgroupHeader(r)
256
352
 
257
353
  const objects: ObjectPayload[] = []
258
354
  let prevObjectId = -1n
259
- let firstObject = true
355
+ let isFirst = true
356
+ let subgroupId = h.subgroupId
260
357
 
261
358
  while (r.remaining > 0) {
262
- const byteOffset = r.offset
263
- const delta = r.readVarInt()
264
- let objectId: bigint
265
- if (firstObject) {
266
- objectId = delta
267
- if (subgroupIsFirstObjId && firstObject) {
268
- subgroupId = objectId
269
- }
270
- firstObject = false
271
- } else {
272
- objectId = prevObjectId + 1n + delta
273
- }
274
- const extensionData = new Uint8Array(0)
275
- let objectProperties: Record<string, bigint> | undefined
276
- if (propertiesPresent) {
277
- const propsLen = Number(r.readVarInt())
278
- if (propsLen > 0) {
279
- objectProperties = decodeObjectProperties(r, propsLen)
280
- }
281
- }
282
- const payloadLength = Number(r.readVarInt())
283
- let payload: Uint8Array
284
- let status: bigint | undefined
285
- let payloadByteOffset: number
286
- if (payloadLength === 0) {
287
- status = r.readVarInt()
288
- payloadByteOffset = r.offset
289
- payload = new Uint8Array(0)
290
- } else {
291
- payloadByteOffset = r.offset
292
- payload = r.readBytesView(payloadLength)
293
- }
294
- const obj: ObjectPayload = {
295
- type: 'object',
296
- byteOffset,
297
- payloadByteOffset,
298
- objectId,
299
- payloadLength,
300
- payload,
301
- extensionData,
302
- }
303
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
304
- if (objectProperties !== undefined)
305
- (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
359
+ const obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, 0)
360
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
361
+ isFirst = false
306
362
  objects.push(obj)
307
- prevObjectId = objectId
363
+ prevObjectId = obj.objectId
308
364
  }
309
365
 
310
366
  const result: SubgroupStream = {
311
367
  type: 'subgroup',
312
- headerType: streamType,
313
- trackAlias,
314
- groupId,
368
+ headerType: h.streamType,
369
+ trackAlias: h.trackAlias,
370
+ groupId: h.groupId,
315
371
  subgroupId,
316
- publisherPriority,
372
+ publisherPriority: h.publisherPriority,
317
373
  objects,
374
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
318
375
  }
319
- if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
320
376
 
321
377
  return {
322
378
  ok: true,
@@ -394,212 +450,175 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
394
450
  }
395
451
  }
396
452
 
397
- export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
398
- try {
399
- const r = new BufferReader(bytes)
400
- const streamType = r.readVarInt()
401
- if (streamType !== FETCH_STREAM_TYPE) {
402
- return {
403
- ok: false,
404
- error: new DecodeError(
405
- 'CONSTRAINT_VIOLATION',
406
- `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
407
- 0,
408
- ),
409
- }
410
- }
411
- const requestId = r.readVarInt()
412
- const objects: FetchObjectPayload[] = []
453
+ /** The delta state a fetch stream carries from one Object to the next. */
454
+ interface FetchObjectState {
455
+ prevGroupId: bigint
456
+ prevSubgroupId: bigint
457
+ prevObjectId: bigint
458
+ prevPriority: number
459
+ first: boolean
460
+ }
413
461
 
414
- let prevGroupId = 0n
415
- let prevSubgroupId = 0n
416
- let prevObjectId = 0n
417
- let prevPriority = 128
418
- let first = true
462
+ function newFetchObjectState(): FetchObjectState {
463
+ return { prevGroupId: 0n, prevSubgroupId: 0n, prevObjectId: 0n, prevPriority: 128, first: true }
464
+ }
419
465
 
420
- while (r.remaining > 0) {
421
- const byteOffset = r.offset
422
- const flags = Number(r.readVarInt())
423
-
424
- let groupId = prevGroupId
425
- let subgroupId = prevSubgroupId
426
- let objectId = prevObjectId + 1n
427
- let payloadLength: number
428
- let payload: Uint8Array
429
- let payloadByteOffset: number
430
- const extensionData = new Uint8Array(0)
431
-
432
- if (flags >= 0x80) {
433
- // End of Range: 0x8C = End of Non-Existent Range, 0x10C = End of Unknown Range
434
- // Per spec §10.4.4.2: Group ID and Object ID are present;
435
- // Subgroup ID, Priority, Properties are not present.
436
- if (flags !== 0x8c && flags !== 0x10c) {
437
- return {
438
- ok: false,
439
- error: new DecodeError(
440
- 'CONSTRAINT_VIOLATION',
441
- `Unknown serialization flags value: 0x${flags.toString(16)}`,
442
- r.offset,
443
- ),
444
- }
445
- }
446
- groupId = r.readVarInt()
447
- objectId = r.readVarInt()
448
- payloadLength = Number(r.readVarInt())
449
- payloadByteOffset = r.offset
450
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
451
- } else if (flags & 0x40) {
452
- // DATAGRAM mode: no subgroup_id field
453
- const objectIdPresent = (flags & 0x04) !== 0
454
- const groupIdPresent = (flags & 0x08) !== 0
455
- const priorityPresent = (flags & 0x10) !== 0
456
- const propsPresent = (flags & 0x20) !== 0
457
-
458
- if (groupIdPresent) {
459
- groupId = r.readVarInt()
460
- }
461
- // No subgroup in datagram mode
462
- if (objectIdPresent) {
463
- objectId = r.readVarInt()
464
- }
465
- if (priorityPresent) {
466
- prevPriority = r.readUint8()
467
- }
468
- let objectProperties: Record<string, bigint> | undefined
469
- if (propsPresent) {
470
- const propsLen = Number(r.readVarInt())
471
- if (propsLen > 0) {
472
- objectProperties = decodeObjectProperties(r, propsLen)
473
- }
474
- }
475
- payloadLength = Number(r.readVarInt())
476
- payloadByteOffset = r.offset
477
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
478
-
479
- const obj: FetchObjectPayload = {
480
- type: 'object',
481
- byteOffset,
482
- payloadByteOffset,
483
- serializationFlags: flags,
484
- groupId,
485
- subgroupId: 0n,
486
- objectId,
487
- publisherPriority: prevPriority,
488
- payloadLength,
489
- payload,
490
- extensionData,
491
- }
492
- if (objectProperties)
493
- (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
494
- objects.push(obj)
495
-
496
- prevGroupId = groupId
497
- prevObjectId = objectId
498
- first = false
499
- continue
500
- } else {
501
- const subgroupEncoding = flags & 0x03
502
- const objectIdPresent = (flags & 0x04) !== 0
503
- const groupIdPresent = (flags & 0x08) !== 0
504
- const priorityPresent = (flags & 0x10) !== 0
505
- const propsPresent = (flags & 0x20) !== 0
506
-
507
- if (groupIdPresent) {
508
- groupId = r.readVarInt()
509
- } else if (first) {
510
- return {
511
- ok: false,
512
- error: new DecodeError(
513
- 'CONSTRAINT_VIOLATION',
514
- 'First fetch object must include groupId',
515
- r.offset,
516
- ),
517
- }
518
- }
466
+ /**
467
+ * One Object off a fetch stream, for both decoders.
468
+ *
469
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
470
+ * cannot report a different shape for the same bytes. It used to return a
471
+ * bare ObjectPayload -- no Serialization Flags, no Group or Subgroup ID, no
472
+ * Priority, no deltas -- with every `byteOffset` zero, and in DATAGRAM mode
473
+ * it resolved the Object ID against state it never carried.
474
+ *
475
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
476
+ * the one-shot decoder, the buffer window's position for the incremental one.
477
+ */
478
+ function readFetchObject(r: BufferReader, base: number, st: FetchObjectState): FetchObjectPayload {
479
+ const byteOffset = base + r.offset
480
+ const flags = Number(r.readVarInt())
481
+
482
+ let groupId = st.prevGroupId
483
+ let subgroupId = st.prevSubgroupId
484
+ let objectId = st.prevObjectId + 1n
485
+ let payloadLength: number
486
+ let payload: Uint8Array
487
+ let payloadByteOffset: number
488
+ const extensionData = new Uint8Array(0)
489
+
490
+ if (flags >= 0x80) {
491
+ // End of Range: 0x8C = End of Non-Existent Range, 0x10C = End of Unknown Range
492
+ // Per spec §10.4.4.2: Group ID and Object ID are present;
493
+ // Subgroup ID, Priority, Properties are not present.
494
+ if (flags !== 0x8c && flags !== 0x10c) {
495
+ throw new DecodeError(
496
+ 'CONSTRAINT_VIOLATION',
497
+ `Unknown serialization flags value: 0x${flags.toString(16)}`,
498
+ r.offset,
499
+ )
500
+ }
501
+ groupId = r.readVarInt()
502
+ objectId = r.readVarInt()
503
+ payloadLength = Number(r.readVarInt())
504
+ payloadByteOffset = base + r.offset
505
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
506
+ } else if (flags & 0x40) {
507
+ // DATAGRAM mode: no subgroup_id field
508
+ const objectIdPresent = (flags & 0x04) !== 0
509
+ const groupIdPresent = (flags & 0x08) !== 0
510
+ const priorityPresent = (flags & 0x10) !== 0
511
+ const propsPresent = (flags & 0x20) !== 0
512
+
513
+ if (groupIdPresent) {
514
+ groupId = r.readVarInt()
515
+ }
516
+ // No subgroup in datagram mode
517
+ if (objectIdPresent) {
518
+ objectId = r.readVarInt()
519
+ }
520
+ if (priorityPresent) {
521
+ st.prevPriority = r.readUint8()
522
+ }
523
+ let objectProperties: Record<string, bigint> | undefined
524
+ if (propsPresent) {
525
+ const propsLen = Number(r.readVarInt())
526
+ if (propsLen > 0) {
527
+ objectProperties = decodeObjectProperties(r, propsLen)
528
+ }
529
+ }
530
+ payloadLength = Number(r.readVarInt())
531
+ payloadByteOffset = base + r.offset
532
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
533
+
534
+ const obj: FetchObjectPayload = {
535
+ type: 'object',
536
+ byteOffset,
537
+ payloadByteOffset,
538
+ serializationFlags: flags,
539
+ groupId,
540
+ subgroupId: 0n,
541
+ objectId,
542
+ publisherPriority: st.prevPriority,
543
+ payloadLength,
544
+ payload,
545
+ extensionData,
546
+ }
547
+ if (objectProperties)
548
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
519
549
 
520
- if (subgroupEncoding === 0x00) {
521
- subgroupId = 0n
522
- } else if (subgroupEncoding === 0x01) {
523
- if (first) {
524
- return {
525
- ok: false,
526
- error: new DecodeError(
527
- 'CONSTRAINT_VIOLATION',
528
- 'First fetch object cannot reference prior subgroupId',
529
- r.offset,
530
- ),
531
- }
532
- }
533
- subgroupId = prevSubgroupId
534
- } else if (subgroupEncoding === 0x02) {
535
- if (first) {
536
- return {
537
- ok: false,
538
- error: new DecodeError(
539
- 'CONSTRAINT_VIOLATION',
540
- 'First fetch object cannot reference prior subgroupId',
541
- r.offset,
542
- ),
543
- }
544
- }
545
- subgroupId = prevSubgroupId + 1n
546
- } else if (subgroupEncoding === 0x03) {
547
- subgroupId = r.readVarInt()
548
- }
550
+ st.prevGroupId = groupId
551
+ st.prevObjectId = objectId
552
+ st.first = false
553
+ return obj
554
+ } else {
555
+ const subgroupEncoding = flags & 0x03
556
+ const objectIdPresent = (flags & 0x04) !== 0
557
+ const groupIdPresent = (flags & 0x08) !== 0
558
+ const priorityPresent = (flags & 0x10) !== 0
559
+ const propsPresent = (flags & 0x20) !== 0
560
+
561
+ if (groupIdPresent) {
562
+ groupId = r.readVarInt()
563
+ } else if (st.first) {
564
+ throw new DecodeError(
565
+ 'CONSTRAINT_VIOLATION',
566
+ 'First fetch object must include groupId',
567
+ r.offset,
568
+ )
569
+ }
549
570
 
550
- if (objectIdPresent) {
551
- objectId = r.readVarInt()
552
- } else if (first) {
553
- return {
554
- ok: false,
555
- error: new DecodeError(
556
- 'CONSTRAINT_VIOLATION',
557
- 'First fetch object must include objectId',
558
- r.offset,
559
- ),
560
- }
561
- }
571
+ if (subgroupEncoding === 0x00) {
572
+ subgroupId = 0n
573
+ } else if (subgroupEncoding === 0x01) {
574
+ if (st.first) {
575
+ throw new DecodeError(
576
+ 'CONSTRAINT_VIOLATION',
577
+ 'First fetch object cannot reference prior subgroupId',
578
+ r.offset,
579
+ )
580
+ }
581
+ subgroupId = st.prevSubgroupId
582
+ } else if (subgroupEncoding === 0x02) {
583
+ if (st.first) {
584
+ throw new DecodeError(
585
+ 'CONSTRAINT_VIOLATION',
586
+ 'First fetch object cannot reference prior subgroupId',
587
+ r.offset,
588
+ )
589
+ }
590
+ subgroupId = st.prevSubgroupId + 1n
591
+ } else if (subgroupEncoding === 0x03) {
592
+ subgroupId = r.readVarInt()
593
+ }
562
594
 
563
- if (priorityPresent) {
564
- prevPriority = r.readUint8()
565
- }
595
+ if (objectIdPresent) {
596
+ objectId = r.readVarInt()
597
+ } else if (st.first) {
598
+ throw new DecodeError(
599
+ 'CONSTRAINT_VIOLATION',
600
+ 'First fetch object must include objectId',
601
+ r.offset,
602
+ )
603
+ }
566
604
 
567
- let objectProperties: Record<string, bigint> | undefined
568
- if (propsPresent) {
569
- const propsLen = Number(r.readVarInt())
570
- if (propsLen > 0) {
571
- objectProperties = decodeObjectProperties(r, propsLen)
572
- }
573
- }
605
+ if (priorityPresent) {
606
+ st.prevPriority = r.readUint8()
607
+ }
574
608
 
575
- payloadLength = Number(r.readVarInt())
576
- payloadByteOffset = r.offset
577
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
578
-
579
- if (objectProperties) {
580
- const obj: FetchObjectPayload = {
581
- type: 'object',
582
- byteOffset,
583
- payloadByteOffset,
584
- serializationFlags: flags,
585
- groupId,
586
- subgroupId,
587
- objectId,
588
- publisherPriority: prevPriority,
589
- payloadLength,
590
- payload,
591
- extensionData,
592
- }
593
- ;(obj as unknown as Record<string, unknown>).objectProperties = objectProperties
594
- objects.push(obj)
595
- prevGroupId = groupId
596
- prevSubgroupId = subgroupId
597
- prevObjectId = objectId
598
- first = false
599
- continue
600
- }
609
+ let objectProperties: Record<string, bigint> | undefined
610
+ if (propsPresent) {
611
+ const propsLen = Number(r.readVarInt())
612
+ if (propsLen > 0) {
613
+ objectProperties = decodeObjectProperties(r, propsLen)
601
614
  }
615
+ }
616
+
617
+ payloadLength = Number(r.readVarInt())
618
+ payloadByteOffset = base + r.offset
619
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
602
620
 
621
+ if (objectProperties) {
603
622
  const obj: FetchObjectPayload = {
604
623
  type: 'object',
605
624
  byteOffset,
@@ -608,17 +627,63 @@ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
608
627
  groupId,
609
628
  subgroupId,
610
629
  objectId,
611
- publisherPriority: prevPriority,
630
+ publisherPriority: st.prevPriority,
612
631
  payloadLength,
613
632
  payload,
614
633
  extensionData,
615
634
  }
616
- objects.push(obj)
635
+ ;(obj as unknown as Record<string, unknown>).objectProperties = objectProperties
636
+ st.prevGroupId = groupId
637
+ st.prevSubgroupId = subgroupId
638
+ st.prevObjectId = objectId
639
+ st.first = false
640
+ return obj
641
+ }
642
+ }
617
643
 
618
- prevGroupId = groupId
619
- prevSubgroupId = subgroupId
620
- prevObjectId = objectId
621
- first = false
644
+ const obj: FetchObjectPayload = {
645
+ type: 'object',
646
+ byteOffset,
647
+ payloadByteOffset,
648
+ serializationFlags: flags,
649
+ groupId,
650
+ subgroupId,
651
+ objectId,
652
+ publisherPriority: st.prevPriority,
653
+ payloadLength,
654
+ payload,
655
+ extensionData,
656
+ }
657
+
658
+ st.prevGroupId = groupId
659
+ st.prevSubgroupId = subgroupId
660
+ st.prevObjectId = objectId
661
+ st.first = false
662
+
663
+ return obj
664
+ }
665
+
666
+ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
667
+ try {
668
+ const r = new BufferReader(bytes)
669
+ const streamType = r.readVarInt()
670
+ if (streamType !== FETCH_STREAM_TYPE) {
671
+ return {
672
+ ok: false,
673
+ error: new DecodeError(
674
+ 'CONSTRAINT_VIOLATION',
675
+ `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
676
+ 0,
677
+ ),
678
+ }
679
+ }
680
+ const requestId = r.readVarInt()
681
+ const objects: FetchObjectPayload[] = []
682
+
683
+ const st = newFetchObjectState()
684
+
685
+ while (r.remaining > 0) {
686
+ objects.push(readFetchObject(r, 0, st))
622
687
  }
623
688
 
624
689
  return {
@@ -652,77 +717,107 @@ export function decodeDataStream(
652
717
 
653
718
  // ─── Data Stream Decoders ──────────────────────────────────────────────────────
654
719
 
720
+ /**
721
+ * A data stream's bytes, accumulated across chunks.
722
+ *
723
+ * The decoders below used to allocate an array of `unread + chunk.length` and
724
+ * copy both halves into it on *every* chunk. Capacity now grows geometrically,
725
+ * so most chunks are a single `set` into spare room.
726
+ *
727
+ * **It never compacts in place.** Object payloads are handed out as views into
728
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
729
+ * object that has already been emitted. The consumed prefix is dropped only
730
+ * when a fresh array is being allocated anyway, which leaves the old array
731
+ * alive for exactly as long as the views into it are.
732
+ */
733
+ class StreamBuffer {
734
+ private buf = new Uint8Array(0)
735
+ /** Bytes written into `buf`. */
736
+ private len = 0
737
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
738
+ offset = 0
739
+ /** Absolute position of `buf[0]` within the stream. */
740
+ base = 0
741
+
742
+ get unread(): number {
743
+ return this.len - this.offset
744
+ }
745
+
746
+ append(chunk: Uint8Array): void {
747
+ if (this.len + chunk.length > this.buf.length) {
748
+ const live = this.len - this.offset
749
+ const needed = live + chunk.length
750
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
751
+ // half free. Sizing to `needed` -- which is what this did first -- leaves
752
+ // no room at all, so the next chunk reallocates too and the buffer
753
+ // reallocates on every single chunk, which is the cost it exists to
754
+ // avoid. Capacity never shrinks, so it settles at roughly twice the
755
+ // largest `live + chunk` this stream has seen.
756
+ let cap = Math.max(4096, this.buf.length)
757
+ while (cap < needed * 2) cap *= 2
758
+ // A fresh array every time, including when the capacity is unchanged:
759
+ // the old one is still pinned by the payload views handed out of it, so
760
+ // it can be neither compacted nor reused.
761
+ const next = new Uint8Array(cap)
762
+ next.set(this.buf.subarray(this.offset, this.len), 0)
763
+ this.buf = next
764
+ this.base += this.offset
765
+ this.len = live
766
+ this.offset = 0
767
+ }
768
+ this.buf.set(chunk, this.len)
769
+ this.len += chunk.length
770
+ }
771
+
772
+ /**
773
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
774
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
775
+ * not the spare room after them.
776
+ */
777
+ written(): Uint8Array {
778
+ return this.buf.subarray(0, this.len)
779
+ }
780
+ }
781
+
655
782
  export function createSubgroupStreamDecoder(): TransformStream<
656
783
  Uint8Array,
657
784
  SubgroupStreamHeader | ObjectPayload
658
785
  > {
659
- let buffer = new Uint8Array(0)
660
- let offset = 0
786
+ const b = new StreamBuffer()
787
+ let header: SubgroupHeaderFields | null = null
661
788
  let headerEmitted = false
662
789
  let prevObjectId = -1n
663
- let firstObject = true
664
- let _subgroupIsFirstObjId = false
665
- let _extensionsPresent = false
790
+ let isFirst = true
791
+
792
+ function emitHeader(
793
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
794
+ h: SubgroupHeaderFields,
795
+ subgroupId: bigint,
796
+ ): void {
797
+ controller.enqueue({
798
+ type: 'subgroup_header',
799
+ headerType: h.streamType,
800
+ trackAlias: h.trackAlias,
801
+ groupId: h.groupId,
802
+ subgroupId,
803
+ publisherPriority: h.publisherPriority,
804
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
805
+ })
806
+ headerEmitted = true
807
+ }
666
808
 
667
809
  return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
668
810
  transform(chunk, controller) {
669
- if (offset > 0) {
670
- buffer = buffer.subarray(offset)
671
- offset = 0
672
- }
673
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
674
- newBuffer.set(buffer, 0)
675
- newBuffer.set(chunk, buffer.length)
676
- buffer = newBuffer
811
+ b.append(chunk)
812
+ // One view and one reader per chunk. Both used to be allocated per
813
+ // object, and an object is the thing there are a great many of.
814
+ const view = b.written()
677
815
 
678
- if (!headerEmitted) {
816
+ if (header === null) {
679
817
  try {
680
- const r = new BufferReader(buffer.subarray(offset))
681
- const streamType = Number(r.readVarInt())
682
-
683
- if (
684
- !(
685
- (streamType >= 0x10 && streamType <= 0x1d) ||
686
- (streamType >= 0x30 && streamType <= 0x3d)
687
- )
688
- ) {
689
- controller.error(
690
- new DecodeError(
691
- 'CONSTRAINT_VIOLATION',
692
- `Expected subgroup stream type, got 0x${streamType.toString(16)}`,
693
- 0,
694
- ),
695
- )
696
- return
697
- }
698
-
699
- _extensionsPresent = (streamType & 0x01) !== 0
700
- const hasSubgroupField = (streamType & 0x04) !== 0
701
- _subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
702
- const hasPriority = streamType < 0x30
703
-
704
- const trackAlias = r.readVarInt()
705
- const groupId = r.readVarInt()
706
-
707
- let subgroupId = 0n
708
- if (hasSubgroupField) {
709
- subgroupId = r.readVarInt()
710
- }
711
-
712
- let publisherPriority = 128
713
- if (hasPriority) {
714
- publisherPriority = r.readUint8()
715
- }
716
-
717
- controller.enqueue({
718
- type: 'subgroup_header',
719
- trackAlias,
720
- groupId,
721
- subgroupId,
722
- publisherPriority,
723
- })
724
- headerEmitted = true
725
- offset += r.offset
818
+ const hr = new BufferReader(view, b.offset)
819
+ header = readSubgroupHeader(hr)
820
+ b.offset = hr.offset
726
821
  } catch (e) {
727
822
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
728
823
  return
@@ -730,50 +825,46 @@ export function createSubgroupStreamDecoder(): TransformStream<
730
825
  controller.error(e)
731
826
  return
732
827
  }
828
+ // Where the Subgroup ID is the first Object's ID, that Object has not
829
+ // been read yet. Holding the header back until it has is the only way
830
+ // to emit the value the one-shot decoder reports; every other mode
831
+ // knows its Subgroup ID already.
832
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
733
833
  }
734
834
 
735
- while (offset < buffer.length) {
835
+ const h = header
836
+ if (h === null) return
837
+
838
+ const r = new BufferReader(view, b.offset)
839
+ while (r.remaining > 0) {
840
+ // Where this Object starts, so a partial one is re-read from the top
841
+ // once the rest of it arrives.
842
+ const start = r.offset
843
+ let obj: ObjectPayload
736
844
  try {
737
- const r = new BufferReader(buffer.subarray(offset))
738
- const delta = r.readVarInt()
739
- let objectId: bigint
740
- if (firstObject) {
741
- objectId = delta
742
- firstObject = false
743
- } else {
744
- objectId = prevObjectId + 1n + delta
745
- }
746
- let extensionData = new Uint8Array(0)
747
- if (_extensionsPresent) {
748
- const extLen = Number(r.readVarInt())
749
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
750
- }
751
- const payloadLength = Number(r.readVarInt())
752
- const payloadByteOffset = r.offset
753
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
754
- controller.enqueue({
755
- type: 'object',
756
- objectId,
757
- payloadLength,
758
- payload,
759
- extensionData,
760
- byteOffset: 0,
761
- payloadByteOffset,
762
- })
763
- offset += r.offset
764
- prevObjectId = objectId
845
+ obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, b.base)
765
846
  } catch (e) {
766
847
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
767
- break
848
+ b.offset = start
849
+ return
768
850
  }
769
851
  controller.error(e)
770
852
  return
771
853
  }
854
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
855
+ isFirst = false
856
+ b.offset = r.offset
857
+ prevObjectId = obj.objectId
858
+ controller.enqueue(obj)
772
859
  }
773
860
  },
774
861
 
775
862
  flush(controller) {
776
- if (offset < buffer.length) {
863
+ // A stream carrying a header and no Objects still has a header to report,
864
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
865
+ // returns when it finds no first Object to derive one from.
866
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
867
+ if (b.unread > 0) {
777
868
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
778
869
  }
779
870
  },
@@ -784,24 +875,23 @@ export function createFetchStreamDecoder(): TransformStream<
784
875
  Uint8Array,
785
876
  FetchStreamHeader | ObjectPayload
786
877
  > {
787
- let buffer = new Uint8Array(0)
788
- let offset = 0
878
+ const b = new StreamBuffer()
789
879
  let headerEmitted = false
880
+ /**
881
+ * The delta state the Objects carry between them. This decoder used to keep
882
+ * none, which is why DATAGRAM-mode Object IDs came out wrong: they resolve
883
+ * against the previous Object's ID.
884
+ */
885
+ const st = newFetchObjectState()
790
886
 
791
887
  return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
792
888
  transform(chunk, controller) {
793
- if (offset > 0) {
794
- buffer = buffer.subarray(offset)
795
- offset = 0
796
- }
797
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
798
- newBuffer.set(buffer, 0)
799
- newBuffer.set(chunk, buffer.length)
800
- buffer = newBuffer
889
+ b.append(chunk)
890
+ const view = b.written()
801
891
 
802
892
  if (!headerEmitted) {
803
893
  try {
804
- const r = new BufferReader(buffer.subarray(offset))
894
+ const r = new BufferReader(view, b.offset)
805
895
  const streamType = r.readVarInt()
806
896
  if (streamType !== FETCH_STREAM_TYPE) {
807
897
  controller.error(
@@ -816,7 +906,7 @@ export function createFetchStreamDecoder(): TransformStream<
816
906
  const requestId = r.readVarInt()
817
907
  controller.enqueue({ type: 'fetch_header', requestId })
818
908
  headerEmitted = true
819
- offset += r.offset
909
+ b.offset = r.offset
820
910
  } catch (e) {
821
911
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
822
912
  return
@@ -826,51 +916,27 @@ export function createFetchStreamDecoder(): TransformStream<
826
916
  }
827
917
  }
828
918
 
829
- while (offset < buffer.length) {
919
+ const r = new BufferReader(view, b.offset)
920
+ while (r.remaining > 0) {
921
+ const start = r.offset
922
+ let obj: FetchObjectPayload
830
923
  try {
831
- const r = new BufferReader(buffer.subarray(offset))
832
- const flags = Number(r.readVarInt())
833
- const objectIdPresent = (flags & 0x04) !== 0
834
- const groupIdPresent = (flags & 0x08) !== 0
835
- const priorityPresent = (flags & 0x10) !== 0
836
- const extensionsPresent = (flags & 0x20) !== 0
837
- const subgroupEncoding = flags & 0x03
838
-
839
- if (groupIdPresent) r.readVarInt()
840
- if (subgroupEncoding === 0x03) r.readVarInt()
841
- let objectId = 0n
842
- if (objectIdPresent) objectId = r.readVarInt()
843
- if (priorityPresent) r.readUint8()
844
- let extensionData = new Uint8Array(0)
845
- if (extensionsPresent) {
846
- const extLen = Number(r.readVarInt())
847
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
848
- }
849
- const payloadLength = Number(r.readVarInt())
850
- const payloadByteOffset = r.offset
851
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
852
- controller.enqueue({
853
- type: 'object',
854
- objectId,
855
- payloadLength,
856
- payload,
857
- extensionData,
858
- byteOffset: 0,
859
- payloadByteOffset,
860
- })
861
- offset += r.offset
924
+ obj = readFetchObject(r, b.base, st)
862
925
  } catch (e) {
863
926
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
864
- break
927
+ b.offset = start
928
+ return
865
929
  }
866
930
  controller.error(e)
867
931
  return
868
932
  }
933
+ b.offset = r.offset
934
+ controller.enqueue(obj)
869
935
  }
870
936
  },
871
937
 
872
938
  flush(controller) {
873
- if (offset < buffer.length) {
939
+ if (b.unread > 0) {
874
940
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
875
941
  }
876
942
  },
@@ -878,56 +944,82 @@ export function createFetchStreamDecoder(): TransformStream<
878
944
  }
879
945
 
880
946
  export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
881
- let buffer = new Uint8Array(0)
882
- let offset = 0
883
- let inner: TransformStream<Uint8Array, DataStreamEvent> | null = null
884
-
885
- return new TransformStream<Uint8Array, DataStreamEvent>({
886
- transform(chunk, controller) {
887
- if (offset > 0) {
888
- buffer = buffer.subarray(offset)
889
- offset = 0
890
- }
891
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
892
- newBuffer.set(buffer, 0)
893
- newBuffer.set(chunk, buffer.length)
894
- buffer = newBuffer
895
-
896
- if (inner === null) {
897
- if (offset >= buffer.length) return
898
- const firstByte = buffer[offset]!
899
-
900
- if ((firstByte >= 0x10 && firstByte <= 0x1d) || (firstByte >= 0x30 && firstByte <= 0x3d)) {
901
- const decoder = createSubgroupStreamDecoder()
902
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
903
- } else if (firstByte === 0x05) {
904
- const decoder = createFetchStreamDecoder()
905
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
906
- } else {
907
- controller.error(
908
- new DecodeError(
909
- 'CONSTRAINT_VIOLATION',
910
- `Unknown data stream type: 0x${firstByte.toString(16)}`,
911
- 0,
912
- ),
913
- )
914
- return
947
+ /**
948
+ * Delegation that actually delegates.
949
+ *
950
+ * This function used to pick an inner decoder and then never write a byte to
951
+ * it: chunks accumulated in a local buffer and `flush` decoded the whole
952
+ * stream in one shot. Every event therefore arrived at end-of-stream, which
953
+ * on a subscription that stays open means no events at all. The inner
954
+ * decoder's readable is now pumped into this one's controller as the bytes
955
+ * arrive.
956
+ *
957
+ * The first byte still decides which decoder to use, on exactly the range
958
+ * this draft accepted before.
959
+ */
960
+ let writer: WritableStreamDefaultWriter<Uint8Array> | null = null
961
+ let pump: Promise<void> = Promise.resolve()
962
+ let broken = false
963
+
964
+ function attach(
965
+ inner: TransformStream<Uint8Array, DataStreamEvent>,
966
+ controller: TransformStreamDefaultController<DataStreamEvent>,
967
+ ): void {
968
+ writer = inner.writable.getWriter()
969
+ const reader = inner.readable.getReader()
970
+ pump = (async () => {
971
+ try {
972
+ for (;;) {
973
+ const { done, value } = await reader.read()
974
+ if (done) break
975
+ controller.enqueue(value)
976
+ }
977
+ } catch (e) {
978
+ // The inner decoder rejected the stream. It owns the diagnosis, so its
979
+ // error is the one that surfaces.
980
+ if (!broken) {
981
+ broken = true
982
+ controller.error(e)
915
983
  }
916
984
  }
917
- },
985
+ })()
986
+ }
918
987
 
919
- flush(controller) {
920
- if (offset >= buffer.length) return
921
- const view = buffer.subarray(offset)
988
+ async function feed(bytes: Uint8Array): Promise<void> {
989
+ if (writer === null || broken || bytes.length === 0) return
990
+ try {
991
+ await writer.write(bytes)
992
+ } catch {
993
+ // Writing to an errored inner stream throws the same error the pump is
994
+ // already reporting; swallow it here so it is reported once.
995
+ broken = true
996
+ }
997
+ }
922
998
 
923
- const firstByte = view[0]!
924
- let result: DecodeResult<Draft17DataStream>
999
+ return new TransformStream<Uint8Array, DataStreamEvent>({
1000
+ async transform(chunk, controller) {
1001
+ if (broken) return
1002
+ if (writer !== null) {
1003
+ await feed(chunk)
1004
+ return
1005
+ }
1006
+ // One byte is enough to choose, and until there is one there is nothing
1007
+ // to choose from.
1008
+ if (chunk.length === 0) return
1009
+ const firstByte = chunk[0]!
925
1010
 
926
1011
  if ((firstByte >= 0x10 && firstByte <= 0x1d) || (firstByte >= 0x30 && firstByte <= 0x3d)) {
927
- result = decodeSubgroupStream(view)
1012
+ attach(
1013
+ createSubgroupStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1014
+ controller,
1015
+ )
928
1016
  } else if (firstByte === 0x05) {
929
- result = decodeFetchStream(view)
1017
+ attach(
1018
+ createFetchStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1019
+ controller,
1020
+ )
930
1021
  } else {
1022
+ broken = true
931
1023
  controller.error(
932
1024
  new DecodeError(
933
1025
  'CONSTRAINT_VIOLATION',
@@ -938,32 +1030,19 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
938
1030
  return
939
1031
  }
940
1032
 
941
- if (!result.ok) {
942
- controller.error(result.error)
943
- return
944
- }
1033
+ // The type byte is part of the stream the inner decoder reads, so the
1034
+ // whole chunk goes in, first byte included.
1035
+ await feed(chunk)
1036
+ },
945
1037
 
946
- const stream = result.value
947
- if (stream.type === 'subgroup') {
948
- controller.enqueue({
949
- type: 'subgroup_header',
950
- trackAlias: stream.trackAlias,
951
- groupId: stream.groupId,
952
- subgroupId: stream.subgroupId,
953
- publisherPriority: stream.publisherPriority,
954
- })
955
- for (const obj of stream.objects) {
956
- controller.enqueue(obj)
957
- }
958
- } else if (stream.type === 'fetch') {
959
- controller.enqueue({
960
- type: 'fetch_header',
961
- requestId: stream.requestId,
962
- })
963
- for (const obj of stream.objects) {
964
- controller.enqueue(obj)
965
- }
1038
+ async flush() {
1039
+ if (writer === null) return
1040
+ try {
1041
+ await writer.close()
1042
+ } catch {
1043
+ // Reported by the pump.
966
1044
  }
1045
+ await pump
967
1046
  },
968
1047
  })
969
1048
  }