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