@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
@@ -130,105 +130,158 @@ export function encodeFetchStream(stream: FetchStream): Uint8Array {
130
130
  /**
131
131
  * Decode a subgroup data stream from raw bytes.
132
132
  */
133
- export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
134
- try {
135
- const r = new BufferReader(bytes)
136
- const streamType = Number(r.readVarInt())
137
-
138
- // Draft-16 subgroup type: bits 0=extensions, 2:1=subgroup_id_mode, 3=end_of_group, 5=no_priority
139
- // Valid range: 0x10-0x1F (with priority) or 0x30-0x3F (without priority)
140
- // Reserved: subgroup_id_mode 0b11 (bits 2:1)
141
- if (
142
- !((streamType >= 0x10 && streamType <= 0x1f) || (streamType >= 0x30 && streamType <= 0x3f)) ||
143
- (streamType & 0x06) === 0x06 // reserved SUBGROUP_ID_MODE (bits 2:1 = 0b11)
144
- ) {
145
- return {
146
- ok: false,
147
- error: new DecodeError(
148
- 'CONSTRAINT_VIOLATION',
149
- `Expected subgroup stream type 0x10-0x1F/0x30-0x3F, got 0x${streamType.toString(16)}`,
150
- 0,
151
- ),
152
- }
153
- }
133
+ /**
134
+ * The SUBGROUP_HEADER fields, read once for both decoders.
135
+ *
136
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` used to parse this
137
+ * header separately and they drifted: the incremental one dropped the header
138
+ * flags, reported no Type Flags, and left the Subgroup ID at zero under the
139
+ * mode that derives it from the first Object. One reader means there is
140
+ * nothing left to keep in sync.
141
+ */
142
+ interface SubgroupHeaderFields {
143
+ readonly streamType: number
144
+ readonly extensionsPresent: boolean
145
+ readonly subgroupIsFirstObjId: boolean
146
+ readonly endOfGroup: boolean
147
+ readonly trackAlias: bigint
148
+ readonly groupId: bigint
149
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
150
+ readonly subgroupId: bigint
151
+ readonly publisherPriority: number
152
+ }
154
153
 
155
- const extensionsPresent = (streamType & 0x01) !== 0
156
- const hasSubgroupField = (streamType & 0x04) !== 0
157
- const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
158
- const endOfGroup = (streamType & 0x08) !== 0
159
- const hasPriority = streamType < 0x30
154
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
155
+ const streamType = Number(r.readVarInt())
156
+ if (
157
+ !((streamType >= 0x10 && streamType <= 0x1f) || (streamType >= 0x30 && streamType <= 0x3f)) ||
158
+ (streamType & 0x06) === 0x06 // reserved SUBGROUP_ID_MODE (bits 2:1 = 0b11)
159
+ ) {
160
+ throw new DecodeError(
161
+ 'CONSTRAINT_VIOLATION',
162
+ `Expected subgroup stream type 0x10-0x1F/0x30-0x3F, got 0x${streamType.toString(16)}`,
163
+ 0,
164
+ )
165
+ }
160
166
 
161
- const trackAlias = r.readVarInt()
162
- const groupId = r.readVarInt()
167
+ const extensionsPresent = (streamType & 0x01) !== 0
168
+ const hasSubgroupField = (streamType & 0x04) !== 0
169
+ const subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
170
+ const endOfGroup = (streamType & 0x08) !== 0
171
+ const hasPriority = streamType < 0x30
163
172
 
164
- let subgroupId = 0n
165
- if (hasSubgroupField) {
166
- subgroupId = r.readVarInt()
167
- }
173
+ const trackAlias = r.readVarInt()
174
+ const groupId = r.readVarInt()
168
175
 
169
- let publisherPriority = 128
170
- if (hasPriority) {
171
- publisherPriority = r.readUint8()
172
- }
176
+ let subgroupId = 0n
177
+ if (hasSubgroupField) {
178
+ subgroupId = r.readVarInt()
179
+ }
180
+
181
+ let publisherPriority = 128
182
+ if (hasPriority) {
183
+ publisherPriority = r.readUint8()
184
+ }
185
+
186
+ return {
187
+ streamType,
188
+ extensionsPresent,
189
+ subgroupIsFirstObjId,
190
+ endOfGroup,
191
+ trackAlias,
192
+ groupId,
193
+ subgroupId,
194
+ publisherPriority,
195
+ }
196
+ }
197
+
198
+ /**
199
+ * One Object off a subgroup stream, for both decoders.
200
+ *
201
+ * `base` is the absolute offset within the stream that `r.offset === 0`
202
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
203
+ * stream and passes 0; the incremental decoder reads from a window over its
204
+ * own buffer and passes that window's position. That is what makes
205
+ * `byteOffset` mean the same thing on both paths, instead of being hardcoded
206
+ * to zero on one of them.
207
+ *
208
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
209
+ * incremental decoder reads as "wait for more bytes" rather than as an error.
210
+ */
211
+ function readSubgroupObject(
212
+ r: BufferReader,
213
+ extensionsPresent: boolean,
214
+ isFirst: boolean,
215
+ prevObjectId: bigint,
216
+ base: number,
217
+ ): ObjectPayload {
218
+ const byteOffset = base + r.offset
219
+ const delta = r.readVarInt()
220
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
221
+
222
+ let extensionData = new Uint8Array(0)
223
+ if (extensionsPresent) {
224
+ const extLen = Number(r.readVarInt())
225
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
226
+ }
227
+
228
+ const payloadLength = Number(r.readVarInt())
229
+ let payload: Uint8Array
230
+ let status: bigint | undefined
231
+ let payloadByteOffset: number
232
+ if (payloadLength === 0) {
233
+ // The Object Status field is sent only when the Payload Length is zero.
234
+ // Skipping it leaves the varint to be read as the next Object's Object ID
235
+ // Delta, which desynchronises the rest of the stream silently.
236
+ status = r.readVarInt()
237
+ payloadByteOffset = base + r.offset
238
+ payload = new Uint8Array(0)
239
+ } else {
240
+ payloadByteOffset = base + r.offset
241
+ payload = r.readBytesView(payloadLength)
242
+ }
243
+
244
+ const obj: ObjectPayload = {
245
+ type: 'object',
246
+ byteOffset,
247
+ payloadByteOffset,
248
+ objectId,
249
+ payloadLength,
250
+ payload,
251
+ extensionData,
252
+ }
253
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
254
+ return obj
255
+ }
256
+
257
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
258
+ try {
259
+ const r = new BufferReader(bytes)
260
+ const h = readSubgroupHeader(r)
173
261
 
174
262
  const objects: ObjectPayload[] = []
175
263
  let prevObjectId = -1n
176
- let firstObject = true
264
+ let isFirst = true
265
+ let subgroupId = h.subgroupId
177
266
 
178
267
  while (r.remaining > 0) {
179
- const byteOffset = r.offset
180
- const delta = r.readVarInt()
181
- let objectId: bigint
182
- if (firstObject) {
183
- objectId = delta
184
- if (subgroupIsFirstObjId && firstObject) {
185
- subgroupId = objectId
186
- }
187
- firstObject = false
188
- } else {
189
- objectId = prevObjectId + 1n + delta
190
- }
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
- const payloadLength = Number(r.readVarInt())
197
- let payload: Uint8Array
198
- let status: bigint | undefined
199
- let payloadByteOffset: number
200
- if (payloadLength === 0) {
201
- status = r.readVarInt()
202
- payloadByteOffset = r.offset
203
- payload = new Uint8Array(0)
204
- } else {
205
- payloadByteOffset = r.offset
206
- payload = r.readBytesView(payloadLength)
207
- }
208
- const obj: ObjectPayload = {
209
- type: 'object',
210
- byteOffset,
211
- payloadByteOffset,
212
- objectId,
213
- payloadLength,
214
- extensionData,
215
- payload,
216
- }
217
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
268
+ const obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, 0)
269
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
270
+ isFirst = false
218
271
  objects.push(obj)
219
- prevObjectId = objectId
272
+ prevObjectId = obj.objectId
220
273
  }
221
274
 
222
275
  const result: SubgroupStream = {
223
276
  type: 'subgroup',
224
- headerType: streamType,
225
- trackAlias,
226
- groupId,
277
+ headerType: h.streamType,
278
+ trackAlias: h.trackAlias,
279
+ groupId: h.groupId,
227
280
  subgroupId,
228
- publisherPriority,
281
+ publisherPriority: h.publisherPriority,
229
282
  objects,
283
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
230
284
  }
231
- if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
232
285
 
233
286
  return {
234
287
  ok: true,
@@ -241,9 +294,6 @@ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupSt
241
294
  }
242
295
  }
