@moqtap/codec 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -2
  3. package/dist/{auth-redaction-DsdLq4Qs.d.cts → auth-redaction-B9JJOlIY.d.cts} +0 -2
  4. package/dist/{auth-redaction-DsdLq4Qs.d.ts → auth-redaction-B9JJOlIY.d.ts} +0 -2
  5. package/dist/chunk-OBATJBNL.cjs +868 -0
  6. package/dist/chunk-T2TCLZZV.js +2304 -0
  7. package/dist/chunk-WB65UG7G.cjs +2304 -0
  8. package/dist/chunk-XPCBPACE.js +868 -0
  9. package/dist/{codec-c0YEwwWn.d.ts → codec-B-oUU-cp.d.ts} +2 -2
  10. package/dist/{codec-DHzKAUCa.d.cts → codec-B3RpCI9k.d.cts} +2 -2
  11. package/dist/{codec-Clu_skx8.d.cts → codec-B5pcOS3m.d.cts} +2 -2
  12. package/dist/{codec-DxtgPxJS.d.ts → codec-BLjxwK2f.d.ts} +2 -2
  13. package/dist/{codec-BkheTXO7.d.ts → codec-BV3CL_3m.d.ts} +2 -2
  14. package/dist/{codec-Bb6GKTrv.d.ts → codec-BYCDWgED.d.ts} +2 -2
  15. package/dist/{codec-ejDu-va6.d.ts → codec-Bwqq7ZQT.d.ts} +2 -2
  16. package/dist/{codec-76xzrLTL.d.cts → codec-BzWxOFby.d.cts} +2 -2
  17. package/dist/{codec-CM5yd6n3.d.ts → codec-C-nJhNNl.d.ts} +2 -2
  18. package/dist/{codec-BgpfHoAd.d.cts → codec-C11VEPbQ.d.cts} +2 -2
  19. package/dist/codec-C9U0Vzy9.d.cts +68 -0
  20. package/dist/{codec-yYKGOipj.d.ts → codec-CCx22jnd.d.ts} +2 -2
  21. package/dist/{codec-mVYqkIUF.d.ts → codec-CP3zkfTv.d.ts} +2 -2
  22. package/dist/{codec-qSbO5nOa.d.ts → codec-CRmOLDpt.d.ts} +2 -2
  23. package/dist/{codec-CpOpemR8.d.cts → codec-CUu56DDU.d.cts} +2 -2
  24. package/dist/codec-Cb8mUre3.d.ts +68 -0
  25. package/dist/{codec-DELe00w_.d.cts → codec-CemqAQdR.d.cts} +2 -2
  26. package/dist/{codec-8p40WdLg.d.ts → codec-CxMvsVMS.d.ts} +2 -2
  27. package/dist/{codec-zfzg9uiy.d.cts → codec-DIgMVTi4.d.cts} +2 -2
  28. package/dist/{codec-DdVBtvx7.d.ts → codec-DYzozF68.d.ts} +2 -2
  29. package/dist/{codec-kaBUzdpV.d.ts → codec-D_O9zxax.d.ts} +2 -2
  30. package/dist/{codec-XLrnreeu.d.cts → codec-Ddc-QxV6.d.cts} +2 -2
  31. package/dist/{codec-C5s1eBJX.d.cts → codec-DoojMl2z.d.cts} +2 -2
  32. package/dist/{codec-DlxGuYMv.d.ts → codec-DqdjiOQF.d.ts} +2 -2
  33. package/dist/{codec-Cshdl9Tt.d.cts → codec-Dstemwrq.d.cts} +2 -2
  34. package/dist/{codec-BN2Bu0KI.d.ts → codec-Dt6nuejp.d.ts} +2 -2
  35. package/dist/{codec-BwVDkokh.d.cts → codec-DuYhd8fP.d.cts} +2 -2
  36. package/dist/{codec-I8u2pfS4.d.cts → codec-DwNOFgPU.d.cts} +2 -2
  37. package/dist/{codec-GAY4lybi.d.cts → codec-l01BpNhF.d.cts} +2 -2
  38. package/dist/{codec-Cb8HC3XV.d.cts → codec-psN2vB6I.d.cts} +2 -2
  39. package/dist/draft07-session.d.cts +3 -3
  40. package/dist/draft07-session.d.ts +3 -3
  41. package/dist/draft07.d.cts +4 -4
  42. package/dist/draft07.d.ts +4 -4
  43. package/dist/draft08-session.d.cts +3 -3
  44. package/dist/draft08-session.d.ts +3 -3
  45. package/dist/draft08.d.cts +5 -5
  46. package/dist/draft08.d.ts +5 -5
  47. package/dist/draft09-session.d.cts +3 -3
  48. package/dist/draft09-session.d.ts +3 -3
  49. package/dist/draft09.d.cts +5 -5
  50. package/dist/draft09.d.ts +5 -5
  51. package/dist/draft10-session.d.cts +3 -3
  52. package/dist/draft10-session.d.ts +3 -3
  53. package/dist/draft10.d.cts +5 -5
  54. package/dist/draft10.d.ts +5 -5
  55. package/dist/draft11-session.d.cts +3 -3
  56. package/dist/draft11-session.d.ts +3 -3
  57. package/dist/draft11.d.cts +5 -5
  58. package/dist/draft11.d.ts +5 -5
  59. package/dist/draft12-session.d.cts +3 -3
  60. package/dist/draft12-session.d.ts +3 -3
  61. package/dist/draft12.d.cts +5 -5
  62. package/dist/draft12.d.ts +5 -5
  63. package/dist/draft13-session.d.cts +3 -3
  64. package/dist/draft13-session.d.ts +3 -3
  65. package/dist/draft13.d.cts +5 -5
  66. package/dist/draft13.d.ts +5 -5
  67. package/dist/draft14-session.d.cts +3 -3
  68. package/dist/draft14-session.d.ts +3 -3
  69. package/dist/draft14.d.cts +5 -5
  70. package/dist/draft14.d.ts +5 -5
  71. package/dist/draft15-session.d.cts +3 -3
  72. package/dist/draft15-session.d.ts +3 -3
  73. package/dist/draft15.d.cts +5 -5
  74. package/dist/draft15.d.ts +5 -5
  75. package/dist/draft16-session.d.cts +3 -3
  76. package/dist/draft16-session.d.ts +3 -3
  77. package/dist/draft16.d.cts +5 -5
  78. package/dist/draft16.d.ts +5 -5
  79. package/dist/draft17-session.d.cts +3 -3
  80. package/dist/draft17-session.d.ts +3 -3
  81. package/dist/draft17.d.cts +4 -4
  82. package/dist/draft17.d.ts +4 -4
  83. package/dist/draft18-session.d.cts +3 -3
  84. package/dist/draft18-session.d.ts +3 -3
  85. package/dist/draft18.d.cts +4 -4
  86. package/dist/draft18.d.ts +4 -4
  87. package/dist/draft19-session.d.cts +3 -3
  88. package/dist/draft19-session.d.ts +3 -3
  89. package/dist/draft19.d.cts +4 -4
  90. package/dist/draft19.d.ts +4 -4
  91. package/dist/draft20-session.d.cts +3 -3
  92. package/dist/draft20-session.d.ts +3 -3
  93. package/dist/draft20.d.cts +4 -4
  94. package/dist/draft20.d.ts +4 -4
  95. package/dist/draft21-session.cjs +8 -0
  96. package/dist/draft21-session.d.cts +195 -0
  97. package/dist/draft21-session.d.ts +195 -0
  98. package/dist/draft21-session.js +8 -0
  99. package/dist/draft21.cjs +222 -0
  100. package/dist/draft21.d.cts +176 -0
  101. package/dist/draft21.d.ts +176 -0
  102. package/dist/draft21.js +222 -0
  103. package/dist/index.cjs +7 -1
  104. package/dist/index.d.cts +23 -18
  105. package/dist/index.d.ts +23 -18
  106. package/dist/index.js +7 -1
  107. package/dist/{session-types-Dn7b3RZM.d.ts → session-types-Cxx7W8zZ.d.ts} +1 -1
  108. package/dist/{session-types-B9idsih-.d.cts → session-types-DZkI_qFG.d.cts} +1 -1
  109. package/dist/session.cjs +5 -1
  110. package/dist/session.d.cts +5 -3
  111. package/dist/session.d.ts +5 -3
  112. package/dist/session.js +5 -1
  113. package/dist/{types-DG5_iKuv.d.cts → types-2NisMA8_.d.cts} +1 -1
  114. package/dist/{types-DG5_iKuv.d.ts → types-2NisMA8_.d.ts} +1 -1
  115. package/dist/types-DDwidj2d.d.cts +371 -0
  116. package/dist/types-DDwidj2d.d.ts +371 -0
  117. package/package.json +14 -2
  118. package/src/core/auth-redaction.ts +0 -2
  119. package/src/core/types.ts +1 -0
  120. package/src/drafts/draft19/codec.ts +1 -1
  121. package/src/drafts/draft20/codec.ts +1 -1
  122. package/src/drafts/draft21/codec.ts +1816 -0
  123. package/src/drafts/draft21/data-streams.ts +1338 -0
  124. package/src/drafts/draft21/error-codes.ts +135 -0
  125. package/src/drafts/draft21/index.ts +168 -0
  126. package/src/drafts/draft21/messages.ts +68 -0
  127. package/src/drafts/draft21/rules.ts +107 -0
  128. package/src/drafts/draft21/session-fsm.ts +1063 -0
  129. package/src/drafts/draft21/session.ts +21 -0
  130. package/src/drafts/draft21/types.ts +473 -0
  131. package/src/index.ts +9 -1
  132. package/src/session.ts +4 -0
