@moqtap/codec 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/auth-redaction-DsdLq4Qs.d.cts +102 -0
  3. package/dist/auth-redaction-DsdLq4Qs.d.ts +102 -0
  4. package/dist/{chunk-RYKQXBG6.cjs → chunk-27K5BUAG.cjs} +481 -517
  5. package/dist/{chunk-WCPENYNE.js → chunk-3RNQMGKA.js} +359 -390
  6. package/dist/{chunk-TSUZ6GJV.js → chunk-5AWIXOLB.js} +438 -469
  7. package/dist/{chunk-4XLYCT5Q.cjs → chunk-5IMHJIAZ.cjs} +52 -2
  8. package/dist/{chunk-KTNERCTZ.cjs → chunk-5JGZCKJO.cjs} +488 -518
  9. package/dist/{chunk-WCTMPDUX.cjs → chunk-73K4OSAN.cjs} +401 -432
  10. package/dist/{chunk-N2V7Z37J.js → chunk-7VQKRGRD.js} +19 -3
  11. package/dist/{chunk-BIBMDTU5.cjs → chunk-A3UCQOIB.cjs} +486 -517
  12. package/dist/{chunk-K2B3OVFK.js → chunk-A7PDMVXM.js} +24 -3
  13. package/dist/{chunk-VZHL2U2N.cjs → chunk-A7VQO6YC.cjs} +54 -38
  14. package/dist/{chunk-AQ445VV7.cjs → chunk-ACQHZLRZ.cjs} +66 -46
  15. package/dist/{chunk-L5DRACXS.js → chunk-BBICOMW6.js} +25 -3
  16. package/dist/{chunk-T4VWPKDG.cjs → chunk-BNIUVOCJ.cjs} +529 -599
  17. package/dist/{chunk-UKKDOOUO.cjs → chunk-CNC6ANS4.cjs} +58 -37
  18. package/dist/{chunk-OZRK73NQ.js → chunk-FDLQSWHK.js} +433 -469
  19. package/dist/{chunk-IJAF6PAY.js → chunk-FFNOE4UR.js} +19 -3
  20. package/dist/{chunk-CZNW3R5E.cjs → chunk-GMENT63T.cjs} +54 -38
  21. package/dist/{chunk-XCRCAUGI.cjs → chunk-HM7BLRVP.cjs} +291 -297
  22. package/dist/{chunk-XHAP2YST.js → chunk-HZONHFNO.js} +19 -3
  23. package/dist/{chunk-SDJTSMOW.cjs → chunk-ILYABWDK.cjs} +60 -38
  24. package/dist/{chunk-IDV2JYJ6.cjs → chunk-KRSFTZVO.cjs} +56 -40
  25. package/dist/{chunk-RIIA4ALQ.cjs → chunk-KSPH6KMH.cjs} +54 -38
  26. package/dist/{chunk-OUNN4IQB.cjs → chunk-KTY4LAUV.cjs} +482 -497
  27. package/dist/{chunk-RZTLQAKF.js → chunk-OJNLJT5D.js} +0 -1
  28. package/dist/{chunk-FT3HDNIB.js → chunk-PJK2ZS3I.js} +467 -537
  29. package/dist/{chunk-G53WYUXL.js → chunk-PSMZKHOC.js} +441 -456
  30. package/dist/{chunk-7RE6KSIY.cjs → chunk-Q5JQGHWY.cjs} +0 -1
  31. package/dist/{chunk-BX2N7TZM.js → chunk-SQTUQY5E.js} +31 -11
  32. package/dist/{chunk-CF5DDD3Q.js → chunk-WCRLAKDW.js} +239 -245
  33. package/dist/{chunk-KYVTM5VI.js → chunk-WL2YUEXH.js} +19 -3
  34. package/dist/{chunk-KGDVC4O6.js → chunk-YCNIVZS7.js} +439 -469
  35. package/dist/{chunk-KLPWJU6W.js → chunk-YRZQVNZO.js} +50 -0
  36. package/dist/{codec-CFWu5KVT.d.cts → codec-76xzrLTL.d.cts} +21 -2
  37. package/dist/{codec-gl4_QEva.d.ts → codec-8p40WdLg.d.ts} +20 -1
  38. package/dist/{codec-Dl_QIXMz.d.ts → codec-BN2Bu0KI.d.ts} +20 -1
  39. package/dist/{codec-Bux_Q40X.d.ts → codec-Bb6GKTrv.d.ts} +21 -2
  40. package/dist/{codec-EvybrBuY.d.cts → codec-BgpfHoAd.d.cts} +20 -1
  41. package/dist/{codec-alVA27Uc.d.ts → codec-BkheTXO7.d.ts} +20 -1
  42. package/dist/{codec-Czejcmxr.d.cts → codec-BwVDkokh.d.cts} +21 -2
  43. package/dist/{codec-BrTi4K7r.d.cts → codec-C5s1eBJX.d.cts} +21 -2
  44. package/dist/codec-CM5yd6n3.d.ts +38 -0
  45. package/dist/{codec-CWnf8mn_.d.cts → codec-Cb8HC3XV.d.cts} +21 -2
  46. package/dist/codec-Clu_skx8.d.cts +38 -0
  47. package/dist/{codec-DzkdW4kO.d.cts → codec-CpOpemR8.d.cts} +20 -1
  48. package/dist/{codec-CXCjya_G.d.cts → codec-Cshdl9Tt.d.cts} +20 -1
  49. package/dist/{codec-f7vxeVIS.d.cts → codec-DELe00w_.d.cts} +21 -2
  50. package/dist/{codec-DNmiG2e0.d.cts → codec-DHzKAUCa.d.cts} +20 -1
  51. package/dist/{codec--CVeqGQv.d.ts → codec-DdVBtvx7.d.ts} +20 -1
  52. package/dist/{codec-CBdOIfMY.d.ts → codec-DlxGuYMv.d.ts} +21 -2
  53. package/dist/{codec-BNy5syCU.d.ts → codec-DxtgPxJS.d.ts} +20 -1
  54. package/dist/{codec-guzqtfMk.d.cts → codec-GAY4lybi.d.cts} +20 -1
  55. package/dist/{codec-Cfo_z5ET.d.cts → codec-I8u2pfS4.d.cts} +20 -1
  56. package/dist/{codec-BxQTybUX.d.cts → codec-XLrnreeu.d.cts} +21 -2
  57. package/dist/{codec-DOmAmFkI.d.ts → codec-c0YEwwWn.d.ts} +20 -1
  58. package/dist/{codec-DaXjRrOV.d.ts → codec-ejDu-va6.d.ts} +21 -2
  59. package/dist/{codec-C3mdsGx3.d.ts → codec-kaBUzdpV.d.ts} +21 -2
  60. package/dist/{codec-CwZ6OCPP.d.ts → codec-mVYqkIUF.d.ts} +21 -2
  61. package/dist/{codec-08ZOr5wI.d.ts → codec-qSbO5nOa.d.ts} +21 -2
  62. package/dist/{codec-Cm3D261p.d.ts → codec-yYKGOipj.d.ts} +20 -1
  63. package/dist/{codec-Bd60SGKH.d.cts → codec-zfzg9uiy.d.cts} +20 -1
  64. package/dist/draft07.cjs +48 -4
  65. package/dist/draft07.d.cts +42 -2
  66. package/dist/draft07.d.ts +42 -2
  67. package/dist/draft07.js +49 -5
  68. package/dist/draft08.cjs +76 -3
  69. package/dist/draft08.d.cts +70 -2
  70. package/dist/draft08.d.ts +70 -2
  71. package/dist/draft08.js +77 -4
  72. package/dist/draft09.cjs +76 -3
  73. package/dist/draft09.d.cts +70 -2
  74. package/dist/draft09.d.ts +70 -2
  75. package/dist/draft09.js +77 -4
  76. package/dist/draft10.cjs +76 -3
  77. package/dist/draft10.d.cts +70 -2
  78. package/dist/draft10.d.ts +70 -2
  79. package/dist/draft10.js +77 -4
  80. package/dist/draft11.cjs +104 -3
  81. package/dist/draft11.d.cts +98 -2
  82. package/dist/draft11.d.ts +98 -2
  83. package/dist/draft11.js +105 -4
  84. package/dist/draft12.cjs +113 -3
  85. package/dist/draft12.d.cts +107 -2
  86. package/dist/draft12.d.ts +107 -2
  87. package/dist/draft12.js +114 -4
  88. package/dist/draft13.cjs +105 -3
  89. package/dist/draft13.d.cts +99 -2
  90. package/dist/draft13.d.ts +99 -2
  91. package/dist/draft13.js +106 -4
  92. package/dist/draft14.cjs +107 -3
  93. package/dist/draft14.d.cts +101 -28
  94. package/dist/draft14.d.ts +101 -28
  95. package/dist/draft14.js +108 -4
  96. package/dist/draft15-session.d.cts +1 -1
  97. package/dist/draft15-session.d.ts +1 -1
  98. package/dist/draft15.cjs +69 -3
  99. package/dist/draft15.d.cts +65 -22
  100. package/dist/draft15.d.ts +65 -22
  101. package/dist/draft15.js +70 -4
  102. package/dist/draft16-session.cjs +2 -2
  103. package/dist/draft16-session.d.cts +1 -1
  104. package/dist/draft16-session.d.ts +1 -1
  105. package/dist/draft16-session.js +1 -1
  106. package/dist/draft16.cjs +76 -8
  107. package/dist/draft16.d.cts +67 -13
  108. package/dist/draft16.d.ts +67 -13
  109. package/dist/draft16.js +73 -5
  110. package/dist/draft17-session.d.cts +1 -1
  111. package/dist/draft17-session.d.ts +1 -1
  112. package/dist/draft17.cjs +77 -3
  113. package/dist/draft17.d.cts +73 -4
  114. package/dist/draft17.d.ts +73 -4
  115. package/dist/draft17.js +78 -4
  116. package/dist/draft18-session.d.cts +1 -1
  117. package/dist/draft18-session.d.ts +1 -1
  118. package/dist/draft18.cjs +8 -3
  119. package/dist/draft18.d.cts +4 -3
  120. package/dist/draft18.d.ts +4 -3
  121. package/dist/draft18.js +9 -4
  122. package/dist/draft19-session.d.cts +1 -1
  123. package/dist/draft19-session.d.ts +1 -1
  124. package/dist/draft19.cjs +11 -4
  125. package/dist/draft19.d.cts +4 -3
  126. package/dist/draft19.d.ts +4 -3
  127. package/dist/draft19.js +12 -5
  128. package/dist/draft20-session.d.cts +1 -1
  129. package/dist/draft20-session.d.ts +1 -1
  130. package/dist/draft20.cjs +8 -3
  131. package/dist/draft20.d.cts +4 -3
  132. package/dist/draft20.d.ts +4 -3
  133. package/dist/draft20.js +9 -4
  134. package/dist/index.cjs +30 -30
  135. package/dist/index.d.cts +21 -20
  136. package/dist/index.d.ts +21 -20
  137. package/dist/index.js +15 -15
  138. package/dist/session.cjs +2 -2
  139. package/dist/session.d.cts +6 -6
  140. package/dist/session.d.ts +6 -6
  141. package/dist/session.js +1 -1
  142. package/dist/{types-C1rxh-id.d.cts → types-Btp416jt.d.cts} +8 -0
  143. package/dist/{types-C1rxh-id.d.ts → types-Btp416jt.d.ts} +8 -0
  144. package/dist/{types-CZTzbIzQ.d.cts → types-BxTL1NXw.d.cts} +7 -0
  145. package/dist/{types-CZTzbIzQ.d.ts → types-BxTL1NXw.d.ts} +7 -0
  146. package/dist/{types-pShrs3fN.d.cts → types-CuibHYcf.d.cts} +7 -0
  147. package/dist/{types-pShrs3fN.d.ts → types-CuibHYcf.d.ts} +7 -0
  148. package/dist/{types-CY-QWxd-.d.cts → types-CwN7AEoL.d.cts} +13 -1
  149. package/dist/{types-CY-QWxd-.d.ts → types-CwN7AEoL.d.ts} +13 -1
  150. package/dist/{types-D4bmW8rY.d.cts → types-DAnalt5K.d.cts} +7 -0
  151. package/dist/{types-D4bmW8rY.d.ts → types-DAnalt5K.d.ts} +7 -0
  152. package/dist/{types-Bg86GGVZ.d.cts → types-DxXNP32l.d.cts} +8 -0
  153. package/dist/{types-Bg86GGVZ.d.ts → types-DxXNP32l.d.ts} +8 -0
  154. package/package.json +3 -3
  155. package/src/core/auth-redaction.ts +192 -0
  156. package/src/drafts/draft07/codec.ts +40 -0
  157. package/src/drafts/draft07/error-codes.ts +44 -0
  158. package/src/drafts/draft07/index.ts +15 -0
  159. package/src/drafts/draft08/codec.ts +40 -0
  160. package/src/drafts/draft08/error-codes.ts +76 -0
  161. package/src/drafts/draft08/index.ts +21 -0
  162. package/src/drafts/draft09/codec.ts +40 -0
  163. package/src/drafts/draft09/error-codes.ts +76 -0
  164. package/src/drafts/draft09/index.ts +21 -0
  165. package/src/drafts/draft10/codec.ts +40 -0
  166. package/src/drafts/draft10/error-codes.ts +76 -0
  167. package/src/drafts/draft10/index.ts +21 -0
  168. package/src/drafts/draft11/codec.ts +60 -0
  169. package/src/drafts/draft11/error-codes.ts +106 -0
  170. package/src/drafts/draft11/index.ts +23 -0
  171. package/src/drafts/draft12/codec.ts +61 -0
  172. package/src/drafts/draft12/error-codes.ts +116 -0
  173. package/src/drafts/draft12/index.ts +25 -0
  174. package/src/drafts/draft13/codec.ts +69 -11
  175. package/src/drafts/draft13/error-codes.ts +107 -0
  176. package/src/drafts/draft13/index.ts +23 -0
  177. package/src/drafts/draft13/types.ts +1 -1
  178. package/src/drafts/draft14/codec.ts +65 -4
  179. package/src/drafts/draft14/data-streams.ts +331 -259
  180. package/src/drafts/draft14/error-codes.ts +109 -0
  181. package/src/drafts/draft14/index.ts +23 -0
  182. package/src/drafts/draft15/codec.ts +65 -8
  183. package/src/drafts/draft15/data-streams.ts +489 -444
  184. package/src/drafts/draft15/error-codes.ts +66 -0
  185. package/src/drafts/draft15/index.ts +15 -0
  186. package/src/drafts/draft15/types.ts +7 -0
  187. package/src/drafts/draft16/codec.ts +146 -52
  188. package/src/drafts/draft16/data-streams.ts +513 -442
  189. package/src/drafts/draft16/error-codes.ts +68 -0
  190. package/src/drafts/draft16/index.ts +15 -0
  191. package/src/drafts/draft16/messages.ts +4 -4
  192. package/src/drafts/draft16/session-fsm.ts +0 -1
  193. package/src/drafts/draft16/types.ts +22 -8
  194. package/src/drafts/draft17/codec.ts +73 -12
  195. package/src/drafts/draft17/data-streams.ts +567 -488
  196. package/src/drafts/draft17/index.ts +15 -0
  197. package/src/drafts/draft17/types.ts +7 -0
  198. package/src/drafts/draft18/codec.ts +66 -9
  199. package/src/drafts/draft18/data-streams.ts +570 -482
  200. package/src/drafts/draft18/index.ts +3 -0
  201. package/src/drafts/draft18/types.ts +16 -8
  202. package/src/drafts/draft19/codec.ts +70 -10
  203. package/src/drafts/draft19/data-streams.ts +570 -482
  204. package/src/drafts/draft19/error-codes.ts +3 -1
  205. package/src/drafts/draft19/index.ts +3 -0
  206. package/src/drafts/draft19/messages.ts +3 -1
  207. package/src/drafts/draft19/types.ts +22 -11
  208. package/src/drafts/draft20/codec.ts +90 -23
  209. package/src/drafts/draft20/data-streams.ts +610 -563
  210. package/src/drafts/draft20/index.ts +3 -0
  211. package/src/drafts/draft20/types.ts +14 -2
  212. package/dist/codec-BJ7BHuiZ.d.ts +0 -18
  213. package/dist/codec-C6Mx4bwz.d.cts +0 -18
