@moqtap/codec 0.8.1 → 0.9.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 (127) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +16 -4
  3. package/dist/chunk-BMJMY6FX.cjs +868 -0
  4. package/dist/chunk-MAXEHYVB.cjs +2362 -0
  5. package/dist/{chunk-3YAR3I3S.js → chunk-Q7NSUXXB.js} +3 -1
  6. package/dist/{chunk-VWXUT5R2.cjs → chunk-TVSFDQYE.cjs} +3 -1
  7. package/dist/chunk-WX5PU7HF.js +868 -0
  8. package/dist/chunk-YJGOD3Y4.js +2362 -0
  9. package/dist/{codec-BLYAy2OP.d.ts → codec--CVeqGQv.d.ts} +1 -1
  10. package/dist/{codec-DujHf55Z.d.ts → codec-BJ7BHuiZ.d.ts} +1 -1
  11. package/dist/{codec-D47r_-Rl.d.ts → codec-BKsVu3Fr.d.ts} +1 -1
  12. package/dist/{codec-BNS3gS77.d.ts → codec-BMwgZIgG.d.ts} +1 -1
  13. package/dist/{codec-CI0BjmHc.d.ts → codec-BNy5syCU.d.ts} +1 -1
  14. package/dist/{codec-v6Vloc7H.d.ts → codec-BQa6Qsav.d.ts} +1 -1
  15. package/dist/codec-Bym-K2q2.d.cts +49 -0
  16. package/dist/{codec-D2JbI2tp.d.ts → codec-C006F75E.d.ts} +1 -1
  17. package/dist/codec-C5evFHL-.d.ts +49 -0
  18. package/dist/{codec-DQWQSam3.d.cts → codec-C6Mx4bwz.d.cts} +1 -1
  19. package/dist/{codec-DL0njGkL.d.ts → codec-CA6nbbEk.d.ts} +1 -1
  20. package/dist/{codec-D89DWXbv.d.cts → codec-CSBFdPgP.d.cts} +1 -1
  21. package/dist/{codec-DvcJgYKq.d.cts → codec-CXCjya_G.d.cts} +1 -1
  22. package/dist/{codec-hvkJfCF5.d.cts → codec-C_xxsbKy.d.cts} +1 -1
  23. package/dist/{codec-BOyrk_C9.d.cts → codec-Cfo_z5ET.d.cts} +1 -1
  24. package/dist/{codec-xeuGZY77.d.cts → codec-CgZdadPt.d.cts} +2 -2
  25. package/dist/{codec-WwwUipaU.d.ts → codec-Cl73EfRW.d.ts} +1 -1
  26. package/dist/{codec-BOtv3R5j.d.cts → codec-CtvFK4aV.d.cts} +1 -1
  27. package/dist/{codec-DAePx2NL.d.ts → codec-CviAa4lp.d.ts} +2 -2
  28. package/dist/{codec-DJmV04i0.d.ts → codec-D4RvFQmu.d.ts} +1 -1
  29. package/dist/{codec-cou9XBMF.d.cts → codec-DAyiF5sq.d.cts} +1 -1
  30. package/dist/{codec-CyApDKwf.d.cts → codec-DJmqoIsp.d.cts} +1 -1
  31. package/dist/{codec-NMsTK0GX.d.cts → codec-DchsPfr3.d.cts} +1 -1
  32. package/dist/{codec-DplRgIKq.d.ts → codec-Dl_QIXMz.d.ts} +1 -1
  33. package/dist/{codec-N3g1lxQQ.d.cts → codec-DzLvG8Wb.d.cts} +1 -1
  34. package/dist/{codec-D95gT98n.d.cts → codec-DzkdW4kO.d.cts} +1 -1
  35. package/dist/{codec-CWj5iTxA.d.ts → codec-O-1Js7h4.d.ts} +1 -1
  36. package/dist/{codec-BqqoYdh_.d.cts → codec-vddqy0KY.d.cts} +1 -1
  37. package/dist/draft07-session.d.cts +3 -3
  38. package/dist/draft07-session.d.ts +3 -3
  39. package/dist/draft07.d.cts +3 -3
  40. package/dist/draft07.d.ts +3 -3
  41. package/dist/draft08-session.d.cts +3 -3
  42. package/dist/draft08-session.d.ts +3 -3
  43. package/dist/draft08.d.cts +4 -4
  44. package/dist/draft08.d.ts +4 -4
  45. package/dist/draft09-session.d.cts +3 -3
  46. package/dist/draft09-session.d.ts +3 -3
  47. package/dist/draft09.d.cts +4 -4
  48. package/dist/draft09.d.ts +4 -4
  49. package/dist/draft10-session.d.cts +3 -3
  50. package/dist/draft10-session.d.ts +3 -3
  51. package/dist/draft10.d.cts +4 -4
  52. package/dist/draft10.d.ts +4 -4
  53. package/dist/draft11-session.d.cts +3 -3
  54. package/dist/draft11-session.d.ts +3 -3
  55. package/dist/draft11.d.cts +4 -4
  56. package/dist/draft11.d.ts +4 -4
  57. package/dist/draft12-session.d.cts +3 -3
  58. package/dist/draft12-session.d.ts +3 -3
  59. package/dist/draft12.d.cts +4 -4
  60. package/dist/draft12.d.ts +4 -4
  61. package/dist/draft13-session.d.cts +3 -3
  62. package/dist/draft13-session.d.ts +3 -3
  63. package/dist/draft13.d.cts +4 -4
  64. package/dist/draft13.d.ts +4 -4
  65. package/dist/draft14-session.d.cts +3 -3
  66. package/dist/draft14-session.d.ts +3 -3
  67. package/dist/draft14.d.cts +4 -4
  68. package/dist/draft14.d.ts +4 -4
  69. package/dist/draft15-session.d.cts +3 -3
  70. package/dist/draft15-session.d.ts +3 -3
  71. package/dist/draft15.d.cts +4 -4
  72. package/dist/draft15.d.ts +4 -4
  73. package/dist/draft16-session.d.cts +3 -3
  74. package/dist/draft16-session.d.ts +3 -3
  75. package/dist/draft16.d.cts +4 -4
  76. package/dist/draft16.d.ts +4 -4
  77. package/dist/draft17-session.d.cts +3 -3
  78. package/dist/draft17-session.d.ts +3 -3
  79. package/dist/draft17.d.cts +3 -3
  80. package/dist/draft17.d.ts +3 -3
  81. package/dist/draft18-session.d.cts +3 -3
  82. package/dist/draft18-session.d.ts +3 -3
  83. package/dist/draft18.d.cts +3 -3
  84. package/dist/draft18.d.ts +3 -3
  85. package/dist/draft19-session.d.cts +3 -3
  86. package/dist/draft19-session.d.ts +3 -3
  87. package/dist/draft19.cjs +2 -2
  88. package/dist/draft19.d.cts +3 -3
  89. package/dist/draft19.d.ts +3 -3
  90. package/dist/draft19.js +1 -1
  91. package/dist/draft20-session.cjs +8 -0
  92. package/dist/draft20-session.d.cts +195 -0
  93. package/dist/draft20-session.d.ts +195 -0
  94. package/dist/draft20-session.js +8 -0
  95. package/dist/draft20.cjs +217 -0
  96. package/dist/draft20.d.cts +175 -0
  97. package/dist/draft20.d.ts +175 -0
  98. package/dist/draft20.js +217 -0
  99. package/dist/index.cjs +9 -3
  100. package/dist/index.d.cts +32 -17
  101. package/dist/index.d.ts +32 -17
  102. package/dist/index.js +8 -2
  103. package/dist/{session-types-KsqyVp6y.d.cts → session-types-B9idsih-.d.cts} +13 -1
  104. package/dist/{session-types-C0l_-Ups.d.ts → session-types-Dn7b3RZM.d.ts} +13 -1
  105. package/dist/session.cjs +5 -1
  106. package/dist/session.d.cts +5 -3
  107. package/dist/session.d.ts +5 -3
  108. package/dist/session.js +5 -1
  109. package/dist/types-CWCQwuvy.d.cts +359 -0
  110. package/dist/types-CWCQwuvy.d.ts +359 -0
  111. package/dist/{types-CdCSIfvI.d.cts → types-DG5_iKuv.d.cts} +1 -1
  112. package/dist/{types-CdCSIfvI.d.ts → types-DG5_iKuv.d.ts} +1 -1
  113. package/package.json +16 -3
  114. package/src/core/session-types.ts +7 -0
  115. package/src/core/types.ts +1 -0
  116. package/src/drafts/draft19/codec.ts +4 -2
  117. package/src/drafts/draft20/codec.ts +1737 -0
  118. package/src/drafts/draft20/data-streams.ts +1291 -0
  119. package/src/drafts/draft20/error-codes.ts +135 -0
  120. package/src/drafts/draft20/index.ts +165 -0
  121. package/src/drafts/draft20/messages.ts +68 -0
  122. package/src/drafts/draft20/rules.ts +107 -0
  123. package/src/drafts/draft20/session-fsm.ts +1063 -0
  124. package/src/drafts/draft20/session.ts +21 -0
  125. package/src/drafts/draft20/types.ts +455 -0
  126. package/src/index.ts +20 -2
  127. package/src/session.ts +4 -0
