@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
@@ -416,106 +416,171 @@ function resolveGroupId(
416
416
  return groupId
417
417
  }
418
418
 
419
- export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
420
- try {
421
- const r = new BufferReader(bytes)
422
- // DECISION (DECISIONS.md D5): permissive on receive. The Type Flags are a
423
- // vi64 and Section 1.4.1 allows non-minimal encodings, so 0x10 may legally
424
- // arrive as the two-byte 0x8010. Read the value, then judge the value.
425
- const typeFlags = r.readVarInt()
426
- const flagsError = subgroupTypeFlagsError(typeFlags)
427
- if (flagsError !== null) {
428
- return { ok: false, error: new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0) }
429
- }
430
- const streamType = Number(typeFlags)
419
+ /**
420
+ * The SUBGROUP_HEADER fields, read once for both decoders.
421
+ *
422
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` share this single
423
+ * reader so they cannot disagree about a header: the Type Flags, the
424
+ * end-of-group and first-object flags, and the Subgroup ID under the mode that
425
+ * derives it from the first Object all come from here. Two readers drift.
426
+ */
427
+ interface SubgroupHeaderFields {
428
+ readonly streamType: number
429
+ readonly propertiesPresent: boolean
430
+ /** Subgroup Mode 0b01: the Subgroup ID is the first Object's ID. */
431
+ readonly subgroupIsFirstObjId: boolean
432
+ readonly endOfGroup: boolean
433
+ readonly firstObjectFlag: boolean
434
+ readonly trackAlias: bigint
435
+ readonly groupId: bigint
436
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
437
+ readonly subgroupId: bigint
438
+ readonly publisherPriority: number
439
+ }
431
440
 
432
- const propertiesPresent = (streamType & 0x01) !== 0
433
- const subgroupMode = (streamType & 0x06) >> 1
434
- const hasSubgroupField = subgroupMode === 0x02
435
- const subgroupIsFirstObjId = subgroupMode === 0x01
436
- const endOfGroup = (streamType & 0x08) !== 0
437
- const hasPriority = (streamType & 0x20) === 0
438
- const firstObject = (streamType & 0x40) !== 0
441
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
442
+ // DECISION (DECISIONS.md D5): permissive on receive. The Type Flags are a
443
+ // vi64 and Section 1.4.1 allows non-minimal encodings, so 0x10 may legally
444
+ // arrive as the two-byte 0x8010. Read the value, then judge the value.
445
+ const typeFlags = r.readVarInt()
446
+ const flagsError = subgroupTypeFlagsError(typeFlags)
447
+ if (flagsError !== null) {
448
+ throw new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0)
449
+ }
450
+ const streamType = Number(typeFlags)
439
451
 
440
- const trackAlias = r.readVarInt()
441
- const groupId = r.readVarInt()
452
+ const propertiesPresent = (streamType & 0x01) !== 0
453
+ const subgroupMode = (streamType & 0x06) >> 1
454
+ const hasSubgroupField = subgroupMode === 0x02
455
+ const subgroupIsFirstObjId = subgroupMode === 0x01
456
+ const endOfGroup = (streamType & 0x08) !== 0
457
+ // DEFAULT_PRIORITY bit (0x20): when set, priority is absent.
458
+ const hasPriority = (streamType & 0x20) === 0
459
+ const firstObjectFlag = (streamType & 0x40) !== 0
442
460
 
443
- let subgroupId = 0n
444
- if (hasSubgroupField) {
445
- subgroupId = r.readVarInt()
446
- }
461
+ const trackAlias = r.readVarInt()
462
+ const groupId = r.readVarInt()
447
463
 
