@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,1737 @@
1
+ import { MoqtBufferReader as BufferReader } from '../../core/buffer-reader.js'
2
+ import { MoqtBufferWriter as BufferWriter } from '../../core/buffer-writer.js'
3
+ import { bytesToHex, hexToBytes } from '../../core/hex.js'
4
+ import type { BaseCodec, DecodeResult } from '../../core/types.js'
5
+ import { DecodeError } from '../../core/types.js'
6
+ import {
7
+ MESSAGE_ID_MAP,
8
+ MSG_FETCH,
9
+ MSG_FETCH_OK,
10
+ MSG_GOAWAY,
11
+ MSG_NAMESPACE,
12
+ MSG_NAMESPACE_DONE,
13
+ MSG_PUBLISH,
14
+ MSG_PUBLISH_DONE,
15
+ MSG_PUBLISH_NAMESPACE,
16
+ MSG_PUBLISH_SKIPPED,
17
+ MSG_PUBLISH_STATE_NOTIFY,
18
+ MSG_REQUEST_ERROR,
19
+ MSG_REQUEST_OK,
20
+ MSG_REQUEST_UPDATE,
21
+ MSG_SETUP,
22
+ MSG_SUBSCRIBE,
23
+ MSG_SUBSCRIBE_NAMESPACE,
24
+ MSG_SUBSCRIBE_OK,
25
+ MSG_SUBSCRIBE_TRACKS,
26
+ MSG_TRACK_STATUS,
27
+ SETUP_OPT_AUTHORITY,
28
+ SETUP_OPT_AUTHORIZATION_TOKEN,
29
+ SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE,
30
+ SETUP_OPT_MAX_FILTER_RANGES,
31
+ SETUP_OPT_MAX_REQUEST_UPDATES,
32
+ SETUP_OPT_MOQT_IMPLEMENTATION,
33
+ SETUP_OPT_PATH,
34
+ } from './messages.js'
35
+ import type {
36
+ AuthorizationToken,
37
+ DatagramObject,
38
+ DataStreamEvent,
39
+ Draft20DataStream,
40
+ Draft20FillParameters,
41
+ Draft20Message,
42
+ Draft20Params,
43
+ Draft20SetupOptions,
44
+ Draft20TrackProperties,
45
+ FetchStream,
46
+ FetchStreamHeader,
47
+ LocationFilter,
48
+ ObjectPayload,
49
+ RangeFilter,
50
+ RangeFilterRange,
51
+ Redirect,
52
+ SubgroupStream,
53
+ SubgroupStreamHeader,
54
+ UnknownParam,
55
+ } from './types.js'
56
+
57
+ const textEncoder = /* @__PURE__ */ new TextEncoder()
58
+ const textDecoder = /* @__PURE__ */ new TextDecoder()
59
+
60
+ const REQUEST_ERROR_REDIRECT = 0x34n
61
+
62
+ /** The largest value MOQT's vi64 can carry (draft-20 Section 1.4.1, nine-byte form). */
63
+ const MAX_U64 = 0xffffffffffffffffn
64
+
65
+ /**
66
+ * `PUBLISH_DONE.Stream Count` "unknown" sentinel — draft-20 Section 10.12.
67
+ *
68
+ * draft-19 used `2^62 - 1`; draft-20 raised it to `2^64 - 1`. Both are
69
+ * encodable because MOQT's vi64 (Section 1.4.1) is a leading-ones-length
70
+ * prefix reaching a full 64 bits in nine bytes — it is NOT the QUIC varint and
71
+ * has no `2^62 - 1` ceiling. On the wire this is `ff` followed by eight `ff`
72
+ * bytes.
73
+ *
74
+ * JS `bigint` is required: `2^64 - 1` does not fit a `number`, and comparing a
75
+ * `Number(stream_count)` against this sentinel would be wrong for every value
76
+ * above `2^53`.
77
+ *
78
+ * Note (SPEC-DELTA Section 11 Q14): unlike the old `2^62 - 1` marker, this
79
+ * value is also a well-formed exact count. There is no longer any headroom
80
+ * that separates "unknown" from "absurdly many streams"; the draft does not
81
+ * acknowledge the collision.
82
+ */
83
+ export const UNKNOWN_STREAM_COUNT = 0xffffffffffffffffn
84
+
85
+ // ─── Setup Options Encoding/Decoding (KVP, no count prefix) ─────────────────
86
+
87
+ function encodeSetupOptions(opts: Draft20SetupOptions, writer: BufferWriter): void {
88
+ // Collect all options sorted by type ID
89
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
90
+
91
+ if (opts.path !== undefined) {
92
+ entries.push({
93
+ type: SETUP_OPT_PATH,
94
+ encode: (w) => {
95
+ const encoded = textEncoder.encode(opts.path!)
96
+ w.writeVarInt(BigInt(encoded.byteLength))
97
+ w.writeBytes(encoded)
98
+ },
99
+ })
100
+ }
101
+ if (opts.authorization_token !== undefined) {
102
+ entries.push({
103
+ type: SETUP_OPT_AUTHORIZATION_TOKEN,
104
+ encode: (w) => {
105
+ const tmpW = new BufferWriter(64)
106
+ encodeAuthorizationToken(opts.authorization_token!, tmpW)
107
+ const raw = tmpW.finish()
108
+ w.writeVarInt(BigInt(raw.byteLength))
109
+ w.writeBytes(raw)
110
+ },
111
+ })
112
+ }
113
+ if (opts.max_auth_token_cache_size !== undefined) {
114
+ entries.push({
115
+ type: SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE,
116
+ encode: (w) => w.writeVarInt(opts.max_auth_token_cache_size!),
117
+ })
118
+ }
119
+ if (opts.authority !== undefined) {
120
+ entries.push({
121
+ type: SETUP_OPT_AUTHORITY,
122
+ encode: (w) => {
123
+ const encoded = textEncoder.encode(opts.authority!)
124
+ w.writeVarInt(BigInt(encoded.byteLength))
125
+ w.writeBytes(encoded)
126
+ },
127
+ })
128
+ }
129
+ if (opts.max_filter_ranges !== undefined) {
130
+ entries.push({
131
+ type: SETUP_OPT_MAX_FILTER_RANGES,
132
+ encode: (w) => w.writeVarInt(opts.max_filter_ranges!),
133
+ })
134
+ }
135
+ if (opts.moqt_implementation !== undefined) {
136
+ entries.push({
137
+ type: SETUP_OPT_MOQT_IMPLEMENTATION,
138
+ encode: (w) => {
139
+ const encoded = textEncoder.encode(opts.moqt_implementation!)
140
+ w.writeVarInt(BigInt(encoded.byteLength))
141
+ w.writeBytes(encoded)
142
+ },
143
+ })
144
+ }
145
+ if (opts.max_request_updates !== undefined) {
146
+ entries.push({
147
+ type: SETUP_OPT_MAX_REQUEST_UPDATES,
148
+ encode: (w) => w.writeVarInt(opts.max_request_updates!),
149
+ })
150
+ }
151
+ if (opts.unknown) {
152
+ for (const u of opts.unknown) {
153
+ const id = BigInt(u.id)
154
+ entries.push({
155
+ type: id,
156
+ encode: (w) => {
157
+ if (id % 2n === 0n) {
158
+ const raw = hexToBytes(u.raw_hex)
159
+ const tmpReader = new BufferReader(raw)
160
+ w.writeVarInt(tmpReader.readVarInt())
161
+ } else {
162
+ const raw = hexToBytes(u.raw_hex)
163
+ w.writeVarInt(BigInt(raw.byteLength))
164
+ w.writeBytes(raw)
165
+ }
166
+ },
167
+ })
168
+ }
169
+ }
170
+
171
+ // Sort by type (ascending) and write with delta encoding
172
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
173
+ let prevType = 0n
174
+ for (const entry of entries) {
175
+ writer.writeVarInt(entry.type - prevType)
176
+ entry.encode(writer)
177
+ prevType = entry.type
178
+ }
179
+ }
180
+
181
+ function decodeSetupOptions(reader: BufferReader, payloadEnd: number): Draft20SetupOptions {
182
+ const result: Draft20SetupOptions = {}
183
+ const unknown: UnknownParam[] = []
184
+ let prevType = 0n
185
+
186
+ while (reader.offset < payloadEnd) {
187
+ const delta = reader.readVarInt()
188
+ const optType = prevType + delta
189
+ prevType = optType
190
+
191
+ if (optType % 2n === 0n) {
192
+ // Even: single varint value
193
+ const value = reader.readVarInt()
194
+ if (optType === SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE) {
195
+ result.max_auth_token_cache_size = value
196
+ } else if (optType === SETUP_OPT_MAX_FILTER_RANGES) {
197
+ result.max_filter_ranges = value
198
+ } else if (optType === SETUP_OPT_MAX_REQUEST_UPDATES) {
199
+ result.max_request_updates = value
200
+ } else {
201
+ // Section 10.3: "endpoints MUST ignore unknown Setup Options" — kept
202
+ // for passthrough rather than discarded, but never an error.
203
+ const tmpWriter = new BufferWriter(16)
204
+ tmpWriter.writeVarInt(value)
205
+ const raw = tmpWriter.finish()
206
+ unknown.push({
207
+ id: `0x${optType.toString(16)}`,
208
+ length: raw.byteLength,
209
+ raw_hex: bytesToHex(raw),
210
+ })
211
+ }
212
+ } else {
213
+ // Odd: length-prefixed bytes
214
+ const length = Number(reader.readVarInt())
215
+ const bytes = reader.readBytes(length)
216
+ if (optType === SETUP_OPT_PATH) {
217
+ result.path = textDecoder.decode(bytes)
218
+ } else if (optType === SETUP_OPT_AUTHORIZATION_TOKEN) {
219
+ result.authorization_token = decodeAuthorizationToken(new BufferReader(bytes))
220
+ } else if (optType === SETUP_OPT_AUTHORITY) {
221
+ result.authority = textDecoder.decode(bytes)
222
+ } else if (optType === SETUP_OPT_MOQT_IMPLEMENTATION) {
223
+ result.moqt_implementation = textDecoder.decode(bytes)
224
+ } else {
225
+ unknown.push({
226
+ id: `0x${optType.toString(16)}`,
227
+ length,
228
+ raw_hex: bytesToHex(bytes),
229
+ })
230
+ }
231
+ }
232
+ }
233
+
234
+ if (unknown.length > 0) result.unknown = unknown
235
+ return result
236
+ }
237
+
238
+ // ─── Authorization Token Encoding/Decoding ────────────────────────────────────
239
+
240
+ function encodeAuthorizationToken(token: AuthorizationToken, w: BufferWriter): void {
241
+ const aliasType = Number(token.alias_type)
242
+ w.writeVarInt(token.alias_type)
243
+ if (aliasType === 0 || aliasType === 1 || aliasType === 2) {
244
+ // DELETE, REGISTER, USE_ALIAS: token_alias present
245
+ w.writeVarInt(token.token_alias ?? 0n)
246
+ }
247
+ if (aliasType === 1 || aliasType === 3) {
248
+ // REGISTER, USE_VALUE: token_type + token_value present. The Token Value is
249
+ // NOT length-prefixed inside the Token structure; it consumes the remainder
250
+ // of the outer length-prefixed buffer (draft-20 Section 10.2.2).
251
+ w.writeVarInt(token.token_type ?? 0n)
252
+ const tv = token.token_value ?? new Uint8Array(0)
253
+ w.writeBytes(tv)
254
+ }
255
+ }
256
+
257
+ function decodeAuthorizationToken(r: BufferReader): AuthorizationToken {
258
+ const alias_type = r.readVarInt()
259
+ const aliasType = Number(alias_type)
260
+ const result: AuthorizationToken = { alias_type }
261
+
262
+ if (aliasType === 0 || aliasType === 1 || aliasType === 2) {
263
+ ;(result as { token_alias: bigint }).token_alias = r.readVarInt()
264
+ }
265
+ if (aliasType === 1 || aliasType === 3) {
266
+ ;(result as { token_type: bigint }).token_type = r.readVarInt()
267
+ // Token Value extends to the end of the outer length-prefixed buffer
268
+ const bytes = r.readBytes(r.remaining)
269
+ ;(result as { token_value: Uint8Array }).token_value = bytes
270
+ }
271
+
272
+ return result
273
+ }
274
+
275
+ // ─── Tuple (Track Namespace) encode/decode for params ────────────────────────
276
+
277
+ function encodeNamespaceTuple(ns: string[], w: BufferWriter): void {
278
+ w.writeVarInt(BigInt(ns.length))
279
+ for (const part of ns) {
280
+ const encoded = textEncoder.encode(part)
281
+ w.writeVarInt(BigInt(encoded.byteLength))
282
+ w.writeBytes(encoded)
283
+ }
284
+ }
285
+
286
+ function decodeNamespaceTuple(r: BufferReader): string[] {
287
+ const count = Number(r.readVarInt())
288
+ const parts: string[] = []
289
+ for (let i = 0; i < count; i++) {
290
+ const len = Number(r.readVarInt())
291
+ parts.push(textDecoder.decode(r.readBytes(len)))
292
+ }
293
+ return parts
294
+ }
295
+
296
+ // ─── Message Parameter type codes (draft-20 Section 15.7, Table 13) ─────────
297
+
298
+ const PARAM_OBJECT_DELIVERY_TIMEOUT = 0x02n
299
+ const PARAM_AUTHORIZATION_TOKEN = 0x03n
300
+ const PARAM_RENDEZVOUS_TIMEOUT = 0x04n
301
+ const PARAM_SUBGROUP_DELIVERY_TIMEOUT = 0x06n
302
+ const PARAM_EXPIRES = 0x08n
303
+ const PARAM_LARGEST_OBJECT = 0x09n
304
+ const PARAM_FILL_TIMEOUT = 0x0an
305
+ const PARAM_FORWARD = 0x10n
306
+ const PARAM_SUBSCRIBER_PRIORITY = 0x20n
307
+ const PARAM_LOCATION_FILTER = 0x21n
308
+ const PARAM_GROUP_ORDER = 0x22n
309
+ const PARAM_FILL_PARAMETERS = 0x23n // NEW in draft-20
310
+ const PARAM_SUBGROUP_FILTER = 0x25n
311
+ const PARAM_OBJECTID_FILTER = 0x26n
312
+ const PARAM_PRIORITY_FILTER = 0x27n
313
+ const PARAM_OBJECT_PROPERTY_FILTER = 0x28n
314
+ const PARAM_TRACK_PROPERTY_FILTER = 0x29n
315
+ const PARAM_NEW_GROUP_REQUEST = 0x32n
316
+ const PARAM_TRACK_NAMESPACE_PREFIX = 0x34n
317
+ const PARAM_INCLUDE_PROPERTIES = 0x35n // NEW in draft-20
318
+
319
+ /**
320
+ * The pseudo message-type used for the parameter scope inside FILL_PARAMETERS.
321
+ *
322
+ * DECISION (DECISIONS.md D2, SPEC-DELTA Section 11 Q2): the nested value is a
323
+ * *separate parameter scope*. draft-20 Section 10.2.15 says "Parameters inside
324
+ * it are not considered to appear in the enclosing message for the purposes of
325
+ * Section 10.2, so a Parameter Type MAY appear both in the message and inside
326
+ * FILL_PARAMETERS." Modelling it as its own scope key falls straight out of
327
+ * that sentence — but the draft never states the consequence explicitly.
328
+ */
329
+ const FILL_SCOPE = 'fill_parameters'
330
+
331
+ // ─── Range Filter Encoding/Decoding (draft-20 Section 5.1.4) ────────────────
332
+ // A Range Filter's length-prefixed value is: SetID (uint8), an optional
333
+ // Property Type (varint, only for the Object/Track Property filters), then a
334
+ // sequence of delta-encoded inclusive Start/End Range pairs. The final End may
335
+ // be omitted (bounded by the length prefix) to indicate an open upper bound.
336
+ // In REQUEST_UPDATE a length-0 value removes the filter.
337
+ //
338
+ // Unchanged from draft-19: same codepoints, same field order, same delta
339
+ // arithmetic. What changed is applicability — PUBLISH_OK was dropped from
340
+ // every one of them, and 0x25-0x28 may now nest inside FILL_PARAMETERS.
341
+
342
+ function encodeRangeFilter(filter: RangeFilter, hasProperty: boolean, w: BufferWriter): void {
343
+ if (filter.removed) {
344
+ // REQUEST_UPDATE removal form: zero-length value
345
+ w.writeVarInt(0n)
346
+ return
347
+ }
348
+ const tmpW = new BufferWriter(32)
349
+ tmpW.writeUint8(Number(filter.set_id ?? 0n))
350
+ if (hasProperty) {
351
+ tmpW.writeVarInt(filter.property_type ?? 0n)
352
+ }
353
+ let prevEnd = 0n
354
+ for (const range of filter.ranges ?? []) {
355
+ tmpW.writeVarInt(range.start - prevEnd)
356
+ if (range.end !== undefined) {
357
+ // Ranges are inclusive Start/End pairs; End is delta-encoded from its
358
+ // own Start with no adjustment. Nothing here is offset by one.
359
+ tmpW.writeVarInt(range.end - range.start)
360
+ prevEnd = range.end
361
+ }
362
+ }
363
+ const raw = tmpW.finish()
364
+ w.writeVarInt(BigInt(raw.byteLength))
365
+ w.writeBytes(raw)
366
+ }
367
+
368
+ function decodeRangeFilter(reader: BufferReader, hasProperty: boolean): RangeFilter {
369
+ const length = Number(reader.readVarInt())
370
+ if (length === 0) {
371
+ // REQUEST_UPDATE removal form
372
+ return { removed: true }
373
+ }
374
+ const end = reader.offset + length
375
+ const set_id = BigInt(reader.readUint8())
376
+ let property_type: bigint | undefined
377
+ if (hasProperty) {
378
+ property_type = reader.readVarInt()
379
+ }
380
+ const ranges: RangeFilterRange[] = []
381
+ let prevEnd = 0n
382
+ while (reader.offset < end) {
383
+ const start = reader.readVarInt() + prevEnd
384
+ if (reader.offset < end) {
385
+ const rangeEnd = reader.readVarInt() + start
386
+ ranges.push({ start, end: rangeEnd })
387
+ prevEnd = rangeEnd
388
+ } else {
389
+ ranges.push({ start })
390
+ }
391
+ }
392
+ return property_type !== undefined ? { set_id, property_type, ranges } : { set_id, ranges }
393
+ }
394
+
395
+ // ─── LOCATION_FILTER (0x21) — restructured in draft-20 (Section 5.1.2) ──────
396
+
397
+ /**
398
+ * Encode a LOCATION_FILTER value.
399
+ *
400
+ * The fields are written in order and the parameter's Length is whatever they
401
+ * come to; the *count* of vi64 fields is the discriminator on the way back in
402
+ * (see {@link decodeLocationFilter}).
403
+ *
404
+ * Note what is absent: no `+ 1` on `end_object`, and no "Object of 0 means the
405
+ * whole group" special case. draft-20 Section 5.1.2 says "A Location filter
406
+ * specifies an inclusive range of Locations", and Section 10.13 repeats it for
407
+ * FETCH. draft-19 Section 10.12.1 encoded a fetch end as "the end Location,
408
+ * plus 1", so an encoder ported forward with its arithmetic intact fetches one
409
+ * object too many. This is DECISIONS.md D4; the change is absent from the
410
+ * draft's own change log, which is why it is called out here rather than
411
+ * assumed obvious.
412
+ */
413
+ function encodeLocationFilter(f: LocationFilter, w: BufferWriter): void {
414
+ if (f.removed) {
415
+ // Length == 0: no filter. In REQUEST_UPDATE this removes an existing one.
416
+ w.writeVarInt(0n)
417
+ return
418
+ }
419
+ const tmpW = new BufferWriter(48)
420
+ if (f.start_group !== undefined) tmpW.writeVarInt(f.start_group)
421
+ if (f.start_object !== undefined) tmpW.writeVarInt(f.start_object)
422
+ if (f.end_group_delta !== undefined) tmpW.writeVarInt(f.end_group_delta)
423
+ if (f.end_object !== undefined) tmpW.writeVarInt(f.end_object)
424
+ const raw = tmpW.finish()
425
+ w.writeVarInt(BigInt(raw.byteLength))
426
+ w.writeBytes(raw)
427
+ }
428
+
429
+ /**
430
+ * Decode a LOCATION_FILTER value.
431
+ *
432
+ * DECISION (DECISIONS.md D3, SPEC-DELTA Section 11 Q3): the field count comes
433
+ * from parsing, never from the byte length. draft-20 Section 5.1.2 says
434
+ * "Length (in bytes) determines how many optional vi64 fields are present",
435
+ * which is not implementable as written: MOQT varints are 1-9 bytes and need
436
+ * not be minimally encoded (Section 1.4.1), so a Length of 2 is equally
437
+ * consistent with two 1-byte fields and one 2-byte field. The only rule that
438
+ * round-trips is to decode vi64 values until exactly Length bytes are consumed
439
+ * and then switch on the count.
440
+ *
441
+ * Two corollaries the draft does not give, chosen here:
442
+ * - a field that would overrun Length is a PROTOCOL_VIOLATION;
443
+ * - more than four decoded values is a PROTOCOL_VIOLATION.
444
+ */
445
+ function decodeLocationFilter(reader: BufferReader): LocationFilter {
446
+ const length = Number(reader.readVarInt())
447
+ // Slice exactly Length bytes so a varint cannot silently run into whatever
448
+ // parameter follows: inside this sub-reader an overrun is an end-of-input.
449
+ const valueBytes = reader.readBytes(length)
450
+ if (length === 0) {
451
+ // The draft does state this one: "A length of 0 indicates no filter, for
452
+ // example to remove the filter in REQUEST_UPDATE."
453
+ return { removed: true }
454
+ }
455
+
456
+ const r = new BufferReader(valueBytes)
457
+ const fields: bigint[] = []
458
+ while (r.remaining > 0) {
459
+ let value: bigint
460
+ try {
461
+ value = r.readVarInt()
462
+ } catch {
463
+ throw new DecodeError(
464
+ 'CONSTRAINT_VIOLATION',
465
+ "a LOCATION_FILTER field must not extend past the parameter's Length",
466
+ reader.offset,
467
+ )
468
+ }
469
+ fields.push(value)
470
+ if (fields.length > 4) {
471
+ throw new DecodeError(
472
+ 'CONSTRAINT_VIOLATION',
473
+ 'a LOCATION_FILTER value may hold at most four vi64 fields',
474
+ reader.offset,
475
+ )
476
+ }
477
+ }
478
+
479
+ const filter: {
480
+ start_group?: bigint
481
+ start_object?: bigint
482
+ end_group_delta?: bigint
483
+ end_object?: bigint
484
+ } = {}
485
+ if (fields.length >= 1) filter.start_group = fields[0] as bigint
486
+ if (fields.length >= 2) filter.start_object = fields[1] as bigint
487
+ if (fields.length >= 3) filter.end_group_delta = fields[2] as bigint
488
+ if (fields.length >= 4) filter.end_object = fields[3] as bigint
489
+
490
+ if (filter.start_group !== undefined && filter.end_group_delta !== undefined) {
491
+ // Section 5.1.2: "If StartGroup + EndGroupDelta exceeds 2^64 - 1, the
492
+ // endpoint MUST close the session with a PROTOCOL_VIOLATION." The sum is
493
+ // the absolute End Group; EndGroupDelta is delta-encoded from StartGroup
494
+ // but both groups are absolute, not relative to Largest Object.
495
+ if (filter.start_group + filter.end_group_delta > MAX_U64) {
496
+ throw new DecodeError(
497
+ 'CONSTRAINT_VIOLATION',
498
+ 'StartGroup + EndGroupDelta exceeds 2^64 - 1',
499
+ reader.offset,
500
+ )
501
+ }
502
+ }
503
+
504
+ return filter
505
+ }
506
+
507
+ /**
508
+ * Which messages each Message Parameter's own definition names.
509
+ *
510
+ * Section 10.2.1: "Each Message Parameter definition indicates the message
511
+ * types in which it can appear. If it appears in some other type of message,
512
+ * the receiving endpoint MUST close the connection with a PROTOCOL_VIOLATION."
513
+ *
514
+ * Read out of draft-20's per-parameter sections, not carried over from
515
+ * draft-19. The biggest delta is that `request_ok` — which is also PUBLISH_OK,
516
+ * REQUEST_UPDATE_OK, TRACK_STATUS_OK, SUBSCRIBE_NAMESPACE_OK,
517
+ * SUBSCRIBE_TRACKS_OK and PUBLISH_NAMESPACE_OK — lost every subscription
518
+ * parameter it used to carry. Only EXPIRES (0x08) and LARGEST_OBJECT (0x09)
519
+ * still name it. Subscription parameters travel on PUBLISH (initial values) and
520
+ * REQUEST_UPDATE (changes) instead.
521
+ *
522
+ * `fill_parameters` in a set means the type is one of the eight Table 6 permits
523
+ * inside FILL_PARAMETERS (Section 10.2.15). TRACK_PROPERTY_FILTER (0x29) is
524
+ * deliberately not among them.
525
+ */
526
+ const PARAMETER_SCOPE = new Map<bigint, Set<string>>([
527
+ // 0x02 OBJECT_DELIVERY_TIMEOUT (10.2.4) — d19 also listed PUBLISH_OK; d20 added PUBLISH.
528
+ [PARAM_OBJECT_DELIVERY_TIMEOUT, new Set(['subscribe', 'publish', 'request_update'])],
529
+ // 0x03 AUTHORIZATION_TOKEN (10.2.2) — any request or response that needs authorization.
530
+ [
531
+ PARAM_AUTHORIZATION_TOKEN,
532
+ new Set([
533
+ 'publish',
534
+ 'subscribe',
535
+ 'request_update',
536
+ 'subscribe_namespace',
537
+ 'subscribe_tracks',
538
+ 'publish_namespace',
539
+ 'track_status',
540
+ 'fetch',
541
+ ]),
542
+ ],
543
+ // 0x04 RENDEZVOUS_TIMEOUT (10.2.6) — "MAY appear in a SUBSCRIBE message", nothing else.
544
+ [PARAM_RENDEZVOUS_TIMEOUT, new Set(['subscribe'])],
545
+ // 0x06 SUBGROUP_DELIVERY_TIMEOUT (10.2.3) — d19 also listed PUBLISH_OK; d20 added PUBLISH.
546
+ [PARAM_SUBGROUP_DELIVERY_TIMEOUT, new Set(['subscribe', 'publish', 'request_update'])],
547
+ // 0x08 EXPIRES (10.2.16) — the one parameter that still names PUBLISH_OK.
548
+ [PARAM_EXPIRES, new Set(['subscribe_ok', 'publish', 'request_ok'])],
549
+ // 0x09 LARGEST_OBJECT (10.2.17) — gained PUBLISH_STATE_NOTIFY in draft-20.
550
+ [
551
+ PARAM_LARGEST_OBJECT,
552
+ new Set(['subscribe_ok', 'publish', 'request_ok', 'publish_state_notify']),
553
+ ],
554
+ // 0x0A FILL_TIMEOUT (10.2.5) — FETCH, or nested inside FILL_PARAMETERS.
555
+ [PARAM_FILL_TIMEOUT, new Set(['fetch', FILL_SCOPE])],
556
+ // 0x10 FORWARD (10.2.18) — lost PUBLISH_OK, gained PUBLISH_STATE_NOTIFY.
557
+ [
558
+ PARAM_FORWARD,
559
+ new Set(['subscribe', 'request_update', 'publish', 'subscribe_tracks', 'publish_state_notify']),
560
+ ],
561
+ // 0x20 SUBSCRIBER_PRIORITY (10.2.7) — lost PUBLISH_OK, gained PUBLISH.
562
+ [
563
+ PARAM_SUBSCRIBER_PRIORITY,
564
+ new Set(['subscribe', 'publish', 'fetch', 'request_update', FILL_SCOPE]),
565
+ ],
566
+ // 0x21 LOCATION_FILTER (10.2.9) — lost PUBLISH_OK, gained PUBLISH and PUBLISH_STATE_NOTIFY.
567
+ [
568
+ PARAM_LOCATION_FILTER,
569
+ new Set([
570
+ 'fetch',
571
+ 'subscribe',
572
+ 'publish',
573
+ 'request_update',
574
+ 'publish_state_notify',
575
+ FILL_SCOPE,
576
+ ]),
577
+ ],
578
+ // 0x22 GROUP_ORDER (10.2.8) — gained PUBLISH and FILL_PARAMETERS nesting.
579
+ [PARAM_GROUP_ORDER, new Set(['subscribe', 'publish', 'subscribe_tracks', 'fetch', FILL_SCOPE])],
580
+ // 0x23 FILL_PARAMETERS (10.2.15) — NEW. Subscriptions only, and never nested
581
+ // in itself: a FETCH already is a fetch, so it has nothing to fill.
582
+ [PARAM_FILL_PARAMETERS, new Set(['subscribe', 'request_update'])],
583
+ // 0x25-0x28 range filters (5.1.4) — lost PUBLISH_OK, gained FILL_PARAMETERS nesting.
584
+ [
585
+ PARAM_SUBGROUP_FILTER,
586
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
587
+ ],
588
+ [
589
+ PARAM_OBJECTID_FILTER,
590
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
591
+ ],
592
+ [
593
+ PARAM_PRIORITY_FILTER,
594
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
595
+ ],
596
+ [
597
+ PARAM_OBJECT_PROPERTY_FILTER,
598
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
599
+ ],
600
+ // 0x29 TRACK_PROPERTY_FILTER (10.2.14) — lost PUBLISH_OK; NOT permitted
601
+ // inside FILL_PARAMETERS. A relay forwarding a downstream FILL_PARAMETERS
602
+ // upstream has to strip it rather than trip this rule.
603
+ [PARAM_TRACK_PROPERTY_FILTER, new Set(['subscribe_tracks', 'request_update'])],
604
+ // 0x32 NEW_GROUP_REQUEST (10.2.19) — lost PUBLISH_OK.
605
+ [PARAM_NEW_GROUP_REQUEST, new Set(['subscribe', 'request_update'])],
606
+ // 0x34 TRACK_NAMESPACE_PREFIX (10.2.20) — REQUEST_UPDATE for a namespace subscription.
607
+ [PARAM_TRACK_NAMESPACE_PREFIX, new Set(['request_update'])],
608
+ // 0x35 INCLUDE_PROPERTIES (10.2.21) — NEW in draft-20.
609
+ [PARAM_INCLUDE_PROPERTIES, new Set(['subscribe', 'track_status', 'fetch', 'subscribe_tracks'])],
610
+ ])
611
+
612
+ function encodeParams(params: Draft20Params, writer: BufferWriter): void {
613
+ // Collect and sort params by type
614
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
615
+
616
+ if (params.object_delivery_timeout !== undefined) {
617
+ entries.push({
618
+ type: PARAM_OBJECT_DELIVERY_TIMEOUT,
619
+ encode: (w) => w.writeVarInt(params.object_delivery_timeout!),
620
+ })
621
+ }
622
+ if (params.authorization_token !== undefined) {
623
+ entries.push({
624
+ type: PARAM_AUTHORIZATION_TOKEN,
625
+ encode: (w) => {
626
+ const tmpW = new BufferWriter(64)
627
+ encodeAuthorizationToken(params.authorization_token!, tmpW)
628
+ const raw = tmpW.finish()
629
+ w.writeVarInt(BigInt(raw.byteLength))
630
+ w.writeBytes(raw)
631
+ },
632
+ })
633
+ }
634
+ if (params.rendezvous_timeout !== undefined) {
635
+ entries.push({
636
+ type: PARAM_RENDEZVOUS_TIMEOUT,
637
+ encode: (w) => w.writeVarInt(params.rendezvous_timeout!),
638
+ })
639
+ }
640
+ if (params.subgroup_delivery_timeout !== undefined) {
641
+ entries.push({
642
+ type: PARAM_SUBGROUP_DELIVERY_TIMEOUT,
643
+ encode: (w) => w.writeVarInt(params.subgroup_delivery_timeout!),
644
+ })
645
+ }
646
+ if (params.expires !== undefined) {
647
+ entries.push({
648
+ type: PARAM_EXPIRES,
649
+ encode: (w) => w.writeVarInt(params.expires!),
650
+ })
651
+ }
652
+ if (params.largest_object !== undefined) {
653
+ entries.push({
654
+ type: PARAM_LARGEST_OBJECT,
655
+ // A Location is "Two consecutive varints (Group, Object)", which is a
656
+ // different value encoding from "Length-prefixed". Nothing states the
657
+ // length, so nothing writes one — even though 0x09 is odd, because the
658
+ // Key-Value-Pair odd/even rule does not apply to Message Parameters.
659
+ encode: (w) => {
660
+ w.writeVarInt(params.largest_object!.group)
661
+ w.writeVarInt(params.largest_object!.object)
662
+ },
663
+ })
664
+ }
665
+ if (params.fill_timeout !== undefined) {
666
+ entries.push({
667
+ type: PARAM_FILL_TIMEOUT,
668
+ encode: (w) => w.writeVarInt(params.fill_timeout!),
669
+ })
670
+ }
671
+ if (params.forward !== undefined) {
672
+ entries.push({
673
+ type: PARAM_FORWARD,
674
+ encode: (w) => w.writeUint8(Number(params.forward!)),
675
+ })
676
+ }
677
+ if (params.subscriber_priority !== undefined) {
678
+ entries.push({
679
+ type: PARAM_SUBSCRIBER_PRIORITY,
680
+ encode: (w) => w.writeUint8(Number(params.subscriber_priority!)),
681
+ })
682
+ }
683
+ if (params.location_filter !== undefined) {
684
+ entries.push({
685
+ type: PARAM_LOCATION_FILTER,
686
+ encode: (w) => encodeLocationFilter(params.location_filter!, w),
687
+ })
688
+ }
689
+ if (params.group_order !== undefined) {
690
+ entries.push({
691
+ type: PARAM_GROUP_ORDER,
692
+ encode: (w) => w.writeUint8(Number(params.group_order!)),
693
+ })
694
+ }
695
+ if (params.fill_parameters !== undefined) {
696
+ entries.push({
697
+ type: PARAM_FILL_PARAMETERS,
698
+ encode: (w) => {
699
+ // DECISION (DECISIONS.md D1, SPEC-DELTA Section 11 Q1): the value is a
700
+ // full parameter block and therefore BEGINS with Number of Parameters.
701
+ // Section 10.2.15 calls it "a sequence of Parameters ... encoded as if
702
+ // they were Parameters for a separate message"; Section 10.2 defines a
703
+ // parameter block as count-bounded rather than length-bounded, because
704
+ // unknown parameters cannot be skipped. The draft does not state the
705
+ // consequence, and the outer length prefix makes the count look
706
+ // redundant — but a block "encoded as if for a message" is a block
707
+ // with the count. An empty FILL_PARAMETERS is therefore Length 1
708
+ // carrying the single byte 0x00, NOT Length 0.
709
+ //
710
+ // DECISION (D2): encodeParams starts its own Type Delta chain from 0,
711
+ // and the caller's chain is untouched — the outer parameter after
712
+ // FILL_PARAMETERS deltas from 0x23, not from the last inner type.
713
+ const tmpW = new BufferWriter(64)
714
+ encodeParams(params.fill_parameters as Draft20Params, tmpW)
715
+ const raw = tmpW.finish()
716
+ w.writeVarInt(BigInt(raw.byteLength))
717
+ w.writeBytes(raw)
718
+ },
719
+ })
720
+ }
721
+ if (params.subgroup_filter !== undefined) {
722
+ entries.push({
723
+ type: PARAM_SUBGROUP_FILTER,
724
+ encode: (w) => encodeRangeFilter(params.subgroup_filter!, false, w),
725
+ })
726
+ }
727
+ if (params.objectid_filter !== undefined) {
728
+ entries.push({
729
+ type: PARAM_OBJECTID_FILTER,
730
+ encode: (w) => encodeRangeFilter(params.objectid_filter!, false, w),
731
+ })
732
+ }
733
+ if (params.priority_filter !== undefined) {
734
+ entries.push({
735
+ type: PARAM_PRIORITY_FILTER,
736
+ encode: (w) => encodeRangeFilter(params.priority_filter!, false, w),
737
+ })
738
+ }
739
+ if (params.object_property_filter !== undefined) {
740
+ entries.push({
741
+ type: PARAM_OBJECT_PROPERTY_FILTER,
742
+ encode: (w) => encodeRangeFilter(params.object_property_filter!, true, w),
743
+ })
744
+ }
745
+ if (params.track_property_filter !== undefined) {
746
+ entries.push({
747
+ type: PARAM_TRACK_PROPERTY_FILTER,
748
+ encode: (w) => encodeRangeFilter(params.track_property_filter!, true, w),
749
+ })
750
+ }
751
+ if (params.new_group_request !== undefined) {
752
+ entries.push({
753
+ type: PARAM_NEW_GROUP_REQUEST,
754
+ encode: (w) => w.writeVarInt(params.new_group_request!),
755
+ })
756
+ }
757
+ if (params.track_namespace_prefix !== undefined) {
758
+ entries.push({
759
+ type: PARAM_TRACK_NAMESPACE_PREFIX,
760
+ // The parameter "uses the Track Namespace encoding", which is a tuple
761
+ // that states its own field count. That is not the Length-prefixed
762
+ // encoding, so there is no byte length ahead of it.
763
+ encode: (w) => {
764
+ encodeNamespaceTuple(params.track_namespace_prefix!, w)
765
+ },
766
+ })
767
+ }
768
+ if (params.include_properties !== undefined) {
769
+ entries.push({
770
+ type: PARAM_INCLUDE_PROPERTIES,
771
+ // uint8: one raw byte, 0 or 1 (draft-20 Section 10.2.21).
772
+ encode: (w) => w.writeUint8(Number(params.include_properties!)),
773
+ })
774
+ }
775
+
776
+ // Unknown params
777
+ if (params.unknown) {
778
+ for (const u of params.unknown) {
779
+ const id = BigInt(u.id)
780
+ entries.push({
781
+ type: id,
782
+ encode: (w) => {
783
+ const raw = hexToBytes(u.raw_hex)
784
+ // For unknown params, we store raw bytes and re-emit them
785
+ w.writeBytes(raw)
786
+ },
787
+ })
788
+ }
789
+ }
790
+
791
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
792
+
793
+ writer.writeVarInt(BigInt(entries.length))
794
+ let prevType = 0n
795
+ for (const entry of entries) {
796
+ writer.writeVarInt(entry.type - prevType)
797
+ entry.encode(writer)
798
+ prevType = entry.type
799
+ }
800
+ }
801
+
802
+ function decodeParams(reader: BufferReader, messageType: string): Draft20Params {
803
+ const count = Number(reader.readVarInt())
804
+ const result: Draft20Params = {}
805
+ let prevType = 0n
806
+
807
+ for (let i = 0; i < count; i++) {
808
+ const delta = reader.readVarInt()
809
+ const paramType = prevType + delta
810
+ prevType = paramType
811
+
812
+ const scope = PARAMETER_SCOPE.get(paramType)
813
+ if (scope !== undefined && !scope.has(messageType)) {
814
+ throw new DecodeError(
815
+ 'CONSTRAINT_VIOLATION',
816
+ `Message Parameter 0x${paramType.toString(16)} may not appear in ${messageType}`,
817
+ reader.offset,
818
+ )
819
+ }
820
+
821
+ if (paramType === PARAM_OBJECT_DELIVERY_TIMEOUT) {
822
+ result.object_delivery_timeout = reader.readVarInt()
823
+ } else if (paramType === PARAM_AUTHORIZATION_TOKEN) {
824
+ const length = Number(reader.readVarInt())
825
+ const bytes = reader.readBytes(length)
826
+ result.authorization_token = decodeAuthorizationToken(new BufferReader(bytes))
827
+ } else if (paramType === PARAM_RENDEZVOUS_TIMEOUT) {
828
+ result.rendezvous_timeout = reader.readVarInt()
829
+ } else if (paramType === PARAM_SUBGROUP_DELIVERY_TIMEOUT) {
830
+ result.subgroup_delivery_timeout = reader.readVarInt()
831
+ } else if (paramType === PARAM_EXPIRES) {
832
+ result.expires = reader.readVarInt()
833
+ } else if (paramType === PARAM_LARGEST_OBJECT) {
834
+ // Location: 2 bare varints (not length-prefixed)
835
+ const group = reader.readVarInt()
836
+ const object = reader.readVarInt()
837
+ result.largest_object = { group, object }
838
+ } else if (paramType === PARAM_FILL_TIMEOUT) {
839
+ result.fill_timeout = reader.readVarInt()
840
+ } else if (paramType === PARAM_FORWARD) {
841
+ // uint8: single raw byte
842
+ result.forward = BigInt(reader.readUint8())
843
+ } else if (paramType === PARAM_SUBSCRIBER_PRIORITY) {
844
+ // uint8: single raw byte
845
+ result.subscriber_priority = BigInt(reader.readUint8())
846
+ } else if (paramType === PARAM_LOCATION_FILTER) {
847
+ result.location_filter = decodeLocationFilter(reader)
848
+ } else if (paramType === PARAM_GROUP_ORDER) {
849
+ // uint8: single raw byte
850
+ result.group_order = BigInt(reader.readUint8())
851
+ } else if (paramType === PARAM_FILL_PARAMETERS) {
852
+ result.fill_parameters = decodeFillParameters(reader)
853
+ } else if (paramType === PARAM_SUBGROUP_FILTER) {
854
+ result.subgroup_filter = decodeRangeFilter(reader, false)
855
+ } else if (paramType === PARAM_OBJECTID_FILTER) {
856
+ result.objectid_filter = decodeRangeFilter(reader, false)
857
+ } else if (paramType === PARAM_PRIORITY_FILTER) {
858
+ result.priority_filter = decodeRangeFilter(reader, false)
859
+ } else if (paramType === PARAM_OBJECT_PROPERTY_FILTER) {
860
+ result.object_property_filter = decodeRangeFilter(reader, true)
861
+ } else if (paramType === PARAM_TRACK_PROPERTY_FILTER) {
862
+ result.track_property_filter = decodeRangeFilter(reader, true)
863
+ } else if (paramType === PARAM_NEW_GROUP_REQUEST) {
864
+ result.new_group_request = reader.readVarInt()
865
+ } else if (paramType === PARAM_TRACK_NAMESPACE_PREFIX) {
866
+ // Track Namespace encoding, read in place: the tuple's own field count
867
+ // bounds it.
868
+ result.track_namespace_prefix = decodeNamespaceTuple(reader)
869
+ } else if (paramType === PARAM_INCLUDE_PROPERTIES) {
870
+ // Section 10.2.21: "The allowed values are 0 (do not send Properties) or
871
+ // 1 (send Properties), and the default is 1. If an endpoint receives a
872
+ // value outside this range, it MUST close the session with
873
+ // PROTOCOL_VIOLATION."
874
+ const value = BigInt(reader.readUint8())
875
+ if (value !== 0n && value !== 1n) {
876
+ throw new DecodeError(
877
+ 'CONSTRAINT_VIOLATION',
878
+ `INCLUDE_PROPERTIES accepts only 0 or 1, got ${value}`,
879
+ reader.offset,
880
+ )
881
+ }
882
+ result.include_properties = value
883
+ } else {
884
+ // Section 10.2: "All Message Parameters MUST be defined in the negotiated
885
+ // version of MOQT or negotiated via Setup Options. An endpoint that
886
+ // receives an unknown Message Parameter MUST close the session with
887
+ // PROTOCOL_VIOLATION."
888
+ //
889
+ // There is no skipping an unknown one and carrying on: the value's
890
+ // encoding comes from its definition, so a receiver that does not know
891
+ // the Type does not know how many bytes it spans either.
892
+ throw new DecodeError(
893
+ 'INVALID_PARAMETER',
894
+ `Unknown Message Parameter type 0x${paramType.toString(16)}`,
895
+ reader.offset,
896
+ )
897
+ }
898
+ }
899
+
900
+ return result
901
+ }
902
+
903
+ /**
904
+ * Decode a FILL_PARAMETERS (0x23) value — draft-20 Section 10.2.15.
905
+ *
906
+ * See the encoder for D1 (the value begins with Number of Parameters) and D2
907
+ * (the Type Delta chain restarts here and the enclosing chain is unaffected).
908
+ * The eight permitted inner types are expressed as the {@link FILL_SCOPE} entry
909
+ * in {@link PARAMETER_SCOPE}, so "an endpoint that receives a parameter inside
910
+ * FILL_PARAMETERS that is not listed above MUST close the session with
911
+ * PROTOCOL_VIOLATION" falls out of the ordinary scope check.
912
+ */
913
+ function decodeFillParameters(reader: BufferReader): Draft20FillParameters {
914
+ const length = Number(reader.readVarInt())
915
+ const valueBytes = reader.readBytes(length)
916
+ const nested = new BufferReader(valueBytes)
917
+ const params = decodeParams(nested, FILL_SCOPE)
918
+ if (nested.remaining > 0) {
919
+ // The parameter count and the byte length disagree. The draft does not say
920
+ // what to do; leaving the slack unread would let a sender smuggle bytes no
921
+ // receiver interprets, so refuse it.
922
+ throw new DecodeError(
923
+ 'CONSTRAINT_VIOLATION',
924
+ `FILL_PARAMETERS has ${nested.remaining} trailing byte(s) after its parameter block`,
925
+ reader.offset,
926
+ )
927
+ }
928
+ return params as Draft20FillParameters
929
+ }
930
+
931
+ // ─── Track Properties Encoding/Decoding (KVP, no count prefix) ──────────────
932
+
933
+ const TPROP_OBJECT_DELIVERY_TIMEOUT = 0x02n
934
+ const TPROP_MAX_CACHE_DURATION = 0x04n
935
+ const TPROP_SUBGROUP_DELIVERY_TIMEOUT = 0x06n
936
+ const TPROP_IMMUTABLE_PROPERTIES = 0x0bn
937
+ const TPROP_DEFAULT_PUBLISHER_PRIORITY = 0x0en
938
+ const TPROP_DEFAULT_PUBLISHER_GROUP_ORDER = 0x22n
939
+ const TPROP_DYNAMIC_GROUPS = 0x30n
940
+
941
+ function encodeTrackProperties(props: Draft20TrackProperties, writer: BufferWriter): void {
942
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
943
+
944
+ if (props.object_delivery_timeout !== undefined) {
945
+ entries.push({
946
+ type: TPROP_OBJECT_DELIVERY_TIMEOUT,
947
+ encode: (w) => w.writeVarInt(props.object_delivery_timeout!),
948
+ })
949
+ }
950
+ if (props.max_cache_duration !== undefined) {
951
+ entries.push({
952
+ type: TPROP_MAX_CACHE_DURATION,
953
+ encode: (w) => w.writeVarInt(props.max_cache_duration!),
954
+ })
955
+ }
956
+ if (props.subgroup_delivery_timeout !== undefined) {
957
+ entries.push({
958
+ type: TPROP_SUBGROUP_DELIVERY_TIMEOUT,
959
+ encode: (w) => w.writeVarInt(props.subgroup_delivery_timeout!),
960
+ })
961
+ }
962
+ if (props.immutable_properties !== undefined) {
963
+ entries.push({
964
+ type: TPROP_IMMUTABLE_PROPERTIES,
965
+ encode: (w) => {
966
+ const raw = props.immutable_properties!
967
+ w.writeVarInt(BigInt(raw.byteLength))
968
+ w.writeBytes(raw)
969
+ },
970
+ })
971
+ }
972
+ if (props.default_publisher_priority !== undefined) {
973
+ entries.push({
974
+ type: TPROP_DEFAULT_PUBLISHER_PRIORITY,
975
+ encode: (w) => w.writeVarInt(props.default_publisher_priority!),
976
+ })
977
+ }
978
+ if (props.default_publisher_group_order !== undefined) {
979
+ entries.push({
980
+ type: TPROP_DEFAULT_PUBLISHER_GROUP_ORDER,
981
+ encode: (w) => w.writeVarInt(props.default_publisher_group_order!),
982
+ })
983
+ }
984
+ if (props.dynamic_groups !== undefined) {
985
+ entries.push({
986
+ type: TPROP_DYNAMIC_GROUPS,
987
+ encode: (w) => w.writeVarInt(props.dynamic_groups!),
988
+ })
989
+ }
990
+
991
+ if (props.unknown) {
992
+ for (const u of props.unknown) {
993
+ const id = BigInt(u.id)
994
+ entries.push({
995
+ type: id,
996
+ encode: (w) => {
997
+ if (id % 2n === 0n) {
998
+ const raw = hexToBytes(u.raw_hex)
999
+ const tmpReader = new BufferReader(raw)
1000
+ w.writeVarInt(tmpReader.readVarInt())
1001
+ } else {
1002
+ const raw = hexToBytes(u.raw_hex)
1003
+ w.writeVarInt(BigInt(raw.byteLength))
1004
+ w.writeBytes(raw)
1005
+ }
1006
+ },
1007
+ })
1008
+ }
1009
+ }
1010
+
1011
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
1012
+
1013
+ let prevType = 0n
1014
+ for (const entry of entries) {
1015
+ writer.writeVarInt(entry.type - prevType)
1016
+ entry.encode(writer)
1017
+ prevType = entry.type
1018
+ }
1019
+ }
1020
+
1021
+ function decodeTrackProperties(reader: BufferReader, payloadEnd: number): Draft20TrackProperties {
1022
+ const result: Draft20TrackProperties = {}
1023
+ const unknown: UnknownParam[] = []
1024
+ let prevType = 0n
1025
+
1026
+ while (reader.offset < payloadEnd) {
1027
+ const delta = reader.readVarInt()
1028
+ const propType = prevType + delta
1029
+ prevType = propType
1030
+
1031
+ if (propType % 2n === 0n) {
1032
+ // Even: single varint value
1033
+ const value = reader.readVarInt()
1034
+ if (propType === TPROP_OBJECT_DELIVERY_TIMEOUT) {
1035
+ result.object_delivery_timeout = value
1036
+ } else if (propType === TPROP_MAX_CACHE_DURATION) {
1037
+ result.max_cache_duration = value
1038
+ } else if (propType === TPROP_SUBGROUP_DELIVERY_TIMEOUT) {
1039
+ result.subgroup_delivery_timeout = value
1040
+ } else if (propType === TPROP_DEFAULT_PUBLISHER_PRIORITY) {
1041
+ result.default_publisher_priority = value
1042
+ } else if (propType === TPROP_DEFAULT_PUBLISHER_GROUP_ORDER) {
1043
+ result.default_publisher_group_order = value
1044
+ } else if (propType === TPROP_DYNAMIC_GROUPS) {
1045
+ result.dynamic_groups = value
1046
+ } else {
1047
+ // Section 15.8: endpoints MUST ignore unknown Property types, skipping
1048
+ // them by the Key-Value-Pair odd/even rule. Kept for passthrough.
1049
+ const tmpWriter = new BufferWriter(16)
1050
+ tmpWriter.writeVarInt(value)
1051
+ const raw = tmpWriter.finish()
1052
+ unknown.push({
1053
+ id: `0x${propType.toString(16)}`,
1054
+ length: raw.byteLength,
1055
+ raw_hex: bytesToHex(raw),
1056
+ })
1057
+ }
1058
+ } else {
1059
+ // Odd: length-prefixed bytes
1060
+ const length = Number(reader.readVarInt())
1061
+ const bytes = reader.readBytes(length)
1062
+ if (propType === TPROP_IMMUTABLE_PROPERTIES) {
1063
+ result.immutable_properties = bytes
1064
+ } else {
1065
+ unknown.push({
1066
+ id: `0x${propType.toString(16)}`,
1067
+ length,
1068
+ raw_hex: bytesToHex(bytes),
1069
+ })
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ if (unknown.length > 0) result.unknown = unknown
1075
+ return result
1076
+ }
1077
+
1078
+ // ─── Redirect Structure Encoding/Decoding ───────────────────────────────────
1079
+
1080
+ function encodeRedirect(redirect: Redirect, w: BufferWriter): void {
1081
+ const uri = textEncoder.encode(redirect.connect_uri)
1082
+ w.writeVarInt(BigInt(uri.byteLength))
1083
+ w.writeBytes(uri)
1084
+ w.writeTuple(redirect.track_namespace)
1085
+ w.writeString(redirect.track_name)
1086
+ }
1087
+
1088
+ function decodeRedirect(r: BufferReader): Redirect {
1089
+ const uriLen = Number(r.readVarInt())
1090
+ const uriBytes = r.readBytes(uriLen)
1091
+ const connect_uri = textDecoder.decode(uriBytes)
1092
+ // draft-20 Section 10.6.1 names this pair "the Redirect target". draft-19's
1093
+ // rule that a zero-length namespace and name meant "reuse the values from the
1094
+ // original request" was deleted, so an empty pair is a literal empty pair.
1095
+ const track_namespace = r.readTuple()
1096
+ const track_name = r.readString()
1097
+ return { connect_uri, track_namespace, track_name }
1098
+ }
1099
+
1100
+ // ─── Payload Encoders ──────────────────────────────────────────────────────────
1101
+
1102
+ function encodeSetupPayload(msg: Draft20Message & { type: 'setup' }, w: BufferWriter): void {
1103
+ encodeSetupOptions(msg.options, w)
1104
+ }
1105
+
1106
+ function encodeSubscribePayload(
1107
+ msg: Draft20Message & { type: 'subscribe' },
1108
+ w: BufferWriter,
1109
+ ): void {
1110
+ w.writeVarInt(msg.request_id)
1111
+ w.writeTuple(msg.track_namespace)
1112
+ w.writeString(msg.track_name)
1113
+ encodeParams(msg.parameters, w)
1114
+ }
1115
+
1116
+ function encodeSubscribeOkPayload(
1117
+ msg: Draft20Message & { type: 'subscribe_ok' },
1118
+ w: BufferWriter,
1119
+ ): void {
1120
+ w.writeVarInt(msg.track_alias)
1121
+ encodeParams(msg.parameters, w)
1122
+ encodeTrackProperties(msg.track_properties, w)
1123
+ }
1124
+
1125
+ function encodeRequestUpdatePayload(
1126
+ msg: Draft20Message & { type: 'request_update' },
1127
+ w: BufferWriter,
1128
+ ): void {
1129
+ w.writeVarInt(msg.request_id)
1130
+ encodeParams(msg.parameters, w)
1131
+ }
1132
+
1133
+ function encodePublishStateNotifyPayload(
1134
+ msg: Draft20Message & { type: 'publish_state_notify' },
1135
+ w: BufferWriter,
1136
+ ): void {
1137
+ // No Request ID: the subscription's bidirectional stream identifies it.
1138
+ encodeParams(msg.parameters, w)
1139
+ }
1140
+
1141
+ function encodePublishPayload(msg: Draft20Message & { type: 'publish' }, w: BufferWriter): void {
1142
+ w.writeVarInt(msg.request_id)
1143
+ w.writeTuple(msg.track_namespace)
1144
+ w.writeString(msg.track_name)
1145
+ w.writeVarInt(msg.track_alias)
1146
+ encodeParams(msg.parameters, w)
1147
+ encodeTrackProperties(msg.track_properties, w)
1148
+ }
1149
+
1150
+ function encodePublishDonePayload(
1151
+ msg: Draft20Message & { type: 'publish_done' },
1152
+ w: BufferWriter,
1153
+ ): void {
1154
+ w.writeVarInt(msg.status_code)
1155
+ // bigint all the way: UNKNOWN_STREAM_COUNT is 2^64 - 1 and MOQT's vi64
1156
+ // reaches it in nine bytes.
1157
+ w.writeVarInt(msg.stream_count)
1158
+ w.writeString(msg.reason_phrase)
1159
+ }
1160
+
1161
+ function encodePublishNamespacePayload(
1162
+ msg: Draft20Message & { type: 'publish_namespace' },
1163
+ w: BufferWriter,
1164
+ ): void {
1165
+ w.writeVarInt(msg.request_id)
1166
+ w.writeTuple(msg.track_namespace)
1167
+ encodeParams(msg.parameters, w)
1168
+ }
1169
+
1170
+ function encodeNamespacePayload(
1171
+ msg: Draft20Message & { type: 'namespace' },
1172
+ w: BufferWriter,
1173
+ ): void {
1174
+ w.writeTuple(msg.namespace_suffix)
1175
+ }
1176
+
1177
+ function encodeNamespaceDonePayload(
1178
+ msg: Draft20Message & { type: 'namespace_done' },
1179
+ w: BufferWriter,
1180
+ ): void {
1181
+ w.writeTuple(msg.namespace_suffix)
1182
+ }
1183
+
1184
+ function encodeSubscribeNamespacePayload(
1185
+ msg: Draft20Message & { type: 'subscribe_namespace' },
1186
+ w: BufferWriter,
1187
+ ): void {
1188
+ w.writeVarInt(msg.request_id)
1189
+ w.writeTuple(msg.namespace_prefix)
1190
+ encodeParams(msg.parameters, w)
1191
+ }
1192
+
1193
+ function encodeSubscribeTracksPayload(
1194
+ msg: Draft20Message & { type: 'subscribe_tracks' },
1195
+ w: BufferWriter,
1196
+ ): void {
1197
+ w.writeVarInt(msg.request_id)
1198
+ w.writeTuple(msg.namespace_prefix)
1199
+ encodeParams(msg.parameters, w)
1200
+ }
1201
+
1202
+ function encodePublishSkippedPayload(
1203
+ msg: Draft20Message & { type: 'publish_skipped' },
1204
+ w: BufferWriter,
1205
+ ): void {
1206
+ w.writeTuple(msg.namespace_suffix)
1207
+ w.writeString(msg.track_name)
1208
+ }
1209
+
1210
+ /**
1211
+ * FETCH (0x16) — draft-20 Section 10.13, Figure 16.
1212
+ *
1213
+ * Track Namespace and Track Name are inline, in exactly the positions they
1214
+ * occupied inside draft-19's Standalone Fetch struct. There is no Fetch Type
1215
+ * ahead of them and no Start/End Location behind them: the range is a
1216
+ * LOCATION_FILTER parameter now, and it is inclusive (see
1217
+ * {@link encodeLocationFilter}).
1218
+ */
1219
+ function encodeFetchPayload(msg: Draft20Message & { type: 'fetch' }, w: BufferWriter): void {
1220
+ w.writeVarInt(msg.request_id)
1221
+ w.writeTuple(msg.track_namespace)
1222
+ w.writeString(msg.track_name)
1223
+ encodeParams(msg.parameters, w)
1224
+ }
1225
+
1226
+ function encodeFetchOkPayload(msg: Draft20Message & { type: 'fetch_ok' }, w: BufferWriter): void {
1227
+ w.writeUint8(msg.end_of_track)
1228
+ // End Location, written as given. draft-19 wrote "the last Object, plus 1"
1229
+ // here and used an Object of 0 to mean the whole group; draft-20 Section
1230
+ // 10.14 dropped both, so this is the last Object the response covers and no
1231
+ // arithmetic is applied on the way out (DECISIONS.md D4).
1232
+ w.writeVarInt(msg.end_group)
1233
+ w.writeVarInt(msg.end_object)
1234
+ encodeParams(msg.parameters, w)
1235
+ encodeTrackProperties(msg.track_properties, w)
1236
+ }
1237
+
1238
+ function encodeTrackStatusPayload(
1239
+ msg: Draft20Message & { type: 'track_status' },
1240
+ w: BufferWriter,
1241
+ ): void {
1242
+ w.writeVarInt(msg.request_id)
1243
+ w.writeTuple(msg.track_namespace)
1244
+ w.writeString(msg.track_name)
1245
+ encodeParams(msg.parameters, w)
1246
+ }
1247
+
1248
+ function encodeRequestOkPayload(
1249
+ msg: Draft20Message & { type: 'request_ok' },
1250
+ w: BufferWriter,
1251
+ ): void {
1252
+ encodeParams(msg.parameters, w)
1253
+ encodeTrackProperties(msg.track_properties, w)
1254
+ }
1255
+
1256
+ function encodeRequestErrorPayload(
1257
+ msg: Draft20Message & { type: 'request_error' },
1258
+ w: BufferWriter,
1259
+ ): void {
1260
+ w.writeVarInt(msg.error_code)
1261
+ w.writeVarInt(msg.retry_interval)
1262
+ w.writeString(msg.reason_phrase)
1263
+ if (msg.error_code === REQUEST_ERROR_REDIRECT && msg.redirect) {
1264
+ encodeRedirect(msg.redirect, w)
1265
+ }
1266
+ }
1267
+
1268
+ function encodeGoAwayPayload(msg: Draft20Message & { type: 'goaway' }, w: BufferWriter): void {
1269
+ w.writeString(msg.new_session_uri)
1270
+ w.writeVarInt(msg.timeout)
1271
+ }
1272
+
1273
+ // ─── Payload Decoders ──────────────────────────────────────────────────────────
1274
+
1275
+ function decodeSetupPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1276
+ const options = decodeSetupOptions(r, payloadEnd)
1277
+ return { type: 'setup', options }
1278
+ }
1279
+
1280
+ function decodeSubscribePayload(r: BufferReader): Draft20Message {
1281
+ const request_id = r.readVarInt()
1282
+ const track_namespace = r.readTuple()
1283
+ const track_name = r.readString()
1284
+ const parameters = decodeParams(r, 'subscribe')
1285
+ return {
1286
+ type: 'subscribe',
1287
+ request_id,
1288
+ track_namespace,
1289
+ track_name,
1290
+ parameters,
1291
+ }
1292
+ }
1293
+
1294
+ function decodeSubscribeOkPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1295
+ const track_alias = r.readVarInt()
1296
+ const parameters = decodeParams(r, 'subscribe_ok')
1297
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1298
+ return { type: 'subscribe_ok', track_alias, parameters, track_properties }
1299
+ }
1300
+
1301
+ function decodeRequestUpdatePayload(r: BufferReader): Draft20Message {
1302
+ const request_id = r.readVarInt()
1303
+ const parameters = decodeParams(r, 'request_update')
1304
+ return {
1305
+ type: 'request_update',
1306
+ request_id,
1307
+ parameters,
1308
+ }
1309
+ }
1310
+
1311
+ function decodePublishStateNotifyPayload(r: BufferReader): Draft20Message {
1312
+ const parameters = decodeParams(r, 'publish_state_notify')
1313
+ return { type: 'publish_state_notify', parameters }
1314
+ }
1315
+
1316
+ function decodePublishPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1317
+ const request_id = r.readVarInt()
1318
+ const track_namespace = r.readTuple()
1319
+ const track_name = r.readString()
1320
+ const track_alias = r.readVarInt()
1321
+ const parameters = decodeParams(r, 'publish')
1322
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1323
+ return {
1324
+ type: 'publish',
1325
+ request_id,
1326
+ track_namespace,
1327
+ track_name,
1328
+ track_alias,
1329
+ parameters,
1330
+ track_properties,
1331
+ }
1332
+ }
1333
+
1334
+ function decodePublishDonePayload(r: BufferReader): Draft20Message {
1335
+ // An unassigned status code decodes. Section 14: "Receipt of an unknown error
1336
+ // code in any error context (Session Termination, REQUEST_ERROR,
1337
+ // PUBLISH_DONE, or Data Stream Reset) MUST be treated as equivalent to
1338
+ // INTERNAL_ERROR for that context. An endpoint MUST NOT close the session
1339
+ // because it received an unknown error code in a REQUEST_ERROR or
1340
+ // PUBLISH_DONE." A retired code is an unknown code, so refusing the frame
1341
+ // here would break that MUST NOT — and a decoder that refuses cannot hand the
1342
+ // message to anything that could apply the INTERNAL_ERROR reading.
1343
+ // RETIRED_PUBLISH_DONE_CODES stays exported for callers that want to flag the
1344
+ // peer as speaking an older draft; it is advisory, not a decode gate.
1345
+ const status_code = r.readVarInt()
1346
+ const stream_count = r.readVarInt()
1347
+ const reason_phrase = r.readString()
1348
+ return { type: 'publish_done', status_code, stream_count, reason_phrase }
1349
+ }
1350
+
1351
+ function decodePublishNamespacePayload(r: BufferReader): Draft20Message {
1352
+ const request_id = r.readVarInt()
1353
+ const track_namespace = r.readTuple()
1354
+ const parameters = decodeParams(r, 'publish_namespace')
1355
+ return {
1356
+ type: 'publish_namespace',
1357
+ request_id,
1358
+ track_namespace,
1359
+ parameters,
1360
+ }
1361
+ }
1362
+
1363
+ function decodeNamespacePayload(r: BufferReader): Draft20Message {
1364
+ const namespace_suffix = r.readTuple()
1365
+ return { type: 'namespace', namespace_suffix }
1366
+ }
1367
+
1368
+ function decodeNamespaceDonePayload(r: BufferReader): Draft20Message {
1369
+ const namespace_suffix = r.readTuple()
1370
+ return { type: 'namespace_done', namespace_suffix }
1371
+ }
1372
+
1373
+ function decodeSubscribeNamespacePayload(r: BufferReader): Draft20Message {
1374
+ const request_id = r.readVarInt()
1375
+ const namespace_prefix = r.readTuple()
1376
+ const parameters = decodeParams(r, 'subscribe_namespace')
1377
+ return {
1378
+ type: 'subscribe_namespace',
1379
+ request_id,
1380
+ namespace_prefix,
1381
+ parameters,
1382
+ }
1383
+ }
1384
+
1385
+ function decodeSubscribeTracksPayload(r: BufferReader): Draft20Message {
1386
+ const request_id = r.readVarInt()
1387
+ const namespace_prefix = r.readTuple()
1388
+ const parameters = decodeParams(r, 'subscribe_tracks')
1389
+ return {
1390
+ type: 'subscribe_tracks',
1391
+ request_id,
1392
+ namespace_prefix,
1393
+ parameters,
1394
+ }
1395
+ }
1396
+
1397
+ function decodePublishSkippedPayload(r: BufferReader): Draft20Message {
1398
+ const namespace_suffix = r.readTuple()
1399
+ const track_name = r.readString()
1400
+ return { type: 'publish_skipped', namespace_suffix, track_name }
1401
+ }
1402
+
1403
+ function decodeFetchPayload(r: BufferReader): Draft20Message {
1404
+ // No Fetch Type varint here. The first field after Request ID is the Track
1405
+ // Namespace tuple's own field count — which is exactly the byte a draft-19
1406
+ // decoder would read as a Fetch Type. See the Draft20Fetch doc comment.
1407
+ const request_id = r.readVarInt()
1408
+ const track_namespace = r.readTuple()
1409
+ const track_name = r.readString()
1410
+ const parameters = decodeParams(r, 'fetch')
1411
+ return {
1412
+ type: 'fetch',
1413
+ request_id,
1414
+ track_namespace,
1415
+ track_name,
1416
+ parameters,
1417
+ }
1418
+ }
1419
+
1420
+ function decodeFetchOkPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1421
+ const end_of_track = r.readUint8()
1422
+ // Inclusive end. No decrement here, as no encoder incremented.
1423
+ const end_group = r.readVarInt()
1424
+ const end_object = r.readVarInt()
1425
+ const parameters = decodeParams(r, 'fetch_ok')
1426
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1427
+ return {
1428
+ type: 'fetch_ok',
1429
+ end_of_track,
1430
+ end_group,
1431
+ end_object,
1432
+ parameters,
1433
+ track_properties,
1434
+ }
1435
+ }
1436
+
1437
+ function decodeTrackStatusPayload(r: BufferReader): Draft20Message {
1438
+ const request_id = r.readVarInt()
1439
+ const track_namespace = r.readTuple()
1440
+ const track_name = r.readString()
1441
+ const parameters = decodeParams(r, 'track_status')
1442
+ return {
1443
+ type: 'track_status',
1444
+ request_id,
1445
+ track_namespace,
1446
+ track_name,
1447
+ parameters,
1448
+ }
1449
+ }
1450
+
1451
+ function decodeRequestOkPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1452
+ const parameters = decodeParams(r, 'request_ok')
1453
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1454
+ return { type: 'request_ok', parameters, track_properties }
1455
+ }
1456
+
1457
+ function decodeRequestErrorPayload(r: BufferReader, payloadEnd: number): Draft20Message {
1458
+ // Unassigned error codes decode — see decodePublishDonePayload for the
1459
+ // Section 14 rule this obeys. RETIRED_REQUEST_ERROR_CODES is advisory.
1460
+ const error_code = r.readVarInt()
1461
+ const retry_interval = r.readVarInt()
1462
+ const reason_phrase = r.readString()
1463
+ let redirect: Redirect | undefined
1464
+ if (error_code === REQUEST_ERROR_REDIRECT && r.offset < payloadEnd) {
1465
+ redirect = decodeRedirect(r)
1466
+ }
1467
+ if (redirect !== undefined) {
1468
+ return { type: 'request_error', error_code, retry_interval, reason_phrase, redirect }
1469
+ }
1470
+ return { type: 'request_error', error_code, retry_interval, reason_phrase }
1471
+ }
1472
+
1473
+ function decodeGoAwayPayload(r: BufferReader): Draft20Message {
1474
+ const new_session_uri = r.readString()
1475
+ const timeout = r.readVarInt()
1476
+ return { type: 'goaway', new_session_uri, timeout }
1477
+ }
1478
+
1479
+ // ─── Public API ────────────────────────────────────────────────────────────────
1480
+
1481
+ /**
1482
+ * Encode a draft-20 control message: type (vi64) + length (uint16 BE) + payload.
1483
+ */
1484
+ export function encodeMessage(message: Draft20Message): Uint8Array {
1485
+ const typeId = MESSAGE_ID_MAP.get(message.type)
1486
+ if (typeId === undefined) {
1487
+ throw new Error(`Unknown message type: ${message.type}`)
1488
+ }
1489
+
1490
+ const payloadWriter = new BufferWriter()
1491
+ encodePayload(message, payloadWriter)
1492
+ const payload = payloadWriter.finishView()
1493
+
1494
+ if (payload.byteLength > 0xffff) {
1495
+ throw new Error(`Payload too large for 16-bit length: ${payload.byteLength}`)
1496
+ }
1497
+
1498
+ const writer = new BufferWriter(payload.byteLength + 16)
1499
+ writer.writeVarInt(typeId)
1500
+ writer.writeUint8((payload.byteLength >> 8) & 0xff)
1501
+ writer.writeUint8(payload.byteLength & 0xff)
1502
+ writer.writeBytes(payload)
1503
+
1504
+ return writer.finish()
1505
+ }
1506
+
1507
+ function encodePayload(msg: Draft20Message, w: BufferWriter): void {
1508
+ switch (msg.type) {
1509
+ case 'setup':
1510
+ return encodeSetupPayload(msg, w)
1511
+ case 'subscribe':
1512
+ return encodeSubscribePayload(msg, w)
1513
+ case 'subscribe_ok':
1514
+ return encodeSubscribeOkPayload(msg, w)
1515
+ case 'request_update':
1516
+ return encodeRequestUpdatePayload(msg, w)
1517
+ case 'publish_state_notify':
1518
+ return encodePublishStateNotifyPayload(msg, w)
1519
+ case 'publish':
1520
+ return encodePublishPayload(msg, w)
1521
+ case 'publish_done':
1522
+ return encodePublishDonePayload(msg, w)
1523
+ case 'publish_namespace':
1524
+ return encodePublishNamespacePayload(msg, w)
1525
+ case 'namespace':
1526
+ return encodeNamespacePayload(msg, w)
1527
+ case 'namespace_done':
1528
+ return encodeNamespaceDonePayload(msg, w)
1529
+ case 'subscribe_namespace':
1530
+ return encodeSubscribeNamespacePayload(msg, w)
1531
+ case 'subscribe_tracks':
1532
+ return encodeSubscribeTracksPayload(msg, w)
1533
+ case 'publish_skipped':
1534
+ return encodePublishSkippedPayload(msg, w)
1535
+ case 'fetch':
1536
+ return encodeFetchPayload(msg, w)
1537
+ case 'fetch_ok':
1538
+ return encodeFetchOkPayload(msg, w)
1539
+ case 'track_status':
1540
+ return encodeTrackStatusPayload(msg, w)
1541
+ case 'request_ok':
1542
+ return encodeRequestOkPayload(msg, w)
1543
+ case 'request_error':
1544
+ return encodeRequestErrorPayload(msg, w)
1545
+ case 'goaway':
1546
+ return encodeGoAwayPayload(msg, w)
1547
+ default: {
1548
+ const _exhaustive: never = msg
1549
+ throw new Error(`Unhandled message type: ${(_exhaustive as Draft20Message).type}`)
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ /**
1555
+ * Decode a draft-20 control message from bytes (type + uint16 length + payload).
1556
+ */
1557
+ export function decodeMessage(bytes: Uint8Array): DecodeResult<Draft20Message> {
1558
+ try {
1559
+ const reader = new BufferReader(bytes)
1560
+ const typeId = reader.readVarInt()
1561
+
1562
+ const lenHi = reader.readUint8()
1563
+ const lenLo = reader.readUint8()
1564
+ const payloadLength = (lenHi << 8) | lenLo
1565
+
1566
+ const payloadBytes = reader.readBytes(payloadLength)
1567
+ const payloadReader = new BufferReader(payloadBytes)
1568
+
1569
+ let message: Draft20Message
1570
+
1571
+ if (typeId === MSG_SETUP) {
1572
+ message = decodeSetupPayload(payloadReader, payloadLength)
1573
+ } else if (typeId === MSG_SUBSCRIBE) {
1574
+ message = decodeSubscribePayload(payloadReader)
1575
+ } else if (typeId === MSG_SUBSCRIBE_OK) {
1576
+ message = decodeSubscribeOkPayload(payloadReader, payloadLength)
1577
+ } else if (typeId === MSG_REQUEST_UPDATE) {
1578
+ message = decodeRequestUpdatePayload(payloadReader)
1579
+ } else if (typeId === MSG_PUBLISH_STATE_NOTIFY) {
1580
+ message = decodePublishStateNotifyPayload(payloadReader)
1581
+ } else if (typeId === MSG_PUBLISH) {
1582
+ message = decodePublishPayload(payloadReader, payloadLength)
1583
+ } else if (typeId === MSG_PUBLISH_DONE) {
1584
+ message = decodePublishDonePayload(payloadReader)
1585
+ } else if (typeId === MSG_PUBLISH_NAMESPACE) {
1586
+ message = decodePublishNamespacePayload(payloadReader)
1587
+ } else if (typeId === MSG_NAMESPACE) {
1588
+ message = decodeNamespacePayload(payloadReader)
1589
+ } else if (typeId === MSG_NAMESPACE_DONE) {
1590
+ message = decodeNamespaceDonePayload(payloadReader)
1591
+ } else if (typeId === MSG_SUBSCRIBE_NAMESPACE) {
1592
+ message = decodeSubscribeNamespacePayload(payloadReader)
1593
+ } else if (typeId === MSG_SUBSCRIBE_TRACKS) {
1594
+ message = decodeSubscribeTracksPayload(payloadReader)
1595
+ } else if (typeId === MSG_PUBLISH_SKIPPED) {
1596
+ message = decodePublishSkippedPayload(payloadReader)
1597
+ } else if (typeId === MSG_FETCH) {
1598
+ message = decodeFetchPayload(payloadReader)
1599
+ } else if (typeId === MSG_FETCH_OK) {
1600
+ message = decodeFetchOkPayload(payloadReader, payloadLength)
1601
+ } else if (typeId === MSG_TRACK_STATUS) {
1602
+ message = decodeTrackStatusPayload(payloadReader)
1603
+ } else if (typeId === MSG_REQUEST_OK) {
1604
+ message = decodeRequestOkPayload(payloadReader, payloadLength)
1605
+ } else if (typeId === MSG_REQUEST_ERROR) {
1606
+ message = decodeRequestErrorPayload(payloadReader, payloadLength)
1607
+ } else if (typeId === MSG_GOAWAY) {
1608
+ message = decodeGoAwayPayload(payloadReader)
1609
+ } else {
1610
+ // Section 10: "An endpoint that receives an unknown message type MUST
1611
+ // close the session."
1612
+ return {
1613
+ ok: false,
1614
+ error: new DecodeError(
1615
+ 'UNKNOWN_MESSAGE_TYPE',
1616
+ `Unknown message type ID: 0x${typeId.toString(16)}`,
1617
+ 0,
1618
+ ),
1619
+ }
1620
+ }
1621
+
1622
+ return { ok: true, value: message, bytesRead: reader.offset }
1623
+ } catch (e) {
1624
+ if (e instanceof DecodeError) {
1625
+ return { ok: false, error: e }
1626
+ }
1627
+ throw e
1628
+ }
1629
+ }
1630
+
1631
+ // ─── Data Stream Encoding/Decoding (re-exported from data-streams.ts) ───────
1632
+
1633
+ import {
1634
+ createDataStreamDecoder,
1635
+ createFetchStreamDecoder,
1636
+ createSubgroupStreamDecoder,
1637
+ decodeDatagram,
1638
+ decodeDataStream,
1639
+ decodeFetchStream,
1640
+ decodeSubgroupStream,
1641
+ encodeDatagram,
1642
+ encodeFetchStream,
1643
+ encodeSubgroupStream,
1644
+ } from './data-streams.js'
1645
+
1646
+ export {
1647
+ createDataStreamDecoder,
1648
+ createFetchStreamDecoder,
1649
+ createSubgroupStreamDecoder,
1650
+ decodeDatagram,
1651
+ decodeDataStream,
1652
+ decodeFetchStream,
1653
+ decodeSubgroupStream,
1654
+ encodeDatagram,
1655
+ encodeFetchStream,
1656
+ encodeSubgroupStream,
1657
+ }
1658
+
1659
+ // ─── Stream Decoders ───────────────────────────────────────────────────────────
1660
+
1661
+ export function createStreamDecoder(): TransformStream<Uint8Array, Draft20Message> {
1662
+ let buffer = new Uint8Array(0)
1663
+ let offset = 0
1664
+
1665
+ return new TransformStream<Uint8Array, Draft20Message>({
1666
+ transform(chunk, controller) {
1667
+ if (offset > 0) {
1668
+ buffer = buffer.subarray(offset)
1669
+ offset = 0
1670
+ }
1671
+ const newBuffer = new Uint8Array(buffer.length + chunk.length)
1672
+ newBuffer.set(buffer, 0)
1673
+ newBuffer.set(chunk, buffer.length)
1674
+ buffer = newBuffer
1675
+
1676
+ while (offset < buffer.length) {
1677
+ const result = decodeMessage(buffer.subarray(offset))
1678
+ if (!result.ok) {
1679
+ if (result.error.code === 'UNEXPECTED_END') {
1680
+ break
1681
+ }
1682
+ controller.error(result.error)
1683
+ return
1684
+ }
1685
+ controller.enqueue(result.value)
1686
+ offset += result.bytesRead
1687
+ }
1688
+ },
1689
+
1690
+ flush(controller) {
1691
+ if (offset < buffer.length) {
1692
+ controller.error(
1693
+ new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete message data', 0),
1694
+ )
1695
+ }
1696
+ },
1697
+ })
1698
+ }
1699
+
1700
+ // ─── Codec Factory ─────────────────────────────────────────────────────────────
1701
+
1702
+ export interface Draft20Codec extends BaseCodec<Draft20Message> {
1703
+ readonly draft: '20'
1704
+ encodeSubgroupStream(stream: SubgroupStream): Uint8Array
1705
+ encodeDatagram(dg: DatagramObject): Uint8Array
1706
+ encodeFetchStream(stream: FetchStream): Uint8Array
1707
+ decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream>
1708
+ decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
1709
+ decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
1710
+ decodeDataStream(
1711
+ streamType: 'subgroup' | 'datagram' | 'fetch',
1712
+ bytes: Uint8Array,
1713
+ ): DecodeResult<Draft20DataStream>
1714
+ createStreamDecoder(): TransformStream<Uint8Array, Draft20Message>
1715
+ createSubgroupStreamDecoder(): TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>
1716
+ createFetchStreamDecoder(): TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>
1717
+ createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent>
1718
+ }
1719
+
1720
+ export function createDraft20Codec(): Draft20Codec {
1721
+ return {
1722
+ draft: '20',
1723
+ encodeMessage,
1724
+ decodeMessage,
1725
+ encodeSubgroupStream,
1726
+ encodeDatagram,
1727
+ encodeFetchStream,
1728
+ decodeSubgroupStream,
1729
+ decodeDatagram,
1730
+ decodeFetchStream,
1731
+ decodeDataStream,
1732
+ createStreamDecoder,
1733
+ createSubgroupStreamDecoder,
1734
+ createFetchStreamDecoder,
1735
+ createDataStreamDecoder,
1736
+ }
1737
+ }