@moqtap/codec 0.9.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 (232) hide show
  1. package/CHANGELOG.md +173 -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-PU2KV7VP.cjs → chunk-27K5BUAG.cjs} +485 -515
  5. package/dist/{chunk-BV6MITLO.js → chunk-3RNQMGKA.js} +365 -390
  6. package/dist/{chunk-2XH6PHXR.js → chunk-5AWIXOLB.js} +442 -467
  7. package/dist/{chunk-4XLYCT5Q.cjs → chunk-5IMHJIAZ.cjs} +52 -2
  8. package/dist/{chunk-TVSFDQYE.cjs → chunk-5JGZCKJO.cjs} +513 -531
  9. package/dist/{chunk-IADVBKEJ.cjs → chunk-73K4OSAN.cjs} +407 -432
  10. package/dist/{chunk-N2V7Z37J.js → chunk-7VQKRGRD.js} +19 -3
  11. package/dist/{chunk-UZO5Z2XT.cjs → chunk-A3UCQOIB.cjs} +490 -515
  12. package/dist/{chunk-JKMYAUGY.js → chunk-A7PDMVXM.js} +35 -11
  13. package/dist/{chunk-VZHL2U2N.cjs → chunk-A7VQO6YC.cjs} +54 -38
  14. package/dist/{chunk-KCH4BQXK.cjs → chunk-ACQHZLRZ.cjs} +73 -47
  15. package/dist/{chunk-H6DTAIWM.js → chunk-BBICOMW6.js} +47 -19
  16. package/dist/{chunk-MAXEHYVB.cjs → chunk-BNIUVOCJ.cjs} +554 -612
  17. package/dist/{chunk-QGIHC6BA.cjs → chunk-CNC6ANS4.cjs} +70 -46
  18. package/dist/{chunk-NH4L6NIY.js → chunk-FDLQSWHK.js} +437 -467
  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-7ENEWAMZ.cjs → chunk-HM7BLRVP.cjs} +305 -305
  22. package/dist/{chunk-XHAP2YST.js → chunk-HZONHFNO.js} +19 -3
  23. package/dist/{chunk-EVCPBFSR.cjs → chunk-ILYABWDK.cjs} +83 -55
  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-UA5XFP4O.cjs → chunk-KTY4LAUV.cjs} +485 -494
  27. package/dist/{chunk-RZTLQAKF.js → chunk-OJNLJT5D.js} +0 -1
  28. package/dist/{chunk-YJGOD3Y4.js → chunk-PJK2ZS3I.js} +492 -550
  29. package/dist/{chunk-TMXRYOBS.js → chunk-PSMZKHOC.js} +444 -453
  30. package/dist/{chunk-7RE6KSIY.cjs → chunk-Q5JQGHWY.cjs} +0 -1
  31. package/dist/{chunk-23KRITJ6.js → chunk-SQTUQY5E.js} +37 -11
  32. package/dist/{chunk-XQGEOFGE.js → chunk-WCRLAKDW.js} +253 -253
  33. package/dist/{chunk-KYVTM5VI.js → chunk-WL2YUEXH.js} +19 -3
  34. package/dist/{chunk-Q7NSUXXB.js → chunk-YCNIVZS7.js} +464 -482
  35. package/dist/{chunk-KLPWJU6W.js → chunk-YRZQVNZO.js} +50 -0
  36. package/dist/{codec-Bym-K2q2.d.cts → codec-76xzrLTL.d.cts} +21 -2
  37. package/dist/{codec-BKsVu3Fr.d.ts → codec-8p40WdLg.d.ts} +21 -2
  38. package/dist/{codec-Dl_QIXMz.d.ts → codec-BN2Bu0KI.d.ts} +20 -1
  39. package/dist/{codec-D4RvFQmu.d.ts → codec-Bb6GKTrv.d.ts} +21 -2
  40. package/dist/{codec-DAyiF5sq.d.cts → codec-BgpfHoAd.d.cts} +21 -2
  41. package/dist/{codec-CA6nbbEk.d.ts → codec-BkheTXO7.d.ts} +21 -2
  42. package/dist/{codec-DchsPfr3.d.cts → codec-BwVDkokh.d.cts} +21 -2
  43. package/dist/{codec-CgZdadPt.d.cts → codec-C5s1eBJX.d.cts} +21 -2
  44. package/dist/codec-CM5yd6n3.d.ts +38 -0
  45. package/dist/{codec-DJmqoIsp.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-CSBFdPgP.d.cts → codec-DELe00w_.d.cts} +21 -2
  50. package/dist/{codec-C_xxsbKy.d.cts → codec-DHzKAUCa.d.cts} +21 -2
  51. package/dist/{codec--CVeqGQv.d.ts → codec-DdVBtvx7.d.ts} +20 -1
  52. package/dist/{codec-CviAa4lp.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-CtvFK4aV.d.cts → codec-GAY4lybi.d.cts} +21 -2
  55. package/dist/{codec-Cfo_z5ET.d.cts → codec-I8u2pfS4.d.cts} +20 -1
  56. package/dist/{codec-vddqy0KY.d.cts → codec-XLrnreeu.d.cts} +21 -2
  57. package/dist/{codec-O-1Js7h4.d.ts → codec-c0YEwwWn.d.ts} +21 -2
  58. package/dist/{codec-Cl73EfRW.d.ts → codec-ejDu-va6.d.ts} +21 -2
  59. package/dist/{codec-C006F75E.d.ts → codec-kaBUzdpV.d.ts} +21 -2
  60. package/dist/{codec-C5evFHL-.d.ts → codec-mVYqkIUF.d.ts} +21 -2
  61. package/dist/{codec-BMwgZIgG.d.ts → codec-qSbO5nOa.d.ts} +21 -2
  62. package/dist/{codec-BQa6Qsav.d.ts → codec-yYKGOipj.d.ts} +21 -2
  63. package/dist/{codec-DzLvG8Wb.d.cts → codec-zfzg9uiy.d.cts} +21 -2
  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-session.d.cts +1 -1
  81. package/dist/draft11-session.d.ts +1 -1
  82. package/dist/draft11.cjs +104 -3
  83. package/dist/draft11.d.cts +100 -4
  84. package/dist/draft11.d.ts +100 -4
  85. package/dist/draft11.js +105 -4
  86. package/dist/draft12-session.d.cts +1 -1
  87. package/dist/draft12-session.d.ts +1 -1
  88. package/dist/draft12.cjs +113 -3
  89. package/dist/draft12.d.cts +109 -4
  90. package/dist/draft12.d.ts +109 -4
  91. package/dist/draft12.js +114 -4
  92. package/dist/draft13-session.d.cts +1 -1
  93. package/dist/draft13-session.d.ts +1 -1
  94. package/dist/draft13.cjs +105 -3
  95. package/dist/draft13.d.cts +101 -4
  96. package/dist/draft13.d.ts +101 -4
  97. package/dist/draft13.js +106 -4
  98. package/dist/draft14-session.d.cts +1 -1
  99. package/dist/draft14-session.d.ts +1 -1
  100. package/dist/draft14.cjs +107 -3
  101. package/dist/draft14.d.cts +103 -30
  102. package/dist/draft14.d.ts +103 -30
  103. package/dist/draft14.js +108 -4
  104. package/dist/draft15-session.d.cts +1 -1
  105. package/dist/draft15-session.d.ts +1 -1
  106. package/dist/draft15.cjs +69 -3
  107. package/dist/draft15.d.cts +65 -22
  108. package/dist/draft15.d.ts +65 -22
  109. package/dist/draft15.js +70 -4
  110. package/dist/draft16-session.cjs +2 -2
  111. package/dist/draft16-session.d.cts +1 -1
  112. package/dist/draft16-session.d.ts +1 -1
  113. package/dist/draft16-session.js +1 -1
  114. package/dist/draft16.cjs +76 -8
  115. package/dist/draft16.d.cts +67 -13
  116. package/dist/draft16.d.ts +67 -13
  117. package/dist/draft16.js +73 -5
  118. package/dist/draft17-session.d.cts +1 -1
  119. package/dist/draft17-session.d.ts +1 -1
  120. package/dist/draft17.cjs +77 -3
  121. package/dist/draft17.d.cts +73 -4
  122. package/dist/draft17.d.ts +73 -4
  123. package/dist/draft17.js +78 -4
  124. package/dist/draft18-session.d.cts +1 -1
  125. package/dist/draft18-session.d.ts +1 -1
  126. package/dist/draft18.cjs +8 -3
  127. package/dist/draft18.d.cts +4 -3
  128. package/dist/draft18.d.ts +4 -3
  129. package/dist/draft18.js +9 -4
  130. package/dist/draft19-session.d.cts +1 -1
  131. package/dist/draft19-session.d.ts +1 -1
  132. package/dist/draft19.cjs +11 -4
  133. package/dist/draft19.d.cts +4 -3
  134. package/dist/draft19.d.ts +4 -3
  135. package/dist/draft19.js +12 -5
  136. package/dist/draft20-session.d.cts +1 -1
  137. package/dist/draft20-session.d.ts +1 -1
  138. package/dist/draft20.cjs +8 -3
  139. package/dist/draft20.d.cts +4 -3
  140. package/dist/draft20.d.ts +4 -3
  141. package/dist/draft20.js +9 -4
  142. package/dist/index.cjs +30 -30
  143. package/dist/index.d.cts +25 -24
  144. package/dist/index.d.ts +25 -24
  145. package/dist/index.js +15 -15
  146. package/dist/session.cjs +2 -2
  147. package/dist/session.d.cts +10 -10
  148. package/dist/session.d.ts +10 -10
  149. package/dist/session.js +1 -1
  150. package/dist/{types-zHgCS6o5.d.cts → types-Btp416jt.d.cts} +15 -7
  151. package/dist/{types-zHgCS6o5.d.ts → types-Btp416jt.d.ts} +15 -7
  152. package/dist/{types-Cm9fjeYC.d.cts → types-BxTL1NXw.d.cts} +9 -2
  153. package/dist/{types-Cm9fjeYC.d.ts → types-BxTL1NXw.d.ts} +9 -2
  154. package/dist/{types-Bio-91Rt.d.cts → types-CI_SX4Xe.d.cts} +2 -2
  155. package/dist/{types-Bio-91Rt.d.ts → types-CI_SX4Xe.d.ts} +2 -2
  156. package/dist/{types-HI_bbXka.d.cts → types-CmqT0jUf.d.cts} +1 -1
  157. package/dist/{types-HI_bbXka.d.ts → types-CmqT0jUf.d.ts} +1 -1
  158. package/dist/{types-CPaYBsMG.d.cts → types-CuibHYcf.d.cts} +9 -2
  159. package/dist/{types-CPaYBsMG.d.ts → types-CuibHYcf.d.ts} +9 -2
  160. package/dist/{types-CWCQwuvy.d.cts → types-CwN7AEoL.d.cts} +24 -12
  161. package/dist/{types-CWCQwuvy.d.ts → types-CwN7AEoL.d.ts} +24 -12
  162. package/dist/{types-Ck45Ex5S.d.cts → types-D0kTgUuL.d.cts} +1 -1
  163. package/dist/{types-Ck45Ex5S.d.ts → types-D0kTgUuL.d.ts} +1 -1
  164. package/dist/{types-D9eiB_9A.d.cts → types-DAnalt5K.d.cts} +9 -2
  165. package/dist/{types-D9eiB_9A.d.ts → types-DAnalt5K.d.ts} +9 -2
  166. package/dist/{types-CXlSn0I9.d.cts → types-DjxexghA.d.cts} +2 -2
  167. package/dist/{types-CXlSn0I9.d.ts → types-DjxexghA.d.ts} +2 -2
  168. package/dist/{types-Cqp10JtB.d.cts → types-DxXNP32l.d.cts} +10 -2
  169. package/dist/{types-Cqp10JtB.d.ts → types-DxXNP32l.d.ts} +10 -2
  170. package/package.json +3 -3
  171. package/src/core/auth-redaction.ts +192 -0
  172. package/src/drafts/draft07/codec.ts +40 -0
  173. package/src/drafts/draft07/error-codes.ts +44 -0
  174. package/src/drafts/draft07/index.ts +15 -0
  175. package/src/drafts/draft08/codec.ts +40 -0
  176. package/src/drafts/draft08/error-codes.ts +76 -0
  177. package/src/drafts/draft08/index.ts +21 -0
  178. package/src/drafts/draft09/codec.ts +40 -0
  179. package/src/drafts/draft09/error-codes.ts +76 -0
  180. package/src/drafts/draft09/index.ts +21 -0
  181. package/src/drafts/draft10/codec.ts +40 -0
  182. package/src/drafts/draft10/error-codes.ts +76 -0
  183. package/src/drafts/draft10/index.ts +21 -0
  184. package/src/drafts/draft11/codec.ts +71 -8
  185. package/src/drafts/draft11/error-codes.ts +106 -0
  186. package/src/drafts/draft11/index.ts +23 -0
  187. package/src/drafts/draft11/types.ts +1 -1
  188. package/src/drafts/draft12/codec.ts +83 -16
  189. package/src/drafts/draft12/error-codes.ts +116 -0
  190. package/src/drafts/draft12/index.ts +25 -0
  191. package/src/drafts/draft12/types.ts +2 -2
  192. package/src/drafts/draft13/codec.ts +75 -11
  193. package/src/drafts/draft13/error-codes.ts +107 -0
  194. package/src/drafts/draft13/index.ts +23 -0
  195. package/src/drafts/draft13/types.ts +3 -3
  196. package/src/drafts/draft14/codec.ts +79 -12
  197. package/src/drafts/draft14/data-streams.ts +331 -259
  198. package/src/drafts/draft14/error-codes.ts +109 -0
  199. package/src/drafts/draft14/index.ts +23 -0
  200. package/src/drafts/draft14/types.ts +1 -1
  201. package/src/drafts/draft15/codec.ts +71 -8
  202. package/src/drafts/draft15/data-streams.ts +489 -444
  203. package/src/drafts/draft15/error-codes.ts +66 -0
  204. package/src/drafts/draft15/index.ts +15 -0
  205. package/src/drafts/draft15/types.ts +9 -2
  206. package/src/drafts/draft16/codec.ts +149 -49
  207. package/src/drafts/draft16/data-streams.ts +513 -442
  208. package/src/drafts/draft16/error-codes.ts +68 -0
  209. package/src/drafts/draft16/index.ts +15 -0
  210. package/src/drafts/draft16/messages.ts +4 -4
  211. package/src/drafts/draft16/session-fsm.ts +0 -1
  212. package/src/drafts/draft16/types.ts +24 -10
  213. package/src/drafts/draft17/codec.ts +77 -10
  214. package/src/drafts/draft17/data-streams.ts +567 -488
  215. package/src/drafts/draft17/index.ts +15 -0
  216. package/src/drafts/draft17/types.ts +9 -2
  217. package/src/drafts/draft18/codec.ts +70 -7
  218. package/src/drafts/draft18/data-streams.ts +570 -482
  219. package/src/drafts/draft18/index.ts +3 -0
  220. package/src/drafts/draft18/types.ts +18 -10
  221. package/src/drafts/draft19/codec.ts +95 -23
  222. package/src/drafts/draft19/data-streams.ts +570 -482
  223. package/src/drafts/draft19/error-codes.ts +3 -1
  224. package/src/drafts/draft19/index.ts +3 -0
  225. package/src/drafts/draft19/messages.ts +3 -1
  226. package/src/drafts/draft19/types.ts +32 -18
  227. package/src/drafts/draft20/codec.ts +115 -36
  228. package/src/drafts/draft20/data-streams.ts +610 -563
  229. package/src/drafts/draft20/index.ts +3 -0
  230. package/src/drafts/draft20/types.ts +31 -13
  231. package/dist/codec-BJ7BHuiZ.d.ts +0 -18
  232. package/dist/codec-C6Mx4bwz.d.cts +0 -18