448
- let publisherPriority = 128
449
- if (hasPriority) {
450
- publisherPriority = r.readUint8()
464
+ let subgroupId = 0n
465
+ if (hasSubgroupField) {
466
+ subgroupId = r.readVarInt()
467
+ }
468
+
469
+ let publisherPriority = 128
470
+ if (hasPriority) {
471
+ publisherPriority = r.readUint8()
472
+ }
473
+
474
+ return {
475
+ streamType,
476
+ propertiesPresent,
477
+ subgroupIsFirstObjId,
478
+ endOfGroup,
479
+ firstObjectFlag,
480
+ trackAlias,
481
+ groupId,
482
+ subgroupId,
483
+ publisherPriority,
484
+ }
485
+ }
486
+
487
+ /**
488
+ * One Object off a subgroup stream, for both decoders.
489
+ *
490
+ * `base` is the absolute offset within the stream that `r.offset === 0`
491
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
492
+ * stream and passes 0; the incremental decoder reads from a window over its
493
+ * buffer and passes that window's position, which is what makes `byteOffset`
494
+ * mean the same thing on both paths instead of being hardcoded to zero on one
495
+ * of them.
496
+ *
497
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
498
+ * incremental decoder treats as "wait for more bytes" rather than as an error.
499
+ */
500
+ function readSubgroupObject(
501
+ r: BufferReader,
502
+ propertiesPresent: boolean,
503
+ isFirst: boolean,
504
+ prevObjectId: bigint,
505
+ base: number,
506
+ ): ObjectPayload {
507
+ const byteOffset = base + r.offset
508
+ const delta = r.readVarInt()
509
+ // Section 11.4.2: "Object ID = previous Object ID + Object ID Delta + 1", or
510
+ // the delta itself for the first Object on the stream.
511
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
512
+
513
+ const extensionData = new Uint8Array(0)
514
+ let objectProperties: Record<string, bigint> | undefined
515
+ if (propertiesPresent) {
516
+ const propsLen = Number(r.readVarInt())
517
+ if (propsLen > 0) {
518
+ objectProperties = decodeObjectProperties(r, propsLen)
451
519
  }
520
+ }
521
+
522
+ const payloadLength = Number(r.readVarInt())
523
+ let payload: Uint8Array
524
+ let status: bigint | undefined
525
+ let payloadByteOffset: number
526
+ if (payloadLength === 0) {
527
+ // Section 11.4.2: "The Object Status field is only sent if the Object
528
+ // Payload Length is zero." Skipping it leaves the varint to be read as the
529
+ // next Object's Object ID Delta, which desynchronises the rest of the
530
+ // stream silently.
531
+ status = r.readVarInt()
532
+ payloadByteOffset = base + r.offset
533
+ payload = new Uint8Array(0)
534
+ } else {
535
+ payloadByteOffset = base + r.offset
536
+ payload = r.readBytesView(payloadLength)
537
+ }
538
+
539
+ const obj: ObjectPayload = {
540
+ type: 'object',
541
+ byteOffset,
542
+ payloadByteOffset,
543
+ objectId,
544
+ objectIdDelta: delta,
545
+ payloadLength,
546
+ payload,
547
+ extensionData,
548
+ }
549
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
550
+ if (objectProperties !== undefined)
551
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
552
+ return obj
553
+ }
554
+
555
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
556
+ try {
557
+ const r = new BufferReader(bytes)
558
+ const h = readSubgroupHeader(r)
452
559
 
453
560
  const objects: ObjectPayload[] = []
454
561
  let prevObjectId = -1n
455
562
  let isFirst = true
563
+ let subgroupId = h.subgroupId
456
564
 
457
565
  while (r.remaining > 0) {
458
- const byteOffset = r.offset
459
- const delta = r.readVarInt()
460
- let objectId: bigint
461
- if (isFirst) {
462
- objectId = delta
463
- if (subgroupIsFirstObjId) {
464
- subgroupId = objectId
465
- }
466
- isFirst = false
467
- } else {
468
- // Section 11.4.2 delta encoding: prior + delta + 1.
469
- objectId = prevObjectId + 1n + delta
470
- }
471
- const extensionData = new Uint8Array(0)
472
- let objectProperties: Record<string, bigint> | undefined
473
- if (propertiesPresent) {
474
- const propsLen = Number(r.readVarInt())
475
- if (propsLen > 0) {
476
- objectProperties = decodeObjectProperties(r, propsLen)
477
- }
478
- }
479
- const payloadLength = Number(r.readVarInt())
480
- let payload: Uint8Array
481
- let status: bigint | undefined
482
- let payloadByteOffset: number
483
- if (payloadLength === 0) {
484
- status = r.readVarInt()
485
- payloadByteOffset = r.offset
486
- payload = new Uint8Array(0)
487
- } else {
488
- payloadByteOffset = r.offset
489
- payload = r.readBytesView(payloadLength)
490
- }
491
- const obj: ObjectPayload = {
492
- type: 'object',
493
- byteOffset,
494
- payloadByteOffset,
495
- objectId,
496
- objectIdDelta: delta,
497
- payloadLength,
498
- payload,
499
- extensionData,
500
- }
501
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
502
- if (objectProperties !== undefined)
503
- (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
566
+ const obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, 0)
567
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
568
+ isFirst = false
504
569
  objects.push(obj)
505
- prevObjectId = objectId
570
+ prevObjectId = obj.objectId
506
571
  }
507
572
 
508
573
  const result: SubgroupStream = {
509
574
  type: 'subgroup',
510
- headerType: streamType,
511
- trackAlias,
512
- groupId,
575
+ headerType: h.streamType,
576
+ trackAlias: h.trackAlias,
577
+ groupId: h.groupId,
513
578
  subgroupId,
514
- publisherPriority,
579
+ publisherPriority: h.publisherPriority,
515
580
  objects,
581
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
582
+ ...(h.firstObjectFlag ? { firstObject: true } : {}),
516
583
  }
517
- if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
518
- if (firstObject) (result as unknown as Record<string, unknown>).firstObject = true
519
584
 
520
585
  return {
521
586
  ok: true,
@@ -639,6 +704,253 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
639
704
  }
640
705
  }
641
706
 