@@ -139,98 +139,151 @@ export function encodeFetchStream(stream: FetchStream): Uint8Array {
139
139
  * Decode a subgroup data stream from raw bytes.
140
140
  * Accepts types 0x10-0x15 and 0x18-0x1D. Delta-decodes Object IDs.
141
141
  */
142
- export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
143
- try {
144
- const r = new BufferReader(bytes)
145
- const streamType = Number(r.readVarInt())
142
+ /**
143
+ * The SUBGROUP_HEADER fields, read once for both decoders.
144
+ *
145
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` used to parse this
146
+ * header separately and they drifted: the incremental one dropped the header
147
+ * flags, reported no Type Flags, and left the Subgroup ID at zero under the
148
+ * mode that derives it from the first Object. One reader means there is
149
+ * nothing left to keep in sync.
150
+ */
151
+ interface SubgroupHeaderFields {
152
+ readonly streamType: number
153
+ readonly extensionsPresent: boolean
154
+ readonly subgroupIsFirstObjId: boolean
155
+ readonly trackAlias: bigint
156
+ readonly groupId: bigint
157
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
158
+ readonly subgroupId: bigint
159
+ readonly publisherPriority: number
160
+ }
146
161
 
147
- if (!isValidSubgroupType(streamType)) {
148
- return {
149
- ok: false,
150
- error: new DecodeError(
151
- 'CONSTRAINT_VIOLATION',
152
- `Expected subgroup stream type 0x10-0x1D, got 0x${streamType.toString(16)}`,
153
- 0,
154
- ),
155
- }
156
- }
162
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
163
+ const streamType = Number(r.readVarInt())
164
+ if (!isValidSubgroupType(streamType)) {
165
+ throw new DecodeError(
166
+ 'CONSTRAINT_VIOLATION',
167
+ `Expected subgroup stream type 0x10-0x1D, got 0x${streamType.toString(16)}`,
168
+ 0,
169
+ )
170
+ }
157
171
 
158
- // Decode type flags
159
- const extensionsPresent = (streamType & 0x01) !== 0
160
- const hasSubgroupField = (streamType & 0x04) !== 0
161
- const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
172
+ const extensionsPresent = (streamType & 0x01) !== 0
173
+ const hasSubgroupField = (streamType & 0x04) !== 0
174
+ const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
162
175
 
163
- const trackAlias = r.readVarInt()
164
- const groupId = r.readVarInt()
176
+ const trackAlias = r.readVarInt()
177
+ const groupId = r.readVarInt()
165
178
 
166
- let subgroupId = 0n
167
- if (hasSubgroupField) {
168
- subgroupId = r.readVarInt()
169
- }
170
- const publisherPriority = r.readUint8()
179
+ let subgroupId = 0n
180
+ if (hasSubgroupField) {
181
+ subgroupId = r.readVarInt()
182
+ }
183
+
184
+ const publisherPriority = r.readUint8()
185
+
186
+ return {
187
+ streamType,
188
+ extensionsPresent,
189
+ subgroupIsFirstObjId,
190
+ trackAlias,
191
+ groupId,
192
+ subgroupId,
193
+ publisherPriority,
194
+ }
195
+ }
196
+
197
+ /**
198
+ * One Object off a subgroup stream, for both decoders.
199
+ *
200
+ * `base` is the absolute offset within the stream that `r.offset === 0`
201
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
202
+ * stream and passes 0; the incremental decoder reads from a window over its
203
+ * own buffer and passes that window's position. That is what makes
204
+ * `byteOffset` mean the same thing on both paths, instead of being hardcoded
205
+ * to zero on one of them.
206
+ *
207
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
208
+ * incremental decoder reads as "wait for more bytes" rather than as an error.
209
+ */
210
+ function readSubgroupObject(
211
+ r: BufferReader,
212
+ extensionsPresent: boolean,
213
+ isFirst: boolean,
214
+ prevObjectId: bigint,
215
+ base: number,
216
+ ): ObjectPayload {
217
+ const byteOffset = base + r.offset
218
+ const delta = r.readVarInt()
219
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
220
+
221
+ let extensionData = new Uint8Array(0)
222
+ if (extensionsPresent) {
223
+ const extLen = Number(r.readVarInt())
224
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
225
+ }
226
+
227
+ const payloadLength = Number(r.readVarInt())
228
+ let payload: Uint8Array
229
+ let status: bigint | undefined
230
+ let payloadByteOffset: number
231
+ if (payloadLength === 0) {
232
+ // The Object Status field is sent only when the Payload Length is zero.
233
+ // Skipping it leaves the varint to be read as the next Object's Object ID
234
+ // Delta, which desynchronises the rest of the stream silently.
235
+ status = r.readVarInt()
236
+ payloadByteOffset = base + r.offset
237
+ payload = new Uint8Array(0)
238
+ } else {
239
+ payloadByteOffset = base + r.offset
240
+ payload = r.readBytesView(payloadLength)
241
+ }
242
+
243
+ const obj: ObjectPayload = {
244
+ type: 'object',
245
+ byteOffset,
246
+ payloadByteOffset,
247
+ objectId,
248
+ payloadLength,
249
+ payload,
250
+ extensionData,
251
+ }
252
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
253
+ return obj
254
+ }
255
+
256
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
257
+ try {
258
+ const r = new BufferReader(bytes)
259
+ const h = readSubgroupHeader(r)
171
260
 
172
261
  const objects: ObjectPayload[] = []
173
262
  let prevObjectId = -1n
174
- let firstObject = true
263
+ let isFirst = true
264
+ let subgroupId = h.subgroupId
175
265
 
176
266
  while (r.remaining > 0) {
177
- const byteOffset = r.offset
178
- const delta = r.readVarInt()
179
- let objectId: bigint
180
- if (firstObject) {
181
- objectId = delta
182
- if (subgroupIsFirstObjId) {
183
- subgroupId = objectId
184
- }
185
- firstObject = false
186
- } else {
187
- objectId = prevObjectId + 1n + delta
188
- }
189
-
190
- // Read extension headers if type indicates they're present
191
- let extensionData = new Uint8Array(0)
192
- if (extensionsPresent) {
193
- const extLen = Number(r.readVarInt())
194
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
195
- }
196
-
197
- const payloadLength = Number(r.readVarInt())
198
- let payload: Uint8Array
199
- let status: bigint | undefined
200
- let payloadByteOffset: number
201
- if (payloadLength === 0) {
202
- status = r.readVarInt()
203
- payloadByteOffset = r.offset
204
- payload = new Uint8Array(0)
205
- } else {
206
- payloadByteOffset = r.offset
207
- payload = r.readBytesView(payloadLength)
208
- }
209
- const obj: ObjectPayload = {
210
- type: 'object',
211
- byteOffset,
212
- payloadByteOffset,
213
- objectId,
214
- payloadLength,
215
- payload,
216
- extensionData,
217
- }
218
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
267
+ const obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, 0)
268
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
269
+ isFirst = false
219
270
  objects.push(obj)
220
- prevObjectId = objectId
271
+ prevObjectId = obj.objectId
272
+ }
273
+
274
+ const result: SubgroupStream = {
275
+ type: 'subgroup',
276
+ headerType: h.streamType,
277
+ trackAlias: h.trackAlias,
278
+ groupId: h.groupId,
279
+ subgroupId,
280
+ publisherPriority: h.publisherPriority,
281
+ objects,
221
282
  }
222
283
 
223
284
  return {
224
285
  ok: true,
225
- value: {
226
- type: 'subgroup',
227
- headerType: streamType,
228
- trackAlias,
229
- groupId,
230
- subgroupId,
231
- publisherPriority,
232
- objects,
233
- },
286
+ value: result,
234
287
  bytesRead: r.offset,
235
288
  }
236
289
  } catch (e) {
@@ -239,10 +292,6 @@ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupSt
239
292
  }
