@moqtap/trace 0.3.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 +93 -5
- package/SPEC.md +707 -13
- package/dist/index.cjs +506 -95
- package/dist/index.d.cts +545 -6
- package/dist/index.d.ts +545 -6
- package/dist/index.js +504 -93
- package/package.json +5 -3
- package/src/binary.ts +776 -105
- package/src/index.ts +9 -4
- package/src/json.ts +7 -0
- package/src/reader.ts +286 -0
- package/src/recorder.ts +189 -12
- package/src/types.ts +263 -2
package/dist/index.js
CHANGED
|
@@ -137,6 +137,11 @@ function cborItemLength(bytes, start) {
|
|
|
137
137
|
|
|
138
138
|
// src/types.ts
|
|
139
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
|
+
}
|
|
140
145
|
|
|
141
146
|
// src/binary.ts
|
|
142
147
|
var codec = new Encoder({ useRecords: false, mapsAsObjects: true, tagUint8Array: false });
|
|
@@ -173,6 +178,31 @@ var INT_TO_EVENT_TYPE = {
|
|
|
173
178
|
10: "subscription-derivation"
|
|
174
179
|
};
|
|
175
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
|
+
}
|
|
176
206
|
var TruncatedTraceError = class extends Error {
|
|
177
207
|
/** Byte offset at which the incomplete item begins. */
|
|
178
208
|
offset;
|
|
@@ -193,20 +223,46 @@ var TruncatedTraceError = class extends Error {
|
|
|
193
223
|
return flatten(this.segments);
|
|
194
224
|
}
|
|
195
225
|
};
|
|
226
|
+
var MalformedHeaderError = class extends Error {
|
|
227
|
+
/** The key at fault, dotted for one inside `"segment"`. */
|
|
228
|
+
key;
|
|
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
|
+
}
|
|
196
245
|
function segmentToCbor(segment) {
|
|
197
|
-
const map = {
|
|
246
|
+
const map = {
|
|
247
|
+
sequence: requiredUint(segment.sequence, "segment.sequence")
|
|
248
|
+
};
|
|
198
249
|
if (segment.durationMs != null) map.durationMs = int(segment.durationMs);
|
|
199
250
|
if (segment.streamId != null) map.streamId = segment.streamId;
|
|
200
251
|
if (segment.continues != null) map.continues = segment.continues;
|
|
201
|
-
return map;
|
|
252
|
+
return withStore(map, segment.extra);
|
|
202
253
|
}
|
|
203
|
-
function cborToSegment(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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)
|
|
209
263
|
};
|
|
264
|
+
const extra = unrecognisedKeys(src);
|
|
265
|
+
return extra == null ? segment : { ...segment, extra };
|
|
210
266
|
}
|
|
211
267
|
function samplingToCbor(sampling) {
|
|
212
268
|
const map = {};
|
|
@@ -218,26 +274,36 @@ function samplingToCbor(sampling) {
|
|
|
218
274
|
if (sampling.rule != null) map.rule = sampling.rule;
|
|
219
275
|
if (sampling.ruleLang != null) map.ruleLang = sampling.ruleLang;
|
|
220
276
|
if (sampling.appliesTo != null) map.appliesTo = sampling.appliesTo.map(int);
|
|
221
|
-
return map;
|
|
277
|
+
return withStore(map, sampling.extra);
|
|
222
278
|
}
|
|
223
|
-
function cborToSampling(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
233
297
|
};
|
|
298
|
+
const extra = unrecognisedKeys(src);
|
|
299
|
+
return extra == null ? sampling : { ...sampling, extra };
|
|
234
300
|
}
|
|
235
301
|
function headerToCbor(header) {
|
|
236
302
|
const map = {
|
|
237
303
|
protocol: header.protocol,
|
|
238
304
|
perspective: header.perspective,
|
|
239
305
|
detail: header.detail,
|
|
240
|
-
startTime:
|
|
306
|
+
startTime: requiredUint(header.startTime, "startTime")
|
|
241
307
|
};
|
|
242
308
|
if (header.endTime != null) map.endTime = int(header.endTime);
|
|
243
309
|
if (header.transport != null) map.transport = header.transport;
|
|
@@ -247,23 +313,31 @@ function headerToCbor(header) {
|
|
|
247
313
|
if (header.segment != null) map.segment = segmentToCbor(header.segment);
|
|
248
314
|
if (header.sampling != null) map.sampling = samplingToCbor(header.sampling);
|
|
249
315
|
if (header.custom != null) map.custom = header.custom;
|
|
250
|
-
return map;
|
|
316
|
+
return withStore(map, header.extra);
|
|
251
317
|
}
|
|
252
318
|
function cborToHeader(obj) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
...
|
|
260
|
-
...
|
|
261
|
-
...
|
|
262
|
-
...
|
|
263
|
-
...
|
|
264
|
-
...
|
|
265
|
-
...
|
|
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)
|
|
266
338
|
};
|
|
339
|
+
const extra = unrecognisedKeys(src);
|
|
340
|
+
return extra == null ? header : { ...header, extra };
|
|
267
341
|
}
|
|
268
342
|
function subscriptionRefToCbor(ref) {
|
|
269
343
|
return [ref.peer, ref.requestId];
|
|
@@ -272,7 +346,12 @@ function cborToSubscriptionRef(value) {
|
|
|
272
346
|
if (!Array.isArray(value) || value.length !== 2) {
|
|
273
347
|
throw new Error("Subscription reference must be [peer, requestId]");
|
|
274
348
|
}
|
|
275
|
-
|
|
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 };
|
|
276
355
|
}
|
|
277
356
|
function toBytes(value) {
|
|
278
357
|
return value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
|
|
@@ -299,7 +378,7 @@ function eventToCbor(event) {
|
|
|
299
378
|
case "control": {
|
|
300
379
|
base.d = event.direction;
|
|
301
380
|
base.mt = int(event.messageType);
|
|
302
|
-
base.msg = event.message;
|
|
381
|
+
base.msg = event.message === void 0 ? {} : event.message;
|
|
303
382
|
if (event.streamId != null) base.sid = event.streamId;
|
|
304
383
|
if (event.raw != null) base.raw = event.raw;
|
|
305
384
|
break;
|
|
@@ -308,6 +387,10 @@ function eventToCbor(event) {
|
|
|
308
387
|
base.sid = event.streamId;
|
|
309
388
|
base.d = event.direction;
|
|
310
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;
|
|
311
394
|
break;
|
|
312
395
|
}
|
|
313
396
|
case "stream-closed": {
|
|
@@ -339,6 +422,10 @@ function eventToCbor(event) {
|
|
|
339
422
|
case "error": {
|
|
340
423
|
base.ec = int(event.errorCode);
|
|
341
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;
|
|
342
429
|
break;
|
|
343
430
|
}
|
|
344
431
|
case "annotation": {
|
|
@@ -374,14 +461,119 @@ function eventToCbor(event) {
|
|
|
374
461
|
case "unknown": {
|
|
375
462
|
base.e = event.eventType;
|
|
376
463
|
for (const [key, value] of Object.entries(event.fields)) {
|
|
377
|
-
base
|
|
464
|
+
setKey(base, key, value);
|
|
378
465
|
}
|
|
379
466
|
break;
|
|
380
467
|
}
|
|
381
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
|
+
}
|
|
474
|
+
}
|
|
382
475
|
return base;
|
|
383
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 bytes?.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
|
+
}
|
|
384
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;
|
|
385
577
|
const seq = Number(obj.n ?? 0);
|
|
386
578
|
const timestamp = Number(obj.t ?? 0);
|
|
387
579
|
const peerFields = obj.p != null ? { peer: obj.p } : {};
|
|
@@ -389,7 +581,7 @@ function cborToEvent(obj) {
|
|
|
389
581
|
if (eventType == null) {
|
|
390
582
|
const fields = {};
|
|
391
583
|
for (const [key, value] of Object.entries(obj)) {
|
|
392
|
-
if (!COMMON_EVENT_KEYS.has(key)) fields
|
|
584
|
+
if (!COMMON_EVENT_KEYS.has(key)) setKey(fields, key, value);
|
|
393
585
|
}
|
|
394
586
|
return {
|
|
395
587
|
type: "unknown",
|
|
@@ -407,11 +599,20 @@ function cborToEvent(obj) {
|
|
|
407
599
|
seq,
|
|
408
600
|
timestamp,
|
|
409
601
|
...peerFields,
|
|
410
|
-
direction:
|
|
411
|
-
messageType: Number(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
602
|
+
direction: take(src, "d"),
|
|
603
|
+
messageType: Number(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)
|
|
415
616
|
};
|
|
416
617
|
case "stream-opened":
|
|
417
618
|
return {
|
|
@@ -419,9 +620,27 @@ function cborToEvent(obj) {
|
|
|
419
620
|
seq,
|
|
420
621
|
timestamp,
|
|
421
622
|
...peerFields,
|
|
422
|
-
streamId:
|
|
423
|
-
direction:
|
|
424
|
-
streamType:
|
|
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)
|
|
425
644
|
};
|
|
426
645
|
case "stream-closed":
|
|
427
646
|
return {
|
|
@@ -429,8 +648,8 @@ function cborToEvent(obj) {
|
|
|
429
648
|
seq,
|
|
430
649
|
timestamp,
|
|
431
650
|
...peerFields,
|
|
432
|
-
streamId:
|
|
433
|
-
errorCode: Number(
|
|
651
|
+
streamId: requireUint(src, "sid"),
|
|
652
|
+
errorCode: Number(take(src, "ec") ?? 0)
|
|
434
653
|
};
|
|
435
654
|
case "object-header":
|
|
436
655
|
return {
|
|
@@ -438,11 +657,11 @@ function cborToEvent(obj) {
|
|
|
438
657
|
seq,
|
|
439
658
|
timestamp,
|
|
440
659
|
...peerFields,
|
|
441
|
-
streamId:
|
|
442
|
-
groupId:
|
|
443
|
-
objectId:
|
|
444
|
-
publisherPriority: Number(
|
|
445
|
-
objectStatus: Number(
|
|
660
|
+
streamId: requireUint(src, "sid"),
|
|
661
|
+
groupId: requireUint(src, "g"),
|
|
662
|
+
objectId: requireUint(src, "o"),
|
|
663
|
+
publisherPriority: Number(take(src, "pp") ?? 0),
|
|
664
|
+
objectStatus: Number(take(src, "os") ?? 0)
|
|
446
665
|
};
|
|
447
666
|
case "object-payload":
|
|
448
667
|
return {
|
|
@@ -450,11 +669,11 @@ function cborToEvent(obj) {
|
|
|
450
669
|
seq,
|
|
451
670
|
timestamp,
|
|
452
671
|
...peerFields,
|
|
453
|
-
streamId:
|
|
454
|
-
groupId:
|
|
455
|
-
objectId:
|
|
456
|
-
size: Number(
|
|
457
|
-
...
|
|
672
|
+
streamId: requireUint(src, "sid"),
|
|
673
|
+
groupId: requireUint(src, "g"),
|
|
674
|
+
objectId: requireUint(src, "o"),
|
|
675
|
+
size: Number(take(src, "sz") ?? 0),
|
|
676
|
+
...optional(src, "pl", "payload", asByteString)
|
|
458
677
|
};
|
|
459
678
|
case "state-change":
|
|
460
679
|
return {
|
|
@@ -462,8 +681,8 @@ function cborToEvent(obj) {
|
|
|
462
681
|
seq,
|
|
463
682
|
timestamp,
|
|
464
683
|
...peerFields,
|
|
465
|
-
from:
|
|
466
|
-
to:
|
|
684
|
+
from: take(src, "from"),
|
|
685
|
+
to: take(src, "to")
|
|
467
686
|
};
|
|
468
687
|
case "error":
|
|
469
688
|
return {
|
|
@@ -471,8 +690,24 @@ function cborToEvent(obj) {
|
|
|
471
690
|
seq,
|
|
472
691
|
timestamp,
|
|
473
692
|
...peerFields,
|
|
474
|
-
errorCode: Number(
|
|
475
|
-
reason:
|
|
693
|
+
errorCode: Number(take(src, "ec") ?? 0),
|
|
694
|
+
reason: 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)
|
|
476
711
|
};
|
|
477
712
|
case "annotation":
|
|
478
713
|
return {
|
|
@@ -480,8 +715,8 @@ function cborToEvent(obj) {
|
|
|
480
715
|
seq,
|
|
481
716
|
timestamp,
|
|
482
717
|
...peerFields,
|
|
483
|
-
label:
|
|
484
|
-
data:
|
|
718
|
+
label: take(src, "label") ?? "",
|
|
719
|
+
data: take(src, "data")
|
|
485
720
|
};
|
|
486
721
|
case "peer-connected":
|
|
487
722
|
return {
|
|
@@ -489,10 +724,13 @@ function cborToEvent(obj) {
|
|
|
489
724
|
seq,
|
|
490
725
|
timestamp,
|
|
491
726
|
...peerFields,
|
|
492
|
-
...
|
|
493
|
-
...
|
|
494
|
-
|
|
495
|
-
|
|
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)
|
|
496
734
|
};
|
|
497
735
|
case "peer-disconnected":
|
|
498
736
|
return {
|
|
@@ -500,8 +738,8 @@ function cborToEvent(obj) {
|
|
|
500
738
|
seq,
|
|
501
739
|
timestamp,
|
|
502
740
|
...peerFields,
|
|
503
|
-
errorCode: Number(
|
|
504
|
-
...
|
|
741
|
+
errorCode: Number(take(src, "ec") ?? 0),
|
|
742
|
+
...optional(src, "reason", "reason", asText)
|
|
505
743
|
};
|
|
506
744
|
case "subscription-derivation":
|
|
507
745
|
return {
|
|
@@ -509,16 +747,16 @@ function cborToEvent(obj) {
|
|
|
509
747
|
seq,
|
|
510
748
|
timestamp,
|
|
511
749
|
...peerFields,
|
|
512
|
-
upstream: cborToSubscriptionRef(
|
|
513
|
-
downstream: (
|
|
514
|
-
kind:
|
|
515
|
-
...
|
|
516
|
-
...
|
|
517
|
-
...
|
|
518
|
-
...
|
|
519
|
-
...
|
|
520
|
-
...
|
|
521
|
-
...
|
|
750
|
+
upstream: cborToSubscriptionRef(take(src, "u")),
|
|
751
|
+
downstream: (take(src, "d") ?? []).map(cborToSubscriptionRef),
|
|
752
|
+
kind: 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)
|
|
522
760
|
};
|
|
523
761
|
}
|
|
524
762
|
}
|
|
@@ -575,26 +813,40 @@ function readSegments(bytes, options) {
|
|
|
575
813
|
const segments = [];
|
|
576
814
|
let events = [];
|
|
577
815
|
let offset = 0;
|
|
816
|
+
const skip = (from, kind, cause) => {
|
|
817
|
+
const next = findNextSegment(bytes, from + 1);
|
|
818
|
+
options?.onRecovered?.({ offset: from, kind, resumedAt: next < 0 ? void 0 : next, cause });
|
|
819
|
+
return next;
|
|
820
|
+
};
|
|
578
821
|
validatePreamble(bytes, 0);
|
|
579
822
|
while (offset < bytes.length) {
|
|
580
823
|
if (startsWithMagic(bytes, offset)) {
|
|
581
824
|
const { headerLength } = validatePreamble(bytes, offset);
|
|
582
825
|
const start = offset + PREAMBLE_SIZE;
|
|
583
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
|
+
}
|
|
584
837
|
events = [];
|
|
585
|
-
segments.push({
|
|
586
|
-
header: cborToHeader(decode(headerBytes)),
|
|
587
|
-
events
|
|
588
|
-
});
|
|
838
|
+
segments.push({ header, events });
|
|
589
839
|
offset = start + headerLength;
|
|
590
840
|
continue;
|
|
591
841
|
}
|
|
592
842
|
let length;
|
|
843
|
+
let failure;
|
|
593
844
|
try {
|
|
594
845
|
length = cborItemLength(bytes, offset);
|
|
595
846
|
} catch (error) {
|
|
596
847
|
if (!recover || !(error instanceof MalformedCborError)) throw error;
|
|
597
848
|
length = null;
|
|
849
|
+
failure = error;
|
|
598
850
|
}
|
|
599
851
|
if (length != null) {
|
|
600
852
|
try {
|
|
@@ -604,11 +856,12 @@ function readSegments(bytes, options) {
|
|
|
604
856
|
continue;
|
|
605
857
|
} catch (error) {
|
|
606
858
|
if (!recover) throw error;
|
|
859
|
+
failure = error;
|
|
607
860
|
}
|
|
608
861
|
} else if (!recover) {
|
|
609
862
|
throw new TruncatedTraceError(offset, segments);
|
|
610
863
|
}
|
|
611
|
-
const next =
|
|
864
|
+
const next = skip(offset, failure === void 0 ? "truncated" : "event", failure);
|
|
612
865
|
if (next < 0) return segments;
|
|
613
866
|
offset = next;
|
|
614
867
|
}
|
|
@@ -700,7 +953,122 @@ function traceToJSON(trace) {
|
|
|
700
953
|
);
|
|
701
954
|
}
|
|
702
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
|
+
offset;
|
|
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
|
+
events;
|
|
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
|
+
|
|
703
1070
|
// src/recorder.ts
|
|
1071
|
+
var MAX_ERROR_RAW_BYTES = 4096;
|
|
704
1072
|
var DETAIL_RANK = {
|
|
705
1073
|
control: 0,
|
|
706
1074
|
headers: 1,
|
|
@@ -720,19 +1088,35 @@ function createRecorder(options) {
|
|
|
720
1088
|
const clock = options.clock ?? (() => Math.round(performance.now() * 1e3));
|
|
721
1089
|
const messageTypeId = options.messageTypeId ?? (() => 0);
|
|
722
1090
|
const events = [];
|
|
1091
|
+
let head = 0;
|
|
1092
|
+
let dropped = 0;
|
|
1093
|
+
const rawRecorded = /* @__PURE__ */ new Set();
|
|
723
1094
|
let _recording = true;
|
|
724
1095
|
let _seq = 0;
|
|
725
1096
|
const startTime = Date.now();
|
|
726
1097
|
function addEvent(event) {
|
|
727
1098
|
if (!_recording) return;
|
|
728
|
-
if (events.length >= maxEvents) {
|
|
729
|
-
|
|
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
|
+
}
|
|
730
1106
|
}
|
|
731
1107
|
events.push(event);
|
|
732
1108
|
}
|
|
733
1109
|
function nextSeq() {
|
|
734
1110
|
return _seq++;
|
|
735
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
|
+
}
|
|
736
1120
|
function wrapSession(session) {
|
|
737
1121
|
return {
|
|
738
1122
|
get phase() {
|
|
@@ -858,14 +1242,27 @@ function createRecorder(options) {
|
|
|
858
1242
|
};
|
|
859
1243
|
addEvent(event);
|
|
860
1244
|
},
|
|
861
|
-
recordError(errorCode, reason) {
|
|
862
|
-
|
|
1245
|
+
recordError(errorCode, reason, details) {
|
|
1246
|
+
const streamId = details?.streamId;
|
|
1247
|
+
const raw = details?.raw;
|
|
1248
|
+
const rawLength = details?.rawLength === null ? void 0 : details?.rawLength ?? raw?.length;
|
|
1249
|
+
const writeRaw = raw != null && detailRank >= DETAIL_RANK.full && claimRawSlot(rawFlow(streamId));
|
|
1250
|
+
const event = {
|
|
863
1251
|
type: "error",
|
|
864
1252
|
seq: nextSeq(),
|
|
865
1253
|
timestamp: clock(),
|
|
866
1254
|
errorCode,
|
|
867
|
-
reason
|
|
868
|
-
|
|
1255
|
+
reason,
|
|
1256
|
+
...streamId != null ? { streamId } : {},
|
|
1257
|
+
...details?.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);
|
|
869
1266
|
},
|
|
870
1267
|
annotate(label, data) {
|
|
871
1268
|
addEvent({
|
|
@@ -887,9 +1284,18 @@ function createRecorder(options) {
|
|
|
887
1284
|
...options.transport != null ? { transport: options.transport } : {},
|
|
888
1285
|
...options.source != null ? { source: options.source } : {},
|
|
889
1286
|
...options.endpoint != null ? { endpoint: options.endpoint } : {},
|
|
890
|
-
...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 } } : {}
|
|
891
1297
|
};
|
|
892
|
-
return { header, events:
|
|
1298
|
+
return { header, events: events.slice(head) };
|
|
893
1299
|
},
|
|
894
1300
|
get recording() {
|
|
895
1301
|
return _recording;
|
|
@@ -898,9 +1304,14 @@ function createRecorder(options) {
|
|
|
898
1304
|
}
|
|
899
1305
|
export {
|
|
900
1306
|
FORMAT_VERSION,
|
|
1307
|
+
MAX_ERROR_RAW_BYTES,
|
|
1308
|
+
MalformedHeaderError,
|
|
901
1309
|
SUPPORTED_VERSIONS,
|
|
902
1310
|
TRACE_ID_LENGTH,
|
|
1311
|
+
TruncatedStreamError,
|
|
903
1312
|
TruncatedTraceError,
|
|
1313
|
+
controlMessageFields,
|
|
1314
|
+
createMoqtraceReader,
|
|
904
1315
|
createMoqtraceWriter,
|
|
905
1316
|
createRecorder,
|
|
906
1317
|
readMoqtrace,
|