707
+ /** The delta state a fetch stream carries from one Object to the next. */
708
+ interface FetchObjectState {
709
+ prevGroupId: bigint
710
+ prevSubgroupId: bigint
711
+ prevObjectId: bigint
712
+ prevPriority: number
713
+ first: boolean
714
+ }
715
+
716
+ function newFetchObjectState(): FetchObjectState {
717
+ return { prevGroupId: 0n, prevSubgroupId: 0n, prevObjectId: 0n, prevPriority: 128, first: true }
718
+ }
719
+
720
+ /**
721
+ * One Object off a fetch stream, for both decoders.
722
+ *
723
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
724
+ * cannot report a different shape for the same bytes: the Serialization Flags,
725
+ * the Group and Subgroup ID, the Priority, the resolved deltas and a true
726
+ * `byteOffset` all come from this one reader, and DATAGRAM-mode Object IDs
727
+ * resolve against the state it carries.
728
+ *
729
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
730
+ * the one-shot decoder, the buffer window's position for the incremental one.
731
+ */
732
+ function readFetchObject(r: BufferReader, base: number, st: FetchObjectState): FetchObjectPayload {
733
+ const byteOffset = base + r.offset
734
+ const flags = Number(r.readVarInt())
735
+
736
+ let groupId = st.prevGroupId
737
+ let subgroupId = st.prevSubgroupId
738
+ // Section 11.4.4.1: absent an Object ID Delta, Object ID is the prior
739
+ // Object's ID plus one. Stream delta encoding, not a range end.
740
+ let objectId = st.prevObjectId + 1n
741
+ let payloadLength: number
742
+ let payload: Uint8Array
743
+ let payloadByteOffset: number
744
+ const extensionData = new Uint8Array(0)
745
+
746
+ if (flags >= 0x80) {
747
+ // End-of-Range marker. Section 11.4.4: "Any other value is a
748
+ // PROTOCOL_VIOLATION."
749
+ if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
750
+ throw new DecodeError(
751
+ 'CONSTRAINT_VIOLATION',
752
+ `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
753
+ r.offset,
754
+ )
755
+ }
756
+ // D7 / Q16: ordinary delta arithmetic (see encodeFetchStream).
757
+ const groupDelta = r.readVarInt()
758
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
759
+ // With a Group ID Delta present, Object ID is the Object ID Delta.
760
+ const objectDelta = r.readVarInt()
761
+ objectId = objectDelta
762
+ // D7 / Q17: Object Payload Length is present, encoded as 0.
763
+ payloadLength = Number(r.readVarInt())
764
+ payloadByteOffset = base + r.offset
765
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
766
+
767
+ const marker: FetchObjectPayload = {
768
+ type: 'object',
769
+ byteOffset,
770
+ payloadByteOffset,
771
+ serializationFlags: flags,
772
+ groupId,
773
+ groupIdDelta: groupDelta,
774
+ // A marker has no Subgroup ID or Priority of its own — Section
775
+ // 11.4.4.2 says neither field is present. The interface requires
776
+ // both, so they carry the last actual Object's values, which is also
777
+ // what a following Object that references "the prior Object" gets.
778
+ subgroupId: st.prevSubgroupId,
779
+ objectId,
780
+ objectIdDelta: objectDelta,
781
+ publisherPriority: st.prevPriority,
782
+ payloadLength,
783
+ payload,
784
+ extensionData,
785
+ }
786
+
787
+ // Section 11.4.4.2: prior Group ID and prior Object ID come FROM the
788
+ // marker; prior Subgroup ID and prior Priority stay with the last
789
+ // actual Object before it, so they are deliberately not updated here.
790
+ st.prevGroupId = groupId
791
+ st.prevObjectId = objectId
792
+ st.first = false
793
+ return marker
794
+ }
795
+
796
+ if (flags & 0x40) {
797
+ // DATAGRAM mode: no subgroup_id field
798
+ const objectIdPresent = (flags & 0x04) !== 0
799
+ const groupIdPresent = (flags & 0x08) !== 0
800
+ const priorityPresent = (flags & 0x10) !== 0
801
+ const propsPresent = (flags & 0x20) !== 0
802
+
803
+ let groupDelta: bigint | undefined
804
+ if (groupIdPresent) {
805
+ // First object's delta IS the absolute group id; subsequent objects:
806
+ // groupId = st.prevGroupId + delta + 1 (ascending).
807
+ groupDelta = r.readVarInt()
808
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
809
+ }
810
+ let objectDelta: bigint | undefined
811
+ if (objectIdPresent) {
812
+ objectDelta = r.readVarInt()
813
+ objectId = resolveObjectId(groupIdPresent, objectDelta, st.prevObjectId)
814
+ }
815
+ if (objectId > MAX_U64) {
816
+ throw new DecodeError('CONSTRAINT_VIOLATION', 'computed Object ID exceeds 2^64 - 1', r.offset)
817
+ }
818
+ if (priorityPresent) {
819
+ st.prevPriority = r.readUint8()
820
+ }
821
+ let objectProperties: Record<string, bigint> | undefined
822
+ if (propsPresent) {
823
+ const propsLen = Number(r.readVarInt())
824
+ if (propsLen > 0) {
825
+ objectProperties = decodeObjectProperties(r, propsLen)
826
+ }
827
+ }
828
+ payloadLength = Number(r.readVarInt())
829
+ payloadByteOffset = base + r.offset
830
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
831
+
832
+ const obj: FetchObjectPayload = {
833
+ type: 'object',
834
+ byteOffset,
835
+ payloadByteOffset,
836
+ serializationFlags: flags,
837
+ groupId,
838
+ subgroupId: 0n,
839
+ objectId,
840
+ publisherPriority: st.prevPriority,
841
+ payloadLength,
842
+ payload,
843
+ extensionData,
844
+ }
845
+ if (groupDelta !== undefined)
846
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
847
+ if (objectDelta !== undefined)
848
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
849
+ if (objectProperties)
850
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
851
+
852
+ st.prevGroupId = groupId
853
+ st.prevObjectId = objectId
854
+ st.first = false
855
+ return obj
856
+ }
857
+
858
+ const subgroupEncoding = flags & 0x03
859
+ const objectIdPresent = (flags & 0x04) !== 0
860
+ const groupIdPresent = (flags & 0x08) !== 0
861
+ const priorityPresent = (flags & 0x10) !== 0
862
+ const propsPresent = (flags & 0x20) !== 0
863
+
864
+ let groupDelta: bigint | undefined
865
+ if (groupIdPresent) {
866
+ groupDelta = r.readVarInt()
867
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
868
+ } else if (st.first) {
869
+ throw new DecodeError(
870
+ 'CONSTRAINT_VIOLATION',
871
+ 'First fetch object must include groupId',
872
+ r.offset,
873
+ )
874
+ }
875
+
876
+ if (subgroupEncoding === 0x00) {
877
+ subgroupId = 0n
878
+ } else if (subgroupEncoding === 0x01 || subgroupEncoding === 0x02) {
879
+ if (st.first) {
880
+ throw new DecodeError(
881
+ 'CONSTRAINT_VIOLATION',
882
+ 'First fetch object cannot reference prior subgroupId',
883
+ r.offset,
884
+ )
885
+ }
886
+ subgroupId = subgroupEncoding === 0x01 ? st.prevSubgroupId : st.prevSubgroupId + 1n
887
+ } else {
888
+ subgroupId = r.readVarInt()
889
+ }
890
+
891
+ let objectDelta: bigint | undefined
892
+ if (objectIdPresent) {
893
+ objectDelta = r.readVarInt()
894
+ objectId = resolveObjectId(groupIdPresent, objectDelta, st.prevObjectId)
895
+ } else if (st.first) {
896
+ throw new DecodeError(
897
+ 'CONSTRAINT_VIOLATION',
898
+ 'First fetch object must include objectId',
899
+ r.offset,
900
+ )
901
+ }
902
+ if (objectId > MAX_U64) {
903
+ throw new DecodeError('CONSTRAINT_VIOLATION', 'computed Object ID exceeds 2^64 - 1', r.offset)
904
+ }
905
+
906
+ if (priorityPresent) {
907
+ st.prevPriority = r.readUint8()
908
+ }
909
+
910
+ let objectProperties: Record<string, bigint> | undefined
911
+ if (propsPresent) {
912
+ const propsLen = Number(r.readVarInt())
913
+ if (propsLen > 0) {
914
+ objectProperties = decodeObjectProperties(r, propsLen)
915
+ }
916
+ }
917
+
918
+ payloadLength = Number(r.readVarInt())
919
+ payloadByteOffset = base + r.offset
920
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
921
+
922
+ // A fetch stream carries no Object Status: status is only present on
923
+ // subscription-delivered objects (Section 11.2.1.1). That is also the
924
+ // one way the two copies of a doubly delivered object differ — the
925
+ // subscription copy can carry a status, the fill copy cannot.
926
+ const obj: FetchObjectPayload = {
927
+ type: 'object',
928
+ byteOffset,
929
+ payloadByteOffset,
930
+ serializationFlags: flags,
931
+ groupId,
932
+ subgroupId,
933
+ objectId,
934
+ publisherPriority: st.prevPriority,
935
+ payloadLength,
936
+ payload,
937
+ extensionData,
938
+ }
939
+ if (groupDelta !== undefined)
940
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
941
+ if (objectDelta !== undefined)
942
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
943
+ if (objectProperties)
944
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
945
+
946
+ st.prevGroupId = groupId
947
+ st.prevSubgroupId = subgroupId
948
+ st.prevObjectId = objectId
949
+ st.first = false
950
+
951
+ return obj
952
+ }
953
+
642
954
  export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
643
955
  try {
644
956
  const r = new BufferReader(bytes)
@@ -660,259 +972,10 @@ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
660
972
  const requestId = r.readVarInt()
661
973
  const objects: FetchObjectPayload[] = []
662
974
 
663
- let prevGroupId = 0n
664
- let prevSubgroupId = 0n
665
- let prevObjectId = 0n
666
- let prevPriority = 128
667
- let first = true
975
+ const st = newFetchObjectState()
668
976
 
669
977
  while (r.remaining > 0) {
670
- const byteOffset = r.offset
671
- const flags = Number(r.readVarInt())
672
-
673
- let groupId = prevGroupId
674
- let subgroupId = prevSubgroupId
675
- // Section 11.4.4.1: absent an Object ID Delta, Object ID is the prior
676
- // Object's ID plus one. Stream delta encoding, not a range end.
677
- let objectId = prevObjectId + 1n
678
- let payloadLength: number
679
- let payload: Uint8Array
680
- let payloadByteOffset: number
681
- const extensionData = new Uint8Array(0)
682
-
683
- if (flags >= 0x80) {
684
- // End-of-Range marker. Section 11.4.4: "Any other value is a
685
- // PROTOCOL_VIOLATION."
686
- if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
687
- return {
688
- ok: false,
689
- error: new DecodeError(
690
- 'CONSTRAINT_VIOLATION',
691
- `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
692
- r.offset,
693
- ),
694
- }
695
- }
696
- // D7 / Q16: ordinary delta arithmetic (see encodeFetchStream).
697
- const groupDelta = r.readVarInt()
698
- groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
699
- // With a Group ID Delta present, Object ID is the Object ID Delta.
700
- const objectDelta = r.readVarInt()
701
- objectId = objectDelta
702
- // D7 / Q17: Object Payload Length is present, encoded as 0.
703
- payloadLength = Number(r.readVarInt())
704
- payloadByteOffset = r.offset
705
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
706
-
707
- const marker: FetchObjectPayload = {
708
- type: 'object',
709
- byteOffset,
710
- payloadByteOffset,
711
- serializationFlags: flags,
712
- groupId,
713
- groupIdDelta: groupDelta,
714
- // A marker has no Subgroup ID or Priority of its own — Section
715
- // 11.4.4.2 says neither field is present. The interface requires
716
- // both, so they carry the last actual Object's values, which is also
717
- // what a following Object that references "the prior Object" gets.
718
- subgroupId: prevSubgroupId,
719
- objectId,
720
- objectIdDelta: objectDelta,
721
- publisherPriority: prevPriority,
722
- payloadLength,
723
- payload,
724
- extensionData,
725
- }
726
- objects.push(marker)
727
-
728
- // Section 11.4.4.2: prior Group ID and prior Object ID come FROM the
729
- // marker; prior Subgroup ID and prior Priority stay with the last
730
- // actual Object before it, so they are deliberately not updated here.
731
- prevGroupId = groupId
732
- prevObjectId = objectId
733
- first = false
734
- continue
735
- }
736
-
737
- if (flags & 0x40) {
738
- // DATAGRAM mode: no subgroup_id field
739
- const objectIdPresent = (flags & 0x04) !== 0
740
- const groupIdPresent = (flags & 0x08) !== 0
741
- const priorityPresent = (flags & 0x10) !== 0
742
- const propsPresent = (flags & 0x20) !== 0
743
-
744
- let groupDelta: bigint | undefined
745
- if (groupIdPresent) {
746
- // First object's delta IS the absolute group id; subsequent objects:
747
- // groupId = prevGroupId + delta + 1 (ascending).
748
- groupDelta = r.readVarInt()
749
- groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
750
- }
751
- let objectDelta: bigint | undefined
752
- if (objectIdPresent) {
753
- objectDelta = r.readVarInt()
754
- objectId = resolveObjectId(groupIdPresent, objectDelta, prevObjectId)
755
- }
756
- if (objectId > MAX_U64) {
757
- return {
758
- ok: false,
759
- error: new DecodeError(
760
- 'CONSTRAINT_VIOLATION',
761
- 'computed Object ID exceeds 2^64 - 1',
762
- r.offset,
763
- ),
764
- }
765
- }
766
- if (priorityPresent) {
767
- prevPriority = r.readUint8()
768
- }
769
- let objectProperties: Record<string, bigint> | undefined
770
- if (propsPresent) {
771
- const propsLen = Number(r.readVarInt())
772
- if (propsLen > 0) {
773
- objectProperties = decodeObjectProperties(r, propsLen)
774
- }
775
- }
776
- payloadLength = Number(r.readVarInt())
777
- payloadByteOffset = r.offset
778
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
779
-
780
- const obj: FetchObjectPayload = {
781
- type: 'object',
782
- byteOffset,
783
- payloadByteOffset,
784
- serializationFlags: flags,
785
- groupId,
786
- subgroupId: 0n,
787
- objectId,
788
- publisherPriority: prevPriority,
789
- payloadLength,
790
- payload,
791
- extensionData,
792
- }
793
- if (groupDelta !== undefined)
794
- (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
795
- if (objectDelta !== undefined)
796
- (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
797
- if (objectProperties)
798
- (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
799
- objects.push(obj)
800
-
801
- prevGroupId = groupId
802
- prevObjectId = objectId
803
- first = false
804
- continue
805
- }
806
-
807
- const subgroupEncoding = flags & 0x03
808
- const objectIdPresent = (flags & 0x04) !== 0
809
- const groupIdPresent = (flags & 0x08) !== 0
810
- const priorityPresent = (flags & 0x10) !== 0
811
- const propsPresent = (flags & 0x20) !== 0
812
-
813
- let groupDelta: bigint | undefined
814
- if (groupIdPresent) {
815
- groupDelta = r.readVarInt()
816
- groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
817
- } else if (first) {
818
- return {
819
- ok: false,
820
- error: new DecodeError(
821
- 'CONSTRAINT_VIOLATION',
822
- 'First fetch object must include groupId',
823
- r.offset,
824
- ),
825
- }
826
- }
827
-
828
- if (subgroupEncoding === 0x00) {
829
- subgroupId = 0n
830
- } else if (subgroupEncoding === 0x01 || subgroupEncoding === 0x02) {
831
- if (first) {
832
- return {
833
- ok: false,
834
- error: new DecodeError(
835
- 'CONSTRAINT_VIOLATION',
836
- 'First fetch object cannot reference prior subgroupId',
837
- r.offset,
838
- ),
839
- }
840
- }
841
- subgroupId = subgroupEncoding === 0x01 ? prevSubgroupId : prevSubgroupId + 1n
842
- } else {
843
- subgroupId = r.readVarInt()
844
- }
845
-
846
- let objectDelta: bigint | undefined
847
- if (objectIdPresent) {
848
- objectDelta = r.readVarInt()
849
- objectId = resolveObjectId(groupIdPresent, objectDelta, prevObjectId)
850
- } else if (first) {
851
- return {
852
- ok: false,
853
- error: new DecodeError(
854
- 'CONSTRAINT_VIOLATION',
855
- 'First fetch object must include objectId',
856
- r.offset,
857
- ),
858
- }
859
- }
860
- if (objectId > MAX_U64) {
861
- return {
862
- ok: false,
863
- error: new DecodeError(
864
- 'CONSTRAINT_VIOLATION',
865
- 'computed Object ID exceeds 2^64 - 1',
866
- r.offset,
867
- ),
868
- }
869
- }
870
-
871
- if (priorityPresent) {
872
- prevPriority = r.readUint8()
873
- }
874
-
875
- let objectProperties: Record<string, bigint> | undefined
876
- if (propsPresent) {
877
- const propsLen = Number(r.readVarInt())
878
- if (propsLen > 0) {
879
- objectProperties = decodeObjectProperties(r, propsLen)
880
- }
881
- }
882
-
883
- payloadLength = Number(r.readVarInt())
884
- payloadByteOffset = r.offset
885
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
886
-
887
- // A fetch stream carries no Object Status: status is only present on
888
- // subscription-delivered objects (Section 11.2.1.1). That is also the
889
- // one way the two copies of a doubly delivered object differ — the
890
- // subscription copy can carry a status, the fill copy cannot.
891
- const obj: FetchObjectPayload = {
892
- type: 'object',
893
- byteOffset,
894
- payloadByteOffset,
895
- serializationFlags: flags,
896
- groupId,
897
- subgroupId,
898
- objectId,
899
- publisherPriority: prevPriority,
900
- payloadLength,
901
- payload,
902
- extensionData,
903
- }
904
- if (groupDelta !== undefined)
905
- (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
906
- if (objectDelta !== undefined)
907
- (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
908
- if (objectProperties)
909
- (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
910
- objects.push(obj)
911
-
912
- prevGroupId = groupId
913
- prevSubgroupId = subgroupId
914
- prevObjectId = objectId
915
- first = false
978
+ objects.push(readFetchObject(r, 0, st))
916
979
  }
917
980
 
918
981
  return {
@@ -946,66 +1009,107 @@ export function decodeDataStream(
946
1009
 
947
1010
  // ─── Data Stream Decoders ──────────────────────────────────────────────────────
948
1011
 
1012
+ /**
1013
+ * A data stream's bytes, accumulated across chunks.
1014
+ *
1015
+ * Capacity grows geometrically, so most chunks are a single `set` into spare
1016
+ * room rather than a fresh `unread + chunk.length` array and two copies.
1017
+ *
1018
+ * **It never compacts in place.** Object payloads are handed out as views into
1019
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
1020
+ * object that has already been emitted. The consumed prefix is dropped only
1021
+ * when a fresh array is being allocated anyway, which leaves the old array
1022
+ * alive for exactly as long as the views into it are.
1023
+ */
1024
+ class StreamBuffer {
1025
+ private buf = new Uint8Array(0)
1026
+ /** Bytes written into `buf`. */
1027
+ private len = 0
1028
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
1029
+ offset = 0
1030
+ /** Absolute position of `buf[0]` within the stream. */
1031
+ base = 0
1032
+
1033
+ get unread(): number {
1034
+ return this.len - this.offset
1035
+ }
1036
+
1037
+ append(chunk: Uint8Array): void {
1038
+ if (this.len + chunk.length > this.buf.length) {
1039
+ const live = this.len - this.offset
1040
+ const needed = live + chunk.length
1041
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
1042
+ // half free. Sizing to `needed` would leave no room at all, so the next
1043
+ // chunk would reallocate too and the buffer would reallocate on every
1044
+ // single chunk, which is the cost it exists to avoid. Capacity never
1045
+ // shrinks, so it settles at roughly twice the largest `live + chunk`
1046
+ // this stream has seen.
1047
+ let cap = Math.max(4096, this.buf.length)
1048
+ while (cap < needed * 2) cap *= 2
1049
+ // A fresh array every time, including when the capacity is unchanged:
1050
+ // the old one is still pinned by the payload views handed out of it, so
1051
+ // it can be neither compacted nor reused.
1052
+ const next = new Uint8Array(cap)
1053
+ next.set(this.buf.subarray(this.offset, this.len), 0)
1054
+ this.buf = next
1055
+ this.base += this.offset
1056
+ this.len = live
1057
+ this.offset = 0
1058
+ }
1059
+ this.buf.set(chunk, this.len)
1060
+ this.len += chunk.length
1061
+ }
1062
+
1063
+ /**
1064
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
1065
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
1066
+ * not the spare room after them.
1067
+ */
1068
+ written(): Uint8Array {
1069
+ return this.buf.subarray(0, this.len)
1070
+ }
1071
+ }
1072
+
949
1073
  export function createSubgroupStreamDecoder(): TransformStream<
950
1074
  Uint8Array,
951
1075
  SubgroupStreamHeader | ObjectPayload
952
1076
  > {
953
- let buffer = new Uint8Array(0)
954
- let offset = 0
1077
+ const b = new StreamBuffer()
1078
+ let header: SubgroupHeaderFields | null = null
955
1079
  let headerEmitted = false
956
1080
  let prevObjectId = -1n
957
- let firstObject = true
958
- let _propertiesPresent = false
1081
+ let isFirst = true
1082
+
1083
+ function emitHeader(
1084
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
1085
+ h: SubgroupHeaderFields,
1086
+ subgroupId: bigint,
1087
+ ): void {
1088
+ controller.enqueue({
1089
+ type: 'subgroup_header',
1090
+ headerType: h.streamType,
1091
+ trackAlias: h.trackAlias,
1092
+ groupId: h.groupId,
1093
+ subgroupId,
1094
+ publisherPriority: h.publisherPriority,
1095
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
1096
+ ...(h.firstObjectFlag ? { firstObject: true } : {}),
1097
+ })
1098
+ headerEmitted = true
1099
+ }
959
1100
 
960
1101
  return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
961
1102
  transform(chunk, controller) {
962
- if (offset > 0) {
963
- buffer = buffer.subarray(offset)
964
- offset = 0
965
- }
966
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
967
- newBuffer.set(buffer, 0)
968
- newBuffer.set(chunk, buffer.length)
969
- buffer = newBuffer
1103
+ b.append(chunk)
1104
+ // One view and one reader per chunk, not per object: an object is the
1105
+ // thing there are a great many of.
1106
+ const view = b.written()
970
1107
 
971
- if (!headerEmitted) {
1108
+ if (header === null) {
972
1109
  try {
973
- const r = new BufferReader(buffer.subarray(offset))
974
- const typeFlags = r.readVarInt()
975
- const flagsError = subgroupTypeFlagsError(typeFlags)
976
- if (flagsError !== null) {
977
- controller.error(new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0))
978
- return
979
- }
980
- const streamType = Number(typeFlags)
981
-
982
- _propertiesPresent = (streamType & 0x01) !== 0
983
- const subgroupMode = (streamType & 0x06) >> 1
984
- const hasSubgroupField = subgroupMode === 0x02
985
- const hasPriority = (streamType & 0x20) === 0
986
-
987
- const trackAlias = r.readVarInt()
988
- const groupId = r.readVarInt()
989
-
990
- let subgroupId = 0n
991
- if (hasSubgroupField) {
992
- subgroupId = r.readVarInt()
993
- }
994
-
995
- let publisherPriority = 128
996
- if (hasPriority) {
997
- publisherPriority = r.readUint8()
998
- }
999
-
1000
- controller.enqueue({
1001
- type: 'subgroup_header',
1002
- trackAlias,
1003
- groupId,
1004
- subgroupId,
1005
- publisherPriority,
1006
- })
1007
- headerEmitted = true
1008
- offset += r.offset
1110
+ const hr = new BufferReader(view, b.offset)
1111
+ header = readSubgroupHeader(hr)
1112
+ b.offset = hr.offset
1009
1113
  } catch (e) {
1010
1114
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1011
1115
  return
@@ -1013,51 +1117,46 @@ export function createSubgroupStreamDecoder(): TransformStream<
1013
1117
  controller.error(e)
1014
1118
  return
1015
1119
  }
1120
+ // Where the Subgroup ID is the first Object's ID, that Object has not
1121
+ // been read yet. Holding the header back until it has is the only way
1122
+ // to emit the value the one-shot decoder reports; every other mode
1123
+ // knows its Subgroup ID already.
1124
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
1016
1125
  }
1017
1126
 
1018
- while (offset < buffer.length) {
1127
+ const h = header
1128
+ if (h === null) return
1129
+
1130
+ const r = new BufferReader(view, b.offset)
1131
+ while (r.remaining > 0) {
1132
+ // Where this Object starts, so a partial one is re-read from the top
1133
+ // once the rest of it arrives.
1134
+ const start = r.offset
1135
+ let obj: ObjectPayload
1019
1136
  try {
1020
- const r = new BufferReader(buffer.subarray(offset))
1021
- const delta = r.readVarInt()
1022
- let objectId: bigint
1023
- if (firstObject) {
1024
- objectId = delta
1025
- firstObject = false
1026
- } else {
1027
- objectId = prevObjectId + 1n + delta
1028
- }
1029
- let extensionData = new Uint8Array(0)
1030
- if (_propertiesPresent) {
1031
- const extLen = Number(r.readVarInt())
1032
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
1033
- }
1034
- const payloadLength = Number(r.readVarInt())
1035
- const payloadByteOffset = r.offset
1036
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
1037
- controller.enqueue({
1038
- type: 'object',
1039
- objectId,
1040
- objectIdDelta: delta,
1041
- payloadLength,
1042
- payload,
1043
- extensionData,
1044
- byteOffset: 0,
1045
- payloadByteOffset,
1046
- })
1047
- offset += r.offset
1048
- prevObjectId = objectId
1137
+ obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, b.base)
1049
1138
  } catch (e) {
1050
1139
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1051
- break
1140
+ b.offset = start
1141
+ return
1052
1142
  }
1053
1143
  controller.error(e)
1054
1144
  return
1055
1145
  }
1146
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
1147
+ isFirst = false
1148
+ b.offset = r.offset
1149
+ prevObjectId = obj.objectId
1150
+ controller.enqueue(obj)
1056
1151
  }
1057
1152
  },
1058
1153
 
1059
1154
  flush(controller) {
1060
- if (offset < buffer.length) {
1155
+ // A stream carrying a header and no Objects still has a header to report,
1156
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
1157
+ // returns when it finds no first Object to derive one from.
1158
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
1159
+ if (b.unread > 0) {
1061
1160
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1062
1161
  }
1063
1162
  },
@@ -1068,24 +1167,23 @@ export function createFetchStreamDecoder(): TransformStream<
1068
1167
  Uint8Array,
1069
1168
  FetchStreamHeader | ObjectPayload
1070
1169
  > {
1071
- let buffer = new Uint8Array(0)
1072
- let offset = 0
1170
+ const b = new StreamBuffer()
1073
1171
  let headerEmitted = false
1172
+ /**
1173
+ * The delta state the Objects carry between them. DATAGRAM-mode Object IDs
1174
+ * resolve against the previous Object's ID, so a decoder that keeps no state
1175
+ * here reports them wrong.
1176
+ */
1177
+ const st = newFetchObjectState()
1074
1178
 
1075
1179
  return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
1076
1180
  transform(chunk, controller) {
1077
- if (offset > 0) {
1078
- buffer = buffer.subarray(offset)
1079
- offset = 0
1080
- }
1081
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
1082
- newBuffer.set(buffer, 0)
1083
- newBuffer.set(chunk, buffer.length)
1084
- buffer = newBuffer
1181
+ b.append(chunk)
1182
+ const view = b.written()
1085
1183
 
1086
1184
  if (!headerEmitted) {
1087
1185
  try {
1088
- const r = new BufferReader(buffer.subarray(offset))
1186
+ const r = new BufferReader(view, b.offset)
1089
1187
  const streamType = r.readVarInt()
1090
1188
  if (streamType !== FETCH_STREAM_TYPE) {
1091
1189
  controller.error(
@@ -1100,7 +1198,7 @@ export function createFetchStreamDecoder(): TransformStream<
1100
1198
  const requestId = r.readVarInt()
1101
1199
  controller.enqueue({ type: 'fetch_header', requestId })
1102
1200
  headerEmitted = true
1103
- offset += r.offset
1201
+ b.offset = r.offset
1104
1202
  } catch (e) {
1105
1203
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1106
1204
  return
@@ -1110,89 +1208,27 @@ export function createFetchStreamDecoder(): TransformStream<
1110
1208
  }
1111
1209
  }
1112
1210
 
1113
- while (offset < buffer.length) {
1211
+ const r = new BufferReader(view, b.offset)
1212
+ while (r.remaining > 0) {
1213
+ const start = r.offset
1214
+ let obj: FetchObjectPayload
1114
1215
  try {
1115
- const r = new BufferReader(buffer.subarray(offset))
1116
- const flags = Number(r.readVarInt())
1117
-
1118
- if (flags >= 0x80) {
1119
- if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
1120
- controller.error(
1121
- new DecodeError(
1122
- 'CONSTRAINT_VIOLATION',
1123
- `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
1124
- 0,
1125
- ),
1126
- )
1127
- return
1128
- }
1129
- r.readVarInt() // Group ID Delta
1130
- const objectDelta = r.readVarInt()
1131
- const markerPayloadLength = Number(r.readVarInt())
1132
- const markerPayloadByteOffset = r.offset
1133
- controller.enqueue({
1134
- type: 'object',
1135
- objectId: objectDelta,
1136
- objectIdDelta: objectDelta,
1137
- payloadLength: markerPayloadLength,
1138
- payload: new Uint8Array(0),
1139
- extensionData: new Uint8Array(0),
1140
- byteOffset: 0,
1141
- payloadByteOffset: markerPayloadByteOffset,
1142
- })
1143
- offset += r.offset
1144
- continue
1145
- }
1146
-
1147
- const objectIdPresent = (flags & 0x04) !== 0
1148
- const groupIdPresent = (flags & 0x08) !== 0
1149
- const priorityPresent = (flags & 0x10) !== 0
1150
- const extensionsPresent = (flags & 0x20) !== 0
1151
- const subgroupEncoding = flags & 0x03
1152
- const datagramMode = (flags & 0x40) !== 0
1153
-
1154
- if (groupIdPresent) r.readVarInt()
1155
- if (!datagramMode && subgroupEncoding === 0x03) r.readVarInt()
1156
- let objectId = 0n
1157
- let objectIdDelta: bigint | undefined
1158
- if (objectIdPresent) {
1159
- objectIdDelta = r.readVarInt()
1160
- objectId = objectIdDelta
1161
- }
1162
- if (priorityPresent) r.readUint8()
1163
- let extensionData = new Uint8Array(0)
1164
- if (extensionsPresent) {
1165
- const extLen = Number(r.readVarInt())
1166
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
1167
- }
1168
- const payloadLength = Number(r.readVarInt())
1169
- const payloadByteOffset = r.offset
1170
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
1171
- const event: ObjectPayload = {
1172
- type: 'object',
1173
- objectId,
1174
- payloadLength,
1175
- payload,
1176
- extensionData,
1177
- byteOffset: 0,
1178
- payloadByteOffset,
1179
- }
1180
- if (objectIdDelta !== undefined)
1181
- (event as unknown as Record<string, unknown>).objectIdDelta = objectIdDelta
1182
- controller.enqueue(event)
1183
- offset += r.offset
1216
+ obj = readFetchObject(r, b.base, st)
1184
1217
  } catch (e) {
1185
1218
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1186
- break
1219
+ b.offset = start
1220
+ return
1187
1221
  }
1188
1222
  controller.error(e)
1189
1223
  return
1190
1224
  }
1225
+ b.offset = r.offset
1226
+ controller.enqueue(obj)
1191
1227
  }
1192
1228
  },
1193
1229
 
1194
1230
  flush(controller) {
1195
- if (offset < buffer.length) {
1231
+ if (b.unread > 0) {
1196
1232
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1197
1233
  }
1198
1234
  },
@@ -1200,56 +1236,80 @@ export function createFetchStreamDecoder(): TransformStream<
1200
1236
  }
1201
1237
 
1202
1238
  export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
1203
- let buffer = new Uint8Array(0)
1204
- let offset = 0
1205
- let inner: TransformStream<Uint8Array, DataStreamEvent> | null = null
1206
-
1207
- return new TransformStream<Uint8Array, DataStreamEvent>({
1208
- transform(chunk, controller) {
1209
- if (offset > 0) {
1210
- buffer = buffer.subarray(offset)
1211
- offset = 0
1212
- }
1213
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
1214
- newBuffer.set(buffer, 0)
1215
- newBuffer.set(chunk, buffer.length)
1216
- buffer = newBuffer
1217
-
1218
- if (inner === null) {
1219
- if (offset >= buffer.length) return
1220
- const firstByte = buffer[offset]!
1221
-
1222
- if (isValidSubgroupType(firstByte)) {
1223
- const decoder = createSubgroupStreamDecoder()
1224
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
1225
- } else if (firstByte === 0x05) {
1226
- const decoder = createFetchStreamDecoder()
1227
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
1228
- } else {
1229
- controller.error(
1230
- new DecodeError(
1231
- 'CONSTRAINT_VIOLATION',
1232
- `Unknown data stream type: 0x${firstByte.toString(16)}`,
1233
- 0,
1234
- ),
1235
- )
1236
- return
1239
+ /**
1240
+ * Delegation that actually delegates.
1241
+ *
1242
+ * The inner decoder's readable is pumped into this one's controller as the
1243
+ * bytes arrive. Buffering chunks locally and decoding the whole stream in
1244
+ * `flush` instead would deliver every event at end-of-stream, which on a
1245
+ * subscription that stays open means no events at all.
1246
+ *
1247
+ * The first byte decides which decoder to use, on exactly the range this
1248
+ * draft accepts.
1249
+ */
1250
+ let writer: WritableStreamDefaultWriter<Uint8Array> | null = null
1251
+ let pump: Promise<void> = Promise.resolve()
1252
+ let broken = false
1253
+
1254
+ function attach(
1255
+ inner: TransformStream<Uint8Array, DataStreamEvent>,
1256
+ controller: TransformStreamDefaultController<DataStreamEvent>,
1257
+ ): void {
1258
+ writer = inner.writable.getWriter()
1259
+ const reader = inner.readable.getReader()
1260
+ pump = (async () => {
1261
+ try {
1262
+ for (;;) {
1263
+ const { done, value } = await reader.read()
1264
+ if (done) break
1265
+ controller.enqueue(value)
1266
+ }
1267
+ } catch (e) {
1268
+ // The inner decoder rejected the stream. It owns the diagnosis, so its
1269
+ // error is the one that surfaces.
1270
+ if (!broken) {
1271
+ broken = true
1272
+ controller.error(e)
1237
1273
  }
1238
1274
  }
1239
- },
1275
+ })()
1276
+ }
1240
1277
 
1241
- flush(controller) {
1242
- if (offset >= buffer.length) return
1243
- const view = buffer.subarray(offset)
1278
+ async function feed(bytes: Uint8Array): Promise<void> {
1279
+ if (writer === null || broken || bytes.length === 0) return
1280
+ try {
1281
+ await writer.write(bytes)
1282
+ } catch {
1283
+ // Writing to an errored inner stream throws the same error the pump is
1284
+ // already reporting; swallow it here so it is reported once.
1285
+ broken = true
1286
+ }
1287
+ }
1244
1288
 
1245
- const firstByte = view[0]!
1246
- let result: DecodeResult<Draft20DataStream>
1289
+ return new TransformStream<Uint8Array, DataStreamEvent>({
1290
+ async transform(chunk, controller) {
1291
+ if (broken) return
1292
+ if (writer !== null) {
1293
+ await feed(chunk)
1294
+ return
1295
+ }
1296
+ // One byte is enough to choose, and until there is one there is nothing
1297
+ // to choose from.
1298
+ if (chunk.length === 0) return
1299
+ const firstByte = chunk[0]!
1247
1300
 
1248
1301
  if (isValidSubgroupType(firstByte)) {
1249
- result = decodeSubgroupStream(view)
1302
+ attach(
1303
+ createSubgroupStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1304
+ controller,
1305
+ )
1250
1306
  } else if (firstByte === 0x05) {
1251
- result = decodeFetchStream(view)
1307
+ attach(
1308
+ createFetchStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1309
+ controller,
1310
+ )
1252
1311
  } else {
1312
+ broken = true
1253
1313
  controller.error(
1254
1314
  new DecodeError(
1255
1315
  'CONSTRAINT_VIOLATION',
@@ -1260,32 +1320,19 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
1260
1320
  return
1261
1321
  }
1262
1322
 
1263
- if (!result.ok) {
1264
- controller.error(result.error)
1265
- return
1266
- }
1323
+ // The type byte is part of the stream the inner decoder reads, so the
1324
+ // whole chunk goes in, first byte included.
1325
+ await feed(chunk)
1326
+ },
1267
1327
 
1268
- const stream = result.value
1269
- if (stream.type === 'subgroup') {
1270
- controller.enqueue({
1271
- type: 'subgroup_header',
1272
- trackAlias: stream.trackAlias,
1273
- groupId: stream.groupId,
1274
- subgroupId: stream.subgroupId,
1275
- publisherPriority: stream.publisherPriority,
1276
- })
1277
- for (const obj of stream.objects) {
1278
- controller.enqueue(obj)
1279
- }
1280
- } else if (stream.type === 'fetch') {
1281
- controller.enqueue({
1282
- type: 'fetch_header',
1283
- requestId: stream.requestId,
1284
- })
1285
- for (const obj of stream.objects) {
1286
- controller.enqueue(obj)
1287
- }
1328
+ async flush() {
1329
+ if (writer === null) return
1330
+ try {
1331
+ await writer.close()
1332
+ } catch {
1333
+ // Reported by the pump.
1288
1334
  }
1335
+ await pump
1289
1336
  },
1290
1337
  })
1291
1338
  }