240
293
  }
241
294
 
242
- /**
243
- * Decode a datagram object from raw bytes.
244
- * Accepts types 0x00-0x07 and 0x20-0x21.
245
- */
246
295
  export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject> {
247
296
  try {
248
297
  const r = new BufferReader(bytes)
@@ -318,6 +367,66 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
318
367
  * Decode a fetch data stream from raw bytes.
319
368
  * Draft-14 fetch: type 0x05, per-object has all fields (no serialization flags).
320
369
  */
370
+ /**
371
+ * Nothing is carried between fetch Objects in this draft: Group ID,
372
+ * Subgroup ID, Object ID and Priority are all absolute on the wire, so the
373
+ * reader is a pure function of the bytes in front of it.
374
+ */
375
+ type FetchObjectState = Record<string, never>
376
+
377
+ function newFetchObjectState(): FetchObjectState {
378
+ return {}
379
+ }
380
+
381
+ /**
382
+ * One Object off a fetch stream, for both decoders.
383
+ *
384
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
385
+ * cannot report a different shape for the same bytes. It used to return a
386
+ * bare ObjectPayload -- no Serialization Flags, no Group or Subgroup ID, no
387
+ * Priority, no deltas -- with every `byteOffset` zero, and in DATAGRAM mode
388
+ * it resolved the Object ID against state it never carried.
389
+ *
390
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
391
+ * the one-shot decoder, the buffer window's position for the incremental one.
392
+ */
393
+ function readFetchObject(r: BufferReader, base: number, _st: FetchObjectState): FetchObjectPayload {
394
+ const byteOffset = base + r.offset
395
+ const groupId = r.readVarInt()
396
+ const subgroupId = r.readVarInt()
397
+ const objectId = r.readVarInt()
398
+ const publisherPriority = r.readUint8()
399
+ const extLen = Number(r.readVarInt())
400
+ const extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
401
+ const payloadLength = Number(r.readVarInt())
402
+ let payload: Uint8Array
403
+ let status: bigint | undefined
404
+ let payloadByteOffset: number
405
+ if (payloadLength === 0) {
406
+ status = r.readVarInt()
407
+ payloadByteOffset = base + r.offset
408
+ payload = new Uint8Array(0)
409
+ } else {
410
+ payloadByteOffset = base + r.offset
411
+ payload = r.readBytesView(payloadLength)
412
+ }
413
+ const obj: FetchObjectPayload = {
414
+ type: 'object',
415
+ byteOffset,
416
+ payloadByteOffset,
417
+ groupId,
418
+ subgroupId,
419
+ objectId,
420
+ publisherPriority,
421
+ payloadLength,
422
+ payload,
423
+ extensionData,
424
+ }
425
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
426
+
427
+ return obj
428
+ }
429
+
321
430
  export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
322
431
  try {
323
432
  const r = new BufferReader(bytes)
@@ -334,40 +443,10 @@ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
334
443
  }
335
444
  const requestId = r.readVarInt()
336
445
  const objects: FetchObjectPayload[] = []
446
+ const st = newFetchObjectState()
447
+
337
448
  while (r.remaining > 0) {
338
- const byteOffset = r.offset
339
- const groupId = r.readVarInt()
340
- const subgroupId = r.readVarInt()
341
- const objectId = r.readVarInt()
342
- const publisherPriority = r.readUint8()
343
- const extLen = Number(r.readVarInt())
344
- const extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
345
- const payloadLength = Number(r.readVarInt())
346
- let payload: Uint8Array
347
- let status: bigint | undefined
348
- let payloadByteOffset: number
349
- if (payloadLength === 0) {
350
- status = r.readVarInt()
351
- payloadByteOffset = r.offset
352
- payload = new Uint8Array(0)
353
- } else {
354
- payloadByteOffset = r.offset
355
- payload = r.readBytesView(payloadLength)
356
- }
357
- const obj: FetchObjectPayload = {
358
- type: 'object',
359
- byteOffset,
360
- payloadByteOffset,
361
- groupId,
362
- subgroupId,
363
- objectId,
364
- publisherPriority,
365
- payloadLength,
366
- payload,
367
- extensionData,
368
- }
369
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
370
- objects.push(obj)
449
+ objects.push(readFetchObject(r, 0, st))
371
450
  }
372
451
  return {
373
452
  ok: true,
@@ -408,62 +487,106 @@ export function decodeDataStream(
408
487
  * First emits a SubgroupStreamHeader, then emits ObjectPayload events.
409
488
  * Accepts types 0x10-0x1D. Delta-decodes Object IDs.
410
489
  */
490
+ /**
491
+ * A data stream's bytes, accumulated across chunks.
492
+ *
493
+ * The decoders below used to allocate an array of `unread + chunk.length` and
494
+ * copy both halves into it on *every* chunk. Capacity now grows geometrically,
495
+ * so most chunks are a single `set` into spare room.
496
+ *
497
+ * **It never compacts in place.** Object payloads are handed out as views into
498
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
499
+ * object that has already been emitted. The consumed prefix is dropped only
500
+ * when a fresh array is being allocated anyway, which leaves the old array
501
+ * alive for exactly as long as the views into it are.
502
+ */
503
+ class StreamBuffer {
504
+ private buf = new Uint8Array(0)
505
+ /** Bytes written into `buf`. */
506
+ private len = 0
507
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
508
+ offset = 0
509
+ /** Absolute position of `buf[0]` within the stream. */
510
+ base = 0
511
+
512
+ get unread(): number {
513
+ return this.len - this.offset
514
+ }
515
+
516
+ append(chunk: Uint8Array): void {
517
+ if (this.len + chunk.length > this.buf.length) {
518
+ const live = this.len - this.offset
519
+ const needed = live + chunk.length
520
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
521
+ // half free. Sizing to `needed` -- which is what this did first -- leaves
522
+ // no room at all, so the next chunk reallocates too and the buffer
523
+ // reallocates on every single chunk, which is the cost it exists to
524
+ // avoid. Capacity never shrinks, so it settles at roughly twice the
525
+ // largest `live + chunk` this stream has seen.
526
+ let cap = Math.max(4096, this.buf.length)
527
+ while (cap < needed * 2) cap *= 2
528
+ // A fresh array every time, including when the capacity is unchanged:
529
+ // the old one is still pinned by the payload views handed out of it, so
530
+ // it can be neither compacted nor reused.
531
+ const next = new Uint8Array(cap)
532
+ next.set(this.buf.subarray(this.offset, this.len), 0)
533
+ this.buf = next
534
+ this.base += this.offset
535
+ this.len = live
536
+ this.offset = 0
537
+ }
538
+ this.buf.set(chunk, this.len)
539
+ this.len += chunk.length
540
+ }
541
+
542
+ /**
543
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
544
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
545
+ * not the spare room after them.
546
+ */
547
+ written(): Uint8Array {
548
+ return this.buf.subarray(0, this.len)
549
+ }
550
+ }
551
+
411
552
  export function createSubgroupStreamDecoder(): TransformStream<
412
553
  Uint8Array,
413
554
  SubgroupStreamHeader | ObjectPayload
414
555
  > {
415
- let buffer = new Uint8Array(0)
416
- let offset = 0
556
+ const b = new StreamBuffer()
557
+ let header: SubgroupHeaderFields | null = null
417
558
  let headerEmitted = false
418
- let extensionsPresent = false
419
559
  let prevObjectId = -1n
420
- let firstObject = true
560
+ let isFirst = true
561
+
562
+ function emitHeader(
563
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
564
+ h: SubgroupHeaderFields,
565
+ subgroupId: bigint,
566
+ ): void {
567
+ controller.enqueue({
568
+ type: 'subgroup_header',
569
+ headerType: h.streamType,
570
+ trackAlias: h.trackAlias,
571
+ groupId: h.groupId,
572
+ subgroupId,
573
+ publisherPriority: h.publisherPriority,
574
+ })
575
+ headerEmitted = true
576
+ }
421
577
 
422
578
  return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
423
579
  transform(chunk, controller) {
424
- if (offset > 0) {
425
- buffer = buffer.subarray(offset)
426
- offset = 0
427
- }
428
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
429
- newBuffer.set(buffer, 0)
430
- newBuffer.set(chunk, buffer.length)
431
- buffer = newBuffer
580
+ b.append(chunk)
581
+ // One view and one reader per chunk. Both used to be allocated per
582
+ // object, and an object is the thing there are a great many of.
583
+ const view = b.written()
432
584
 
433
- if (!headerEmitted) {
585
+ if (header === null) {
434
586
  try {
435
- const r = new BufferReader(buffer.subarray(offset))
436
- const streamType = Number(r.readVarInt())
437
- if (!isValidSubgroupType(streamType)) {
438
- controller.error(
439
- new DecodeError(
440
- 'CONSTRAINT_VIOLATION',
441
- `Expected subgroup stream type 0x10-0x1D, got 0x${streamType.toString(16)}`,
442
- 0,
443
- ),
444
- )
445
- return
446
- }
447
- extensionsPresent = (streamType & 0x01) !== 0
448
- const hasSubgroupField = (streamType & 0x04) !== 0
449
- const trackAlias = r.readVarInt()
450
- const groupId = r.readVarInt()
451
- let subgroupId = 0n
452
- if (hasSubgroupField) {
453
- subgroupId = r.readVarInt()
454
- }
455
- const publisherPriority = r.readUint8()
456
-
457
- controller.enqueue({
458
- type: 'subgroup_header',
459
- headerType: streamType,
460
- trackAlias,
461
- groupId,
462
- subgroupId,
463
- publisherPriority,
464
- })
465
- headerEmitted = true
466
- offset += r.offset
587
+ const hr = new BufferReader(view, b.offset)
588
+ header = readSubgroupHeader(hr)
589
+ b.offset = hr.offset
467
590
  } catch (e) {
468
591
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
469
592
  return
@@ -471,92 +594,73 @@ export function createSubgroupStreamDecoder(): TransformStream<
471
594
  controller.error(e)
472
595
  return
473
596
  }
597
+ // Where the Subgroup ID is the first Object's ID, that Object has not
598
+ // been read yet. Holding the header back until it has is the only way
599
+ // to emit the value the one-shot decoder reports; every other mode
600
+ // knows its Subgroup ID already.
601
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
474
602
  }
475
603
 
476
- while (offset < buffer.length) {
477
- try {
478
- const r = new BufferReader(buffer.subarray(offset))
479
- const byteOffset = r.offset
480
- const delta = r.readVarInt()
481
- const objectId = firstObject ? delta : prevObjectId + 1n + delta
482
- firstObject = false
604
+ const h = header
605
+ if (h === null) return
483
606
 
484
- let extensionData = new Uint8Array(0)
485
- if (extensionsPresent) {
486
- const extLen = Number(r.readVarInt())
487
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
488
- }
489
-
490
- const payloadLength = Number(r.readVarInt())
491
- let payload: Uint8Array
492
- let status: bigint | undefined
493
- let payloadByteOffset: number
494
- if (payloadLength === 0) {
495
- status = r.readVarInt()
496
- payloadByteOffset = r.offset
497
- payload = new Uint8Array(0)
498
- } else {
499
- payloadByteOffset = r.offset
500
- payload = r.readBytesView(payloadLength)
501
- }
502
- const obj: ObjectPayload = {
503
- type: 'object',
504
- byteOffset,
505
- payloadByteOffset,
506
- objectId,
507
- payloadLength,
508
- payload,
509
- extensionData,
510
- }
511
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
512
- controller.enqueue(obj)
513
- prevObjectId = objectId
514
- offset += r.offset
607
+ const r = new BufferReader(view, b.offset)
608
+ while (r.remaining > 0) {
609
+ // Where this Object starts, so a partial one is re-read from the top
610
+ // once the rest of it arrives.
611
+ const start = r.offset
612
+ let obj: ObjectPayload
613
+ try {
614
+ obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, b.base)
515
615
  } catch (e) {
516
616
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
517
- break
617
+ b.offset = start
618
+ return
518
619
  }
519
620
  controller.error(e)
520
621
  return
521
622
  }
623
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
624
+ isFirst = false
625
+ b.offset = r.offset
626
+ prevObjectId = obj.objectId
627
+ controller.enqueue(obj)
522
628
  }
523
629
  },
524
630
 
525
631
  flush(controller) {
526
- if (offset < buffer.length) {
632
+ // A stream carrying a header and no Objects still has a header to report,
633
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
634
+ // returns when it finds no first Object to derive one from.
635
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
636
+ if (b.unread > 0) {
527
637
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
528
638
  }
529
639
  },
530
640
  })
531
641
  }
532
642
 
533
- /**
534
- * Create a TransformStream that decodes a fetch data stream.
535
- * First emits a FetchStreamHeader, then emits FetchObjectPayload events.
536
- * Draft-14 fetch: type 0x05, per-object has all fields.
537
- */
538
643
  export function createFetchStreamDecoder(): TransformStream<
539
644
  Uint8Array,
540
645
  FetchStreamHeader | ObjectPayload
541
646
  > {
542
- let buffer = new Uint8Array(0)
543
- let offset = 0
647
+ const b = new StreamBuffer()
544
648
  let headerEmitted = false
649
+ /**
650
+ * The delta state the Objects carry between them. This decoder used to keep
651
+ * none, which is why DATAGRAM-mode Object IDs came out wrong: they resolve
652
+ * against the previous Object's ID.
653
+ */
654
+ const st = newFetchObjectState()
545
655
 
546
656
  return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
547
657
  transform(chunk, controller) {
548
- if (offset > 0) {
549
- buffer = buffer.subarray(offset)
550
- offset = 0
551
- }
552
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
553
- newBuffer.set(buffer, 0)
554
- newBuffer.set(chunk, buffer.length)
555
- buffer = newBuffer
658
+ b.append(chunk)
659
+ const view = b.written()
556
660
 
557
661
  if (!headerEmitted) {
558
662
  try {
559
- const r = new BufferReader(buffer.subarray(offset))
663
+ const r = new BufferReader(view, b.offset)
560
664
  const streamType = r.readVarInt()
561
665
  if (streamType !== FETCH_STREAM_TYPE) {
562
666
  controller.error(
@@ -572,7 +676,7 @@ export function createFetchStreamDecoder(): TransformStream<
572
676
 
573
677
  controller.enqueue({ type: 'fetch_header', requestId })
574
678
  headerEmitted = true
575
- offset += r.offset
679
+ b.offset = r.offset
576
680
  } catch (e) {
577
681
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
578
682
  return
@@ -582,65 +686,33 @@ export function createFetchStreamDecoder(): TransformStream<
582
686
  }
583
687
  }
584
688
 
585
- while (offset < buffer.length) {
689
+ const r = new BufferReader(view, b.offset)
690
+ while (r.remaining > 0) {
691
+ const start = r.offset
692
+ let obj: FetchObjectPayload
586
693
  try {
587
- const r = new BufferReader(buffer.subarray(offset))
588
- const byteOffset = r.offset
589
- const groupId = r.readVarInt()
590
- const subgroupId = r.readVarInt()
591
- const objectId = r.readVarInt()
592
- const publisherPriority = r.readUint8()
593
- const extLen = Number(r.readVarInt())
594
- const extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
595
- const payloadLength = Number(r.readVarInt())
596
- let payload: Uint8Array
597
- let status: bigint | undefined
598
- let payloadByteOffset: number
599
- if (payloadLength === 0) {
600
- status = r.readVarInt()
601
- payloadByteOffset = r.offset
602
- payload = new Uint8Array(0)
603
- } else {
604
- payloadByteOffset = r.offset
605
- payload = r.readBytesView(payloadLength)
606
- }
607
- const obj: FetchObjectPayload = {
608
- type: 'object',
609
- byteOffset,
610
- payloadByteOffset,
611
- groupId,
612
- subgroupId,
613
- objectId,
614
- publisherPriority,
615
- payloadLength,
616
- payload,
617
- extensionData,
618
- }
619
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
620
- controller.enqueue(obj)
621
- offset += r.offset
694
+ obj = readFetchObject(r, b.base, st)
622
695
  } catch (e) {
623
696
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
624
- break
697
+ b.offset = start
698
+ return
625
699
  }
626
700
  controller.error(e)
627
701
  return
628
702
  }
703
+ b.offset = r.offset
704
+ controller.enqueue(obj)
629
705
  }
630
706
  },
631
707
 
632
708
  flush(controller) {
633
- if (offset < buffer.length) {
709
+ if (b.unread > 0) {
634
710
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
635
711
  }
636
712
  },
637
713
  })
638
714
  }
639
715
 
640
- /**
641
- * Create a unified auto-detecting data stream decoder.
642
- * Reads the stream type varint: 0x10-0x1D = subgroup, 0x05 = fetch.
643
- */
644
716
  export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
645
717
  let buffer = new Uint8Array(0)
646
718
  let offset = 0