243
296
 
244
- /**
245
- * Decode a datagram object from raw bytes.
246
- */
247
297
  export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject> {
248
298
  try {
249
299
  const r = new BufferReader(bytes)
@@ -309,6 +359,165 @@ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
309
359
  /**
310
360
  * Decode a fetch data stream from raw bytes.
311
361
  */
362
+ /** The delta state a fetch stream carries from one Object to the next. */
363
+ interface FetchObjectState {
364
+ prevGroupId: bigint
365
+ prevSubgroupId: bigint
366
+ prevObjectId: bigint
367
+ prevPriority: number
368
+ first: boolean
369
+ }
370
+
371
+ function newFetchObjectState(): FetchObjectState {
372
+ return { prevGroupId: 0n, prevSubgroupId: 0n, prevObjectId: 0n, prevPriority: 128, first: true }
373
+ }
374
+
375
+ /**
376
+ * One Object off a fetch stream, for both decoders.
377
+ *
378
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
379
+ * cannot report a different shape for the same bytes. It used to return a
380
+ * bare ObjectPayload -- no Serialization Flags, no Group or Subgroup ID, no
381
+ * Priority, no deltas -- with every `byteOffset` zero, and in DATAGRAM mode
382
+ * it resolved the Object ID against state it never carried.
383
+ *
384
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
385
+ * the one-shot decoder, the buffer window's position for the incremental one.
386
+ */
387
+ function readFetchObject(r: BufferReader, base: number, st: FetchObjectState): FetchObjectPayload {
388
+ // Both branches below build the Object; the carried-state updates that
389
+ // follow them are shared, so the result is held rather than returned twice.
390
+ let obj: FetchObjectPayload
391
+
392
+ const byteOffset = base + r.offset
393
+ const flags = Number(r.readVarInt())
394
+
395
+ let groupId = st.prevGroupId
396
+ let subgroupId = st.prevSubgroupId
397
+ let objectId = st.prevObjectId + 1n
398
+ let payloadLength: number
399
+ let payload: Uint8Array
400
+
401
+ if (flags >= 0x80) {
402
+ // End of Range: 0x8C = End of Non-Existent Range, 0x10C = End of Unknown Range
403
+ // Per spec §10.4.4.2: Group ID and Object ID are present;
404
+ // Subgroup ID, Priority, Extensions are not present.
405
+ if (flags !== 0x8c && flags !== 0x10c) {
406
+ throw new DecodeError(
407
+ 'CONSTRAINT_VIOLATION',
408
+ `Unknown serialization flags value: 0x${flags.toString(16)}`,
409
+ r.offset,
410
+ )
411
+ }
412
+ groupId = r.readVarInt()
413
+ objectId = r.readVarInt()
414
+ payloadLength = Number(r.readVarInt())
415
+ const payloadByteOffset = base + r.offset
416
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
417
+ obj = {
418
+ type: 'object',
419
+ byteOffset,
420
+ payloadByteOffset,
421
+ serializationFlags: flags,
422
+ groupId,
423
+ subgroupId,
424
+ objectId,
425
+ publisherPriority: st.prevPriority,
426
+ payloadLength,
427
+ extensionData: new Uint8Array(0),
428
+ payload,
429
+ }
430
+ } else {
431
+ const datagramForwarding = (flags & 0x40) !== 0
432
+ const subgroupEncoding = datagramForwarding ? -1 : flags & 0x03
433
+ const objectIdPresent = (flags & 0x04) !== 0
434
+ const groupIdPresent = (flags & 0x08) !== 0
435
+ const priorityPresent = (flags & 0x10) !== 0
436
+ const extensionsPresent = (flags & 0x20) !== 0
437
+
438
+ if (groupIdPresent) {
439
+ groupId = r.readVarInt()
440
+ } else if (st.first) {
441
+ throw new DecodeError(
442
+ 'CONSTRAINT_VIOLATION',
443
+ 'First fetch object must include groupId',
444
+ r.offset,
445
+ )
446
+ }
447
+
448
+ if (datagramForwarding) {
449
+ // Datagram forwarding: no subgroup field
450
+ } else if (subgroupEncoding === 0x00) {
451
+ subgroupId = 0n
452
+ } else if (subgroupEncoding === 0x01) {
453
+ if (st.first) {
454
+ throw new DecodeError(
455
+ 'CONSTRAINT_VIOLATION',
456
+ 'First fetch object cannot reference prior subgroupId',
457
+ r.offset,
458
+ )
459
+ }
460
+ // subgroupId = st.prevSubgroupId (already set by default)
461
+ } else if (subgroupEncoding === 0x02) {
462
+ if (st.first) {
463
+ throw new DecodeError(
464
+ 'CONSTRAINT_VIOLATION',
465
+ 'First fetch object cannot reference prior subgroupId',
466
+ r.offset,
467
+ )
468
+ }
469
+ subgroupId = st.prevSubgroupId + 1n
470
+ } else {
471
+ subgroupId = r.readVarInt()
472
+ }
473
+
474
+ if (objectIdPresent) {
475
+ objectId = r.readVarInt()
476
+ } else if (st.first) {
477
+ throw new DecodeError(
478
+ 'CONSTRAINT_VIOLATION',
479
+ 'First fetch object must include objectId',
480
+ r.offset,
481
+ )
482
+ }
483
+
484
+ if (priorityPresent) {
485
+ st.prevPriority = r.readUint8()
486
+ }
487
+
488
+ let extensionData = new Uint8Array(0)
489
+ if (extensionsPresent) {
490
+ const extLen = Number(r.readVarInt())
491
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
492
+ }
493
+
494
+ payloadLength = Number(r.readVarInt())
495
+ const payloadByteOffset = base + r.offset
496
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
497
+
498
+ obj = {
499
+ type: 'object',
500
+ byteOffset,
501
+ payloadByteOffset,
502
+ serializationFlags: flags,
503
+ groupId,
504
+ subgroupId,
505
+ objectId,
506
+ publisherPriority: st.prevPriority,
507
+ payloadLength,
508
+ extensionData,
509
+ payload,
510
+ }
511
+ }
512
+
513
+ st.prevGroupId = groupId
514
+ st.prevSubgroupId = subgroupId
515
+ st.prevObjectId = objectId
516
+ st.first = false
517
+
518
+ return obj
519
+ }
520
+
312
521
  export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
313
522
  try {
314
523
  const r = new BufferReader(bytes)
@@ -326,153 +535,10 @@ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
326
535
  const requestId = r.readVarInt()
327
536
  const objects: FetchObjectPayload[] = []
328
537
 
329
- let prevGroupId = 0n
330
- let prevSubgroupId = 0n
331
- let prevObjectId = 0n
332
- let prevPriority = 128
333
- let first = true
538
+ const st = newFetchObjectState()
334
539
 
335
540
  while (r.remaining > 0) {
336
- const byteOffset = r.offset
337
- const flags = Number(r.readVarInt())
338
-
339
- let groupId = prevGroupId
340
- let subgroupId = prevSubgroupId
341
- let objectId = prevObjectId + 1n
342
- let payloadLength: number
343
- let payload: Uint8Array
344
-
345
- if (flags >= 0x80) {
346
- // End of Range: 0x8C = End of Non-Existent Range, 0x10C = End of Unknown Range
347
- // Per spec §10.4.4.2: Group ID and Object ID are present;
348
- // Subgroup ID, Priority, Extensions are not present.
349
- if (flags !== 0x8c && flags !== 0x10c) {
350
- return {
351
- ok: false,
352
- error: new DecodeError(
353
- 'CONSTRAINT_VIOLATION',
354
- `Unknown serialization flags value: 0x${flags.toString(16)}`,
355
- r.offset,
356
- ),
357
- }
358
- }
359
- groupId = r.readVarInt()
360
- objectId = r.readVarInt()
361
- payloadLength = Number(r.readVarInt())
362
- const payloadByteOffset = r.offset
363
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
364
- objects.push({
365
- type: 'object',
366
- byteOffset,
367
- payloadByteOffset,
368
- serializationFlags: flags,
369
- groupId,
370
- subgroupId,
371
- objectId,
372
- publisherPriority: prevPriority,
373
- payloadLength,
374
- extensionData: new Uint8Array(0),
375
- payload,
376
- })
377
- } else {
378
- const datagramForwarding = (flags & 0x40) !== 0
379
- const subgroupEncoding = datagramForwarding ? -1 : flags & 0x03
380
- const objectIdPresent = (flags & 0x04) !== 0
381
- const groupIdPresent = (flags & 0x08) !== 0
382
- const priorityPresent = (flags & 0x10) !== 0
383
- const extensionsPresent = (flags & 0x20) !== 0
384
-
385
- if (groupIdPresent) {
386
- groupId = r.readVarInt()
387
- } else if (first) {
388
- return {
389
- ok: false,
390
- error: new DecodeError(
391
- 'CONSTRAINT_VIOLATION',
392
- 'First fetch object must include groupId',
393
- r.offset,
394
- ),
395
- }
396
- }
397
-
398
- if (datagramForwarding) {
399
- // Datagram forwarding: no subgroup field
400
- } else if (subgroupEncoding === 0x00) {
401
- subgroupId = 0n
402
- } else if (subgroupEncoding === 0x01) {
403
- if (first) {
404
- return {
405
- ok: false,
406
- error: new DecodeError(
407
- 'CONSTRAINT_VIOLATION',
408
- 'First fetch object cannot reference prior subgroupId',
409
- r.offset,
410
- ),
411
- }
412
- }
413
- // subgroupId = prevSubgroupId (already set by default)
414
- } else if (subgroupEncoding === 0x02) {
415
- if (first) {
416
- return {
417
- ok: false,
418
- error: new DecodeError(
419
- 'CONSTRAINT_VIOLATION',
420
- 'First fetch object cannot reference prior subgroupId',
421
- r.offset,
422
- ),
423
- }
424
- }
425
- subgroupId = prevSubgroupId + 1n
426
- } else {
427
- subgroupId = r.readVarInt()
428
- }
429
-
430
- if (objectIdPresent) {
431
- objectId = r.readVarInt()
432
- } else if (first) {
433
- return {
434
- ok: false,
435
- error: new DecodeError(
436
- 'CONSTRAINT_VIOLATION',
437
- 'First fetch object must include objectId',
438
- r.offset,
439
- ),
440
- }
441
- }
442
-
443
- if (priorityPresent) {
444
- prevPriority = r.readUint8()
445
- }
446
-
447
- let extensionData = new Uint8Array(0)
448
- if (extensionsPresent) {
449
- const extLen = Number(r.readVarInt())
450
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
451
- }
452
-
453
- payloadLength = Number(r.readVarInt())
454
- const payloadByteOffset = r.offset
455
- payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
456
-
457
- objects.push({
458
- type: 'object',
459
- byteOffset,
460
- payloadByteOffset,
461
- serializationFlags: flags,
462
- groupId,
463
- subgroupId,
464
- objectId,
465
- publisherPriority: prevPriority,
466
- payloadLength,
467
- extensionData,
468
- payload,
469
- })
470
- }
471
-
472
- prevGroupId = groupId
473
- prevSubgroupId = subgroupId
474
- prevObjectId = objectId
475
- first = false
541
+ objects.push(readFetchObject(r, 0, st))
476
542
  }
477
543
 
478
544
  return {
@@ -509,77 +575,107 @@ export function decodeDataStream(
509
575
 
510
576
  // ─── Data Stream Decoders ──────────────────────────────────────────────────────
511
577
 
578
+ /**
579
+ * A data stream's bytes, accumulated across chunks.
580
+ *
581
+ * The decoders below used to allocate an array of `unread + chunk.length` and
582
+ * copy both halves into it on *every* chunk. Capacity now grows geometrically,
583
+ * so most chunks are a single `set` into spare room.
584
+ *
585
+ * **It never compacts in place.** Object payloads are handed out as views into
586
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
587
+ * object that has already been emitted. The consumed prefix is dropped only
588
+ * when a fresh array is being allocated anyway, which leaves the old array
589
+ * alive for exactly as long as the views into it are.
590
+ */
591
+ class StreamBuffer {
592
+ private buf = new Uint8Array(0)
593
+ /** Bytes written into `buf`. */
594
+ private len = 0
595
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
596
+ offset = 0
597
+ /** Absolute position of `buf[0]` within the stream. */
598
+ base = 0
599
+
600
+ get unread(): number {
601
+ return this.len - this.offset
602
+ }
603
+
604
+ append(chunk: Uint8Array): void {
605
+ if (this.len + chunk.length > this.buf.length) {
606
+ const live = this.len - this.offset
607
+ const needed = live + chunk.length
608
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
609
+ // half free. Sizing to `needed` -- which is what this did first -- leaves
610
+ // no room at all, so the next chunk reallocates too and the buffer
611
+ // reallocates on every single chunk, which is the cost it exists to
612
+ // avoid. Capacity never shrinks, so it settles at roughly twice the
613
+ // largest `live + chunk` this stream has seen.
614
+ let cap = Math.max(4096, this.buf.length)
615
+ while (cap < needed * 2) cap *= 2
616
+ // A fresh array every time, including when the capacity is unchanged:
617
+ // the old one is still pinned by the payload views handed out of it, so
618
+ // it can be neither compacted nor reused.
619
+ const next = new Uint8Array(cap)
620
+ next.set(this.buf.subarray(this.offset, this.len), 0)
621
+ this.buf = next
622
+ this.base += this.offset
623
+ this.len = live
624
+ this.offset = 0
625
+ }
626
+ this.buf.set(chunk, this.len)
627
+ this.len += chunk.length
628
+ }
629
+
630
+ /**
631
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
632
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
633
+ * not the spare room after them.
634
+ */
635
+ written(): Uint8Array {
636
+ return this.buf.subarray(0, this.len)
637
+ }
638
+ }
639
+
512
640
  export function createSubgroupStreamDecoder(): TransformStream<
513
641
  Uint8Array,
514
642
  SubgroupStreamHeader | ObjectPayload
515
643
  > {
516
- let buffer = new Uint8Array(0)
517
- let offset = 0
644
+ const b = new StreamBuffer()
645
+ let header: SubgroupHeaderFields | null = null
518
646
  let headerEmitted = false
519
647
  let prevObjectId = -1n
520
- let firstObject = true
521
- let _subgroupIsFirstObjId = false
522
- let _extensionsPresent = false
648
+ let isFirst = true
649
+
650
+ function emitHeader(
651
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
652
+ h: SubgroupHeaderFields,
653
+ subgroupId: bigint,
654
+ ): void {
655
+ controller.enqueue({
656
+ type: 'subgroup_header',
657
+ headerType: h.streamType,
658
+ trackAlias: h.trackAlias,
659
+ groupId: h.groupId,
660
+ subgroupId,
661
+ publisherPriority: h.publisherPriority,
662
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
663
+ })
664
+ headerEmitted = true
665
+ }
523
666
 
524
667
  return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
525
668
  transform(chunk, controller) {
526
- if (offset > 0) {
527
- buffer = buffer.subarray(offset)
528
- offset = 0
529
- }
530
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
531
- newBuffer.set(buffer, 0)
532
- newBuffer.set(chunk, buffer.length)
533
- buffer = newBuffer
669
+ b.append(chunk)
670
+ // One view and one reader per chunk. Both used to be allocated per
671
+ // object, and an object is the thing there are a great many of.
672
+ const view = b.written()
534
673
 
535
- if (!headerEmitted) {
674
+ if (header === null) {
536
675
  try {
537
- const r = new BufferReader(buffer.subarray(offset))
538
- const streamType = Number(r.readVarInt())
539
-
540
- if (
541
- !(
542
- (streamType >= 0x10 && streamType <= 0x1f) ||
543
- (streamType >= 0x30 && streamType <= 0x3f)
544
- )
545
- ) {
546
- controller.error(
547
- new DecodeError(
548
- 'CONSTRAINT_VIOLATION',
549
- `Expected subgroup stream type, got 0x${streamType.toString(16)}`,
550
- 0,
551
- ),
552
- )
553
- return
554
- }
555
-
556
- _extensionsPresent = (streamType & 0x01) !== 0
557
- const hasSubgroupField = (streamType & 0x04) !== 0
558
- _subgroupIsFirstObjId = (streamType & 0x02) !== 0 && !hasSubgroupField
559
- const hasPriority = streamType < 0x30
560
-
561
- const trackAlias = r.readVarInt()
562
- const groupId = r.readVarInt()
563
-
564
- let subgroupId = 0n
565
- if (hasSubgroupField) {
566
- subgroupId = r.readVarInt()
567
- }
568
-
569
- let publisherPriority = 128
570
- if (hasPriority) {
571
- publisherPriority = r.readUint8()
572
- }
573
-
574
- controller.enqueue({
575
- type: 'subgroup_header',
576
- trackAlias,
577
- groupId,
578
- subgroupId,
579
- publisherPriority,
580
- })
581
- headerEmitted = true
582
- offset += r.offset
676
+ const hr = new BufferReader(view, b.offset)
677
+ header = readSubgroupHeader(hr)
678
+ b.offset = hr.offset
583
679
  } catch (e) {
584
680
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
585
681
  return
@@ -587,59 +683,46 @@ export function createSubgroupStreamDecoder(): TransformStream<
587
683
  controller.error(e)
588
684
  return
589
685
  }
686
+ // Where the Subgroup ID is the first Object's ID, that Object has not
687
+ // been read yet. Holding the header back until it has is the only way
688
+ // to emit the value the one-shot decoder reports; every other mode
689
+ // knows its Subgroup ID already.
690
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
590
691
  }
591
692
 
592
- while (offset < buffer.length) {
693
+ const h = header
694
+ if (h === null) return
695
+
696
+ const r = new BufferReader(view, b.offset)
697
+ while (r.remaining > 0) {
698
+ // Where this Object starts, so a partial one is re-read from the top
699
+ // once the rest of it arrives.
700
+ const start = r.offset
701
+ let obj: ObjectPayload
593
702
  try {
594
- const r = new BufferReader(buffer.subarray(offset))
595
- const delta = r.readVarInt()
596
- let objectId: bigint
597
- if (firstObject) {
598
- objectId = delta
599
- firstObject = false
600
- } else {
601
- objectId = prevObjectId + 1n + delta
602
- }
603
- let extensionData = new Uint8Array(0)
604
- if (_extensionsPresent) {
605
- const extLen = Number(r.readVarInt())
606
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
607
- }
608
- const payloadLength = Number(r.readVarInt())
609
- let payloadByteOffset: number
610
- let status: bigint | undefined
611
- if (payloadLength === 0) {
612
- status = r.readVarInt()
613
- payloadByteOffset = r.offset
614
- } else {
615
- payloadByteOffset = r.offset
616
- }
617
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
618
- const obj: ObjectPayload = {
619
- type: 'object',
620
- byteOffset: 0,
621
- payloadByteOffset,
622
- objectId,
623
- payloadLength,
624
- payload,
625
- extensionData,
626
- }
627
- if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
628
- controller.enqueue(obj)
629
- offset += r.offset
630
- prevObjectId = objectId
703
+ obj = readSubgroupObject(r, h.extensionsPresent, isFirst, prevObjectId, b.base)
631
704
  } catch (e) {
632
705
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
633
- break
706
+ b.offset = start
707
+ return
634
708
  }
635
709
  controller.error(e)
636
710
  return
637
711
  }
712
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
713
+ isFirst = false
714
+ b.offset = r.offset
715
+ prevObjectId = obj.objectId
716
+ controller.enqueue(obj)
638
717
  }
639
718
  },
640
719
 
641
720
  flush(controller) {
642
- if (offset < buffer.length) {
721
+ // A stream carrying a header and no Objects still has a header to report,
722
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
723
+ // returns when it finds no first Object to derive one from.
724
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
725
+ if (b.unread > 0) {
643
726
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
644
727
  }
645
728
  },
@@ -650,24 +733,23 @@ export function createFetchStreamDecoder(): TransformStream<
650
733
  Uint8Array,
651
734
  FetchStreamHeader | ObjectPayload
652
735
  > {
653
- let buffer = new Uint8Array(0)
654
- let offset = 0
736
+ const b = new StreamBuffer()
655
737
  let headerEmitted = false
738
+ /**
739
+ * The delta state the Objects carry between them. This decoder used to keep
740
+ * none, which is why DATAGRAM-mode Object IDs came out wrong: they resolve
741
+ * against the previous Object's ID.
742
+ */
743
+ const st = newFetchObjectState()
656
744
 
657
745
  return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
658
746
  transform(chunk, controller) {
659
- if (offset > 0) {
660
- buffer = buffer.subarray(offset)
661
- offset = 0
662
- }
663
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
664
- newBuffer.set(buffer, 0)
665
- newBuffer.set(chunk, buffer.length)
666
- buffer = newBuffer
747
+ b.append(chunk)
748
+ const view = b.written()
667
749
 
668
750
  if (!headerEmitted) {
669
751
  try {
670
- const r = new BufferReader(buffer.subarray(offset))
752
+ const r = new BufferReader(view, b.offset)
671
753
  const streamType = r.readVarInt()
672
754
  if (streamType !== FETCH_STREAM_TYPE) {
673
755
  controller.error(
@@ -682,7 +764,7 @@ export function createFetchStreamDecoder(): TransformStream<
682
764
  const requestId = r.readVarInt()
683
765
  controller.enqueue({ type: 'fetch_header', requestId })
684
766
  headerEmitted = true
685
- offset += r.offset
767
+ b.offset = r.offset
686
768
  } catch (e) {
687
769
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
688
770
  return
@@ -692,51 +774,27 @@ export function createFetchStreamDecoder(): TransformStream<
692
774
  }
693
775
  }
694
776
 
695
- while (offset < buffer.length) {
777
+ const r = new BufferReader(view, b.offset)
778
+ while (r.remaining > 0) {
779
+ const start = r.offset
780
+ let obj: FetchObjectPayload
696
781
  try {
697
- const r = new BufferReader(buffer.subarray(offset))
698
- const flags = Number(r.readVarInt())
699
- const objectIdPresent = (flags & 0x04) !== 0
700
- const groupIdPresent = (flags & 0x08) !== 0
701
- const priorityPresent = (flags & 0x10) !== 0
702
- const extensionsPresent = (flags & 0x20) !== 0
703
- const subgroupEncoding = flags & 0x03
704
-
705
- if (groupIdPresent) r.readVarInt()
706
- if (subgroupEncoding === 0x03) r.readVarInt()
707
- let objectId = 0n
708
- if (objectIdPresent) objectId = r.readVarInt()
709
- if (priorityPresent) r.readUint8()
710
- let extensionData = new Uint8Array(0)
711
- if (extensionsPresent) {
712
- const extLen = Number(r.readVarInt())
713
- extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
714
- }
715
- const payloadLength = Number(r.readVarInt())
716
- const payloadByteOffset = r.offset
717
- const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
718
- controller.enqueue({
719
- type: 'object',
720
- byteOffset: 0,
721
- payloadByteOffset,
722
- objectId,
723
- payloadLength,
724
- payload,
725
- extensionData,
726
- })
727
- offset += r.offset
782
+ obj = readFetchObject(r, b.base, st)
728
783
  } catch (e) {
729
784
  if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
730
- break
785
+ b.offset = start
786
+ return
731
787
  }
732
788
  controller.error(e)
733
789
  return
734
790
  }
791
+ b.offset = r.offset
792
+ controller.enqueue(obj)
735
793
  }
736
794
  },
737
795
 
738
796
  flush(controller) {
739
- if (offset < buffer.length) {
797
+ if (b.unread > 0) {
740
798
  controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
741
799
  }
742
800
  },
@@ -744,56 +802,82 @@ export function createFetchStreamDecoder(): TransformStream<
744
802
  }
745
803
 
746
804
  export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
747
- let buffer = new Uint8Array(0)
748
- let offset = 0
749
- let inner: TransformStream<Uint8Array, DataStreamEvent> | null = null
750
-
751
- return new TransformStream<Uint8Array, DataStreamEvent>({
752
- transform(chunk, controller) {
753
- if (offset > 0) {
754
- buffer = buffer.subarray(offset)
755
- offset = 0
756
- }
757
- const newBuffer = new Uint8Array(buffer.length + chunk.length)
758
- newBuffer.set(buffer, 0)
759
- newBuffer.set(chunk, buffer.length)
760
- buffer = newBuffer
761
-
762
- if (inner === null) {
763
- if (offset >= buffer.length) return
764
- const firstByte = buffer[offset]!
765
-
766
- if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
767
- const decoder = createSubgroupStreamDecoder()
768
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
769
- } else if (firstByte === 0x05) {
770
- const decoder = createFetchStreamDecoder()
771
- inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
772
- } else {
773
- controller.error(
774
- new DecodeError(
775
- 'CONSTRAINT_VIOLATION',
776
- `Unknown data stream type: 0x${firstByte.toString(16)}`,
777
- 0,
778
- ),
779
- )
780
- return
805
+ /**
806
+ * Delegation that actually delegates.
807
+ *
808
+ * This function used to pick an inner decoder and then never write a byte to
809
+ * it: chunks accumulated in a local buffer and `flush` decoded the whole
810
+ * stream in one shot. Every event therefore arrived at end-of-stream, which
811
+ * on a subscription that stays open means no events at all. The inner
812
+ * decoder's readable is now pumped into this one's controller as the bytes
813
+ * arrive.
814
+ *
815
+ * The first byte still decides which decoder to use, on exactly the range
816
+ * this draft accepted before.
817
+ */
818
+ let writer: WritableStreamDefaultWriter<Uint8Array> | null = null
819
+ let pump: Promise<void> = Promise.resolve()
820
+ let broken = false
821
+
822
+ function attach(
823
+ inner: TransformStream<Uint8Array, DataStreamEvent>,
824
+ controller: TransformStreamDefaultController<DataStreamEvent>,
825
+ ): void {
826
+ writer = inner.writable.getWriter()
827
+ const reader = inner.readable.getReader()
828
+ pump = (async () => {
829
+ try {
830
+ for (;;) {
831
+ const { done, value } = await reader.read()
832
+ if (done) break
833
+ controller.enqueue(value)
834
+ }
835
+ } catch (e) {
836
+ // The inner decoder rejected the stream. It owns the diagnosis, so its
837
+ // error is the one that surfaces.
838
+ if (!broken) {
839
+ broken = true
840
+ controller.error(e)
781
841
  }
782
842
  }
783
- },
843
+ })()
844
+ }
784
845
 
785
- flush(controller) {
786
- if (offset >= buffer.length) return
787
- const view = buffer.subarray(offset)
846
+ async function feed(bytes: Uint8Array): Promise<void> {
847
+ if (writer === null || broken || bytes.length === 0) return
848
+ try {
849
+ await writer.write(bytes)
850
+ } catch {
851
+ // Writing to an errored inner stream throws the same error the pump is
852
+ // already reporting; swallow it here so it is reported once.
853
+ broken = true
854
+ }
855
+ }
788
856
 
789
- const firstByte = view[0]!
790
- let result: DecodeResult<Draft16DataStream>
857
+ return new TransformStream<Uint8Array, DataStreamEvent>({
858
+ async transform(chunk, controller) {
859
+ if (broken) return
860
+ if (writer !== null) {
861
+ await feed(chunk)
862
+ return
863
+ }
864
+ // One byte is enough to choose, and until there is one there is nothing
865
+ // to choose from.
866
+ if (chunk.length === 0) return
867
+ const firstByte = chunk[0]!
791
868
 
792
869
  if ((firstByte >= 0x10 && firstByte <= 0x1f) || (firstByte >= 0x30 && firstByte <= 0x3f)) {
793
- result = decodeSubgroupStream(view)
870
+ attach(
871
+ createSubgroupStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
872
+ controller,
873
+ )
794
874
  } else if (firstByte === 0x05) {
795
- result = decodeFetchStream(view)
875
+ attach(
876
+ createFetchStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
877
+ controller,
878
+ )
796
879
  } else {
880
+ broken = true
797
881
  controller.error(
798
882
  new DecodeError(
799
883
  'CONSTRAINT_VIOLATION',
@@ -804,32 +888,19 @@ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStrea
804
888
  return
805
889
  }
806
890
 
807
- if (!result.ok) {
808
- controller.error(result.error)
809
- return
810
- }
891
+ // The type byte is part of the stream the inner decoder reads, so the
892
+ // whole chunk goes in, first byte included.
893
+ await feed(chunk)
894
+ },
811
895
 
812
- const stream = result.value
813
- if (stream.type === 'subgroup') {
814
- controller.enqueue({
815
- type: 'subgroup_header',
816
- trackAlias: stream.trackAlias,
817
- groupId: stream.groupId,
818
- subgroupId: stream.subgroupId,
819
- publisherPriority: stream.publisherPriority,
820
- })
821
- for (const obj of stream.objects) {
822
- controller.enqueue(obj)
823
- }
824
- } else if (stream.type === 'fetch') {
825
- controller.enqueue({
826
- type: 'fetch_header',
827
- requestId: stream.requestId,
828
- })
829
- for (const obj of stream.objects) {
830
- controller.enqueue(obj)
831
- }
896
+ async flush() {
897
+ if (writer === null) return
898
+ try {
899
+ await writer.close()
900
+ } catch {
901
+ // Reported by the pump.
832
902
  }
903
+ await pump
833
904
  },
834
905
  })
835
906
  }