@moqtap/trace 0.2.0 → 0.4.1
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/README.md +184 -1
- package/SPEC.md +1083 -122
- package/dist/index.cjs +915 -97
- package/dist/index.d.cts +794 -9
- package/dist/index.d.ts +794 -9
- package/dist/index.js +914 -96
- package/package.json +6 -4
- package/src/binary.ts +1178 -97
- package/src/cbor-scan.ts +185 -0
- package/src/index.ts +25 -3
- package/src/json.ts +7 -0
- package/src/reader.ts +286 -0
- package/src/recorder.ts +199 -12
- package/src/types.ts +460 -3
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,155 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/binary.ts
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/binary.ts
|
|
2
2
|
var _cborx = require('cbor-x');
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// src/cbor-scan.ts
|
|
5
|
+
var INCOMPLETE = null;
|
|
6
|
+
var MalformedCborError = class extends Error {
|
|
7
|
+
|
|
8
|
+
constructor(message, offset) {
|
|
9
|
+
super(`${message} at byte ${offset}`);
|
|
10
|
+
this.name = "MalformedCborError";
|
|
11
|
+
this.offset = offset;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var MAJOR_UNSIGNED = 0;
|
|
15
|
+
var MAJOR_NEGATIVE = 1;
|
|
16
|
+
var MAJOR_BYTES = 2;
|
|
17
|
+
var MAJOR_TEXT = 3;
|
|
18
|
+
var MAJOR_ARRAY = 4;
|
|
19
|
+
var MAJOR_MAP = 5;
|
|
20
|
+
var MAJOR_TAG = 6;
|
|
21
|
+
var MAJOR_SIMPLE = 7;
|
|
22
|
+
var AI_ONE_BYTE = 24;
|
|
23
|
+
var AI_TWO_BYTES = 25;
|
|
24
|
+
var AI_FOUR_BYTES = 26;
|
|
25
|
+
var AI_EIGHT_BYTES = 27;
|
|
26
|
+
var AI_INDEFINITE = 31;
|
|
27
|
+
var BREAK = 255;
|
|
28
|
+
function headerExtraBytes(additionalInfo, offset) {
|
|
29
|
+
if (additionalInfo < AI_ONE_BYTE) return 0;
|
|
30
|
+
switch (additionalInfo) {
|
|
31
|
+
case AI_ONE_BYTE:
|
|
32
|
+
return 1;
|
|
33
|
+
case AI_TWO_BYTES:
|
|
34
|
+
return 2;
|
|
35
|
+
case AI_FOUR_BYTES:
|
|
36
|
+
return 4;
|
|
37
|
+
case AI_EIGHT_BYTES:
|
|
38
|
+
return 8;
|
|
39
|
+
case AI_INDEFINITE:
|
|
40
|
+
return 0;
|
|
41
|
+
default:
|
|
42
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function readArgument(bytes, offset, additionalInfo) {
|
|
46
|
+
if (additionalInfo < AI_ONE_BYTE) return additionalInfo;
|
|
47
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
48
|
+
switch (additionalInfo) {
|
|
49
|
+
case AI_ONE_BYTE:
|
|
50
|
+
return view.getUint8(offset + 1);
|
|
51
|
+
case AI_TWO_BYTES:
|
|
52
|
+
return view.getUint16(offset + 1, false);
|
|
53
|
+
case AI_FOUR_BYTES:
|
|
54
|
+
return view.getUint32(offset + 1, false);
|
|
55
|
+
case AI_EIGHT_BYTES: {
|
|
56
|
+
const value = view.getBigUint64(offset + 1, false);
|
|
57
|
+
return Number(value);
|
|
58
|
+
}
|
|
59
|
+
default:
|
|
60
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function skipItem(bytes, offset, allowBreak) {
|
|
64
|
+
if (offset >= bytes.length) return INCOMPLETE;
|
|
65
|
+
const initial = bytes[offset];
|
|
66
|
+
if (initial === BREAK) {
|
|
67
|
+
if (!allowBreak) {
|
|
68
|
+
throw new MalformedCborError("break outside an indefinite-length item", offset);
|
|
69
|
+
}
|
|
70
|
+
return offset + 1;
|
|
71
|
+
}
|
|
72
|
+
const major = initial >> 5;
|
|
73
|
+
const additionalInfo = initial & 31;
|
|
74
|
+
const extra = headerExtraBytes(additionalInfo, offset);
|
|
75
|
+
if (offset + 1 + extra > bytes.length) return INCOMPLETE;
|
|
76
|
+
const indefinite = additionalInfo === AI_INDEFINITE;
|
|
77
|
+
if (indefinite && (major === MAJOR_UNSIGNED || major === MAJOR_NEGATIVE || major === MAJOR_TAG)) {
|
|
78
|
+
throw new MalformedCborError(`major type ${major} cannot be indefinite-length`, offset);
|
|
79
|
+
}
|
|
80
|
+
const afterHeader = offset + 1 + extra;
|
|
81
|
+
switch (major) {
|
|
82
|
+
case MAJOR_UNSIGNED:
|
|
83
|
+
case MAJOR_NEGATIVE:
|
|
84
|
+
return afterHeader;
|
|
85
|
+
case MAJOR_BYTES:
|
|
86
|
+
case MAJOR_TEXT: {
|
|
87
|
+
if (!indefinite) {
|
|
88
|
+
const length = readArgument(bytes, offset, additionalInfo);
|
|
89
|
+
const end = afterHeader + length;
|
|
90
|
+
return end > bytes.length ? INCOMPLETE : end;
|
|
91
|
+
}
|
|
92
|
+
let cursor = afterHeader;
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
if (cursor >= bytes.length) return INCOMPLETE;
|
|
95
|
+
if (bytes[cursor] === BREAK) return cursor + 1;
|
|
96
|
+
const next = skipItem(bytes, cursor, false);
|
|
97
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
98
|
+
cursor = next;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
case MAJOR_ARRAY:
|
|
102
|
+
case MAJOR_MAP: {
|
|
103
|
+
const perEntry = major === MAJOR_MAP ? 2 : 1;
|
|
104
|
+
if (!indefinite) {
|
|
105
|
+
const count = readArgument(bytes, offset, additionalInfo) * perEntry;
|
|
106
|
+
let cursor2 = afterHeader;
|
|
107
|
+
for (let i = 0; i < count; i++) {
|
|
108
|
+
const next = skipItem(bytes, cursor2, false);
|
|
109
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
110
|
+
cursor2 = next;
|
|
111
|
+
}
|
|
112
|
+
return cursor2;
|
|
113
|
+
}
|
|
114
|
+
let cursor = afterHeader;
|
|
115
|
+
for (; ; ) {
|
|
116
|
+
if (cursor >= bytes.length) return INCOMPLETE;
|
|
117
|
+
if (bytes[cursor] === BREAK) return cursor + 1;
|
|
118
|
+
for (let i = 0; i < perEntry; i++) {
|
|
119
|
+
const next = skipItem(bytes, cursor, false);
|
|
120
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
121
|
+
cursor = next;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
case MAJOR_TAG:
|
|
126
|
+
return skipItem(bytes, afterHeader, false);
|
|
127
|
+
case MAJOR_SIMPLE:
|
|
128
|
+
return afterHeader;
|
|
129
|
+
default:
|
|
130
|
+
throw new MalformedCborError(`unknown major type ${major}`, offset);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function cborItemLength(bytes, start) {
|
|
134
|
+
const end = skipItem(bytes, start, false);
|
|
135
|
+
return end === INCOMPLETE ? INCOMPLETE : end - start;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/types.ts
|
|
139
|
+
var TRACE_ID_LENGTH = 16;
|
|
140
|
+
function controlMessageFields(message) {
|
|
141
|
+
if (message === null || typeof message !== "object") return void 0;
|
|
142
|
+
if (Array.isArray(message) || ArrayBuffer.isView(message)) return void 0;
|
|
143
|
+
return message;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/binary.ts
|
|
147
|
+
var codec = new (0, _cborx.Encoder)({ useRecords: false, mapsAsObjects: true, tagUint8Array: false });
|
|
4
148
|
var encode = (value) => codec.encode(value);
|
|
5
149
|
var decode = (bytes) => codec.decode(bytes);
|
|
6
|
-
var decodeMultiple = (bytes, cb) => {
|
|
7
|
-
codec.decodeMultiple(bytes, cb);
|
|
8
|
-
};
|
|
9
150
|
var MAGIC = new Uint8Array([77, 79, 81, 84, 82, 65, 67, 69]);
|
|
10
|
-
var FORMAT_VERSION =
|
|
151
|
+
var FORMAT_VERSION = 2;
|
|
152
|
+
var SUPPORTED_VERSIONS = [1, FORMAT_VERSION];
|
|
11
153
|
var PREAMBLE_SIZE = 16;
|
|
12
154
|
var EVENT_TYPE_TO_INT = {
|
|
13
155
|
control: 0,
|
|
@@ -17,7 +159,10 @@ var EVENT_TYPE_TO_INT = {
|
|
|
17
159
|
"object-payload": 4,
|
|
18
160
|
"state-change": 5,
|
|
19
161
|
error: 6,
|
|
20
|
-
annotation: 7
|
|
162
|
+
annotation: 7,
|
|
163
|
+
"peer-connected": 8,
|
|
164
|
+
"peer-disconnected": 9,
|
|
165
|
+
"subscription-derivation": 10
|
|
21
166
|
};
|
|
22
167
|
var INT_TO_EVENT_TYPE = {
|
|
23
168
|
0: "control",
|
|
@@ -27,48 +172,213 @@ var INT_TO_EVENT_TYPE = {
|
|
|
27
172
|
4: "object-payload",
|
|
28
173
|
5: "state-change",
|
|
29
174
|
6: "error",
|
|
30
|
-
7: "annotation"
|
|
175
|
+
7: "annotation",
|
|
176
|
+
8: "peer-connected",
|
|
177
|
+
9: "peer-disconnected",
|
|
178
|
+
10: "subscription-derivation"
|
|
179
|
+
};
|
|
180
|
+
var COMMON_EVENT_KEYS = /* @__PURE__ */ new Set(["n", "t", "p", "e"]);
|
|
181
|
+
function setKey(target, key, value) {
|
|
182
|
+
Object.defineProperty(target, key, {
|
|
183
|
+
value,
|
|
184
|
+
writable: true,
|
|
185
|
+
enumerable: true,
|
|
186
|
+
configurable: true
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function unrecognisedKeys(src) {
|
|
190
|
+
let extra;
|
|
191
|
+
for (const [key, value] of Object.entries(src.obj)) {
|
|
192
|
+
if (src.used.has(key)) continue;
|
|
193
|
+
extra ??= {};
|
|
194
|
+
setKey(extra, key, value);
|
|
195
|
+
}
|
|
196
|
+
return extra;
|
|
197
|
+
}
|
|
198
|
+
function withStore(map, store) {
|
|
199
|
+
if (store == null) return map;
|
|
200
|
+
for (const [key, value] of Object.entries(store)) {
|
|
201
|
+
if (Object.hasOwn(map, key)) continue;
|
|
202
|
+
setKey(map, key, value);
|
|
203
|
+
}
|
|
204
|
+
return map;
|
|
205
|
+
}
|
|
206
|
+
var TruncatedTraceError = class extends Error {
|
|
207
|
+
/** Byte offset at which the incomplete item begins. */
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Everything that decoded before the cut, one entry per segment. A
|
|
211
|
+
* truncated trace is still evidence, and the events before the cut are
|
|
212
|
+
* exactly as valid as they were.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
constructor(offset, segments) {
|
|
216
|
+
super(`Trace truncated: the item starting at byte ${offset} is incomplete`);
|
|
217
|
+
this.name = "TruncatedTraceError";
|
|
218
|
+
this.offset = offset;
|
|
219
|
+
this.segments = segments;
|
|
220
|
+
}
|
|
221
|
+
/** What decoded, flattened as {@link readMoqtrace} would return it. */
|
|
222
|
+
get trace() {
|
|
223
|
+
return flatten(this.segments);
|
|
224
|
+
}
|
|
31
225
|
};
|
|
226
|
+
var MalformedHeaderError = class extends Error {
|
|
227
|
+
/** The key at fault, dotted for one inside `"segment"`. */
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param key the key at fault
|
|
231
|
+
* @param fault what was wrong with it, as a predicate on the key
|
|
232
|
+
*/
|
|
233
|
+
constructor(key, fault) {
|
|
234
|
+
super(`Malformed header: ${JSON.stringify(key)} ${fault}`);
|
|
235
|
+
this.name = "MalformedHeaderError";
|
|
236
|
+
this.key = key;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
function requiredUint(value, reportedAs) {
|
|
240
|
+
if (!Number.isInteger(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
|
241
|
+
throw new MalformedHeaderError(reportedAs, "must be an unsigned integer");
|
|
242
|
+
}
|
|
243
|
+
return int(value);
|
|
244
|
+
}
|
|
245
|
+
function segmentToCbor(segment) {
|
|
246
|
+
const map = {
|
|
247
|
+
sequence: requiredUint(segment.sequence, "segment.sequence")
|
|
248
|
+
};
|
|
249
|
+
if (segment.durationMs != null) map.durationMs = int(segment.durationMs);
|
|
250
|
+
if (segment.streamId != null) map.streamId = segment.streamId;
|
|
251
|
+
if (segment.continues != null) map.continues = segment.continues;
|
|
252
|
+
return withStore(map, segment.extra);
|
|
253
|
+
}
|
|
254
|
+
function cborToSegment(value) {
|
|
255
|
+
const obj = asMap(value);
|
|
256
|
+
if (obj == null) return void 0;
|
|
257
|
+
const src = { obj, used: /* @__PURE__ */ new Set() };
|
|
258
|
+
const segment = {
|
|
259
|
+
sequence: requireHeaderUint(src, "sequence", "segment.sequence"),
|
|
260
|
+
...optional(src, "durationMs", "durationMs", asUintNumber),
|
|
261
|
+
...optional(src, "streamId", "streamId", asText),
|
|
262
|
+
...optional(src, "continues", "continues", asBoolean)
|
|
263
|
+
};
|
|
264
|
+
const extra = unrecognisedKeys(src);
|
|
265
|
+
return extra == null ? segment : { ...segment, extra };
|
|
266
|
+
}
|
|
267
|
+
function samplingToCbor(sampling) {
|
|
268
|
+
const map = {};
|
|
269
|
+
if (sampling.effectiveRate != null) map.effectiveRate = sampling.effectiveRate;
|
|
270
|
+
if (sampling.maxEventsPerSec != null) map.maxEventsPerSec = int(sampling.maxEventsPerSec);
|
|
271
|
+
if (sampling.dropPolicy != null) map.dropPolicy = sampling.dropPolicy;
|
|
272
|
+
if (sampling.droppedTotal != null) map.droppedTotal = int(sampling.droppedTotal);
|
|
273
|
+
if (sampling.droppedSegment != null) map.droppedSegment = int(sampling.droppedSegment);
|
|
274
|
+
if (sampling.rule != null) map.rule = sampling.rule;
|
|
275
|
+
if (sampling.ruleLang != null) map.ruleLang = sampling.ruleLang;
|
|
276
|
+
if (sampling.appliesTo != null) map.appliesTo = sampling.appliesTo.map(int);
|
|
277
|
+
return withStore(map, sampling.extra);
|
|
278
|
+
}
|
|
279
|
+
function cborToSampling(value) {
|
|
280
|
+
const obj = asMap(value);
|
|
281
|
+
if (obj == null) return void 0;
|
|
282
|
+
const src = { obj, used: /* @__PURE__ */ new Set() };
|
|
283
|
+
const sampling = {
|
|
284
|
+
// A rate is a fraction, so this is the one header number that is not an
|
|
285
|
+
// integer — and the one whose range the format states, so it is the one
|
|
286
|
+
// key here checked for more than its type. It is tested rather than
|
|
287
|
+
// converted: `Number('half')` is `NaN`, a rate no file carried and none
|
|
288
|
+
// can be written back from.
|
|
289
|
+
...optional(src, "effectiveRate", "effectiveRate", asRate),
|
|
290
|
+
...optional(src, "maxEventsPerSec", "maxEventsPerSec", asUintNumber),
|
|
291
|
+
...optional(src, "dropPolicy", "dropPolicy", asText),
|
|
292
|
+
...optional(src, "droppedTotal", "droppedTotal", asUintNumber),
|
|
293
|
+
...optional(src, "droppedSegment", "droppedSegment", asUintNumber),
|
|
294
|
+
...optional(src, "rule", "rule", asText),
|
|
295
|
+
...optional(src, "ruleLang", "ruleLang", asText),
|
|
296
|
+
...optional(src, "appliesTo", "appliesTo", asUintNumbers)
|
|
297
|
+
};
|
|
298
|
+
const extra = unrecognisedKeys(src);
|
|
299
|
+
return extra == null ? sampling : { ...sampling, extra };
|
|
300
|
+
}
|
|
32
301
|
function headerToCbor(header) {
|
|
33
302
|
const map = {
|
|
34
303
|
protocol: header.protocol,
|
|
35
304
|
perspective: header.perspective,
|
|
36
305
|
detail: header.detail,
|
|
37
|
-
startTime: header.startTime
|
|
306
|
+
startTime: requiredUint(header.startTime, "startTime")
|
|
38
307
|
};
|
|
39
|
-
if (header.endTime != null) map.endTime = header.endTime;
|
|
308
|
+
if (header.endTime != null) map.endTime = int(header.endTime);
|
|
40
309
|
if (header.transport != null) map.transport = header.transport;
|
|
41
310
|
if (header.source != null) map.source = header.source;
|
|
42
311
|
if (header.endpoint != null) map.endpoint = header.endpoint;
|
|
43
312
|
if (header.sessionId != null) map.sessionId = header.sessionId;
|
|
313
|
+
if (header.segment != null) map.segment = segmentToCbor(header.segment);
|
|
314
|
+
if (header.sampling != null) map.sampling = samplingToCbor(header.sampling);
|
|
44
315
|
if (header.custom != null) map.custom = header.custom;
|
|
45
|
-
return map;
|
|
316
|
+
return withStore(map, header.extra);
|
|
46
317
|
}
|
|
47
318
|
function cborToHeader(obj) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
...
|
|
55
|
-
...
|
|
56
|
-
...
|
|
57
|
-
...
|
|
58
|
-
...
|
|
319
|
+
const src = { obj, used: /* @__PURE__ */ new Set() };
|
|
320
|
+
const header = {
|
|
321
|
+
protocol: requireHeaderText(src, "protocol"),
|
|
322
|
+
perspective: requireHeaderText(src, "perspective"),
|
|
323
|
+
detail: requireHeaderText(src, "detail"),
|
|
324
|
+
startTime: requireHeaderUint(src, "startTime"),
|
|
325
|
+
...optional(src, "endTime", "endTime", asUintNumber),
|
|
326
|
+
...optional(src, "transport", "transport", asText),
|
|
327
|
+
...optional(src, "source", "source", asText),
|
|
328
|
+
...optional(src, "endpoint", "endpoint", asText),
|
|
329
|
+
...optional(src, "sessionId", "sessionId", asText),
|
|
330
|
+
...optional(src, "segment", "segment", cborToSegment),
|
|
331
|
+
...optional(src, "sampling", "sampling", cborToSampling),
|
|
332
|
+
// Not `Record<string, unknown>` by assertion but by test. A `"custom"`
|
|
333
|
+
// that is not a map used to reach this field wearing that type, so the
|
|
334
|
+
// first caller to iterate its keys met a shape it was told could not
|
|
335
|
+
// occur. The bytes survive either way; only one of the two keeps the
|
|
336
|
+
// field's type true.
|
|
337
|
+
...optional(src, "custom", "custom", asMap)
|
|
59
338
|
};
|
|
339
|
+
const extra = unrecognisedKeys(src);
|
|
340
|
+
return extra == null ? header : { ...header, extra };
|
|
341
|
+
}
|
|
342
|
+
function subscriptionRefToCbor(ref) {
|
|
343
|
+
return [ref.peer, ref.requestId];
|
|
344
|
+
}
|
|
345
|
+
function cborToSubscriptionRef(value) {
|
|
346
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
347
|
+
throw new Error("Subscription reference must be [peer, requestId]");
|
|
348
|
+
}
|
|
349
|
+
const peer = asText(value[0]);
|
|
350
|
+
const requestId = asUint(value[1]);
|
|
351
|
+
if (peer == null || requestId == null) {
|
|
352
|
+
throw new Error("Subscription reference must be a [text, unsigned integer] pair");
|
|
353
|
+
}
|
|
354
|
+
return { peer, requestId };
|
|
355
|
+
}
|
|
356
|
+
function toBytes(value) {
|
|
357
|
+
return value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
|
|
358
|
+
}
|
|
359
|
+
function int(value) {
|
|
360
|
+
const outsideInt32 = value > 4294967295 || value < -2147483648;
|
|
361
|
+
return Number.isInteger(value) && outsideInt32 ? BigInt(value) : value;
|
|
362
|
+
}
|
|
363
|
+
function checkTraceId(value) {
|
|
364
|
+
const bytes = toBytes(value);
|
|
365
|
+
if (bytes.length !== TRACE_ID_LENGTH) {
|
|
366
|
+
throw new Error(`traceId is ${bytes.length} bytes, must be ${TRACE_ID_LENGTH}`);
|
|
367
|
+
}
|
|
368
|
+
return bytes;
|
|
60
369
|
}
|
|
61
370
|
function eventToCbor(event) {
|
|
62
371
|
const base = {
|
|
63
|
-
n: event.seq,
|
|
64
|
-
t: event.timestamp
|
|
65
|
-
e: EVENT_TYPE_TO_INT[event.type]
|
|
372
|
+
n: int(event.seq),
|
|
373
|
+
t: int(event.timestamp)
|
|
66
374
|
};
|
|
375
|
+
if (event.peer != null) base.p = event.peer;
|
|
376
|
+
if (event.type !== "unknown") base.e = EVENT_TYPE_TO_INT[event.type];
|
|
67
377
|
switch (event.type) {
|
|
68
378
|
case "control": {
|
|
69
379
|
base.d = event.direction;
|
|
70
|
-
base.mt = event.messageType;
|
|
71
|
-
base.msg = event.message;
|
|
380
|
+
base.mt = int(event.messageType);
|
|
381
|
+
base.msg = event.message === void 0 ? {} : event.message;
|
|
72
382
|
if (event.streamId != null) base.sid = event.streamId;
|
|
73
383
|
if (event.raw != null) base.raw = event.raw;
|
|
74
384
|
break;
|
|
@@ -77,26 +387,30 @@ function eventToCbor(event) {
|
|
|
77
387
|
base.sid = event.streamId;
|
|
78
388
|
base.d = event.direction;
|
|
79
389
|
base.st = event.streamType;
|
|
390
|
+
if (event.trackAlias != null) base.ta = event.trackAlias;
|
|
391
|
+
if (event.subgroupId != null) base.sg = event.subgroupId;
|
|
392
|
+
if (event.fetchRequestId != null) base.fri = event.fetchRequestId;
|
|
393
|
+
if (event.groupId != null) base.g = event.groupId;
|
|
80
394
|
break;
|
|
81
395
|
}
|
|
82
396
|
case "stream-closed": {
|
|
83
397
|
base.sid = event.streamId;
|
|
84
|
-
base.ec = event.errorCode;
|
|
398
|
+
base.ec = int(event.errorCode);
|
|
85
399
|
break;
|
|
86
400
|
}
|
|
87
401
|
case "object-header": {
|
|
88
402
|
base.sid = event.streamId;
|
|
89
403
|
base.g = event.groupId;
|
|
90
404
|
base.o = event.objectId;
|
|
91
|
-
base.pp = event.publisherPriority;
|
|
92
|
-
base.os = event.objectStatus;
|
|
405
|
+
base.pp = int(event.publisherPriority);
|
|
406
|
+
base.os = int(event.objectStatus);
|
|
93
407
|
break;
|
|
94
408
|
}
|
|
95
409
|
case "object-payload": {
|
|
96
410
|
base.sid = event.streamId;
|
|
97
411
|
base.g = event.groupId;
|
|
98
412
|
base.o = event.objectId;
|
|
99
|
-
base.sz = event.size;
|
|
413
|
+
base.sz = int(event.size);
|
|
100
414
|
if (event.payload != null) base.pl = event.payload;
|
|
101
415
|
break;
|
|
102
416
|
}
|
|
@@ -106,8 +420,12 @@ function eventToCbor(event) {
|
|
|
106
420
|
break;
|
|
107
421
|
}
|
|
108
422
|
case "error": {
|
|
109
|
-
base.ec = event.errorCode;
|
|
423
|
+
base.ec = int(event.errorCode);
|
|
110
424
|
base.reason = event.reason;
|
|
425
|
+
if (event.streamId != null) base.sid = event.streamId;
|
|
426
|
+
if (event.errorKind != null) base.ek = event.errorKind;
|
|
427
|
+
if (event.rawLength != null) base.rawlen = int(event.rawLength);
|
|
428
|
+
if (event.raw != null) base.raw = event.raw;
|
|
111
429
|
break;
|
|
112
430
|
}
|
|
113
431
|
case "annotation": {
|
|
@@ -115,96 +433,330 @@ function eventToCbor(event) {
|
|
|
115
433
|
if (event.data !== void 0) base.data = event.data;
|
|
116
434
|
break;
|
|
117
435
|
}
|
|
436
|
+
case "peer-connected": {
|
|
437
|
+
if (event.endpoint != null) base.endpoint = event.endpoint;
|
|
438
|
+
if (event.transport != null) base.transport = event.transport;
|
|
439
|
+
if (event.role != null) base.role = event.role;
|
|
440
|
+
if (event.side != null) base.side = event.side;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case "peer-disconnected": {
|
|
444
|
+
base.ec = int(event.errorCode);
|
|
445
|
+
if (event.reason != null) base.reason = event.reason;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "subscription-derivation": {
|
|
449
|
+
base.u = subscriptionRefToCbor(event.upstream);
|
|
450
|
+
base.d = event.downstream.map(subscriptionRefToCbor);
|
|
451
|
+
base.kind = event.kind;
|
|
452
|
+
if (event.traceId != null) base.traceId = checkTraceId(event.traceId);
|
|
453
|
+
if (event.namespace != null) base.ns = event.namespace;
|
|
454
|
+
if (event.trackName != null) base.tn = event.trackName;
|
|
455
|
+
if (event.tDownstreamReceived != null) base.tdr = int(event.tDownstreamReceived);
|
|
456
|
+
if (event.tUpstreamSent != null) base.tus = int(event.tUpstreamSent);
|
|
457
|
+
if (event.tUpstreamOkReceived != null) base.tuo = int(event.tUpstreamOkReceived);
|
|
458
|
+
if (event.tDownstreamOkSent != null) base.tdo = int(event.tDownstreamOkSent);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "unknown": {
|
|
462
|
+
base.e = event.eventType;
|
|
463
|
+
for (const [key, value] of Object.entries(event.fields)) {
|
|
464
|
+
setKey(base, key, value);
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (event.type !== "unknown" && event.extra != null) {
|
|
470
|
+
for (const [key, value] of Object.entries(event.extra)) {
|
|
471
|
+
if (COMMON_EVENT_KEYS.has(key) || Object.hasOwn(base, key)) continue;
|
|
472
|
+
setKey(base, key, value);
|
|
473
|
+
}
|
|
118
474
|
}
|
|
119
475
|
return base;
|
|
120
476
|
}
|
|
477
|
+
function asUint(value) {
|
|
478
|
+
if (typeof value === "bigint") return value >= 0n ? value : void 0;
|
|
479
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return void 0;
|
|
480
|
+
return BigInt(value);
|
|
481
|
+
}
|
|
482
|
+
function asUintNumber(value) {
|
|
483
|
+
const int2 = asUint(value);
|
|
484
|
+
return int2 == null || int2 > BigInt(Number.MAX_SAFE_INTEGER) ? void 0 : Number(int2);
|
|
485
|
+
}
|
|
486
|
+
function asText(value) {
|
|
487
|
+
return typeof value === "string" ? value : void 0;
|
|
488
|
+
}
|
|
489
|
+
function asBoolean(value) {
|
|
490
|
+
return typeof value === "boolean" ? value : void 0;
|
|
491
|
+
}
|
|
492
|
+
function asNumber(value) {
|
|
493
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
494
|
+
}
|
|
495
|
+
function asRate(value) {
|
|
496
|
+
const rate = asNumber(value);
|
|
497
|
+
return rate != null && rate > 0 && rate <= 1 ? rate : void 0;
|
|
498
|
+
}
|
|
499
|
+
function asUintNumbers(value) {
|
|
500
|
+
if (!Array.isArray(value)) return void 0;
|
|
501
|
+
const ids = [];
|
|
502
|
+
for (const item of value) {
|
|
503
|
+
const id = asUintNumber(item);
|
|
504
|
+
if (id == null) return void 0;
|
|
505
|
+
ids.push(id);
|
|
506
|
+
}
|
|
507
|
+
return ids;
|
|
508
|
+
}
|
|
509
|
+
function asMap(value) {
|
|
510
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
511
|
+
const proto = Object.getPrototypeOf(value);
|
|
512
|
+
if (proto !== Object.prototype && proto !== null) return void 0;
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
function asByteString(value) {
|
|
516
|
+
return value instanceof Uint8Array ? toBytes(value) : void 0;
|
|
517
|
+
}
|
|
518
|
+
function asByteStrings(value) {
|
|
519
|
+
if (!Array.isArray(value)) return void 0;
|
|
520
|
+
const parts = [];
|
|
521
|
+
for (const item of value) {
|
|
522
|
+
const bytes = asByteString(item);
|
|
523
|
+
if (bytes == null) return void 0;
|
|
524
|
+
parts.push(bytes);
|
|
525
|
+
}
|
|
526
|
+
return parts;
|
|
527
|
+
}
|
|
528
|
+
function asTraceId(value) {
|
|
529
|
+
const bytes = asByteString(value);
|
|
530
|
+
return _optionalChain([bytes, 'optionalAccess', _ => _.length]) === TRACE_ID_LENGTH ? bytes : void 0;
|
|
531
|
+
}
|
|
532
|
+
function take(src, key) {
|
|
533
|
+
src.used.add(key);
|
|
534
|
+
return src.obj[key];
|
|
535
|
+
}
|
|
536
|
+
function requireUint(src, key) {
|
|
537
|
+
const value = asUint(take(src, key));
|
|
538
|
+
if (value == null) {
|
|
539
|
+
throw new Error(`Event key ${JSON.stringify(key)} must be an unsigned integer`);
|
|
540
|
+
}
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
function requireHeaderText(src, key) {
|
|
544
|
+
const present = Object.hasOwn(src.obj, key);
|
|
545
|
+
const value = asText(take(src, key));
|
|
546
|
+
if (value == null) {
|
|
547
|
+
throw new MalformedHeaderError(key, present ? "must be a text string" : "is missing");
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
function requireHeaderUint(src, key, reportedAs = key) {
|
|
552
|
+
const present = Object.hasOwn(src.obj, key);
|
|
553
|
+
const value = asUintNumber(take(src, key));
|
|
554
|
+
if (value == null) {
|
|
555
|
+
throw new MalformedHeaderError(
|
|
556
|
+
reportedAs,
|
|
557
|
+
present ? "must be an unsigned integer" : "is missing"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return value;
|
|
561
|
+
}
|
|
562
|
+
function optional(src, key, name, read) {
|
|
563
|
+
const usable = read(src.obj[key]);
|
|
564
|
+
if (usable === void 0) return {};
|
|
565
|
+
src.used.add(key);
|
|
566
|
+
return { [name]: usable };
|
|
567
|
+
}
|
|
121
568
|
function cborToEvent(obj) {
|
|
569
|
+
const src = { obj, used: new Set(COMMON_EVENT_KEYS) };
|
|
570
|
+
const event = decodeEvent(src);
|
|
571
|
+
if (event.type === "unknown") return event;
|
|
572
|
+
const extra = unrecognisedKeys(src);
|
|
573
|
+
return extra == null ? event : { ...event, extra };
|
|
574
|
+
}
|
|
575
|
+
function decodeEvent(src) {
|
|
576
|
+
const obj = src.obj;
|
|
577
|
+
const seq = Number(_nullishCoalesce(obj.n, () => ( 0)));
|
|
578
|
+
const timestamp = Number(_nullishCoalesce(obj.t, () => ( 0)));
|
|
579
|
+
const peerFields = obj.p != null ? { peer: obj.p } : {};
|
|
122
580
|
const eventType = INT_TO_EVENT_TYPE[obj.e];
|
|
123
581
|
if (eventType == null) {
|
|
582
|
+
const fields = {};
|
|
583
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
584
|
+
if (!COMMON_EVENT_KEYS.has(key)) setKey(fields, key, value);
|
|
585
|
+
}
|
|
124
586
|
return {
|
|
125
|
-
type: "
|
|
126
|
-
seq
|
|
127
|
-
timestamp
|
|
128
|
-
|
|
129
|
-
|
|
587
|
+
type: "unknown",
|
|
588
|
+
seq,
|
|
589
|
+
timestamp,
|
|
590
|
+
...peerFields,
|
|
591
|
+
eventType: Number(_nullishCoalesce(obj.e, () => ( -1))),
|
|
592
|
+
fields
|
|
130
593
|
};
|
|
131
594
|
}
|
|
132
|
-
const seq = Number(_nullishCoalesce(obj.n, () => ( 0)));
|
|
133
|
-
const timestamp = Number(_nullishCoalesce(obj.t, () => ( 0)));
|
|
134
595
|
switch (eventType) {
|
|
135
596
|
case "control":
|
|
136
597
|
return {
|
|
137
598
|
type: "control",
|
|
138
599
|
seq,
|
|
139
600
|
timestamp,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
601
|
+
...peerFields,
|
|
602
|
+
direction: take(src, "d"),
|
|
603
|
+
messageType: Number(_nullishCoalesce(take(src, "mt"), () => ( 0))),
|
|
604
|
+
// Handed over exactly as it decoded. A `"msg"` that is not a map is
|
|
605
|
+
// still the only decode the recording has — the corpus captures carry
|
|
606
|
+
// a Rust `Debug` string here — and the format requires a reader to
|
|
607
|
+
// preserve it rather than reject the event or substitute a map. That
|
|
608
|
+
// includes `null`, which is a value a writer chose to put on the wire,
|
|
609
|
+
// not an absence; absence is the single case normalised, to `{}`, so
|
|
610
|
+
// that a caller reading keys off a conforming file never meets
|
|
611
|
+
// `undefined`. It is the one key with no unusable shape: the field's
|
|
612
|
+
// type is `unknown`, so every value is one it can hold.
|
|
613
|
+
message: Object.hasOwn(obj, "msg") ? take(src, "msg") : {},
|
|
614
|
+
...optional(src, "sid", "streamId", asUint),
|
|
615
|
+
...optional(src, "raw", "raw", asByteString)
|
|
145
616
|
};
|
|
146
617
|
case "stream-opened":
|
|
147
618
|
return {
|
|
148
619
|
type: "stream-opened",
|
|
149
620
|
seq,
|
|
150
621
|
timestamp,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
622
|
+
...peerFields,
|
|
623
|
+
streamId: requireUint(src, "sid"),
|
|
624
|
+
direction: take(src, "d"),
|
|
625
|
+
streamType: take(src, "st"),
|
|
626
|
+
// `bigint`, not `number`, even though a writer that used a narrow CBOR
|
|
627
|
+
// integer hands these back as JS numbers. They are the same
|
|
628
|
+
// identifiers an object-header event carries, and that event converts;
|
|
629
|
+
// leaving one a number here makes `opened.groupId === header.groupId`
|
|
630
|
+
// read `42 === 42n` and answer `false` for values that match.
|
|
631
|
+
//
|
|
632
|
+
// Tested rather than converted, and optional rather than required.
|
|
633
|
+
// `BigInt(obj.ta)` threw on a text or fractional value and took the
|
|
634
|
+
// whole file down with it, since nothing catches one event's decode; it
|
|
635
|
+
// answered `1n` for `true` and `4n` for `"4"`, inventing an identifier
|
|
636
|
+
// the file never carried; and the owned-key list then kept the
|
|
637
|
+
// survivors out of `extra`, so adding these four keys *lost* values
|
|
638
|
+
// this reader had preserved before it knew them. A value of any other
|
|
639
|
+
// shape now stays where it was, on the event as an unrecognised key.
|
|
640
|
+
...optional(src, "ta", "trackAlias", asUint),
|
|
641
|
+
...optional(src, "sg", "subgroupId", asUint),
|
|
642
|
+
...optional(src, "fri", "fetchRequestId", asUint),
|
|
643
|
+
...optional(src, "g", "groupId", asUint)
|
|
154
644
|
};
|
|
155
645
|
case "stream-closed":
|
|
156
646
|
return {
|
|
157
647
|
type: "stream-closed",
|
|
158
648
|
seq,
|
|
159
649
|
timestamp,
|
|
160
|
-
|
|
161
|
-
|
|
650
|
+
...peerFields,
|
|
651
|
+
streamId: requireUint(src, "sid"),
|
|
652
|
+
errorCode: Number(_nullishCoalesce(take(src, "ec"), () => ( 0)))
|
|
162
653
|
};
|
|
163
654
|
case "object-header":
|
|
164
655
|
return {
|
|
165
656
|
type: "object-header",
|
|
166
657
|
seq,
|
|
167
658
|
timestamp,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
659
|
+
...peerFields,
|
|
660
|
+
streamId: requireUint(src, "sid"),
|
|
661
|
+
groupId: requireUint(src, "g"),
|
|
662
|
+
objectId: requireUint(src, "o"),
|
|
663
|
+
publisherPriority: Number(_nullishCoalesce(take(src, "pp"), () => ( 0))),
|
|
664
|
+
objectStatus: Number(_nullishCoalesce(take(src, "os"), () => ( 0)))
|
|
173
665
|
};
|
|
174
666
|
case "object-payload":
|
|
175
667
|
return {
|
|
176
668
|
type: "object-payload",
|
|
177
669
|
seq,
|
|
178
670
|
timestamp,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
671
|
+
...peerFields,
|
|
672
|
+
streamId: requireUint(src, "sid"),
|
|
673
|
+
groupId: requireUint(src, "g"),
|
|
674
|
+
objectId: requireUint(src, "o"),
|
|
675
|
+
size: Number(_nullishCoalesce(take(src, "sz"), () => ( 0))),
|
|
676
|
+
...optional(src, "pl", "payload", asByteString)
|
|
184
677
|
};
|
|
185
678
|
case "state-change":
|
|
186
679
|
return {
|
|
187
680
|
type: "state-change",
|
|
188
681
|
seq,
|
|
189
682
|
timestamp,
|
|
190
|
-
|
|
191
|
-
|
|
683
|
+
...peerFields,
|
|
684
|
+
from: take(src, "from"),
|
|
685
|
+
to: take(src, "to")
|
|
192
686
|
};
|
|
193
687
|
case "error":
|
|
194
688
|
return {
|
|
195
689
|
type: "error",
|
|
196
690
|
seq,
|
|
197
691
|
timestamp,
|
|
198
|
-
|
|
199
|
-
|
|
692
|
+
...peerFields,
|
|
693
|
+
errorCode: Number(_nullishCoalesce(take(src, "ec"), () => ( 0))),
|
|
694
|
+
reason: _nullishCoalesce(take(src, "reason"), () => ( "")),
|
|
695
|
+
// Optional, so an unusable value costs the key and not the event: the
|
|
696
|
+
// error code and reason are the point of the event and are fine.
|
|
697
|
+
// `asUint` rather than a conversion, for the reason Event 1 gives —
|
|
698
|
+
// `BigInt(true)` is `1n`, a stream this file never named.
|
|
699
|
+
...optional(src, "sid", "streamId", asUint),
|
|
700
|
+
// An error kind this version has never heard of is still text, and
|
|
701
|
+
// still kept: the vocabulary is open and `ErrorKind` widens to any
|
|
702
|
+
// string for exactly that. What `asText` refuses is a value that is
|
|
703
|
+
// not text at all.
|
|
704
|
+
...optional(src, "ek", "errorKind", asText),
|
|
705
|
+
...optional(src, "rawlen", "rawLength", asUintNumber),
|
|
706
|
+
// No length check. The 4096-byte cap binds a recorder; a reader that
|
|
707
|
+
// refused a longer value — or shortened it — would reject a file it was
|
|
708
|
+
// required to accept, or destroy the evidence the field exists to
|
|
709
|
+
// carry.
|
|
710
|
+
...optional(src, "raw", "raw", asByteString)
|
|
200
711
|
};
|
|
201
712
|
case "annotation":
|
|
202
713
|
return {
|
|
203
714
|
type: "annotation",
|
|
204
715
|
seq,
|
|
205
716
|
timestamp,
|
|
206
|
-
|
|
207
|
-
|
|
717
|
+
...peerFields,
|
|
718
|
+
label: _nullishCoalesce(take(src, "label"), () => ( "")),
|
|
719
|
+
data: take(src, "data")
|
|
720
|
+
};
|
|
721
|
+
case "peer-connected":
|
|
722
|
+
return {
|
|
723
|
+
type: "peer-connected",
|
|
724
|
+
seq,
|
|
725
|
+
timestamp,
|
|
726
|
+
...peerFields,
|
|
727
|
+
...optional(src, "endpoint", "endpoint", asText),
|
|
728
|
+
...optional(src, "transport", "transport", asText),
|
|
729
|
+
// A role or side this version has never heard of is still text, and
|
|
730
|
+
// still kept: the unions they widen to exist for exactly that. What
|
|
731
|
+
// `asText` refuses is a value that is not text at all.
|
|
732
|
+
...optional(src, "role", "role", asText),
|
|
733
|
+
...optional(src, "side", "side", asText)
|
|
734
|
+
};
|
|
735
|
+
case "peer-disconnected":
|
|
736
|
+
return {
|
|
737
|
+
type: "peer-disconnected",
|
|
738
|
+
seq,
|
|
739
|
+
timestamp,
|
|
740
|
+
...peerFields,
|
|
741
|
+
errorCode: Number(_nullishCoalesce(take(src, "ec"), () => ( 0))),
|
|
742
|
+
...optional(src, "reason", "reason", asText)
|
|
743
|
+
};
|
|
744
|
+
case "subscription-derivation":
|
|
745
|
+
return {
|
|
746
|
+
type: "subscription-derivation",
|
|
747
|
+
seq,
|
|
748
|
+
timestamp,
|
|
749
|
+
...peerFields,
|
|
750
|
+
upstream: cborToSubscriptionRef(take(src, "u")),
|
|
751
|
+
downstream: (_nullishCoalesce(take(src, "d"), () => ( []))).map(cborToSubscriptionRef),
|
|
752
|
+
kind: _nullishCoalesce(take(src, "kind"), () => ( "")),
|
|
753
|
+
...optional(src, "traceId", "traceId", asTraceId),
|
|
754
|
+
...optional(src, "ns", "namespace", asByteStrings),
|
|
755
|
+
...optional(src, "tn", "trackName", asByteString),
|
|
756
|
+
...optional(src, "tdr", "tDownstreamReceived", asUintNumber),
|
|
757
|
+
...optional(src, "tus", "tUpstreamSent", asUintNumber),
|
|
758
|
+
...optional(src, "tuo", "tUpstreamOkReceived", asUintNumber),
|
|
759
|
+
...optional(src, "tdo", "tDownstreamOkSent", asUintNumber)
|
|
208
760
|
};
|
|
209
761
|
}
|
|
210
762
|
}
|
|
@@ -217,26 +769,104 @@ function writePreamble(headerCbor) {
|
|
|
217
769
|
buf.set(headerCbor, PREAMBLE_SIZE);
|
|
218
770
|
return buf;
|
|
219
771
|
}
|
|
220
|
-
function
|
|
221
|
-
if (
|
|
772
|
+
function startsWithMagic(bytes, offset) {
|
|
773
|
+
if (offset + MAGIC.length > bytes.length) return false;
|
|
774
|
+
for (let i = 0; i < MAGIC.length; i++) {
|
|
775
|
+
if (bytes[offset + i] !== MAGIC[i]) return false;
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
function validatePreamble(bytes, offset = 0) {
|
|
780
|
+
if (bytes.length - offset < PREAMBLE_SIZE) {
|
|
222
781
|
throw new Error("File too short: expected at least 16 bytes");
|
|
223
782
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
227
|
-
}
|
|
783
|
+
if (!startsWithMagic(bytes, offset)) {
|
|
784
|
+
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
228
785
|
}
|
|
229
786
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
230
|
-
const version = view.getUint32(8, true);
|
|
231
|
-
if (version
|
|
232
|
-
throw new Error(
|
|
787
|
+
const version = view.getUint32(offset + 8, true);
|
|
788
|
+
if (!SUPPORTED_VERSIONS.includes(version)) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`Unsupported format version: ${version} (supported: ${SUPPORTED_VERSIONS.join(", ")})`
|
|
791
|
+
);
|
|
233
792
|
}
|
|
234
|
-
const headerLength = view.getUint32(12, true);
|
|
235
|
-
if (PREAMBLE_SIZE + headerLength > bytes.length) {
|
|
793
|
+
const headerLength = view.getUint32(offset + 12, true);
|
|
794
|
+
if (offset + PREAMBLE_SIZE + headerLength > bytes.length) {
|
|
236
795
|
throw new Error("File truncated: header extends beyond file");
|
|
237
796
|
}
|
|
238
797
|
return { version, headerLength };
|
|
239
798
|
}
|
|
799
|
+
function flatten(segments) {
|
|
800
|
+
const first = segments[0];
|
|
801
|
+
if (first == null) return void 0;
|
|
802
|
+
if (segments.length === 1) return first;
|
|
803
|
+
return { header: first.header, events: segments.flatMap((segment) => segment.events) };
|
|
804
|
+
}
|
|
805
|
+
function findNextSegment(bytes, from) {
|
|
806
|
+
for (let offset = from; offset + MAGIC.length <= bytes.length; offset++) {
|
|
807
|
+
if (startsWithMagic(bytes, offset)) return offset;
|
|
808
|
+
}
|
|
809
|
+
return -1;
|
|
810
|
+
}
|
|
811
|
+
function readSegments(bytes, options) {
|
|
812
|
+
const recover = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _2 => _2.recover]), () => ( false));
|
|
813
|
+
const segments = [];
|
|
814
|
+
let events = [];
|
|
815
|
+
let offset = 0;
|
|
816
|
+
const skip = (from, kind, cause) => {
|
|
817
|
+
const next = findNextSegment(bytes, from + 1);
|
|
818
|
+
_optionalChain([options, 'optionalAccess', _3 => _3.onRecovered, 'optionalCall', _4 => _4({ offset: from, kind, resumedAt: next < 0 ? void 0 : next, cause })]);
|
|
819
|
+
return next;
|
|
820
|
+
};
|
|
821
|
+
validatePreamble(bytes, 0);
|
|
822
|
+
while (offset < bytes.length) {
|
|
823
|
+
if (startsWithMagic(bytes, offset)) {
|
|
824
|
+
const { headerLength } = validatePreamble(bytes, offset);
|
|
825
|
+
const start = offset + PREAMBLE_SIZE;
|
|
826
|
+
const headerBytes = bytes.subarray(start, start + headerLength);
|
|
827
|
+
let header;
|
|
828
|
+
try {
|
|
829
|
+
header = cborToHeader(decode(headerBytes));
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (!recover) throw error;
|
|
832
|
+
const next2 = skip(offset, "header", error);
|
|
833
|
+
if (next2 < 0) return segments;
|
|
834
|
+
offset = next2;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
events = [];
|
|
838
|
+
segments.push({ header, events });
|
|
839
|
+
offset = start + headerLength;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
let length;
|
|
843
|
+
let failure;
|
|
844
|
+
try {
|
|
845
|
+
length = cborItemLength(bytes, offset);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (!recover || !(error instanceof MalformedCborError)) throw error;
|
|
848
|
+
length = null;
|
|
849
|
+
failure = error;
|
|
850
|
+
}
|
|
851
|
+
if (length != null) {
|
|
852
|
+
try {
|
|
853
|
+
const item = decode(bytes.subarray(offset, offset + length));
|
|
854
|
+
events.push(cborToEvent(item));
|
|
855
|
+
offset += length;
|
|
856
|
+
continue;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (!recover) throw error;
|
|
859
|
+
failure = error;
|
|
860
|
+
}
|
|
861
|
+
} else if (!recover) {
|
|
862
|
+
throw new TruncatedTraceError(offset, segments);
|
|
863
|
+
}
|
|
864
|
+
const next = skip(offset, failure === void 0 ? "truncated" : "event", failure);
|
|
865
|
+
if (next < 0) return segments;
|
|
866
|
+
offset = next;
|
|
867
|
+
}
|
|
868
|
+
return segments;
|
|
869
|
+
}
|
|
240
870
|
function writeMoqtrace(trace) {
|
|
241
871
|
const headerCbor = encode(headerToCbor(trace.header));
|
|
242
872
|
const preamble = writePreamble(headerCbor);
|
|
@@ -256,22 +886,35 @@ function writeMoqtrace(trace) {
|
|
|
256
886
|
}
|
|
257
887
|
return result;
|
|
258
888
|
}
|
|
259
|
-
function
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
889
|
+
function writeMoqtraceSegments(segments) {
|
|
890
|
+
const chunks = segments.map((segment, index) => {
|
|
891
|
+
if (segment.header.segment == null) {
|
|
892
|
+
throw new Error(`Segment ${index} has no 'segment' metadata in its header`);
|
|
893
|
+
}
|
|
894
|
+
return writeMoqtrace(segment);
|
|
895
|
+
});
|
|
896
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
897
|
+
const result = new Uint8Array(total);
|
|
898
|
+
let offset = 0;
|
|
899
|
+
for (const chunk of chunks) {
|
|
900
|
+
result.set(chunk, offset);
|
|
901
|
+
offset += chunk.length;
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
function readMoqtrace(bytes, options) {
|
|
906
|
+
const trace = flatten(readSegments(bytes, options));
|
|
907
|
+
if (trace == null) {
|
|
908
|
+
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
269
909
|
}
|
|
270
|
-
return
|
|
910
|
+
return trace;
|
|
911
|
+
}
|
|
912
|
+
function readMoqtraceSegments(bytes, options) {
|
|
913
|
+
return readSegments(bytes, options);
|
|
271
914
|
}
|
|
272
915
|
function readMoqtraceHeader(bytes) {
|
|
273
916
|
const { headerLength } = validatePreamble(bytes);
|
|
274
|
-
const headerBytes = bytes.
|
|
917
|
+
const headerBytes = bytes.subarray(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
|
|
275
918
|
return cborToHeader(decode(headerBytes));
|
|
276
919
|
}
|
|
277
920
|
function createMoqtraceWriter(header) {
|
|
@@ -283,6 +926,12 @@ function createMoqtraceWriter(header) {
|
|
|
283
926
|
},
|
|
284
927
|
writeEvent(event) {
|
|
285
928
|
return encode(eventToCbor(event));
|
|
929
|
+
},
|
|
930
|
+
startSegment(next) {
|
|
931
|
+
if (next.segment == null) {
|
|
932
|
+
throw new Error("A segment header must carry 'segment' metadata");
|
|
933
|
+
}
|
|
934
|
+
return writePreamble(encode(headerToCbor(next)));
|
|
286
935
|
}
|
|
287
936
|
};
|
|
288
937
|
}
|
|
@@ -304,7 +953,122 @@ function traceToJSON(trace) {
|
|
|
304
953
|
);
|
|
305
954
|
}
|
|
306
955
|
|
|
956
|
+
// src/reader.ts
|
|
957
|
+
var TruncatedStreamError = class extends Error {
|
|
958
|
+
/**
|
|
959
|
+
* Offset into the whole stream at which the incomplete item begins — not
|
|
960
|
+
* where the bytes ran out. It matches the Rust reader's
|
|
961
|
+
* `MoqTraceError::Truncated { offset }`, so the same cut is named the same
|
|
962
|
+
* number in both languages.
|
|
963
|
+
*/
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* How many events reached the caller before the cut.
|
|
967
|
+
*
|
|
968
|
+
* The events themselves are gone — that is what makes this error different
|
|
969
|
+
* from {@link TruncatedTraceError} — but their count is not, and it costs one
|
|
970
|
+
* integer to keep. It is the difference between "the stream was cut" and "the
|
|
971
|
+
* stream was cut after 4,812 events at byte 190,224", and only the second
|
|
972
|
+
* tells whoever is holding a damaged capture how much of it they still have.
|
|
973
|
+
*/
|
|
974
|
+
|
|
975
|
+
constructor(offset, events) {
|
|
976
|
+
super(
|
|
977
|
+
`Trace stream truncated: the item starting at byte ${offset} is incomplete, after ${events} event${events === 1 ? "" : "s"}`
|
|
978
|
+
);
|
|
979
|
+
this.name = "TruncatedStreamError";
|
|
980
|
+
this.offset = offset;
|
|
981
|
+
this.events = events;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
function createMoqtraceReader() {
|
|
985
|
+
let buffer = new Uint8Array(0);
|
|
986
|
+
let consumed = 0;
|
|
987
|
+
let delivered = 0;
|
|
988
|
+
let current;
|
|
989
|
+
let ended = false;
|
|
990
|
+
function take2(count) {
|
|
991
|
+
const slice = buffer.subarray(0, count);
|
|
992
|
+
buffer = buffer.subarray(count);
|
|
993
|
+
consumed += count;
|
|
994
|
+
return slice;
|
|
995
|
+
}
|
|
996
|
+
function tryReadSegment() {
|
|
997
|
+
if (buffer.length < PREAMBLE_SIZE) return void 0;
|
|
998
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
999
|
+
const version = view.getUint32(8, true);
|
|
1000
|
+
if (!SUPPORTED_VERSIONS.includes(version)) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`Unsupported format version: ${version} (supported: ${SUPPORTED_VERSIONS.join(", ")})`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const headerLength = view.getUint32(12, true);
|
|
1006
|
+
if (buffer.length < PREAMBLE_SIZE + headerLength) return void 0;
|
|
1007
|
+
take2(PREAMBLE_SIZE);
|
|
1008
|
+
const headerBytes = take2(headerLength);
|
|
1009
|
+
return cborToHeader(decode(headerBytes));
|
|
1010
|
+
}
|
|
1011
|
+
function drain(atEnd) {
|
|
1012
|
+
const items = [];
|
|
1013
|
+
for (; ; ) {
|
|
1014
|
+
if (current === void 0) {
|
|
1015
|
+
if (buffer.length >= MAGIC.length && !startsWithMagic(buffer, 0)) {
|
|
1016
|
+
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
1017
|
+
}
|
|
1018
|
+
const header = tryReadSegment();
|
|
1019
|
+
if (header === void 0) break;
|
|
1020
|
+
current = header;
|
|
1021
|
+
items.push({ kind: "segment", header });
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (buffer.length === 0) break;
|
|
1025
|
+
if (buffer.length < MAGIC.length && !atEnd) break;
|
|
1026
|
+
if (startsWithMagic(buffer, 0)) {
|
|
1027
|
+
const header = tryReadSegment();
|
|
1028
|
+
if (header === void 0) break;
|
|
1029
|
+
current = header;
|
|
1030
|
+
items.push({ kind: "segment", header });
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
const length = cborItemLength(buffer, 0);
|
|
1034
|
+
if (length === null) break;
|
|
1035
|
+
const item = decode(take2(length));
|
|
1036
|
+
items.push({ kind: "event", event: cborToEvent(item) });
|
|
1037
|
+
delivered += 1;
|
|
1038
|
+
}
|
|
1039
|
+
return items;
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
push(chunk) {
|
|
1043
|
+
if (ended) throw new Error("Cannot push to a reader that has ended");
|
|
1044
|
+
if (chunk.length > 0) {
|
|
1045
|
+
const grown = new Uint8Array(buffer.length + chunk.length);
|
|
1046
|
+
grown.set(buffer, 0);
|
|
1047
|
+
grown.set(chunk, buffer.length);
|
|
1048
|
+
buffer = grown;
|
|
1049
|
+
}
|
|
1050
|
+
return drain(false);
|
|
1051
|
+
},
|
|
1052
|
+
end() {
|
|
1053
|
+
if (ended) return [];
|
|
1054
|
+
ended = true;
|
|
1055
|
+
const items = drain(true);
|
|
1056
|
+
if (buffer.length > 0 || current === void 0) {
|
|
1057
|
+
throw new TruncatedStreamError(consumed, delivered);
|
|
1058
|
+
}
|
|
1059
|
+
return items;
|
|
1060
|
+
},
|
|
1061
|
+
get header() {
|
|
1062
|
+
return current;
|
|
1063
|
+
},
|
|
1064
|
+
get pending() {
|
|
1065
|
+
return buffer.length;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
307
1070
|
// src/recorder.ts
|
|
1071
|
+
var MAX_ERROR_RAW_BYTES = 4096;
|
|
308
1072
|
var DETAIL_RANK = {
|
|
309
1073
|
control: 0,
|
|
310
1074
|
headers: 1,
|
|
@@ -315,23 +1079,44 @@ var DETAIL_RANK = {
|
|
|
315
1079
|
function createRecorder(options) {
|
|
316
1080
|
const detail = options.detail;
|
|
317
1081
|
const detailRank = DETAIL_RANK[detail];
|
|
1082
|
+
if (detailRank === void 0) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
`Unknown detail level '${detail}': cannot record at a level this build does not implement`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
318
1087
|
const maxEvents = _nullishCoalesce(options.maxEvents, () => ( 1e5));
|
|
319
1088
|
const clock = _nullishCoalesce(options.clock, () => ( (() => Math.round(performance.now() * 1e3))));
|
|
320
1089
|
const messageTypeId = _nullishCoalesce(options.messageTypeId, () => ( (() => 0)));
|
|
321
1090
|
const events = [];
|
|
1091
|
+
let head = 0;
|
|
1092
|
+
let dropped = 0;
|
|
1093
|
+
const rawRecorded = /* @__PURE__ */ new Set();
|
|
322
1094
|
let _recording = true;
|
|
323
1095
|
let _seq = 0;
|
|
324
1096
|
const startTime = Date.now();
|
|
325
1097
|
function addEvent(event) {
|
|
326
1098
|
if (!_recording) return;
|
|
327
|
-
if (events.length >= maxEvents) {
|
|
328
|
-
|
|
1099
|
+
if (events.length - head >= maxEvents) {
|
|
1100
|
+
head++;
|
|
1101
|
+
dropped++;
|
|
1102
|
+
if (head >= 4096 || head >= maxEvents) {
|
|
1103
|
+
events.splice(0, head);
|
|
1104
|
+
head = 0;
|
|
1105
|
+
}
|
|
329
1106
|
}
|
|
330
1107
|
events.push(event);
|
|
331
1108
|
}
|
|
332
1109
|
function nextSeq() {
|
|
333
1110
|
return _seq++;
|
|
334
1111
|
}
|
|
1112
|
+
function rawFlow(streamId) {
|
|
1113
|
+
return streamId == null ? "no-stream" : `stream:${streamId}`;
|
|
1114
|
+
}
|
|
1115
|
+
function claimRawSlot(flow) {
|
|
1116
|
+
if (rawRecorded.has(flow)) return false;
|
|
1117
|
+
rawRecorded.add(flow);
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
335
1120
|
function wrapSession(session) {
|
|
336
1121
|
return {
|
|
337
1122
|
get phase() {
|
|
@@ -457,14 +1242,27 @@ function createRecorder(options) {
|
|
|
457
1242
|
};
|
|
458
1243
|
addEvent(event);
|
|
459
1244
|
},
|
|
460
|
-
recordError(errorCode, reason) {
|
|
461
|
-
|
|
1245
|
+
recordError(errorCode, reason, details) {
|
|
1246
|
+
const streamId = _optionalChain([details, 'optionalAccess', _5 => _5.streamId]);
|
|
1247
|
+
const raw = _optionalChain([details, 'optionalAccess', _6 => _6.raw]);
|
|
1248
|
+
const rawLength = _optionalChain([details, 'optionalAccess', _7 => _7.rawLength]) === null ? void 0 : _nullishCoalesce(_optionalChain([details, 'optionalAccess', _8 => _8.rawLength]), () => ( _optionalChain([raw, 'optionalAccess', _9 => _9.length])));
|
|
1249
|
+
const writeRaw = raw != null && detailRank >= DETAIL_RANK.full && claimRawSlot(rawFlow(streamId));
|
|
1250
|
+
const event = {
|
|
462
1251
|
type: "error",
|
|
463
1252
|
seq: nextSeq(),
|
|
464
1253
|
timestamp: clock(),
|
|
465
1254
|
errorCode,
|
|
466
|
-
reason
|
|
467
|
-
|
|
1255
|
+
reason,
|
|
1256
|
+
...streamId != null ? { streamId } : {},
|
|
1257
|
+
..._optionalChain([details, 'optionalAccess', _10 => _10.errorKind]) != null ? { errorKind: details.errorKind } : {},
|
|
1258
|
+
// A size, gated with the other sizes and one level below the bytes: an
|
|
1259
|
+
// error may name a data stream, so at 'control' this length would be a
|
|
1260
|
+
// fact about media volume in a trace whose declared level excludes it.
|
|
1261
|
+
// Written whether or not the bytes are, which is most of its value.
|
|
1262
|
+
...rawLength != null && detailRank >= DETAIL_RANK["headers+sizes"] ? { rawLength } : {},
|
|
1263
|
+
...writeRaw && raw != null ? { raw: raw.length > MAX_ERROR_RAW_BYTES ? raw.slice(0, MAX_ERROR_RAW_BYTES) : raw } : {}
|
|
1264
|
+
};
|
|
1265
|
+
addEvent(event);
|
|
468
1266
|
},
|
|
469
1267
|
annotate(label, data) {
|
|
470
1268
|
addEvent({
|
|
@@ -486,9 +1284,18 @@ function createRecorder(options) {
|
|
|
486
1284
|
...options.transport != null ? { transport: options.transport } : {},
|
|
487
1285
|
...options.source != null ? { source: options.source } : {},
|
|
488
1286
|
...options.endpoint != null ? { endpoint: options.endpoint } : {},
|
|
489
|
-
...options.sessionId != null ? { sessionId: options.sessionId } : {}
|
|
1287
|
+
...options.sessionId != null ? { sessionId: options.sessionId } : {},
|
|
1288
|
+
// Carried through so a recorder with something to say the format has
|
|
1289
|
+
// no key for can say it, and so a header this package builds has the
|
|
1290
|
+
// same shape as one it reads. `writeMoqtrace` writes the store after
|
|
1291
|
+
// the keys above and drops any entry naming one of them.
|
|
1292
|
+
...options.extra != null ? { extra: options.extra } : {},
|
|
1293
|
+
// Only when something was actually dropped: the key's presence is
|
|
1294
|
+
// what tells a reader this trace is incomplete, so writing it with a
|
|
1295
|
+
// zero would make every complete trace look sampled.
|
|
1296
|
+
...dropped > 0 ? { sampling: { dropPolicy: "head", droppedTotal: dropped } } : {}
|
|
490
1297
|
};
|
|
491
|
-
return { header, events:
|
|
1298
|
+
return { header, events: events.slice(head) };
|
|
492
1299
|
},
|
|
493
1300
|
get recording() {
|
|
494
1301
|
return _recording;
|
|
@@ -502,4 +1309,15 @@ function createRecorder(options) {
|
|
|
502
1309
|
|
|
503
1310
|
|
|
504
1311
|
|
|
505
|
-
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
exports.FORMAT_VERSION = FORMAT_VERSION; exports.MAX_ERROR_RAW_BYTES = MAX_ERROR_RAW_BYTES; exports.MalformedHeaderError = MalformedHeaderError; exports.SUPPORTED_VERSIONS = SUPPORTED_VERSIONS; exports.TRACE_ID_LENGTH = TRACE_ID_LENGTH; exports.TruncatedStreamError = TruncatedStreamError; exports.TruncatedTraceError = TruncatedTraceError; exports.controlMessageFields = controlMessageFields; exports.createMoqtraceReader = createMoqtraceReader; exports.createMoqtraceWriter = createMoqtraceWriter; exports.createRecorder = createRecorder; exports.readMoqtrace = readMoqtrace; exports.readMoqtraceHeader = readMoqtraceHeader; exports.readMoqtraceSegments = readMoqtraceSegments; exports.traceToJSON = traceToJSON; exports.writeMoqtrace = writeMoqtrace; exports.writeMoqtraceSegments = writeMoqtraceSegments;
|