@@ -0,0 +1,1291 @@
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
+ Draft20DataStream,
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-20 Section 11.4.4, 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.4, 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-20 Section 11.4.2) ───────────────────
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-20's OWN rules, computed from Section 11.4.2's three
110
+ // conditions. They are NOT the datagram rules (Section 11.3.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 10 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-20 Section 11.3.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.3.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-20 Section 1.4.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 1.4.1 permits
188
+ // non-minimal encodings, and Section 11.4.2 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.4.2: "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.3.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.4.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.4.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.4.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.4.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-20 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-20 Section 11.4.4.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
+ export function decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream> {
420
+ try {
421
+ const r = new BufferReader(bytes)
422
+ // DECISION (DECISIONS.md D5): permissive on receive. The Type Flags are a
423
+ // vi64 and Section 1.4.1 allows non-minimal encodings, so 0x10 may legally
424
+ // arrive as the two-byte 0x8010. Read the value, then judge the value.
425
+ const typeFlags = r.readVarInt()
426
+ const flagsError = subgroupTypeFlagsError(typeFlags)
427
+ if (flagsError !== null) {
428
+ return { ok: false, error: new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0) }
429
+ }
430
+ const streamType = Number(typeFlags)
431
+
432
+ const propertiesPresent = (streamType & 0x01) !== 0
433
+ const subgroupMode = (streamType & 0x06) >> 1
434
+ const hasSubgroupField = subgroupMode === 0x02
435
+ const subgroupIsFirstObjId = subgroupMode === 0x01
436
+ const endOfGroup = (streamType & 0x08) !== 0
437
+ const hasPriority = (streamType & 0x20) === 0
438
+ const firstObject = (streamType & 0x40) !== 0
439
+
440
+ const trackAlias = r.readVarInt()
441
+ const groupId = r.readVarInt()
442
+
443
+ let subgroupId = 0n
444
+ if (hasSubgroupField) {
445
+ subgroupId = r.readVarInt()
446
+ }
447
+
448
+ let publisherPriority = 128
449
+ if (hasPriority) {
450
+ publisherPriority = r.readUint8()
451
+ }
452
+
453
+ const objects: ObjectPayload[] = []
454
+ let prevObjectId = -1n
455
+ let isFirst = true
456
+
457
+ while (r.remaining > 0) {
458
+ const byteOffset = r.offset
459
+ const delta = r.readVarInt()
460
+ let objectId: bigint
461
+ if (isFirst) {
462
+ objectId = delta
463
+ if (subgroupIsFirstObjId) {
464
+ subgroupId = objectId
465
+ }
466
+ isFirst = false
467
+ } else {
468
+ // Section 11.4.2 delta encoding: prior + delta + 1.
469
+ objectId = prevObjectId + 1n + delta
470
+ }
471
+ const extensionData = new Uint8Array(0)
472
+ let objectProperties: Record<string, bigint> | undefined
473
+ if (propertiesPresent) {
474
+ const propsLen = Number(r.readVarInt())
475
+ if (propsLen > 0) {
476
+ objectProperties = decodeObjectProperties(r, propsLen)
477
+ }
478
+ }
479
+ const payloadLength = Number(r.readVarInt())
480
+ let payload: Uint8Array
481
+ let status: bigint | undefined
482
+ let payloadByteOffset: number
483
+ if (payloadLength === 0) {
484
+ status = r.readVarInt()
485
+ payloadByteOffset = r.offset
486
+ payload = new Uint8Array(0)
487
+ } else {
488
+ payloadByteOffset = r.offset
489
+ payload = r.readBytesView(payloadLength)
490
+ }
491
+ const obj: ObjectPayload = {
492
+ type: 'object',
493
+ byteOffset,
494
+ payloadByteOffset,
495
+ objectId,
496
+ objectIdDelta: delta,
497
+ payloadLength,
498
+ payload,
499
+ extensionData,
500
+ }
501
+ if (status !== undefined) (obj as unknown as Record<string, unknown>).status = status
502
+ if (objectProperties !== undefined)
503
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
504
+ objects.push(obj)
505
+ prevObjectId = objectId
506
+ }
507
+
508
+ const result: SubgroupStream = {
509
+ type: 'subgroup',
510
+ headerType: streamType,
511
+ trackAlias,
512
+ groupId,
513
+ subgroupId,
514
+ publisherPriority,
515
+ objects,
516
+ }
517
+ if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
518
+ if (firstObject) (result as unknown as Record<string, unknown>).firstObject = true
519
+
520
+ return {
521
+ ok: true,
522
+ value: result,
523
+ bytesRead: r.offset,
524
+ }
525
+ } catch (e) {
526
+ if (e instanceof DecodeError) return { ok: false, error: e }
527
+ throw e
528
+ }
529
+ }
530
+
531
+ export function decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject> {
532
+ try {
533
+ const r = new BufferReader(bytes)
534
+ // D5: permissive on receive — the Type Flags are a vi64 (see
535
+ // decodeSubgroupStream).
536
+ const typeFlags = r.readVarInt()
537
+ const flagsError = datagramTypeFlagsError(typeFlags)
538
+ if (flagsError !== null) {
539
+ return { ok: false, error: new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0) }
540
+ }
541
+ const dgType = Number(typeFlags)
542
+
543
+ const objectIdAbsent = (dgType & 0x04) !== 0
544
+ const endOfGroup = (dgType & 0x02) !== 0
545
+ const isStatus = (dgType & 0x20) !== 0
546
+ const defaultPriority = (dgType & 0x08) !== 0
547
+ const propertiesPresent = (dgType & 0x01) !== 0
548
+
549
+ const trackAlias = r.readVarInt()
550
+ const groupId = r.readVarInt()
551
+ let objectId = 0n
552
+ if (!objectIdAbsent) {
553
+ objectId = r.readVarInt()
554
+ }
555
+
556
+ let publisherPriority = 128
557
+ if (!defaultPriority) {
558
+ publisherPriority = r.readUint8()
559
+ }
560
+
561
+ let objectProperties: Record<string, bigint> | undefined
562
+ if (propertiesPresent) {
563
+ const propsLen = Number(r.readVarInt())
564
+ if (propsLen === 0) {
565
+ // Section 11.3.1: "If an endpoint receives a datagram with the
566
+ // PROPERTIES bit set and an Properties Length of 0, it MUST close the
567
+ // session with a PROTOCOL_VIOLATION." A subgroup stream is the
568
+ // opposite: there a zero-length Properties field is normal.
569
+ return {
570
+ ok: false,
571
+ error: new DecodeError(
572
+ 'CONSTRAINT_VIOLATION',
573
+ 'a datagram with the PROPERTIES bit set must carry a non-empty Properties structure',
574
+ r.offset,
575
+ ),
576
+ }
577
+ }
578
+ objectProperties = decodeObjectProperties(r, propsLen)
579
+ }
580
+
581
+ let objectStatus: bigint | undefined
582
+ let payload: Uint8Array
583
+ if (isStatus) {
584
+ objectStatus = r.readVarInt()
585
+ if (propertiesPresent && objectStatus !== OBJECT_STATUS_NORMAL) {
586
+ // Section 11.3.1: STATUS and PROPERTIES both set with a status other
587
+ // than Normal is a PROTOCOL_VIOLATION, "because only Normal Objects
588
+ // can have Properties".
589
+ return {
590
+ ok: false,
591
+ error: new DecodeError(
592
+ 'CONSTRAINT_VIOLATION',
593
+ `only a Normal (0x0) Object may carry Properties, got status 0x${objectStatus.toString(16)}`,
594
+ r.offset,
595
+ ),
596
+ }
597
+ }
598
+ if (r.remaining > 0) {
599
+ // The STATUS bit says the Object Status occupies the position an
600
+ // Object Payload would have, so trailing bytes are not a payload this
601
+ // frame can hold.
602
+ return {
603
+ ok: false,
604
+ error: new DecodeError(
605
+ 'CONSTRAINT_VIOLATION',
606
+ `a status datagram must not carry a payload (${r.remaining} trailing byte(s))`,
607
+ r.offset,
608
+ ),
609
+ }
610
+ }
611
+ payload = new Uint8Array(0)
612
+ } else {
613
+ // No length field: the payload is everything left in the datagram.
614
+ payload = r.readBytesView(r.remaining)
615
+ }
616
+ const payloadLength = payload.byteLength
617
+
618
+ const result: DatagramObject = {
619
+ type: 'datagram',
620
+ datagramType: dgType,
621
+ trackAlias,
622
+ groupId,
623
+ objectId,
624
+ publisherPriority,
625
+ payloadLength,
626
+ payload,
627
+ }
628
+
629
+ if (endOfGroup) (result as unknown as Record<string, unknown>).endOfGroup = true
630
+ if (objectStatus !== undefined)
631
+ (result as unknown as Record<string, unknown>).objectStatus = objectStatus
632
+ if (objectProperties !== undefined)
633
+ (result as unknown as Record<string, unknown>).objectProperties = objectProperties
634
+
635
+ return { ok: true, value: result, bytesRead: r.offset }
636
+ } catch (e) {
637
+ if (e instanceof DecodeError) return { ok: false, error: e }
638
+ throw e
639
+ }
640
+ }
641
+
642
+ export function decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream> {
643
+ try {
644
+ const r = new BufferReader(bytes)
645
+ const streamType = r.readVarInt()
646
+ if (streamType !== FETCH_STREAM_TYPE) {
647
+ return {
648
+ ok: false,
649
+ error: new DecodeError(
650
+ 'CONSTRAINT_VIOLATION',
651
+ `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
652
+ 0,
653
+ ),
654
+ }
655
+ }
656
+ // Section 11.4.4: "all objects on the stream belong to the track requested
657
+ // in the message identified by Request ID" — "the message", not "the FETCH
658
+ // message", because a fill fetch stream (Section 5.1.3) names a SUBSCRIBE
659
+ // or a REQUEST_UPDATE here.
660
+ const requestId = r.readVarInt()
661
+ const objects: FetchObjectPayload[] = []
662
+
663
+ let prevGroupId = 0n
664
+ let prevSubgroupId = 0n
665
+ let prevObjectId = 0n
666
+ let prevPriority = 128
667
+ let first = true
668
+
669
+ while (r.remaining > 0) {
670
+ const byteOffset = r.offset
671
+ const flags = Number(r.readVarInt())
672
+
673
+ let groupId = prevGroupId
674
+ let subgroupId = prevSubgroupId
675
+ // Section 11.4.4.1: absent an Object ID Delta, Object ID is the prior
676
+ // Object's ID plus one. Stream delta encoding, not a range end.
677
+ let objectId = prevObjectId + 1n
678
+ let payloadLength: number
679
+ let payload: Uint8Array
680
+ let payloadByteOffset: number
681
+ const extensionData = new Uint8Array(0)
682
+
683
+ if (flags >= 0x80) {
684
+ // End-of-Range marker. Section 11.4.4: "Any other value is a
685
+ // PROTOCOL_VIOLATION."
686
+ if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
687
+ return {
688
+ ok: false,
689
+ error: new DecodeError(
690
+ 'CONSTRAINT_VIOLATION',
691
+ `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
692
+ r.offset,
693
+ ),
694
+ }
695
+ }
696
+ // D7 / Q16: ordinary delta arithmetic (see encodeFetchStream).
697
+ const groupDelta = r.readVarInt()
698
+ groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
699
+ // With a Group ID Delta present, Object ID is the Object ID Delta.
700
+ const objectDelta = r.readVarInt()
701
+ objectId = objectDelta
702
+ // D7 / Q17: Object Payload Length is present, encoded as 0.
703
+ payloadLength = Number(r.readVarInt())
704
+ payloadByteOffset = r.offset
705
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
706
+
707
+ const marker: FetchObjectPayload = {
708
+ type: 'object',
709
+ byteOffset,
710
+ payloadByteOffset,
711
+ serializationFlags: flags,
712
+ groupId,
713
+ groupIdDelta: groupDelta,
714
+ // A marker has no Subgroup ID or Priority of its own — Section
715
+ // 11.4.4.2 says neither field is present. The interface requires
716
+ // both, so they carry the last actual Object's values, which is also
717
+ // what a following Object that references "the prior Object" gets.
718
+ subgroupId: prevSubgroupId,
719
+ objectId,
720
+ objectIdDelta: objectDelta,
721
+ publisherPriority: prevPriority,
722
+ payloadLength,
723
+ payload,
724
+ extensionData,
725
+ }
726
+ objects.push(marker)
727
+
728
+ // Section 11.4.4.2: prior Group ID and prior Object ID come FROM the
729
+ // marker; prior Subgroup ID and prior Priority stay with the last
730
+ // actual Object before it, so they are deliberately not updated here.
731
+ prevGroupId = groupId
732
+ prevObjectId = objectId
733
+ first = false
734
+ continue
735
+ }
736
+
737
+ if (flags & 0x40) {
738
+ // DATAGRAM mode: no subgroup_id field
739
+ const objectIdPresent = (flags & 0x04) !== 0
740
+ const groupIdPresent = (flags & 0x08) !== 0
741
+ const priorityPresent = (flags & 0x10) !== 0
742
+ const propsPresent = (flags & 0x20) !== 0
743
+
744
+ let groupDelta: bigint | undefined
745
+ if (groupIdPresent) {
746
+ // First object's delta IS the absolute group id; subsequent objects:
747
+ // groupId = prevGroupId + delta + 1 (ascending).
748
+ groupDelta = r.readVarInt()
749
+ groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
750
+ }
751
+ let objectDelta: bigint | undefined
752
+ if (objectIdPresent) {
753
+ objectDelta = r.readVarInt()
754
+ objectId = resolveObjectId(groupIdPresent, objectDelta, prevObjectId)
755
+ }
756
+ if (objectId > MAX_U64) {
757
+ return {
758
+ ok: false,
759
+ error: new DecodeError(
760
+ 'CONSTRAINT_VIOLATION',
761
+ 'computed Object ID exceeds 2^64 - 1',
762
+ r.offset,
763
+ ),
764
+ }
765
+ }
766
+ if (priorityPresent) {
767
+ prevPriority = r.readUint8()
768
+ }
769
+ let objectProperties: Record<string, bigint> | undefined
770
+ if (propsPresent) {
771
+ const propsLen = Number(r.readVarInt())
772
+ if (propsLen > 0) {
773
+ objectProperties = decodeObjectProperties(r, propsLen)
774
+ }
775
+ }
776
+ payloadLength = Number(r.readVarInt())
777
+ payloadByteOffset = r.offset
778
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
779
+
780
+ const obj: FetchObjectPayload = {
781
+ type: 'object',
782
+ byteOffset,
783
+ payloadByteOffset,
784
+ serializationFlags: flags,
785
+ groupId,
786
+ subgroupId: 0n,
787
+ objectId,
788
+ publisherPriority: prevPriority,
789
+ payloadLength,
790
+ payload,
791
+ extensionData,
792
+ }
793
+ if (groupDelta !== undefined)
794
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
795
+ if (objectDelta !== undefined)
796
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
797
+ if (objectProperties)
798
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
799
+ objects.push(obj)
800
+
801
+ prevGroupId = groupId
802
+ prevObjectId = objectId
803
+ first = false
804
+ continue
805
+ }
806
+
807
+ const subgroupEncoding = flags & 0x03
808
+ const objectIdPresent = (flags & 0x04) !== 0
809
+ const groupIdPresent = (flags & 0x08) !== 0
810
+ const priorityPresent = (flags & 0x10) !== 0
811
+ const propsPresent = (flags & 0x20) !== 0
812
+
813
+ let groupDelta: bigint | undefined
814
+ if (groupIdPresent) {
815
+ groupDelta = r.readVarInt()
816
+ groupId = resolveGroupId(first, groupDelta, prevGroupId, r.offset)
817
+ } else if (first) {
818
+ return {
819
+ ok: false,
820
+ error: new DecodeError(
821
+ 'CONSTRAINT_VIOLATION',
822
+ 'First fetch object must include groupId',
823
+ r.offset,
824
+ ),
825
+ }
826
+ }
827
+
828
+ if (subgroupEncoding === 0x00) {
829
+ subgroupId = 0n
830
+ } else if (subgroupEncoding === 0x01 || subgroupEncoding === 0x02) {
831
+ if (first) {
832
+ return {
833
+ ok: false,
834
+ error: new DecodeError(
835
+ 'CONSTRAINT_VIOLATION',
836
+ 'First fetch object cannot reference prior subgroupId',
837
+ r.offset,
838
+ ),
839
+ }
840
+ }
841
+ subgroupId = subgroupEncoding === 0x01 ? prevSubgroupId : prevSubgroupId + 1n
842
+ } else {
843
+ subgroupId = r.readVarInt()
844
+ }
845
+
846
+ let objectDelta: bigint | undefined
847
+ if (objectIdPresent) {
848
+ objectDelta = r.readVarInt()
849
+ objectId = resolveObjectId(groupIdPresent, objectDelta, prevObjectId)
850
+ } else if (first) {
851
+ return {
852
+ ok: false,
853
+ error: new DecodeError(
854
+ 'CONSTRAINT_VIOLATION',
855
+ 'First fetch object must include objectId',
856
+ r.offset,
857
+ ),
858
+ }
859
+ }
860
+ if (objectId > MAX_U64) {
861
+ return {
862
+ ok: false,
863
+ error: new DecodeError(
864
+ 'CONSTRAINT_VIOLATION',
865
+ 'computed Object ID exceeds 2^64 - 1',
866
+ r.offset,
867
+ ),
868
+ }
869
+ }
870
+
871
+ if (priorityPresent) {
872
+ prevPriority = r.readUint8()
873
+ }
874
+
875
+ let objectProperties: Record<string, bigint> | undefined
876
+ if (propsPresent) {
877
+ const propsLen = Number(r.readVarInt())
878
+ if (propsLen > 0) {
879
+ objectProperties = decodeObjectProperties(r, propsLen)
880
+ }
881
+ }
882
+
883
+ payloadLength = Number(r.readVarInt())
884
+ payloadByteOffset = r.offset
885
+ payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
886
+
887
+ // A fetch stream carries no Object Status: status is only present on
888
+ // subscription-delivered objects (Section 11.2.1.1). That is also the
889
+ // one way the two copies of a doubly delivered object differ — the
890
+ // subscription copy can carry a status, the fill copy cannot.
891
+ const obj: FetchObjectPayload = {
892
+ type: 'object',
893
+ byteOffset,
894
+ payloadByteOffset,
895
+ serializationFlags: flags,
896
+ groupId,
897
+ subgroupId,
898
+ objectId,
899
+ publisherPriority: prevPriority,
900
+ payloadLength,
901
+ payload,
902
+ extensionData,
903
+ }
904
+ if (groupDelta !== undefined)
905
+ (obj as unknown as Record<string, unknown>).groupIdDelta = groupDelta
906
+ if (objectDelta !== undefined)
907
+ (obj as unknown as Record<string, unknown>).objectIdDelta = objectDelta
908
+ if (objectProperties)
909
+ (obj as unknown as Record<string, unknown>).objectProperties = objectProperties
910
+ objects.push(obj)
911
+
912
+ prevGroupId = groupId
913
+ prevSubgroupId = subgroupId
914
+ prevObjectId = objectId
915
+ first = false
916
+ }
917
+
918
+ return {
919
+ ok: true,
920
+ value: { type: 'fetch', requestId, objects },
921
+ bytesRead: r.offset,
922
+ }
923
+ } catch (e) {
924
+ if (e instanceof DecodeError) return { ok: false, error: e }
925
+ throw e
926
+ }
927
+ }
928
+
929
+ export function decodeDataStream(
930
+ streamType: 'subgroup' | 'datagram' | 'fetch',
931
+ bytes: Uint8Array,
932
+ ): DecodeResult<Draft20DataStream> {
933
+ switch (streamType) {
934
+ case 'subgroup':
935
+ return decodeSubgroupStream(bytes)
936
+ case 'datagram':
937
+ return decodeDatagram(bytes)
938
+ case 'fetch':
939
+ return decodeFetchStream(bytes)
940
+ default: {
941
+ const _exhaustive: never = streamType
942
+ throw new Error(`Unknown stream type: ${_exhaustive}`)
943
+ }
944
+ }
945
+ }
946
+
947
+ // ─── Data Stream Decoders ──────────────────────────────────────────────────────
948
+
949
+ export function createSubgroupStreamDecoder(): TransformStream<
950
+ Uint8Array,
951
+ SubgroupStreamHeader | ObjectPayload
952
+ > {
953
+ let buffer = new Uint8Array(0)
954
+ let offset = 0
955
+ let headerEmitted = false
956
+ let prevObjectId = -1n
957
+ let firstObject = true
958
+ let _propertiesPresent = false
959
+
960
+ return new TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>({
961
+ transform(chunk, controller) {
962
+ if (offset > 0) {
963
+ buffer = buffer.subarray(offset)
964
+ offset = 0
965
+ }
966
+ const newBuffer = new Uint8Array(buffer.length + chunk.length)
967
+ newBuffer.set(buffer, 0)
968
+ newBuffer.set(chunk, buffer.length)
969
+ buffer = newBuffer
970
+
971
+ if (!headerEmitted) {
972
+ try {
973
+ const r = new BufferReader(buffer.subarray(offset))
974
+ const typeFlags = r.readVarInt()
975
+ const flagsError = subgroupTypeFlagsError(typeFlags)
976
+ if (flagsError !== null) {
977
+ controller.error(new DecodeError('CONSTRAINT_VIOLATION', flagsError, 0))
978
+ return
979
+ }
980
+ const streamType = Number(typeFlags)
981
+
982
+ _propertiesPresent = (streamType & 0x01) !== 0
983
+ const subgroupMode = (streamType & 0x06) >> 1
984
+ const hasSubgroupField = subgroupMode === 0x02
985
+ const hasPriority = (streamType & 0x20) === 0
986
+
987
+ const trackAlias = r.readVarInt()
988
+ const groupId = r.readVarInt()
989
+
990
+ let subgroupId = 0n
991
+ if (hasSubgroupField) {
992
+ subgroupId = r.readVarInt()
993
+ }
994
+
995
+ let publisherPriority = 128
996
+ if (hasPriority) {
997
+ publisherPriority = r.readUint8()
998
+ }
999
+
1000
+ controller.enqueue({
1001
+ type: 'subgroup_header',
1002
+ trackAlias,
1003
+ groupId,
1004
+ subgroupId,
1005
+ publisherPriority,
1006
+ })
1007
+ headerEmitted = true
1008
+ offset += r.offset
1009
+ } catch (e) {
1010
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1011
+ return
1012
+ }
1013
+ controller.error(e)
1014
+ return
1015
+ }
1016
+ }
1017
+
1018
+ while (offset < buffer.length) {
1019
+ try {
1020
+ const r = new BufferReader(buffer.subarray(offset))
1021
+ const delta = r.readVarInt()
1022
+ let objectId: bigint
1023
+ if (firstObject) {
1024
+ objectId = delta
1025
+ firstObject = false
1026
+ } else {
1027
+ objectId = prevObjectId + 1n + delta
1028
+ }
1029
+ let extensionData = new Uint8Array(0)
1030
+ if (_propertiesPresent) {
1031
+ const extLen = Number(r.readVarInt())
1032
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
1033
+ }
1034
+ const payloadLength = Number(r.readVarInt())
1035
+ const payloadByteOffset = r.offset
1036
+ const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
1037
+ controller.enqueue({
1038
+ type: 'object',
1039
+ objectId,
1040
+ objectIdDelta: delta,
1041
+ payloadLength,
1042
+ payload,
1043
+ extensionData,
1044
+ byteOffset: 0,
1045
+ payloadByteOffset,
1046
+ })
1047
+ offset += r.offset
1048
+ prevObjectId = objectId
1049
+ } catch (e) {
1050
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1051
+ break
1052
+ }
1053
+ controller.error(e)
1054
+ return
1055
+ }
1056
+ }
1057
+ },
1058
+
1059
+ flush(controller) {
1060
+ if (offset < buffer.length) {
1061
+ controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1062
+ }
1063
+ },
1064
+ })
1065
+ }
1066
+
1067
+ export function createFetchStreamDecoder(): TransformStream<
1068
+ Uint8Array,
1069
+ FetchStreamHeader | ObjectPayload
1070
+ > {
1071
+ let buffer = new Uint8Array(0)
1072
+ let offset = 0
1073
+ let headerEmitted = false
1074
+
1075
+ return new TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>({
1076
+ transform(chunk, controller) {
1077
+ if (offset > 0) {
1078
+ buffer = buffer.subarray(offset)
1079
+ offset = 0
1080
+ }
1081
+ const newBuffer = new Uint8Array(buffer.length + chunk.length)
1082
+ newBuffer.set(buffer, 0)
1083
+ newBuffer.set(chunk, buffer.length)
1084
+ buffer = newBuffer
1085
+
1086
+ if (!headerEmitted) {
1087
+ try {
1088
+ const r = new BufferReader(buffer.subarray(offset))
1089
+ const streamType = r.readVarInt()
1090
+ if (streamType !== FETCH_STREAM_TYPE) {
1091
+ controller.error(
1092
+ new DecodeError(
1093
+ 'CONSTRAINT_VIOLATION',
1094
+ `Expected fetch stream type 0x05, got 0x${streamType.toString(16)}`,
1095
+ 0,
1096
+ ),
1097
+ )
1098
+ return
1099
+ }
1100
+ const requestId = r.readVarInt()
1101
+ controller.enqueue({ type: 'fetch_header', requestId })
1102
+ headerEmitted = true
1103
+ offset += r.offset
1104
+ } catch (e) {
1105
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1106
+ return
1107
+ }
1108
+ controller.error(e)
1109
+ return
1110
+ }
1111
+ }
1112
+
1113
+ while (offset < buffer.length) {
1114
+ try {
1115
+ const r = new BufferReader(buffer.subarray(offset))
1116
+ const flags = Number(r.readVarInt())
1117
+
1118
+ if (flags >= 0x80) {
1119
+ if (!FETCH_END_OF_RANGE_MARKERS.has(flags)) {
1120
+ controller.error(
1121
+ new DecodeError(
1122
+ 'CONSTRAINT_VIOLATION',
1123
+ `0x${flags.toString(16)} is neither a flag combination below 128 nor one of the three End of Range markers`,
1124
+ 0,
1125
+ ),
1126
+ )
1127
+ return
1128
+ }
1129
+ r.readVarInt() // Group ID Delta
1130
+ const objectDelta = r.readVarInt()
1131
+ const markerPayloadLength = Number(r.readVarInt())
1132
+ const markerPayloadByteOffset = r.offset
1133
+ controller.enqueue({
1134
+ type: 'object',
1135
+ objectId: objectDelta,
1136
+ objectIdDelta: objectDelta,
1137
+ payloadLength: markerPayloadLength,
1138
+ payload: new Uint8Array(0),
1139
+ extensionData: new Uint8Array(0),
1140
+ byteOffset: 0,
1141
+ payloadByteOffset: markerPayloadByteOffset,
1142
+ })
1143
+ offset += r.offset
1144
+ continue
1145
+ }
1146
+
1147
+ const objectIdPresent = (flags & 0x04) !== 0
1148
+ const groupIdPresent = (flags & 0x08) !== 0
1149
+ const priorityPresent = (flags & 0x10) !== 0
1150
+ const extensionsPresent = (flags & 0x20) !== 0
1151
+ const subgroupEncoding = flags & 0x03
1152
+ const datagramMode = (flags & 0x40) !== 0
1153
+
1154
+ if (groupIdPresent) r.readVarInt()
1155
+ if (!datagramMode && subgroupEncoding === 0x03) r.readVarInt()
1156
+ let objectId = 0n
1157
+ let objectIdDelta: bigint | undefined
1158
+ if (objectIdPresent) {
1159
+ objectIdDelta = r.readVarInt()
1160
+ objectId = objectIdDelta
1161
+ }
1162
+ if (priorityPresent) r.readUint8()
1163
+ let extensionData = new Uint8Array(0)
1164
+ if (extensionsPresent) {
1165
+ const extLen = Number(r.readVarInt())
1166
+ extensionData = extLen > 0 ? r.readBytesView(extLen) : new Uint8Array(0)
1167
+ }
1168
+ const payloadLength = Number(r.readVarInt())
1169
+ const payloadByteOffset = r.offset
1170
+ const payload = payloadLength > 0 ? r.readBytesView(payloadLength) : new Uint8Array(0)
1171
+ const event: ObjectPayload = {
1172
+ type: 'object',
1173
+ objectId,
1174
+ payloadLength,
1175
+ payload,
1176
+ extensionData,
1177
+ byteOffset: 0,
1178
+ payloadByteOffset,
1179
+ }
1180
+ if (objectIdDelta !== undefined)
1181
+ (event as unknown as Record<string, unknown>).objectIdDelta = objectIdDelta
1182
+ controller.enqueue(event)
1183
+ offset += r.offset
1184
+ } catch (e) {
1185
+ if (e instanceof DecodeError && e.code === 'UNEXPECTED_END') {
1186
+ break
1187
+ }
1188
+ controller.error(e)
1189
+ return
1190
+ }
1191
+ }
1192
+ },
1193
+
1194
+ flush(controller) {
1195
+ if (offset < buffer.length) {
1196
+ controller.error(new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete data', 0))
1197
+ }
1198
+ },
1199
+ })
1200
+ }
1201
+
1202
+ export function createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent> {
1203
+ let buffer = new Uint8Array(0)
1204
+ let offset = 0
1205
+ let inner: TransformStream<Uint8Array, DataStreamEvent> | null = null
1206
+
1207
+ return new TransformStream<Uint8Array, DataStreamEvent>({
1208
+ transform(chunk, controller) {
1209
+ if (offset > 0) {
1210
+ buffer = buffer.subarray(offset)
1211
+ offset = 0
1212
+ }
1213
+ const newBuffer = new Uint8Array(buffer.length + chunk.length)
1214
+ newBuffer.set(buffer, 0)
1215
+ newBuffer.set(chunk, buffer.length)
1216
+ buffer = newBuffer
1217
+
1218
+ if (inner === null) {
1219
+ if (offset >= buffer.length) return
1220
+ const firstByte = buffer[offset]!
1221
+
1222
+ if (isValidSubgroupType(firstByte)) {
1223
+ const decoder = createSubgroupStreamDecoder()
1224
+ inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
1225
+ } else if (firstByte === 0x05) {
1226
+ const decoder = createFetchStreamDecoder()
1227
+ inner = decoder as unknown as TransformStream<Uint8Array, DataStreamEvent>
1228
+ } else {
1229
+ controller.error(
1230
+ new DecodeError(
1231
+ 'CONSTRAINT_VIOLATION',
1232
+ `Unknown data stream type: 0x${firstByte.toString(16)}`,
1233
+ 0,
1234
+ ),
1235
+ )
1236
+ return
1237
+ }
1238
+ }
1239
+ },
1240
+
1241
+ flush(controller) {
1242
+ if (offset >= buffer.length) return
1243
+ const view = buffer.subarray(offset)
1244
+
1245
+ const firstByte = view[0]!
1246
+ let result: DecodeResult<Draft20DataStream>
1247
+
1248
+ if (isValidSubgroupType(firstByte)) {
1249
+ result = decodeSubgroupStream(view)
1250
+ } else if (firstByte === 0x05) {
1251
+ result = decodeFetchStream(view)
1252
+ } else {
1253
+ controller.error(
1254
+ new DecodeError(
1255
+ 'CONSTRAINT_VIOLATION',
1256
+ `Unknown data stream type: 0x${firstByte.toString(16)}`,
1257
+ 0,
1258
+ ),
1259
+ )
1260
+ return
1261
+ }
1262
+
1263
+ if (!result.ok) {
1264
+ controller.error(result.error)
1265
+ return
1266
+ }
1267
+
1268
+ const stream = result.value
1269
+ if (stream.type === 'subgroup') {
1270
+ controller.enqueue({
1271
+ type: 'subgroup_header',
1272
+ trackAlias: stream.trackAlias,
1273
+ groupId: stream.groupId,
1274
+ subgroupId: stream.subgroupId,
1275
+ publisherPriority: stream.publisherPriority,
1276
+ })
1277
+ for (const obj of stream.objects) {
1278
+ controller.enqueue(obj)
1279
+ }
1280
+ } else if (stream.type === 'fetch') {
1281
+ controller.enqueue({
1282
+ type: 'fetch_header',
1283
+ requestId: stream.requestId,
1284
+ })
1285
+ for (const obj of stream.objects) {
1286
+ controller.enqueue(obj)
1287
+ }
1288
+ }
1289
+ },
1290
+ })
1291
+ }