@@ -140,111 +140,158 @@ export function encodeFetchStream(stream: FetchStream): Uint8Array {
140
140
  /**
141
141
  * Decode a subgroup data stream from raw bytes.
142
142
  */
143
- export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
144
- try {
145
- const r = new BufferReader(bytes)
146
- const streamType = Number(r.readVarInt())
147
-
148
- // Draft-15 Table 6 assigns 0x10-0x15, 0x18-0x1D, 0x30-0x35 and 0x38-0x3D:
149
- // the end-of-group marker at 0x08 doubles the range the previous drafts
150
- // used. What the table leaves out is every Type whose two Subgroup ID mode
151
- // bits are 0b11. Draft-16 names those reserved in prose; draft-15 only
152
- // omits them, which comes to the same refusal by a different route.
153
- if (
154
- !((streamType >= 0x10 && streamType <= 0x1f) || (streamType >= 0x30 && streamType <= 0x3f)) ||
155
- (streamType & 0x06) === 0x06
156
- ) {
157
- return {
158
- ok: false,
159
- error: new DecodeError(
160
- 'CONSTRAINT_VIOLATION',
161
- `Expected subgroup stream type 0x10-0x1F/0x30-0x3F, got 0x${streamType.toString(16)}`,
162
- 0,
163
- ),
164
- }
165
- }
143
+ /**
144
+ * The SUBGROUP_HEADER fields, read once for both decoders.
145
+ *
146
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` used to parse this
147
+ * header separately and they drifted: the incremental one dropped the header
148
+ * flags, reported no Type Flags, and left the Subgroup ID at zero under the
149
+ * mode that derives it from the first Object. One reader means there is
150
+ * nothing left to keep in sync.
151
+ */
152
+ interface SubgroupHeaderFields {
153
+ readonly streamType: number
154
+ readonly extensionsPresent: boolean
155
+ readonly subgroupIsFirstObjId: boolean
156
+ readonly endOfGroup: boolean
157
+ readonly trackAlias: bigint
158
+ readonly groupId: bigint
159
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
160
+ readonly subgroupId: bigint
161
+ readonly publisherPriority: number
162
+ }
166
163
 
167
- // Decode type flags
168
- // Draft-15 Table 6, over the 0x10 base:
169
- // bit 0 (0x01): extensions present
170
- // bit 1 (0x02): subgroup id is the first Object ID, not transmitted
171
- // bit 2 (0x04): explicit subgroup_id field present
172
- // bit 3 (0x08): contains end of group
173
- // bit 5 (0x20): no priority (>= 0x30)
174
- const extensionsPresent = (streamType & 0x01) !== 0
175
- const endOfGroup = (streamType & 0x08) !== 0
176
- const hasSubgroupField = (streamType & 0x04) !== 0
177
- const hasPriority = streamType < 0x30
164
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
165
+ const streamType = Number(r.readVarInt())
166
+ if (
167
+ !((streamType >= 0x10 && streamType <= 0x1f) || (streamType >= 0x30 && streamType <= 0x3f)) ||
168
+ (streamType & 0x06) === 0x06
169
+ ) {
170
+ throw new DecodeError(
171
+ 'CONSTRAINT_VIOLATION',
172
+ `Expected subgroup stream type 0x10-0x1F/0x30-0x3F, got 0x${streamType.toString(16)}`,
173
+ 0,
174
+ )
175
+ }
178
176
 
179
- const trackAlias = r.readVarInt()
180
- const groupId = r.readVarInt()
177
+ const extensionsPresent = (streamType & 0x01) !== 0
178
+ const endOfGroup = (streamType & 0x08) !== 0
179
+ const hasSubgroupField = (streamType & 0x04) !== 0
180
+ const hasPriority = streamType < 0x30
181
+ const subgroupIsFirstObjId = false
181
182
 
182
- let subgroupId = 0n
183
- if (hasSubgroupField) {
184
- subgroupId = r.readVarInt()
185
- }
183
+ const trackAlias = r.readVarInt()
184
+ const groupId = r.readVarInt()
186
185
 
187
- let publisherPriority = 128 // default
188
- if (hasPriority) {
189
- publisherPriority = r.readUint8()
190
- }
186
+ let subgroupId = 0n
187
+ if (hasSubgroupField) {
188
+ subgroupId = r.readVarInt()
189
+ }
190
+
191
+ let publisherPriority = 128
192
+ if (hasPriority) {
193
+ publisherPriority = r.readUint8()
194
+ }
195
+
196
+ return {
197
+ streamType,
198
+ extensionsPresent,
199
+ subgroupIsFirstObjId,
200
+ endOfGroup,
201
+ trackAlias,
202
+ groupId,
203
+ subgroupId,
204
+ publisherPriority,
205
+ }
206
+ }
207
+
208
+ /**
209
+ * One Object off a subgroup stream, for both decoders.
210
+ *
211
+ * `base` is the absolute offset within the stream that `r.offset === 0`
212
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
213
+ * stream and passes 0; the incremental decoder reads from a window over its
214
+ * own buffer and passes that window's position. That is what makes
215
+ * `byteOffset` mean the same thing on both paths, instead of being hardcoded
216
+ * to zero on one of them.
217
+ *
218
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
219
+ * incremental decoder reads as "wait for more bytes" rather than as an error.
220
+ */
221
+ function readSubgroupObject(
222
+ r: BufferReader,
223
+ extensionsPresent: boolean,
224
+ isFirst: boolean,
225
+ prevObjectId: bigint,
226
+ base: number,
227
+ ): ObjectPayload {
228
+ const byteOffset = base + r.offset
229
+ const delta = r.readVarInt()
230
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
231
+
232
+ let extensionData = new Uint8Array(0)
233
+ if (extensionsPresent) {
234
+ const extLen = Number(r.readVarInt())
235
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
236
+ }
237
+
238
+ const payloadLength = Number(r.readVarInt())
239
+ let payload: Uint8Array
240
+ let status: bigint | undefined
241
+ let payloadByteOffset: number
242
+ if (payloadLength === 0) {
243
+ // The Object Status field is sent only when the Payload Length is zero.
244
+ // Skipping it leaves the varint to be read as the next Object's Object ID
245
+ // Delta, which desynchronises the rest of the stream silently.
246
+ status = r.readVarInt()
247
+ payloadByteOffset = base + r.offset
248
+ payload = new Uint8Array(0)
249
+ } else {
250
+ payloadByteOffset = base + r.offset
251
+ payload = r.readBytesView(payloadLength)
252
+ }
253
+
254
+ const obj: ObjectPayload = {
255
+ type: 'object',
256
+ byteOffset,
257
+ payloadByteOffset,
258
+ objectId,
259
+ payloadLength,
260
+ payload,
261
+ extensionData,
262
+ }
263
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
264
+ return obj
265
+ }
266
+
267
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
268
+ try {
269
+ const r = new BufferReader(bytes)
270
+ const h = readSubgroupHeader(r)
191
271
 
192
272
  const objects: ObjectPayload[] = []
193
273
  let prevObjectId = -1n
194
- let firstObject = true
274
+ let isFirst = true
275
+ let subgroupId = h.subgroupId
195
276
 
196
277
  while (r.remaining > 0) {
197
- const byteOffset = r.offset
198
- const delta = r.readVarInt()
199
- let objectId: bigint
200
- if (firstObject) {
201
- objectId = delta
202
- firstObject = false
203
- } else {
204
- objectId = prevObjectId + 1n + delta
205
- }
206
- let extensionData = new Uint8Array(0)
207
- if (extensionsPresent) {
208
- const extLen = Number(r.readVarInt())
209
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
210
- }
211
- const payloadLength = Number(r.readVarInt())
212
- let payload: Uint8Array
213
- let status: bigint | undefined
214
- let payloadByteOffset: number
215
- if (payloadLength === 0) {
216
- // When payload_length is 0, an object status varint follows
217
- status = r.readVarInt()
218
- payloadByteOffset = r.offset
219
- payload = new Uint8Array(0)
220
- } else {
221
- payloadByteOffset = r.offset
222
- payload = r.readBytesView(payloadLength)
223
- }
224
- const obj: ObjectPayload = {
225
- type: 'object',
226
- byteOffset,
227
- payloadByteOffset,
228
- objectId,
229
- payloadLength,
230
- extensionData,
231
- payload,
232
- }
233
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
278
+ const obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, 0)
279
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
280
+ isFirst = false
234
281
  objects.push(obj)
235
- prevObjectId = objectId
282
+ prevObjectId = obj.objectId
236
283
  }
237
284
 
238
285
  const result: SubgroupStream = {
239
286
  type: 'subgroup',
240
- headerType: streamType,
241
- trackAlias,
242
- groupId,
287
+ headerType: h.streamType,
288
+ trackAlias: h.trackAlias,
289
+ groupId: h.groupId,
243
290
  subgroupId,
244
- publisherPriority,
291
+ publisherPriority: h.publisherPriority,
245
292
  objects,
293
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
246
294
  }
247
- if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
248
295
 
249
296
  return {
250
297
  ok: true,
@@ -257,9 +304,6 @@ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupSt
257
304
  }
258
305
  }
