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