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