@@ -0,0 +1,1338 @@
1
+ import { MoqtBufferReader as BufferReader } from '../../core/buffer-reader.js'
2
+ import { MoqtBufferWriter as BufferWriter } from '../../core/buffer-writer.js'
3
+ import type { DecodeResult } from '../../core/types.js'
4
+ import { DecodeError } from '../../core/types.js'
5
+ import type {
6
+ DatagramObject,
7
+ DataStreamEvent,
8
+ Draft21DataStream,
9
+ FetchObjectPayload,
10
+ FetchStream,
11
+ FetchStreamHeader,
12
+ ObjectPayload,
13
+ SubgroupStream,
14
+ SubgroupStreamHeader,
15
+ } from './types.js'
16
+
17
+ // ─── Data Stream Encoding/Decoding ─────────────────────────────────────────
18
+
19
+ const FETCH_STREAM_TYPE = 0x05n
20
+
21
+ // Object property type IDs
22
+ const OBJPROP_PRIOR_GROUP_ID_GAP = 0x3cn
23
+ const OBJPROP_PRIOR_OBJECT_ID_GAP = 0x3en
24
+
25
+ const KNOWN_OBJ_PROPS: ReadonlyMap<bigint, string> = new Map([
26
+ [OBJPROP_PRIOR_OBJECT_ID_GAP, 'prior_object_id_gap'],
27
+ [OBJPROP_PRIOR_GROUP_ID_GAP, 'prior_group_id_gap'],
28
+ ])
29
+
30
+ // ─── Fetch stream Serialization Flags (draft-21 Section 11.4.1, Table 7) ────
31
+
32
+ /** End of Non-Existent Range. */
33
+ const FETCH_MARKER_NON_EXISTENT = 0x8c
34
+ /** End of Unknown Range. */
35
+ const FETCH_MARKER_UNKNOWN = 0x10c
36
+ /**
37
+ * End of Timed-Out Range — NEW in draft-20 (Sections 11.4.1, 11.4.4.2).
38
+ *
39
+ * Every Object between the last serialized Object, if any, and this Location
40
+ * inclusive timed out: the relay's FILL_TIMEOUT budget ran out. draft-19
41
+ * reported these as Unknown gaps (0x10C) and had no distinct marker, so this
42
+ * value takes over the FILL_TIMEOUT outcome from 0x10C.
43
+ */
44
+ const FETCH_MARKER_TIMED_OUT = 0x20c
45
+
46
+ const FETCH_END_OF_RANGE_MARKERS: ReadonlySet<number> = new Set([
47
+ FETCH_MARKER_NON_EXISTENT,
48
+ FETCH_MARKER_UNKNOWN,
49
+ FETCH_MARKER_TIMED_OUT,
50
+ ])
51
+
52
+ function decodeObjectProperties(r: BufferReader, propsLength: number): Record<string, bigint> {
53
+ const endOff = r.offset + propsLength
54
+ const props: Record<string, bigint> = {}
55
+ let prevType = 0n
56
+
57
+ while (r.offset < endOff) {
58
+ const delta = r.readVarInt()
59
+ const propType = prevType + delta
60
+ prevType = propType
61
+
62
+ if (propType % 2n === 0n) {
63
+ const value = r.readVarInt()
64
+ const name = KNOWN_OBJ_PROPS.get(propType) ?? `0x${propType.toString(16)}`
65
+ props[name] = value
66
+ } else {
67
+ const length = Number(r.readVarInt())
68
+ // Skip unknown odd properties
69
+ r.readBytes(length)
70
+ }
71
+ }
72
+ return props
73
+ }
74
+
75
+ function encodeObjectProperties(props: Record<string, bigint>, w: BufferWriter): void {
76
+ const reverseMap = new Map<string, bigint>()
77
+ for (const [id, name] of KNOWN_OBJ_PROPS) {
78
+ reverseMap.set(name, id)
79
+ }
80
+
81
+ const entries: Array<{ type: bigint; value: bigint }> = []
82
+ for (const [name, value] of Object.entries(props)) {
83
+ const typeId = reverseMap.get(name)
84
+ if (typeId !== undefined) {
85
+ entries.push({ type: typeId, value })
86
+ }
87
+ }
88
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
89
+
90
+ let prevType = 0n
91
+ for (const entry of entries) {
92
+ w.writeVarInt(entry.type - prevType)
93
+ w.writeVarInt(entry.value)
94
+ prevType = entry.type
95
+ }
96
+ }
97
+
98
+ // ─── SUBGROUP_HEADER Type Flags (draft-21 Section 11.3.1) ───────────────────
99
+ //
100
+ // | Bit | Mask | Meaning
101
+ // | 0 | 0x01 | PROPERTIES — Object Properties present on EVERY object here
102
+ // | 1-2 | 0x06 | SUBGROUP_ID_MODE: 00 = id is 0, 01 = id is the first Object's
103
+ // | | | Object ID, 10 = the Subgroup ID field is present, 11 = reserved
104
+ // | 3 | 0x08 | END_OF_GROUP
105
+ // | 4 | 0x10 | structural — MUST be 1
106
+ // | 5 | 0x20 | DEFAULT_PRIORITY — Publisher Priority field omitted
107
+ // | 6 | 0x40 | FIRST_OBJECT
108
+ //
109
+ // These are draft-21's OWN rules, computed from Section 11.3.1's three
110
+ // conditions. They are NOT the datagram rules (Section 11.2.1): bit 4 is
111
+ // required here and forbidden there, and SUBGROUP_HEADER has no "unspecified
112
+ // bit" condition at all, because bits 0 through 6 are all specified for it.
113
+ // SPEC-DELTA Section 9 item 3 flags exactly this conflation as a mistake, so
114
+ // neither rule set is derived from the other.
115
+ //
116
+ // The resulting valid set — 0x10-0x15, 0x18-0x1D, 0x30-0x35, 0x38-0x3D,
117
+ // 0x50-0x55, 0x58-0x5D, 0x70-0x75, 0x78-0x7D — is byte for byte draft-19's
118
+ // enumeration, so this is a restatement, not a widening or a narrowing.
119
+
120
+ /** Returns null when valid, or the reason it is not. */
121
+ function subgroupTypeFlagsError(typeFlags: bigint): string | null {
122
+ // 3. "Values of 128 or greater."
123
+ if (typeFlags >= 128n) {
124
+ return `SUBGROUP_HEADER Type Flags of 128 or greater are invalid (got ${typeFlags})`
125
+ }
126
+ const v = Number(typeFlags)
127
+ // 2. "Values where bit 4 is not set. Bit 4 MUST be 1 for SUBGROUP_HEADER."
128
+ if ((v & 0x10) === 0) {
129
+ return `SUBGROUP_HEADER Type Flags 0x${v.toString(16)} must have bit 4 (0x10) set`
130
+ }
131
+ // 1. "Values with SUBGROUP_ID_MODE set to 0b11. This mode is reserved."
132
+ if ((v & 0x06) === 0x06) {
133
+ return `SUBGROUP_HEADER Type Flags 0x${v.toString(16)} sets SUBGROUP_ID_MODE to the reserved 0b11`
134
+ }
135
+ return null
136
+ }
137
+
138
+ /** Whether a first byte could begin a SUBGROUP_HEADER — used to sniff a stream's type. */
139
+ function isValidSubgroupType(streamType: number): boolean {
140
+ return subgroupTypeFlagsError(BigInt(streamType)) === null
141
+ }
142
+
143
+ // ─── OBJECT_DATAGRAM Type Flags (draft-21 Section 11.2.1) ───────────────────
144
+ //
145
+ // | Bit | Mask | Meaning
146
+ // | 0 | 0x01 | PROPERTIES
147
+ // | 1 | 0x02 | END_OF_GROUP
148
+ // | 2 | 0x04 | ZERO_OBJECT_ID — Object ID field omitted, id is 0
149
+ // | 3 | 0x08 | DEFAULT_PRIORITY — Publisher Priority field omitted
150
+ // | 4 | 0x10 | RESERVED — MUST be 0
151
+ // | 5 | 0x20 | STATUS — Object Status present instead of an Object Payload
152
+ // | 6+ | | no specified meaning
153
+ //
154
+ // Section 11.2.1's invalid values: STATUS and END_OF_GROUP both set; bit 4 set;
155
+ // any bit set whose meaning is not specified. Unlike SUBGROUP_HEADER there is
156
+ // no explicit "128 or greater" bullet — a value of 128 or more necessarily
157
+ // sets an unspecified bit, so the third rule catches it. The other two
158
+ // conditions (PROPERTIES with a zero Properties Length, and Properties on a
159
+ // non-Normal Object) depend on fields past the flags and are checked inline.
160
+
161
+ /** Bits with a specified meaning that a sender is allowed to set. */
162
+ const DATAGRAM_ALLOWED_BITS = 0x2fn // 0x01 | 0x02 | 0x04 | 0x08 | 0x20
163
+
164
+ /** Returns null when valid, or the reason it is not. */
165
+ function datagramTypeFlagsError(typeFlags: bigint): string | null {
166
+ if ((typeFlags & 0x10n) !== 0n) {
167
+ return `datagram Type Flags bit 4 (0x10) is reserved and MUST be zero (got 0x${typeFlags.toString(16)})`
168
+ }
169
+ if ((typeFlags & ~DATAGRAM_ALLOWED_BITS) !== 0n) {
170
+ return `datagram Type Flags 0x${typeFlags.toString(16)} sets a bit whose meaning is not specified`
171
+ }
172
+ const v = Number(typeFlags)
173
+ if ((v & 0x20) !== 0 && (v & 0x02) !== 0) {
174
+ return `datagram Type Flags 0x${v.toString(16)} sets both the STATUS and END_OF_GROUP bits`
175
+ }
176
+ return null
177
+ }
178
+
179
+ const OBJECT_STATUS_NORMAL = 0n
180
+
181
+ /** The largest value a Group ID or Object ID can take (draft-21 Section 8.1). */
182
+ const MAX_U64 = 0xffffffffffffffffn
183
+
184
+ export function encodeSubgroupStream(stream: SubgroupStream): Uint8Array {
185
+ const w = new BufferWriter()
186
+ const streamType = stream.headerType
187
+ // DECISION (DECISIONS.md D5): strict on send. Section 8.1 permits
188
+ // non-minimal encodings, and Section 11.3.1 words its third invalidity rule
189
+ // as "values of 128 or greater (i.e., any value that requires more than a
190
+ // one-byte variable-length integer encoding)" — two clauses that come apart
191
+ // under that allowance. writeVarInt always emits the minimal form, so a
192
+ // legal Type Flags value below 128 always goes out as one byte and can never
193
+ // trip a peer that reads the parenthetical literally.
194
+ w.writeVarInt(BigInt(streamType))
195
+
196
+ const propertiesPresent = (streamType & 0x01) !== 0
197
+ const subgroupMode = (streamType & 0x06) >> 1
198
+ const hasSubgroupField = subgroupMode === 0x02
199
+ // DEFAULT_PRIORITY bit (0x20): when set, priority is absent
200
+ const hasPriority = (streamType & 0x20) === 0
201
+
202
+ w.writeVarInt(stream.trackAlias)
203
+ w.writeVarInt(stream.groupId)
204
+ if (hasSubgroupField) {
205
+ w.writeVarInt(stream.subgroupId)
206
+ }
207
+ if (hasPriority) {
208
+ w.writeUint8(stream.publisherPriority)
209
+ }
210
+ let prevObjectId = -1n
211
+ for (const obj of stream.objects) {
212
+ // Section 11.3.1: "Object ID = previous Object ID + Object ID Delta + 1",
213
+ // or the delta itself for the first Object on the stream. This +1 is the
214
+ // subgroup delta encoding, not a range end — nothing to do with the
215
+ // inclusive/exclusive flip of DECISIONS.md D4.
216
+ const delta = prevObjectId < 0n ? obj.objectId : obj.objectId - prevObjectId - 1n
217
+ w.writeVarInt(delta)
218
+ if (propertiesPresent) {
219
+ if (obj.objectProperties && Object.keys(obj.objectProperties).length > 0) {
220
+ const tmpW = new BufferWriter(32)
221
+ encodeObjectProperties(obj.objectProperties, tmpW)
222
+ const raw = tmpW.finish()
223
+ w.writeVarInt(BigInt(raw.byteLength))
224
+ w.writeBytes(raw)
225
+ } else {
226
+ // Legal here and only here: on a subgroup stream the field is present
227
+ // on every object and an empty one sets Properties Length to 0. The
228
+ // same thing on a datagram is a PROTOCOL_VIOLATION.
229
+ w.writeVarInt(0n)
230
+ }
231
+ }
232
+ w.writeVarInt(BigInt(obj.payloadLength))
233
+ if (obj.payloadLength === 0) {
234
+ // Object Status is serialized only when Object Payload Length is 0.
235
+ w.writeVarInt(obj.status ?? OBJECT_STATUS_NORMAL)
236
+ } else {
237
+ w.writeBytes(obj.payload)
238
+ }
239
+ prevObjectId = obj.objectId
240
+ }
241
+ return w.finish()
242
+ }
243
+
244
+ export function encodeDatagram(dg: DatagramObject): Uint8Array {
245
+ const w = new BufferWriter()
246
+ const dgType = dg.datagramType
247
+ // D5: minimal on send. See encodeSubgroupStream.
248
+ w.writeVarInt(BigInt(dgType))
249
+ w.writeVarInt(dg.trackAlias)
250
+ w.writeVarInt(dg.groupId)
251
+
252
+ const objectIdAbsent = (dgType & 0x04) !== 0
253
+ const isStatus = (dgType & 0x20) !== 0
254
+ const defaultPriority = (dgType & 0x08) !== 0
255
+ const propertiesPresent = (dgType & 0x01) !== 0
256
+
257
+ if (!objectIdAbsent) {
258
+ w.writeVarInt(dg.objectId)
259
+ }
260
+ if (!defaultPriority) {
261
+ w.writeUint8(dg.publisherPriority)
262
+ }
263
+
264
+ if (propertiesPresent) {
265
+ if (dg.objectProperties && Object.keys(dg.objectProperties).length > 0) {
266
+ const tmpW = new BufferWriter(32)
267
+ encodeObjectProperties(dg.objectProperties, tmpW)
268
+ const raw = tmpW.finish()
269
+ w.writeVarInt(BigInt(raw.byteLength))
270
+ w.writeBytes(raw)
271
+ } else {
272
+ // Section 11.2.1 makes this a PROTOCOL_VIOLATION on receipt. An object
273
+ // with no properties leaves the PROPERTIES bit clear instead.
274
+ w.writeVarInt(0n)
275
+ }
276
+ }
277
+
278
+ if (isStatus) {
279
+ // Status and payload occupy the same position; only one is present.
280
+ w.writeVarInt(dg.objectStatus ?? OBJECT_STATUS_NORMAL)
281
+ } else {
282
+ w.writeBytes(dg.payload)
283
+ }
284
+ return w.finish()
285
+ }
286
+
287
+ export function encodeFetchStream(stream: FetchStream): Uint8Array {
288
+ const w = new BufferWriter()
289
+ w.writeVarInt(FETCH_STREAM_TYPE)
290
+ w.writeVarInt(stream.requestId)
291
+
292
+ let prevGroupId = 0n
293
+ let prevObjectId = 0n
294
+ let first = true
295
+
296
+ for (const obj of stream.objects) {
297
+ w.writeVarInt(BigInt(obj.serializationFlags))
298
+ const flags = obj.serializationFlags
299
+ if (flags >= 0x80) {
300
+ // End-of-Range marker (0x8C / 0x10C / 0x20C).
301
+ //
302
+ // DECISION (DECISIONS.md D7, SPEC-DELTA Section 11 Q16): the ordinary
303
+ // Section 11.4.1.1 delta arithmetic applies to the marker's two fields.
304
+ // All three marker values carry the low bits 0x0C — the ordinary
305
+ // "Group ID Delta present, Object ID Delta present" pattern — and
306
+ // Section 11.4.1.2 says only that "the Group ID and Object ID fields are
307
+ // present". It does not say whether they are deltas or absolutes; the
308
+ // flags are literally the normal flags, so they are treated as normal.
309
+ if (first) w.writeVarInt(obj.groupId)
310
+ else w.writeVarInt(obj.groupId - prevGroupId - 1n)
311
+ w.writeVarInt(obj.objectId)
312
+ // DECISION (D7, Q17): Object Payload Length IS present on a marker,
313
+ // encoded as 0. Section 11.4.1.2 lists what is absent — "Subgroup ID,
314
+ // Priority and Properties" — and does not name Object Payload Length,
315
+ // which Figure 28 marks mandatory. Omitting a field the figure requires
316
+ // is what desynchronises a fetch stream.
317
+ w.writeVarInt(BigInt(obj.payloadLength))
318
+ } else if (flags & 0x40) {
319
+ // DATAGRAM mode: no subgroup_id field
320
+ if (flags & 0x08) {
321
+ // Ascending order: Group ID = prior + delta + 1. Section 11.4.1.1
322
+ // delta encoding; not a range end.
323
+ if (first) w.writeVarInt(obj.groupId)
324
+ else w.writeVarInt(obj.groupId - prevGroupId - 1n)
325
+ }
326
+ if (flags & 0x04) w.writeVarInt(objectIdDeltaFor(flags, obj.objectId, prevObjectId))
327
+ if (flags & 0x10) w.writeUint8(obj.publisherPriority)
328
+ if (flags & 0x20) {
329
+ if (obj.objectProperties && Object.keys(obj.objectProperties).length > 0) {
330
+ const tmpW = new BufferWriter(32)
331
+ encodeObjectProperties(obj.objectProperties, tmpW)
332
+ const raw = tmpW.finish()
333
+ w.writeVarInt(BigInt(raw.byteLength))
334
+ w.writeBytes(raw)
335
+ } else {
336
+ w.writeVarInt(0n)
337
+ }
338
+ }
339
+ w.writeVarInt(BigInt(obj.payloadLength))
340
+ if (obj.payloadLength > 0) {
341
+ w.writeBytes(obj.payload)
342
+ }
343
+ } else {
344
+ if (flags & 0x08) {
345
+ if (first) w.writeVarInt(obj.groupId)
346
+ else w.writeVarInt(obj.groupId - prevGroupId - 1n)
347
+ }
348
+ const subgroupEncoding = flags & 0x03
349
+ if (subgroupEncoding === 0x03) w.writeVarInt(obj.subgroupId)
350
+ if (flags & 0x04) w.writeVarInt(objectIdDeltaFor(flags, obj.objectId, prevObjectId))
351
+ if (flags & 0x10) w.writeUint8(obj.publisherPriority)
352
+ if (flags & 0x20) {
353
+ if (obj.objectProperties && Object.keys(obj.objectProperties).length > 0) {
354
+ const tmpW = new BufferWriter(32)
355
+ encodeObjectProperties(obj.objectProperties, tmpW)
356
+ const raw = tmpW.finish()
357
+ w.writeVarInt(BigInt(raw.byteLength))
358
+ w.writeBytes(raw)
359
+ } else {
360
+ w.writeVarInt(0n)
361
+ }
362
+ }
363
+ w.writeVarInt(BigInt(obj.payloadLength))
364
+ if (obj.payloadLength > 0) {
365
+ w.writeBytes(obj.payload)
366
+ }
367
+ }
368
+ prevGroupId = obj.groupId
369
+ prevObjectId = obj.objectId
370
+ first = false
371
+ }
372
+ return w.finish()
373
+ }
374
+
375
+ /**
376
+ * The value to write into the Object ID Delta slot, per draft-21 Section
377
+ * 11.4.4.1: "When the Group ID Delta field is present, the Object ID is the
378
+ * value of Object ID Delta if present. When the Group ID Delta field is not
379
+ * present, the Object ID is the prior Object's ID plus the Object ID Delta if
380
+ * present."
381
+ *
382
+ * So the field is absolute alongside a Group ID Delta and relative without
383
+ * one — and the relative form adds no 1, unlike the Group ID Delta and unlike
384
+ * the subgroup-stream Object ID Delta.
385
+ */
386
+ function objectIdDeltaFor(flags: number, objectId: bigint, prevObjectId: bigint): bigint {
387
+ return (flags & 0x08) !== 0 ? objectId : objectId - prevObjectId
388
+ }
389
+
390
+ /** The decode-side counterpart of {@link objectIdDeltaFor}. */
391
+ function resolveObjectId(groupIdPresent: boolean, delta: bigint, prevObjectId: bigint): bigint {
392
+ return groupIdPresent ? delta : prevObjectId + delta
393
+ }
394
+
395
+ /**
396
+ * Resolve a Group ID Delta, per draft-21 Section 11.4.1.1.
397
+ *
398
+ * The first Object's delta IS the absolute Group ID. On any later Object,
399
+ * "the Group ID is the prior Object's Group ID plus the Group ID Delta + 1"
400
+ * for Ascending group order. Descending order subtracts `delta + 1` instead —
401
+ * but Group Order is a subscription/fetch parameter carried on the control
402
+ * stream, and this decoder sees only the data stream, so Ascending is assumed.
403
+ * A caller that knows the negotiated order and needs Descending has to
404
+ * recompute from the `groupIdDelta` this decoder preserves on each object.
405
+ */
406
+ function resolveGroupId(
407
+ first: boolean,
408
+ delta: bigint,
409
+ prevGroupId: bigint,
410
+ offset: number,
411
+ ): bigint {
412
+ const groupId = first ? delta : prevGroupId + delta + 1n
413
+ if (groupId > MAX_U64) {
414
+ throw new DecodeError('CONSTRAINT_VIOLATION', 'computed Group ID exceeds 2^64 - 1', offset)
415
+ }
416
+ return groupId
417
+ }
418
+
419
+ /**
420
+ * The SUBGROUP_HEADER fields, read once for both decoders.
421
+ *
422
+ * `decodeSubgroupStream` and `createSubgroupStreamDecoder` share this single
423
+ * reader so they cannot disagree about a header: the Type Flags, the
424
+ * end-of-group and first-object flags, and the Subgroup ID under the mode that
425
+ * derives it from the first Object all come from here. Two readers drift.
426
+ */
427
+ interface SubgroupHeaderFields {
428
+ readonly streamType: number
429
+ readonly propertiesPresent: boolean
430
+ /** Subgroup Mode 0b01: the Subgroup ID is the first Object's ID. */
431
+ readonly subgroupIsFirstObjId: boolean
432
+ readonly endOfGroup: boolean
433
+ readonly firstObjectFlag: boolean
434
+ readonly trackAlias: bigint
435
+ readonly groupId: bigint
436
+ /** Zero under `subgroupIsFirstObjId` until the first Object has been read. */
437
+ readonly subgroupId: bigint
438
+ readonly publisherPriority: number
439
+ }
440
+
441
+ function readSubgroupHeader(r: BufferReader): SubgroupHeaderFields {
442
+ // DECISION (DECISIONS.md D5): permissive on receive. The Type Flags are a
443
+ // vi64 and Section 8.1 allows non-minimal encodings, so 0x10 may legally
444
+ // arrive as the two-byte 0x8010. Read the value, then judge the value.
445
+ const typeFlags = r.readVarInt()
446
+ const flagsError = subgroupTypeFlagsError(typeFlags)
447
+ if (flagsError !== null) {
448
+ throw new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0)
449
+ }
450
+ const streamType = Number(typeFlags)
451
+
452
+ const propertiesPresent = (streamType & 0x01) !== 0
453
+ const subgroupMode = (streamType & 0x06) >> 1
454
+ const hasSubgroupField = subgroupMode === 0x02
455
+ const subgroupIsFirstObjId = subgroupMode === 0x01
456
+ const endOfGroup = (streamType & 0x08) !== 0
457
+ // DEFAULT_PRIORITY bit (0x20): when set, priority is absent.
458
+ const hasPriority = (streamType & 0x20) === 0
459
+ const firstObjectFlag = (streamType & 0x40) !== 0
460
+
461
+ const trackAlias = r.readVarInt()
462
+ const groupId = r.readVarInt()
463
+
464
+ let subgroupId = 0n
465
+ if (hasSubgroupField) {
466
+ subgroupId = r.readVarInt()
467
+ }
468
+
469
+ let publisherPriority = 128
470
+ if (hasPriority) {
471
+ publisherPriority = r.readUint8()
472
+ }
473
+
474
+ return {
475
+ streamType,
476
+ propertiesPresent,
477
+ subgroupIsFirstObjId,
478
+ endOfGroup,
479
+ firstObjectFlag,
480
+ trackAlias,
481
+ groupId,
482
+ subgroupId,
483
+ publisherPriority,
484
+ }
485
+ }
486
+
487
+ /**
488
+ * One Object off a subgroup stream, for both decoders.
489
+ *
490
+ * `base` is the absolute offset within the stream that `r.offset === 0`
491
+ * corresponds to. The one-shot decoder reads from a reader spanning the whole
492
+ * stream and passes 0; the incremental decoder reads from a window over its
493
+ * buffer and passes that window's position, which is what makes `byteOffset`
494
+ * mean the same thing on both paths instead of being hardcoded to zero on one
495
+ * of them.
496
+ *
497
+ * Throws `UNEXPECTED_END` when the Object is not yet complete, which the
498
+ * incremental decoder treats as "wait for more bytes" rather than as an error.
499
+ */
500
+ function readSubgroupObject(
501
+ r: BufferReader,
502
+ propertiesPresent: boolean,
503
+ isFirst: boolean,
504
+ prevObjectId: bigint,
505
+ base: number,
506
+ ): ObjectPayload {
507
+ const byteOffset = base + r.offset
508
+ const delta = r.readVarInt()
509
+ // Section 11.3.1: "Object ID = previous Object ID + Object ID Delta + 1", or
510
+ // the delta itself for the first Object on the stream.
511
+ const objectId = isFirst ? delta : prevObjectId + 1n + delta
512
+
513
+ const extensionData = new Uint8Array(0)
514
+ let objectProperties: Record<string, bigint> | undefined
515
+ if (propertiesPresent) {
516
+ const propsLen = Number(r.readVarInt())
517
+ if (propsLen > 0) {
518
+ objectProperties = decodeObjectProperties(r, propsLen)
519
+ }
520
+ }
521
+
522
+ const payloadLength = Number(r.readVarInt())
523
+ let payload: Uint8Array
524
+ let status: bigint | undefined
525
+ let payloadByteOffset: number
526
+ if (payloadLength === 0) {
527
+ // Section 11.3.1: "The Object Status field is only sent if the Object
528
+ // Payload Length is zero." Skipping it leaves the varint to be read as the
529
+ // next Object's Object ID Delta, which desynchronises the rest of the
530
+ // stream silently.
531
+ status = r.readVarInt()
532
+ payloadByteOffset = base + r.offset
533
+ payload = new Uint8Array(0)
534
+ } else {
535
+ payloadByteOffset = base + r.offset
536
+ payload = r.readBytesView(payloadLength)
537
+ }
538
+
539
+ const obj: ObjectPayload = {
540
+ type: 'object',
541
+ byteOffset,
542
+ payloadByteOffset,
543
+ objectId,
544
+ objectIdDelta: delta,
545
+ payloadLength,
546
+ payload,
547
+ extensionData,
548
+ }
549
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
550
+ if (objectProperties !== undefined)
551
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
552
+ return obj
553
+ }
554
+
555
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
556
+ try {
557
+ const r = new BufferReader(bytes)
558
+ const h = readSubgroupHeader(r)
559
+
560
+ const objects: ObjectPayload[] = []
561
+ let prevObjectId = -1n
562
+ let isFirst = true
563
+ let subgroupId = h.subgroupId
564
+
565
+ while (r.remaining > 0) {
566
+ const obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, 0)
567
+ if (isFirst && h.subgroupIsFirstObjId) subgroupId = obj.objectId
568
+ isFirst = false
569
+ objects.push(obj)
570
+ prevObjectId = obj.objectId
571
+ }
572
+
573
+ const result: SubgroupStream = {
574
+ type: 'subgroup',
575
+ headerType: h.streamType,
576
+ trackAlias: h.trackAlias,
577
+ groupId: h.groupId,
578
+ subgroupId,
579
+ publisherPriority: h.publisherPriority,
580
+ objects,
581
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
582
+ ...(h.firstObjectFlag ? { firstObject: true } : {}),
583
+ }
584
+
585
+ return {
586
+ ok: true,
587
+ value: result,
588
+ bytesRead: r.offset,
589
+ }
590
+ } catch (e) {
591
+ if (e instanceof DecodeError) return { ok: false, error: e }
592
+ throw e
593
+ }
594
+ }
595
+
596
+ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject> {
597
+ try {
598
+ const r = new BufferReader(bytes)
599
+ // D5: permissive on receive — the Type Flags are a vi64 (see
600
+ // decodeSubgroupStream).
601
+ const typeFlags = r.readVarInt()
602
+ const flagsError = datagramTypeFlagsError(typeFlags)
603
+ if (flagsError !== null) {
604
+ return { ok: false, error: new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0) }
605
+ }
606
+ const dgType = Number(typeFlags)
607
+
608
+ const objectIdAbsent = (dgType & 0x04) !== 0
609
+ const endOfGroup = (dgType & 0x02) !== 0
610
+ const isStatus = (dgType & 0x20) !== 0
611
+ const defaultPriority = (dgType & 0x08) !== 0
612
+ const propertiesPresent = (dgType & 0x01) !== 0
613
+
614
+ const trackAlias = r.readVarInt()
615
+ const groupId = r.readVarInt()
616
+ let objectId = 0n
617
+ if (!objectIdAbsent) {
618
+ objectId = r.readVarInt()
619
+ }
620
+
621
+ let publisherPriority = 128
622
+ if (!defaultPriority) {
623
+ publisherPriority = r.readUint8()
624
+ }
625
+
626
+ let objectProperties: Record<string, bigint> | undefined
627
+ if (propertiesPresent) {
628
+ const propsLen = Number(r.readVarInt())
629
+ if (propsLen === 0) {
630
+ // Section 11.2.1: "If an endpoint receives a datagram with the
631
+ // PROPERTIES bit set and an Properties Length of 0, it MUST close the
632
+ // session with a PROTOCOL_VIOLATION." A subgroup stream is the
633
+ // opposite: there a zero-length Properties field is normal.
634
+ return {
635
+ ok: false,
636
+ error: new DecodeError(
637
+ 'CONSTRAINT_VIOLATION',
638
+ 'a datagram with the PROPERTIES bit set must carry a non-empty Properties structure',
639
+ r.offset,
640
+ ),
641
+ }
642
+ }
643
+ objectProperties = decodeObjectProperties(r, propsLen)
644
+ }
645
+
646
+ let objectStatus: bigint | undefined
647
+ let payload: Uint8Array
648
+ if (isStatus) {
649
+ objectStatus = r.readVarInt()
650
+ if (propertiesPresent && objectStatus !== OBJECT_STATUS_NORMAL) {
651
+ // Section 11.2.1: STATUS and PROPERTIES both set with a status other
652
+ // than Normal is a PROTOCOL_VIOLATION, "because only Normal Objects
653
+ // can have Properties".
654
+ return {
655
+ ok: false,
656
+ error: new DecodeError(
657
+ 'CONSTRAINT_VIOLATION',
658
+ `only a Normal (0x0) Object may carry Properties, got status 0x${objectStatus.toString(16)}`,
659
+ r.offset,
660
+ ),
661
+ }
662
+ }
663
+ if (r.remaining > 0) {
664
+ // The STATUS bit says the Object Status occupies the position an
665
+ // Object Payload would have, so trailing bytes are not a payload this
666
+ // frame can hold.
667
+ return {
668
+ ok: false,
669
+ error: new DecodeError(
670
+ 'CONSTRAINT_VIOLATION',
671
+ `a status datagram must not carry a payload (${r.remaining} trailing byte(s))`,
672
+ r.offset,
673
+ ),
674
+ }
675
+ }
676
+ payload = new Uint8Array(0)
677
+ } else {
678
+ // No length field: the payload is everything left in the datagram.
679
+ payload = r.readBytesView(r.remaining)
680
+ }
681
+ const payloadLength = payload.byteLength
682
+
683
+ const result: DatagramObject = {
684
+ type: 'datagram',
685
+ datagramType: dgType,
686
+ trackAlias,
687
+ groupId,
688
+ objectId,
689
+ publisherPriority,
690
+ payloadLength,
691
+ payload,
692
+ }
693
+
694
+ if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
695
+ if (objectStatus !== undefined)
696
+ (result as unknown as Record<string, unknown>).objectStatus = objectStatus
697
+ if (objectProperties !== undefined)
698
+ (result as unknown as Record<string, unknown>).objectProperties = objectProperties
699
+
700
+ return { ok: true, value: result, bytesRead: r.offset }
701
+ } catch (e) {
702
+ if (e instanceof DecodeError) return { ok: false, error: e }
703
+ throw e
704
+ }
705
+ }
706
+
707
+ /** The delta state a fetch stream carries from one Object to the next. */
708
+ interface FetchObjectState {
709
+ prevGroupId: bigint
710
+ prevSubgroupId: bigint
711
+ prevObjectId: bigint
712
+ prevPriority: number
713
+ first: boolean
714
+ }
715
+
716
+ function newFetchObjectState(): FetchObjectState {
717
+ return { prevGroupId: 0n, prevSubgroupId: 0n, prevObjectId: 0n, prevPriority: 128, first: true }
718
+ }
719
+
720
+ /**
721
+ * One Object off a fetch stream, for both decoders.
722
+ *
723
+ * Lifted out of `decodeFetchStream` unchanged so the incremental decoder
724
+ * cannot report a different shape for the same bytes: the Serialization Flags,
725
+ * the Group and Subgroup ID, the Priority, the resolved deltas and a true
726
+ * `byteOffset` all come from this one reader, and DATAGRAM-mode Object IDs
727
+ * resolve against the state it carries.
728
+ *
729
+ * `base` is the absolute offset that `r.offset === 0` corresponds to: 0 for
730
+ * the one-shot decoder, the buffer window's position for the incremental one.
731
+ */
732
+ function readFetchObject(r: BufferReader, base: number, st: FetchObjectState): FetchObjectPayload {
733
+ const byteOffset = base + r.offset
734
+ const flags = Number(r.readVarInt())
735
+
736
+ let groupId = st.prevGroupId
737
+ let subgroupId = st.prevSubgroupId
738
+ // Section 11.4.1.1: absent an Object ID Delta, Object ID is the prior
739
+ // Object's ID plus one. Stream delta encoding, not a range end.
740
+ let objectId = st.prevObjectId + 1n
741
+ let payloadLength: number
742
+ let payload: Uint8Array
743
+ let payloadByteOffset: number
744
+ const extensionData = new Uint8Array(0)
745
+
746
+ if (flags >= 0x80) {
747
+ // End-of-Range marker. Section 11.4.1: "Any other value is a
748
+ // PROTOCOL_VIOLATION."
749
+ if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
750
+ throw new DecodeError(
751
+ 'CONSTRAINT_VIOLATION',
752
+ `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
753
+ r.offset,
754
+ )
755
+ }
756
+ // D7 / Q16: ordinary delta arithmetic (see encodeFetchStream).
757
+ const groupDelta = r.readVarInt()
758
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
759
+ // With a Group ID Delta present, Object ID is the Object ID Delta.
760
+ const objectDelta = r.readVarInt()
761
+ objectId = objectDelta
762
+ // D7 / Q17: Object Payload Length is present, encoded as 0.
763
+ payloadLength = Number(r.readVarInt())
764
+ payloadByteOffset = base + r.offset
765
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
766
+
767
+ const marker: FetchObjectPayload = {
768
+ type: 'object',
769
+ byteOffset,
770
+ payloadByteOffset,
771
+ serializationFlags: flags,
772
+ groupId,
773
+ groupIdDelta: groupDelta,
774
+ // A marker has no Subgroup ID or Priority of its own — Section
775
+ // 11.4.4.2 says neither field is present. The interface requires
776
+ // both, so they carry the last actual Object's values, which is also
777
+ // what a following Object that references "the prior Object" gets.
778
+ subgroupId: st.prevSubgroupId,
779
+ objectId,
780
+ objectIdDelta: objectDelta,
781
+ publisherPriority: st.prevPriority,
782
+ payloadLength,
783
+ payload,
784
+ extensionData,
785
+ }
786
+
787
+ // Section 11.4.1.2: prior Group ID and prior Object ID come FROM the
788
+ // marker; prior Subgroup ID and prior Priority stay with the last
789
+ // actual Object before it, so they are deliberately not updated here.
790
+ st.prevGroupId = groupId
791
+ st.prevObjectId = objectId
792
+ st.first = false
793
+ return marker
794
+ }
795
+
796
+ if (flags & 0x40) {
797
+ // DATAGRAM mode: no subgroup_id field
798
+ const objectIdPresent = (flags & 0x04) !== 0
799
+ const groupIdPresent = (flags & 0x08) !== 0
800
+ const priorityPresent = (flags & 0x10) !== 0
801
+ const propsPresent = (flags & 0x20) !== 0
802
+
803
+ let groupDelta: bigint | undefined
804
+ if (groupIdPresent) {
805
+ // First object's delta IS the absolute group id; subsequent objects:
806
+ // groupId = st.prevGroupId + delta + 1 (ascending).
807
+ groupDelta = r.readVarInt()
808
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
809
+ }
810
+ let objectDelta: bigint | undefined
811
+ if (objectIdPresent) {
812
+ objectDelta = r.readVarInt()
813
+ objectId = resolveObjectId(groupIdPresent, objectDelta, st.prevObjectId)
814
+ }
815
+ if (objectId > MAX_U64) {
816
+ throw new DecodeError('CONSTRAINT_VIOLATION', 'computed Object ID exceeds 2^64 - 1', r.offset)
817
+ }
818
+ if (priorityPresent) {
819
+ st.prevPriority = r.readUint8()
820
+ }
821
+ let objectProperties: Record<string, bigint> | undefined
822
+ if (propsPresent) {
823
+ const propsLen = Number(r.readVarInt())
824
+ if (propsLen > 0) {
825
+ objectProperties = decodeObjectProperties(r, propsLen)
826
+ }
827
+ }
828
+ payloadLength = Number(r.readVarInt())
829
+ payloadByteOffset = base + r.offset
830
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
831
+
832
+ const obj: FetchObjectPayload = {
833
+ type: 'object',
834
+ byteOffset,
835
+ payloadByteOffset,
836
+ serializationFlags: flags,
837
+ groupId,
838
+ subgroupId: 0n,
839
+ objectId,
840
+ publisherPriority: st.prevPriority,
841
+ payloadLength,
842
+ payload,
843
+ extensionData,
844
+ }
845
+ if (groupDelta !== undefined)
846
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
847
+ if (objectDelta !== undefined)
848
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
849
+ if (objectProperties)
850
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
851
+
852
+ st.prevGroupId = groupId
853
+ st.prevObjectId = objectId
854
+ st.first = false
855
+ return obj
856
+ }
857
+
858
+ const subgroupEncoding = flags & 0x03
859
+ const objectIdPresent = (flags & 0x04) !== 0
860
+ const groupIdPresent = (flags & 0x08) !== 0
861
+ const priorityPresent = (flags & 0x10) !== 0
862
+ const propsPresent = (flags & 0x20) !== 0
863
+
864
+ let groupDelta: bigint | undefined
865
+ if (groupIdPresent) {
866
+ groupDelta = r.readVarInt()
867
+ groupId = resolveGroupId(st.first, groupDelta, st.prevGroupId, r.offset)
868
+ } else if (st.first) {
869
+ throw new DecodeError(
870
+ 'CONSTRAINT_VIOLATION',
871
+ 'First fetch object must include groupId',
872
+ r.offset,
873
+ )
874
+ }
875
+
876
+ if (subgroupEncoding === 0x00) {
877
+ subgroupId = 0n
878
+ } else if (subgroupEncoding === 0x01 || subgroupEncoding === 0x02) {
879
+ if (st.first) {
880
+ throw new DecodeError(
881
+ 'CONSTRAINT_VIOLATION',
882
+ 'First fetch object cannot reference prior subgroupId',
883
+ r.offset,
884
+ )
885
+ }
886
+ subgroupId = subgroupEncoding === 0x01 ? st.prevSubgroupId : st.prevSubgroupId + 1n
887
+ } else {
888
+ subgroupId = r.readVarInt()
889
+ }
890
+
891
+ let objectDelta: bigint | undefined
892
+ if (objectIdPresent) {
893
+ objectDelta = r.readVarInt()
894
+ objectId = resolveObjectId(groupIdPresent, objectDelta, st.prevObjectId)
895
+ } else if (st.first) {
896
+ throw new DecodeError(
897
+ 'CONSTRAINT_VIOLATION',
898
+ 'First fetch object must include objectId',
899
+ r.offset,
900
+ )
901
+ }
902
+ if (objectId > MAX_U64) {
903
+ throw new DecodeError('CONSTRAINT_VIOLATION', 'computed Object ID exceeds 2^64 - 1', r.offset)
904
+ }
905
+
906
+ if (priorityPresent) {
907
+ st.prevPriority = r.readUint8()
908
+ }
909
+
910
+ let objectProperties: Record<string, bigint> | undefined
911
+ if (propsPresent) {
912
+ const propsLen = Number(r.readVarInt())
913
+ if (propsLen > 0) {
914
+ objectProperties = decodeObjectProperties(r, propsLen)
915
+ }
916
+ }
917
+
918
+ payloadLength = Number(r.readVarInt())
919
+ payloadByteOffset = base + r.offset
920
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
921
+
922
+ // A fetch stream carries no Object Status: status is only present on
923
+ // subscription-delivered objects (Section 11.1.2). That is also the
924
+ // one way the two copies of a doubly delivered object differ — the
925
+ // subscription copy can carry a status, the fill copy cannot.
926
+ const obj: FetchObjectPayload = {
927
+ type: 'object',
928
+ byteOffset,
929
+ payloadByteOffset,
930
+ serializationFlags: flags,
931
+ groupId,
932
+ subgroupId,
933
+ objectId,
934
+ publisherPriority: st.prevPriority,
935
+ payloadLength,
936
+ payload,
937
+ extensionData,
938
+ }
939
+ if (groupDelta !== undefined)
940
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
941
+ if (objectDelta !== undefined)
942
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
943
+ if (objectProperties)
944
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
945
+
946
+ st.prevGroupId = groupId
947
+ st.prevSubgroupId = subgroupId
948
+ st.prevObjectId = objectId
949
+ st.first = false
950
+
951
+ return obj
952
+ }
953
+
954
+ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
955
+ try {
956
+ const r = new BufferReader(bytes)
957
+ const streamType = r.readVarInt()
958
+ if (streamType !== FETCH_STREAM_TYPE) {
959
+ return {
960
+ ok: false,
961
+ error: new DecodeError(
962
+ 'CONSTRAINT_VIOLATION',
963
+ `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
964
+ 0,
965
+ ),
966
+ }
967
+ }
968
+ // Section 11.4.1: "all objects on the stream belong to the track requested
969
+ // in the message identified by Request ID" — "the message", not "the FETCH
970
+ // message", because a fill fetch stream (Section 3.4) names a SUBSCRIBE
971
+ // or a REQUEST_UPDATE here.
972
+ const requestId = r.readVarInt()
973
+ const objects: FetchObjectPayload[] = []
974
+
975
+ const st = newFetchObjectState()
976
+
977
+ while (r.remaining > 0) {
978
+ objects.push(readFetchObject(r, 0, st))
979
+ }
980
+
981
+ return {
982
+ ok: true,
983
+ value: { type: 'fetch', requestId, objects },
984
+ bytesRead: r.offset,
985
+ }
986
+ } catch (e) {
987
+ if (e instanceof DecodeError) return { ok: false, error: e }
988
+ throw e
989
+ }
990
+ }
991
+
992
+ export function decodeDataStream(
993
+ streamType: 'subgroup' | 'datagram' | 'fetch',
994
+ bytes: Uint8Array,
995
+ ): DecodeResult<Draft21DataStream> {
996
+ switch (streamType) {
997
+ case 'subgroup':
998
+ return decodeSubgroupStream(bytes)
999
+ case 'datagram':
1000
+ return decodeDatagram(bytes)
1001
+ case 'fetch':
1002
+ return decodeFetchStream(bytes)
1003
+ default: {
1004
+ const _exhaustive: never = streamType
1005
+ throw new Error(`Unknown stream type: ${_exhaustive}`)
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ // ─── Data Stream Decoders ──────────────────────────────────────────────────────
1011
+
1012
+ /**
1013
+ * A data stream's bytes, accumulated across chunks.
1014
+ *
1015
+ * Capacity grows geometrically, so most chunks are a single `set` into spare
1016
+ * room rather than a fresh `unread + chunk.length` array and two copies.
1017
+ *
1018
+ * **It never compacts in place.** Object payloads are handed out as views into
1019
+ * this buffer (`readBytesView`), so moving bytes within it would corrupt an
1020
+ * object that has already been emitted. The consumed prefix is dropped only
1021
+ * when a fresh array is being allocated anyway, which leaves the old array
1022
+ * alive for exactly as long as the views into it are.
1023
+ */
1024
+ class StreamBuffer {
1025
+ private buf = new Uint8Array(0)
1026
+ /** Bytes written into `buf`. */
1027
+ private len = 0
1028
+ /** Consumed prefix of `buf`; `base + offset` is the position in the stream. */
1029
+ offset = 0
1030
+ /** Absolute position of `buf[0]` within the stream. */
1031
+ base = 0
1032
+
1033
+ get unread(): number {
1034
+ return this.len - this.offset
1035
+ }
1036
+
1037
+ append(chunk: Uint8Array): void {
1038
+ if (this.len + chunk.length > this.buf.length) {
1039
+ const live = this.len - this.offset
1040
+ const needed = live + chunk.length
1041
+ // Grow until reclaiming the consumed prefix leaves the buffer at least
1042
+ // half free. Sizing to `needed` would leave no room at all, so the next
1043
+ // chunk would reallocate too and the buffer would reallocate on every
1044
+ // single chunk, which is the cost it exists to avoid. Capacity never
1045
+ // shrinks, so it settles at roughly twice the largest `live + chunk`
1046
+ // this stream has seen.
1047
+ let cap = Math.max(4096, this.buf.length)
1048
+ while (cap < needed * 2) cap *= 2
1049
+ // A fresh array every time, including when the capacity is unchanged:
1050
+ // the old one is still pinned by the payload views handed out of it, so
1051
+ // it can be neither compacted nor reused.
1052
+ const next = new Uint8Array(cap)
1053
+ next.set(this.buf.subarray(this.offset, this.len), 0)
1054
+ this.buf = next
1055
+ this.base += this.offset
1056
+ this.len = live
1057
+ this.offset = 0
1058
+ }
1059
+ this.buf.set(chunk, this.len)
1060
+ this.len += chunk.length
1061
+ }
1062
+
1063
+ /**
1064
+ * The written bytes, for a reader positioned at `offset`. Bounded by `len`
1065
+ * rather than by capacity, so `BufferReader.remaining` counts real bytes and
1066
+ * not the spare room after them.
1067
+ */
1068
+ written(): Uint8Array {
1069
+ return this.buf.subarray(0, this.len)
1070
+ }
1071
+ }
1072
+
1073
+ export function createSubgroupStreamDecoder(): TransformStream<
1074
+ Uint8Array,
1075
+ SubgroupStreamHeader | ObjectPayload
1076
+ > {
1077
+ const b = new StreamBuffer()
1078
+ let header: SubgroupHeaderFields | null = null
1079
+ let headerEmitted = false
1080
+ let prevObjectId = -1n
1081
+ let isFirst = true
1082
+
1083
+ function emitHeader(
1084
+ controller: TransformStreamDefaultController<SubgroupStreamHeader | ObjectPayload>,
1085
+ h: SubgroupHeaderFields,
1086
+ subgroupId: bigint,
1087
+ ): void {
1088
+ controller.enqueue({
1089
+ type: 'subgroup_header',
1090
+ headerType: h.streamType,
1091
+ trackAlias: h.trackAlias,
1092
+ groupId: h.groupId,
1093
+ subgroupId,
1094
+ publisherPriority: h.publisherPriority,
1095
+ ...(h.endOfGroup ? { endOfGroup: true } : {}),
1096
+ ...(h.firstObjectFlag ? { firstObject: true } : {}),
1097
+ })
1098
+ headerEmitted = true
1099
+ }
1100
+
1101
+ return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
1102
+ transform(chunk, controller) {
1103
+ b.append(chunk)
1104
+ // One view and one reader per chunk, not per object: an object is the
1105
+ // thing there are a great many of.
1106
+ const view = b.written()
1107
+
1108
+ if (header === null) {
1109
+ try {
1110
+ const hr = new BufferReader(view, b.offset)
1111
+ header = readSubgroupHeader(hr)
1112
+ b.offset = hr.offset
1113
+ } catch (e) {
1114
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1115
+ return
1116
+ }
1117
+ controller.error(e)
1118
+ return
1119
+ }
1120
+ // Where the Subgroup ID is the first Object's ID, that Object has not
1121
+ // been read yet. Holding the header back until it has is the only way
1122
+ // to emit the value the one-shot decoder reports; every other mode
1123
+ // knows its Subgroup ID already.
1124
+ if (!header.subgroupIsFirstObjId) emitHeader(controller, header, header.subgroupId)
1125
+ }
1126
+
1127
+ const h = header
1128
+ if (h === null) return
1129
+
1130
+ const r = new BufferReader(view, b.offset)
1131
+ while (r.remaining > 0) {
1132
+ // Where this Object starts, so a partial one is re-read from the top
1133
+ // once the rest of it arrives.
1134
+ const start = r.offset
1135
+ let obj: ObjectPayload
1136
+ try {
1137
+ obj = readSubgroupObject(r, h.propertiesPresent, isFirst, prevObjectId, b.base)
1138
+ } catch (e) {
1139
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1140
+ b.offset = start
1141
+ return
1142
+ }
1143
+ controller.error(e)
1144
+ return
1145
+ }
1146
+ if (!headerEmitted) emitHeader(controller, h, obj.objectId)
1147
+ isFirst = false
1148
+ b.offset = r.offset
1149
+ prevObjectId = obj.objectId
1150
+ controller.enqueue(obj)
1151
+ }
1152
+ },
1153
+
1154
+ flush(controller) {
1155
+ // A stream carrying a header and no Objects still has a header to report,
1156
+ // and its Subgroup ID is then zero -- the same value the one-shot decoder
1157
+ // returns when it finds no first Object to derive one from.
1158
+ if (header !== null && !headerEmitted) emitHeader(controller, header, 0n)
1159
+ if (b.unread > 0) {
1160
+ controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1161
+ }
1162
+ },
1163
+ })
1164
+ }
1165
+
1166
+ export function createFetchStreamDecoder(): TransformStream<
1167
+ Uint8Array,
1168
+ FetchStreamHeader | ObjectPayload
1169
+ > {
1170
+ const b = new StreamBuffer()
1171
+ let headerEmitted = false
1172
+ /**
1173
+ * The delta state the Objects carry between them. DATAGRAM-mode Object IDs
1174
+ * resolve against the previous Object's ID, so a decoder that keeps no state
1175
+ * here reports them wrong.
1176
+ */
1177
+ const st = newFetchObjectState()
1178
+
1179
+ return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
1180
+ transform(chunk, controller) {
1181
+ b.append(chunk)
1182
+ const view = b.written()
1183
+
1184
+ if (!headerEmitted) {
1185
+ try {
1186
+ const r = new BufferReader(view, b.offset)
1187
+ const streamType = r.readVarInt()
1188
+ if (streamType !== FETCH_STREAM_TYPE) {
1189
+ controller.error(
1190
+ new DecodeError(
1191
+ 'CONSTRAINT_VIOLATION',
1192
+ `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
1193
+ 0,
1194
+ ),
1195
+ )
1196
+ return
1197
+ }
1198
+ const requestId = r.readVarInt()
1199
+ controller.enqueue({ type: 'fetch_header', requestId })
1200
+ headerEmitted = true
1201
+ b.offset = r.offset
1202
+ } catch (e) {
1203
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1204
+ return
1205
+ }
1206
+ controller.error(e)
1207
+ return
1208
+ }
1209
+ }
1210
+
1211
+ const r = new BufferReader(view, b.offset)
1212
+ while (r.remaining > 0) {
1213
+ const start = r.offset
1214
+ let obj: FetchObjectPayload
1215
+ try {
1216
+ obj = readFetchObject(r, b.base, st)
1217
+ } catch (e) {
1218
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1219
+ b.offset = start
1220
+ return
1221
+ }
1222
+ controller.error(e)
1223
+ return
1224
+ }
1225
+ b.offset = r.offset
1226
+ controller.enqueue(obj)
1227
+ }
1228
+ },
1229
+
1230
+ flush(controller) {
1231
+ if (b.unread > 0) {
1232
+ controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1233
+ }
1234
+ },
1235
+ })
1236
+ }
1237
+
1238
+ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
1239
+ /**
1240
+ * Delegation that actually delegates.
1241
+ *
1242
+ * The inner decoder's readable is pumped into this one's controller as the
1243
+ * bytes arrive. Buffering chunks locally and decoding the whole stream in
1244
+ * `flush` instead would deliver every event at end-of-stream, which on a
1245
+ * subscription that stays open means no events at all.
1246
+ *
1247
+ * The first byte decides which decoder to use, on exactly the range this
1248
+ * draft accepts.
1249
+ */
1250
+ let writer: WritableStreamDefaultWriter<Uint8Array> | null = null
1251
+ let pump: Promise<void> = Promise.resolve()
1252
+ let broken = false
1253
+
1254
+ function attach(
1255
+ inner: TransformStream<Uint8Array, DataStreamEvent>,
1256
+ controller: TransformStreamDefaultController<DataStreamEvent>,
1257
+ ): void {
1258
+ writer = inner.writable.getWriter()
1259
+ const reader = inner.readable.getReader()
1260
+ pump = (async () => {
1261
+ try {
1262
+ for (;;) {
1263
+ const { done, value } = await reader.read()
1264
+ if (done) break
1265
+ controller.enqueue(value)
1266
+ }
1267
+ } catch (e) {
1268
+ // The inner decoder rejected the stream. It owns the diagnosis, so its
1269
+ // error is the one that surfaces.
1270
+ if (!broken) {
1271
+ broken = true
1272
+ controller.error(e)
1273
+ }
1274
+ }
1275
+ })()
1276
+ }
1277
+
1278
+ async function feed(bytes: Uint8Array): Promise<void> {
1279
+ if (writer === null || broken || bytes.length === 0) return
1280
+ try {
1281
+ await writer.write(bytes)
1282
+ } catch {
1283
+ // Writing to an errored inner stream throws the same error the pump is
1284
+ // already reporting; swallow it here so it is reported once.
1285
+ broken = true
1286
+ }
1287
+ }
1288
+
1289
+ return new TransformStream<Uint8Array, DataStreamEvent>({
1290
+ async transform(chunk, controller) {
1291
+ if (broken) return
1292
+ if (writer !== null) {
1293
+ await feed(chunk)
1294
+ return
1295
+ }
1296
+ // One byte is enough to choose, and until there is one there is nothing
1297
+ // to choose from.
1298
+ if (chunk.length === 0) return
1299
+ const firstByte = chunk[0]!
1300
+
1301
+ if (isValidSubgroupType(firstByte)) {
1302
+ attach(
1303
+ createSubgroupStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1304
+ controller,
1305
+ )
1306
+ } else if (firstByte === 0x05) {
1307
+ attach(
1308
+ createFetchStreamDecoder() as unknown as TransformStream<Uint8Array, DataStreamEvent>,
1309
+ controller,
1310
+ )
1311
+ } else {
1312
+ broken = true
1313
+ controller.error(
1314
+ new DecodeError(
1315
+ 'CONSTRAINT_VIOLATION',
1316
+ `Unknown data stream type: 0x${firstByte.toString(16)}`,
1317
+ 0,
1318
+ ),
1319
+ )
1320
+ return
1321
+ }
1322
+
1323
+ // The type byte is part of the stream the inner decoder reads, so the
1324
+ // whole chunk goes in, first byte included.
1325
+ await feed(chunk)
1326
+ },
1327
+
1328
+ async flush() {
1329
+ if (writer === null) return
1330
+ try {
1331
+ await writer.close()
1332
+ } catch {
1333
+ // Reported by the pump.
1334
+ }
1335
+ await pump
1336
+ },
1337
+ })
1338
+ }