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