259
306
 
260
- /**
261
- * Decode a datagram object from raw bytes.
262
- */
263
307
  export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject> {
264
308
  try {
265
309
  const r = new BufferReader(bytes)
@@ -331,6 +375,141 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
331
375
  /**
332
376
  * Decode a fetch data stream from raw bytes.
333
377
  */
378
+ /** The delta state a fetch stream carries from one Object to the next. */
379
+ interface FetchObjectState {
380
+ prevGroupId: bigint
381
+ prevSubgroupId: bigint
382
+ prevObjectId: bigint
383
+ prevPriority: number
384
+ first: boolean
385
+ }
386
+
387
+ function newFetchObjectState(): FetchObjectState {
388
+ return { prevGroupId: 0n, prevSubgroupId: 0n, prevObjectId: 0n, prevPriority: 128, first: true }
389
+ }
390
+
391
+ /**
392
+ * One Object off a fetch stream, for both decoders.
393
+ *
394
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
395
+ * cannot report a different shape for the same bytes. It used to return a
396
+ * bare ObjectPayload -- no Serialization Flags, no Group or Subgroup ID, no
397
+ * Priority, no deltas -- with every `byteOffset` zero, and in DATAGRAM mode
398
+ * it resolved the Object ID against state it never carried.
399
+ *
400
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
401
+ * the one-shot decoder, the buffer window's position for the incremental one.
402
+ */
403
+ function readFetchObject(r: BufferReader, base: number, st: FetchObjectState): FetchObjectPayload {
404
+ const byteOffset = base + r.offset
405
+ const flags = r.readUint8()
406
+ const subgroupEncoding = flags & 0x03
407
+ const objectIdPresent = (flags & 0x04) !== 0
408
+ const groupIdPresent = (flags & 0x08) !== 0
409
+ const priorityPresent = (flags & 0x10) !== 0
410
+ const extensionsPresent = (flags & 0x20) !== 0
411
+
412
+ if (flags & 0xc0) {
413
+ throw new DecodeError(
414
+ 'CONSTRAINT_VIOLATION',
415
+ 'Reserved bits set in fetch object flags',
416
+ r.offset,
417
+ )
418
+ }
419
+
420
+ let groupId = st.prevGroupId
421
+ if (groupIdPresent) {
422
+ groupId = r.readVarInt()
423
+ } else if (st.first) {
424
+ throw new DecodeError(
425
+ 'CONSTRAINT_VIOLATION',
426
+ 'First fetch object must include groupId',
427
+ r.offset,
428
+ )
429
+ }
430
+
431
+ let subgroupId: bigint
432
+ if (subgroupEncoding === 0x00) {
433
+ subgroupId = 0n
434
+ } else if (subgroupEncoding === 0x01) {
435
+ if (st.first) {
436
+ throw new DecodeError(
437
+ 'CONSTRAINT_VIOLATION',
438
+ 'First fetch object cannot reference prior subgroupId',
439
+ r.offset,
440
+ )
441
+ }
442
+ subgroupId = st.prevSubgroupId
443
+ } else if (subgroupEncoding === 0x02) {
444
+ if (st.first) {
445
+ throw new DecodeError(
446
+ 'CONSTRAINT_VIOLATION',
447
+ 'First fetch object cannot reference prior subgroupId',
448
+ r.offset,
449
+ )
450
+ }
451
+ subgroupId = st.prevSubgroupId + 1n
452
+ } else {
453
+ subgroupId = r.readVarInt()
454
+ }
455
+
456
+ let objectId = st.prevObjectId + 1n
457
+ if (objectIdPresent) {
458
+ objectId = r.readVarInt()
459
+ } else if (st.first) {
460
+ throw new DecodeError(
461
+ 'CONSTRAINT_VIOLATION',
462
+ 'First fetch object must include objectId',
463
+ r.offset,
464
+ )
465
+ }
466
+
467
+ if (priorityPresent) {
468
+ st.prevPriority = r.readUint8()
469
+ }
470
+
471
+ let extensionData = new Uint8Array(0)
472
+ if (extensionsPresent) {
473
+ const extLen = Number(r.readVarInt())
474
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
475
+ }
476
+
477
+ const payloadLength = Number(r.readVarInt())
478
+ let payload: Uint8Array
479
+ let status: bigint | undefined
480
+ let payloadByteOffset: number
481
+ if (payloadLength > 0) {
482
+ payloadByteOffset = base + r.offset
483
+ payload = r.readBytesView(payloadLength)
484
+ } else {
485
+ status = r.readVarInt()
486
+ payloadByteOffset = base + r.offset
487
+ payload = new Uint8Array(0)
488
+ }
489
+
490
+ const obj: FetchObjectPayload = {
491
+ type: 'object',
492
+ byteOffset,
493
+ payloadByteOffset,
494
+ serializationFlags: flags,
495
+ groupId,
496
+ subgroupId,
497
+ objectId,
498
+ publisherPriority: st.prevPriority,
499
+ payloadLength,
500
+ extensionData,
501
+ payload,
502
+ }
503
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
504
+
505
+ st.prevGroupId = groupId
506
+ st.prevSubgroupId = subgroupId
507
+ st.prevObjectId = objectId
508
+ st.first = false
509
+
510
+ return obj
511
+ }
512
+
334
513
  export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
335
514
  try {
336
515
  const r = new BufferReader(bytes)
@@ -348,134 +527,10 @@ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
348
527
  const requestId = r.readVarInt()
349
528
  const objects: FetchObjectPayload[] = []
350
529
 
351
- let prevGroupId = 0n
352
- let prevSubgroupId = 0n
353
- let prevObjectId = 0n
354
- let prevPriority = 128
355
- let first = true
530
+ const st = newFetchObjectState()
356
531
 
357
532
  while (r.remaining > 0) {
358
- const byteOffset = r.offset
359
- const flags = r.readUint8()
360
- const subgroupEncoding = flags & 0x03
361
- const objectIdPresent = (flags & 0x04) !== 0
362
- const groupIdPresent = (flags & 0x08) !== 0
363
- const priorityPresent = (flags & 0x10) !== 0
364
- const extensionsPresent = (flags & 0x20) !== 0
365
-
366
- if (flags & 0xc0) {
367
- return {
368
- ok: false,
369
- error: new DecodeError(
370
- 'CONSTRAINT_VIOLATION',
371
- 'Reserved bits set in fetch object flags',
372
- r.offset,
373
- ),
374
- }
375
- }
376
-
377
- let groupId = prevGroupId
378
- if (groupIdPresent) {
379
- groupId = r.readVarInt()
380
- } else if (first) {
381
- return {
382
- ok: false,
383
- error: new DecodeError(
384
- 'CONSTRAINT_VIOLATION',
385
- 'First fetch object must include groupId',
386
- r.offset,
387
- ),
388
- }
389
- }
390
-
391
- let subgroupId: bigint
392
- if (subgroupEncoding === 0x00) {
393
- subgroupId = 0n
394
- } else if (subgroupEncoding === 0x01) {
395
- if (first) {
396
- return {
397
- ok: false,
398
- error: new DecodeError(
399
- 'CONSTRAINT_VIOLATION',
400
- 'First fetch object cannot reference prior subgroupId',
401
- r.offset,
402
- ),
403
- }
404
- }
405
- subgroupId = prevSubgroupId
406
- } else if (subgroupEncoding === 0x02) {
407
- if (first) {
408
- return {
409
- ok: false,
410
- error: new DecodeError(
411
- 'CONSTRAINT_VIOLATION',
412
- 'First fetch object cannot reference prior subgroupId',
413
- r.offset,
414
- ),
415
- }
416
- }
417
- subgroupId = prevSubgroupId + 1n
418
- } else {
419
- subgroupId = r.readVarInt()
420
- }
421
-
422
- let objectId = prevObjectId + 1n
423
- if (objectIdPresent) {
424
- objectId = r.readVarInt()
425
- } else if (first) {
426
- return {
427
- ok: false,
428
- error: new DecodeError(
429
- 'CONSTRAINT_VIOLATION',
430
- 'First fetch object must include objectId',
431
- r.offset,
432
- ),
433
- }
434
- }
435
-
436
- if (priorityPresent) {
437
- prevPriority = r.readUint8()
438
- }
439
-
440
- let extensionData = new Uint8Array(0)
441
- if (extensionsPresent) {
442
- const extLen = Number(r.readVarInt())
443
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
444
- }
445
-
446
- const payloadLength = Number(r.readVarInt())
447
- let payload: Uint8Array
448
- let status: bigint | undefined
449
- let payloadByteOffset: number
450
- if (payloadLength > 0) {
451
- payloadByteOffset = r.offset
452
- payload = r.readBytesView(payloadLength)
453
- } else {
454
- status = r.readVarInt()
455
- payloadByteOffset = r.offset
456
- payload = new Uint8Array(0)
457
- }
458
-
459
- const obj: FetchObjectPayload = {
460
- type: 'object',
461
- byteOffset,
462
- payloadByteOffset,
463
- serializationFlags: flags,
464
- groupId,
465
- subgroupId,
466
- objectId,
467
- publisherPriority: prevPriority,
468
- payloadLength,
469
- extensionData,
470
- payload,
471
- }
472
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
473
- objects.push(obj)
474
-
475
- prevGroupId = groupId
476
- prevSubgroupId = subgroupId
477
- prevObjectId = objectId
478
- first = false
533
+ objects.push(readFetchObject(r, 0, st))
479
534
  }
480
535
 
481
536
  return {
@@ -515,78 +570,107 @@ export function decodeDataStream(
515
570
  /**
516
571
  * Create a TransformStream that decodes a subgroup data stream.
517
572
  */
573
+ /**
574
+ * A data stream's bytes, accumulated across chunks.
575
+ *
576
+ * The decoders below used to allocate an array of `unread + chunk.length` and
577
+ * copy both halves into it on *every* chunk. Capacity now grows geometrically,
578
+ * so most chunks are a single `set` into spare room.
579
+ *
580
+ * **It never compacts in place.** Object payloads are handed out as views into
581
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
582
+ * object that has already been emitted. The consumed prefix is dropped only
583
+ * when a fresh array is being allocated anyway, which leaves the old array
584
+ * alive for exactly as long as the views into it are.
585
+ */
586
+ class StreamBuffer {
587
+ private buf = new Uint8Array(0)
588
+ /** Bytes written into `buf`. */
589
+ private len = 0
590
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
591
+ offset = 0
592
+ /** Absolute position of `buf[0]` within the stream. */
593
+ base = 0
594
+
595
+ get unread(): number {
596
+ return this.len - this.offset
597
+ }
598
+
599
+ append(chunk: Uint8Array): void {
600
+ if (this.len + chunk.length > this.buf.length) {
601
+ const live = this.len - this.offset
602
+ const needed = live + chunk.length
603
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
604
+ // half free. Sizing to `needed` -- which is what this did first -- leaves
605
+ // no room at all, so the next chunk reallocates too and the buffer
606
+ // reallocates on every single chunk, which is the cost it exists to
607
+ // avoid. Capacity never shrinks, so it settles at roughly twice the
608
+ // largest `live + chunk` this stream has seen.
609
+ let cap = Math.max(4096, this.buf.length)
610
+ while (cap < needed * 2) cap *= 2
611
+ // A fresh array every time, including when the capacity is unchanged:
612
+ // the old one is still pinned by the payload views handed out of it, so
613
+ // it can be neither compacted nor reused.
614
+ const next = new Uint8Array(cap)
615
+ next.set(this.buf.subarray(this.offset, this.len), 0)
616
+ this.buf = next
617
+ this.base += this.offset
618
+ this.len = live
619
+ this.offset = 0
620
+ }
621
+ this.buf.set(chunk, this.len)
622
+ this.len += chunk.length
623
+ }
624
+
625
+ /**
626
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
627
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
628
+ * not the spare room after them.
629
+ */
630
+ written(): Uint8Array {
631
+ return this.buf.subarray(0, this.len)
632
+ }
633
+ }
634
+
518
635
  export function createSubgroupStreamDecoder(): TransformStream<
519
636
  Uint8Array,
520
637
  SubgroupStreamHeader | ObjectPayload
521
638
  > {
522
- let buffer = new Uint8Array(0)
523
- let offset = 0
639
+ const b = new StreamBuffer()
640
+ let header: SubgroupHeaderFields | null = null
524
641
  let headerEmitted = false
525
642
  let prevObjectId = -1n
526
- let firstObject = true
527
- let _subgroupIsFirstObjId = false
528
- let _extensionsPresent = false
643
+ let isFirst = true
644
+
645
+ function emitHeader(
646
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
647
+ h: SubgroupHeaderFields,
648
+ subgroupId: bigint,
649
+ ): void {
650
+ controller.enqueue({
651
+ type: 'subgroup_header',
652
+ headerType: h.streamType,
653
+ trackAlias: h.trackAlias,
654
+ groupId: h.groupId,
655
+ subgroupId,
656
+ publisherPriority: h.publisherPriority,
657
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
658
+ })
659
+ headerEmitted = true
660
+ }
529
661
 
530
662
  return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
531
663
  transform(chunk, controller) {
532
- if (offset > 0) {
533
- buffer = buffer.subarray(offset)
534
- offset = 0
535
- }
536
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
537
- newBuffer.set(buffer, 0)
538
- newBuffer.set(chunk, buffer.length)
539
- buffer = newBuffer
664
+ b.append(chunk)
665
+ // One view and one reader per chunk. Both used to be allocated per
666
+ // object, and an object is the thing there are a great many of.
667
+ const view = b.written()
540
668
 
541
- if (!headerEmitted) {
669
+ if (header === null) {
542
670
  try {
543
- const r = new BufferReader(buffer.subarray(offset))
544
- const streamType = Number(r.readVarInt())
545
-
546
- if (
547
- !(
548
- (streamType >= 0x10 && streamType <= 0x1f) ||
549
- (streamType >= 0x30 && streamType <= 0x3f)
550
- )
551
- ) {
552
- controller.error(
553
- new DecodeError(
554
- 'CONSTRAINT_VIOLATION',
555
- `Expected subgroup stream type, got 0x${streamType.toString(16)}`,
556
- 0,
557
- ),
558
- )
559
- return
560
- }
561
-
562
- _extensionsPresent = (streamType & 0x01) !== 0
563
- const hasSubgroupField = (streamType & 0x04) !== 0
564
- // Draft-15: bit 1 = end_of_group (not subgroupIsFirstObjId)
565
- _subgroupIsFirstObjId = false
566
- const hasPriority = streamType < 0x30
567
-
568
- const trackAlias = r.readVarInt()
569
- const groupId = r.readVarInt()
570
-
571
- let subgroupId = 0n
572
- if (hasSubgroupField) {
573
- subgroupId = r.readVarInt()
574
- }
575
-
576
- let publisherPriority = 128
577
- if (hasPriority) {
578
- publisherPriority = r.readUint8()
579
- }
580
-
581
- controller.enqueue({
582
- type: 'subgroup_header',
583
- trackAlias,
584
- groupId,
585
- subgroupId,
586
- publisherPriority,
587
- })
588
- headerEmitted = true
589
- offset += r.offset
671
+ const hr = new BufferReader(view, b.offset)
672
+ header = readSubgroupHeader(hr)
673
+ b.offset = hr.offset
590
674
  } catch (e) {
591
675
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
592
676
  return
@@ -594,83 +678,73 @@ export function createSubgroupStreamDecoder(): TransformStream<
594
678
  controller.error(e)
595
679
  return
596
680
  }
681
+ // Where the Subgroup ID is the first Object's ID, that Object has not
682
+ // been read yet. Holding the header back until it has is the only way
683
+ // to emit the value the one-shot decoder reports; every other mode
684
+ // knows its Subgroup ID already.
685
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
597
686
  }
598
687
 
599
- // Parse objects with delta-encoded IDs
600
- while (offset < buffer.length) {
688
+ const h = header
689
+ if (h === null) return
690
+
691
+ const r = new BufferReader(view, b.offset)
692
+ while (r.remaining > 0) {
693
+ // Where this Object starts, so a partial one is re-read from the top
694
+ // once the rest of it arrives.
695
+ const start = r.offset
696
+ let obj: ObjectPayload
601
697
  try {
602
- const r = new BufferReader(buffer.subarray(offset))
603
- const byteOffset = r.offset
604
- const delta = r.readVarInt()
605
- let objectId: bigint
606
- if (firstObject) {
607
- objectId = delta
608
- firstObject = false
609
- } else {
610
- objectId = prevObjectId + 1n + delta
611
- }
612
- let extensionData = new Uint8Array(0)
613
- if (_extensionsPresent) {
614
- const extLen = Number(r.readVarInt())
615
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
616
- }
617
- const payloadLength = Number(r.readVarInt())
618
- const payloadByteOffset = r.offset
619
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
620
- controller.enqueue({
621
- type: 'object',
622
- byteOffset,
623
- payloadByteOffset,
624
- objectId,
625
- payloadLength,
626
- extensionData,
627
- payload,
628
- })
629
- offset += r.offset
630
- prevObjectId = objectId
698
+ obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, b.base)
631
699
  } catch (e) {
632
700
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
633
- break
701
+ b.offset = start
702
+ return
634
703
  }
635
704
  controller.error(e)
636
705
  return
637
706
  }
707
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
708
+ isFirst = false
709
+ b.offset = r.offset
710
+ prevObjectId = obj.objectId
711
+ controller.enqueue(obj)
638
712
  }
639
713
  },
640
714
 
641
715
  flush(controller) {
642
- if (offset < buffer.length) {
716
+ // A stream carrying a header and no Objects still has a header to report,
717
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
718
+ // returns when it finds no first Object to derive one from.
719
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
720
+ if (b.unread > 0) {
643
721
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
644
722
  }
645
723
  },
646
724
  })
647
725
  }
648
726
 
649
- /**
650
- * Create a TransformStream that decodes a fetch data stream.
651
- */
652
727
  export function createFetchStreamDecoder(): TransformStream<
653
728
  Uint8Array,
654
729
  FetchStreamHeader | ObjectPayload
655
730
  > {
656
- let buffer = new Uint8Array(0)
657
- let offset = 0
731
+ const b = new StreamBuffer()
658
732
  let headerEmitted = false
733
+ /**
734
+ * The delta state the Objects carry between them. This decoder used to keep
735
+ * none, which is why DATAGRAM-mode Object IDs came out wrong: they resolve
736
+ * against the previous Object's ID.
737
+ */
738
+ const st = newFetchObjectState()
659
739
 
660
740
  return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
661
741
  transform(chunk, controller) {
662
- if (offset > 0) {
663
- buffer = buffer.subarray(offset)
664
- offset = 0
665
- }
666
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
667
- newBuffer.set(buffer, 0)
668
- newBuffer.set(chunk, buffer.length)
669
- buffer = newBuffer
742
+ b.append(chunk)
743
+ const view = b.written()
670
744
 
671
745
  if (!headerEmitted) {
672
746
  try {
673
- const r = new BufferReader(buffer.subarray(offset))
747
+ const r = new BufferReader(view, b.offset)
674
748
  const streamType = r.readVarInt()
675
749
  if (streamType !== FETCH_STREAM_TYPE) {
676
750
  controller.error(
@@ -685,7 +759,7 @@ export function createFetchStreamDecoder(): TransformStream<
685
759
  const requestId = r.readVarInt()
686
760
  controller.enqueue({ type: 'fetch_header', requestId })
687
761
  headerEmitted = true
688
- offset += r.offset
762
+ b.offset = r.offset
689
763
  } catch (e) {
690
764
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
691
765
  return
@@ -695,126 +769,110 @@ export function createFetchStreamDecoder(): TransformStream<
695
769
  }
696
770
  }
697
771
 
698
- // Parse fetch objects with serialization flags
699
- // Simplified: just read flags + explicit fields
700
- while (offset < buffer.length) {
772
+ const r = new BufferReader(view, b.offset)
773
+ while (r.remaining > 0) {
774
+ const start = r.offset
775
+ let obj: FetchObjectPayload
701
776
  try {
702
- const r = new BufferReader(buffer.subarray(offset))
703
- const flags = r.readUint8()
704
- const objectIdPresent = (flags & 0x04) !== 0
705
- const groupIdPresent = (flags & 0x08) !== 0
706
- const priorityPresent = (flags & 0x10) !== 0
707
- const extensionsPresent = (flags & 0x20) !== 0
708
- const subgroupEncoding = flags & 0x03
709
-
710
- if (groupIdPresent) r.readVarInt() // groupId — consumed
711
- if (subgroupEncoding === 0x03) r.readVarInt() // subgroupId — consumed
712
- let objectId = 0n
713
- if (objectIdPresent) objectId = r.readVarInt()
714
- if (priorityPresent) r.readUint8() // priority — consumed
715
- let extensionData = new Uint8Array(0)
716
- if (extensionsPresent) {
717
- const extLen = Number(r.readVarInt())
718
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
719
- }
720
- const payloadLength = Number(r.readVarInt())
721
- const payloadByteOffset = r.offset
722
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
723
- controller.enqueue({
724
- type: 'object',
725
- objectId,
726
- payloadLength,
727
- extensionData,
728
- payload,
729
- byteOffset: 0,
730
- payloadByteOffset,
731
- })
732
- offset += r.offset
777
+ obj = readFetchObject(r, b.base, st)
733
778
  } catch (e) {
734
779
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
735
- break
780
+ b.offset = start
781
+ return
736
782
  }
737
783
  controller.error(e)
738
784
  return
739
785
  }
786
+ b.offset = r.offset
787
+ controller.enqueue(obj)
740
788
  }
741
789
  },
742
790
 
743
791
  flush(controller) {
744
- if (offset < buffer.length) {
792
+ if (b.unread > 0) {
745
793
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
746
794
  }
747
795
  },
748
796
  })
749
797
  }
750
798
 
751
- /**
752
- * Create a unified auto-detecting data stream decoder.
753
- */
754
799
  export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
755
- let buffer = new Uint8Array(0)
756
- let offset = 0
757
- let inner: TransformStream<Uint8Array, DataStreamEvent> | null = null
758
- const _innerWriter: WritableStreamDefaultWriter<Uint8Array> | null = null
759
- const _innerReader: ReadableStreamDefaultReader<DataStreamEvent> | null = null
760
-
761
- return new TransformStream<Uint8Array, DataStreamEvent>({
762
- transform(chunk, controller) {
763
- if (offset > 0) {
764
- buffer = buffer.subarray(offset)
765
- offset = 0
766
- }
767
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
768
- newBuffer.set(buffer, 0)
769
- newBuffer.set(chunk, buffer.length)
770
- buffer = newBuffer
771
-
772
- if (inner === null) {
773
- // Need at least one byte to determine type
774
- if (offset >= buffer.length) return
775
- const firstByte = buffer[offset]!
776
-
777
- if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
778
- // Subgroup delegate to subgroup decoder
779
- // We need to feed the full buffer including the type byte
780
- const decoder = createSubgroupStreamDecoder()
781
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
782
- } else if (firstByte === 0x05) {
783
- // Fetch
784
- const decoder = createFetchStreamDecoder()
785
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
786
- } else {
787
- controller.error(
788
- new DecodeError(
789
- 'CONSTRAINT_VIOLATION',
790
- `Unknown data stream type: 0x${firstByte.toString(16)}`,
791
- 0,
792
- ),
793
- )
794
- return
800
+ /**
801
+ * Delegation that actually delegates.
802
+ *
803
+ * This function used to pick an inner decoder and then never write a byte to
804
+ * it: chunks accumulated in a local buffer and `flush` decoded the whole
805
+ * stream in one shot. Every event therefore arrived at end-of-stream, which
806
+ * on a subscription that stays open means no events at all. The inner
807
+ * decoder's readable is now pumped into this one's controller as the bytes
808
+ * arrive.
809
+ *
810
+ * The first byte still decides which decoder to use, on exactly the range
811
+ * this draft accepted before.
812
+ */
813
+ let writer: WritableStreamDefaultWriter<Uint8Array> | null = null
814
+ let pump: Promise<void> = Promise.resolve()
815
+ let broken = false
816
+
817
+ function attach(
818
+ inner: TransformStream<Uint8Array, DataStreamEvent>,
819
+ controller: TransformStreamDefaultController<DataStreamEvent>,
820
+ ): void {
821
+ writer = inner.writable.getWriter()
822
+ const reader = inner.readable.getReader()
823
+ pump = (async () => {
824
+ try {
825
+ for (;;) {
826
+ const { done, value } = await reader.read()
827
+ if (done) break
828
+ controller.enqueue(value)
829
+ }
830
+ } catch (e) {
831
+ // The inner decoder rejected the stream. It owns the diagnosis, so its
832
+ // error is the one that surfaces.
833
+ if (!broken) {
834
+ broken = true
835
+ controller.error(e)
795
836
  }
796
-
797
- // For simplicity, process inline instead of piping
798
- // Just re-parse from buffer using the appropriate one-shot decoder
799
837
  }
838
+ })()
839
+ }
800
840
 
801
- // Since inner TransformStream piping is complex, use a simpler approach:
802
- // Accumulate and attempt decode when flush is called
803
- // For the streaming case, we just buffer everything
804
- },
805
-
806
- flush(controller) {
807
- if (offset >= buffer.length) return
808
- const view = buffer.subarray(offset)
841
+ async function feed(bytes: Uint8Array): Promise<void> {
842
+ if (writer === null || broken || bytes.length === 0) return
843
+ try {
844
+ await writer.write(bytes)
845
+ } catch {
846
+ // Writing to an errored inner stream throws the same error the pump is
847
+ // already reporting; swallow it here so it is reported once.
848
+ broken = true
849
+ }
850
+ }
809
851
 
810
- const firstByte = view[0]!
811
- let result: DecodeResult<Draft15DataStream>
852
+ return new TransformStream<Uint8Array, DataStreamEvent>({
853
+ async transform(chunk, controller) {
854
+ if (broken) return
855
+ if (writer !== null) {
856
+ await feed(chunk)
857
+ return
858
+ }
859
+ // One byte is enough to choose, and until there is one there is nothing
860
+ // to choose from.
861
+ if (chunk.length === 0) return
862
+ const firstByte = chunk[0]!
812
863
 
813
864
  if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
814
- result = decodeSubgroupStream(view)
865
+ attach(
866
+ createSubgroupStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
867
+ controller,
868
+ )
815
869
  } else if (firstByte === 0x05) {
816
- result = decodeFetchStream(view)
870
+ attach(
871
+ createFetchStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
872
+ controller,
873
+ )
817
874
  } else {
875
+ broken = true
818
876
  controller.error(
819
877
  new DecodeError(
820
878
  'CONSTRAINT_VIOLATION',
@@ -825,32 +883,19 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
825
883
  return
826
884
  }
827
885
 
828
- if (!result.ok) {
829
- controller.error(result.error)
830
- return
831
- }
886
+ // The type byte is part of the stream the inner decoder reads, so the
887
+ // whole chunk goes in, first byte included.
888
+ await feed(chunk)
889
+ },
832
890
 
833
- const stream = result.value
834
- if (stream.type === 'subgroup') {
835
- controller.enqueue({
836
- type: 'subgroup_header',
837
- trackAlias: stream.trackAlias,
838
- groupId: stream.groupId,
839
- subgroupId: stream.subgroupId,
840
- publisherPriority: stream.publisherPriority,
841
- })
842
- for (const obj of stream.objects) {
843
- controller.enqueue(obj)
844
- }
845
- } else if (stream.type === 'fetch') {
846
- controller.enqueue({
847
- type: 'fetch_header',
848
- requestId: stream.requestId,
849
- })
850
- for (const obj of stream.objects) {
851
- controller.enqueue(obj)
852
- }
891
+ async flush() {
892
+ if (writer === null) return
893
+ try {
894
+ await writer.close()
895
+ } catch {
896
+ // Reported by the pump.
853
897
  }
898
+ await pump
854
899
  },
855
900
  })
856
901
  }