@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/dist/index.js CHANGED
@@ -1,13 +1,155 @@
1
1
  // src/binary.ts
2
2
  import { Encoder } from "cbor-x";
3
- var codec = new Encoder({ useRecords: false, mapsAsObjects: true });
3
+
4
+ // src/cbor-scan.ts
5
+ var INCOMPLETE = null;
6
+ var MalformedCborError = class extends Error {
7
+ offset;
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 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 = 1;
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"
31
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
+ offset;
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
+ segments;
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
+ }
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
+ }
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
- return {
49
- protocol: obj.protocol,
50
- perspective: obj.perspective,
51
- detail: obj.detail,
52
- startTime: obj.startTime,
53
- ...obj.endTime != null ? { endTime: obj.endTime } : {},
54
- ...obj.transport != null ? { transport: obj.transport } : {},
55
- ...obj.source != null ? { source: obj.source } : {},
56
- ...obj.endpoint != null ? { endpoint: obj.endpoint } : {},
57
- ...obj.sessionId != null ? { sessionId: obj.sessionId } : {},
58
- ...obj.custom != null ? { custom: obj.custom } : {}
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 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
+ }
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(obj.n ?? 0);
578
+ const timestamp = Number(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: "annotation",
126
- seq: Number(obj.n ?? 0),
127
- timestamp: Number(obj.t ?? 0),
128
- label: `unknown-event-${obj.e}`,
129
- data: obj
587
+ type: "unknown",
588
+ seq,
589
+ timestamp,
590
+ ...peerFields,
591
+ eventType: Number(obj.e ?? -1),
592
+ fields
130
593
  };
131
594
  }
132
- const seq = Number(obj.n ?? 0);
133
- const timestamp = Number(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
- direction: obj.d,
141
- messageType: Number(obj.mt ?? 0),
142
- message: obj.msg ?? {},
143
- ...obj.sid != null ? { streamId: BigInt(obj.sid) } : {},
144
- ...obj.raw != null ? { raw: obj.raw } : {}
601
+ ...peerFields,
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)
145
616
  };
146
617
  case "stream-opened":
147
618
  return {
148
619
  type: "stream-opened",
149
620
  seq,
150
621
  timestamp,
151
- streamId: BigInt(obj.sid),
152
- direction: obj.d,
153
- streamType: obj.st
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
- streamId: BigInt(obj.sid),
161
- errorCode: Number(obj.ec ?? 0)
650
+ ...peerFields,
651
+ streamId: requireUint(src, "sid"),
652
+ errorCode: Number(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
- streamId: BigInt(obj.sid),
169
- groupId: BigInt(obj.g),
170
- objectId: BigInt(obj.o),
171
- publisherPriority: Number(obj.pp ?? 0),
172
- objectStatus: Number(obj.os ?? 0)
659
+ ...peerFields,
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)
173
665
  };
174
666
  case "object-payload":
175
667
  return {
176
668
  type: "object-payload",
177
669
  seq,
178
670
  timestamp,
179
- streamId: BigInt(obj.sid),
180
- groupId: BigInt(obj.g),
181
- objectId: BigInt(obj.o),
182
- size: Number(obj.sz ?? 0),
183
- ...obj.pl != null ? { payload: obj.pl } : {}
671
+ ...peerFields,
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)
184
677
  };
185
678
  case "state-change":
186
679
  return {
187
680
  type: "state-change",
188
681
  seq,
189
682
  timestamp,
190
- from: obj.from,
191
- to: obj.to
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
- errorCode: Number(obj.ec ?? 0),
199
- reason: obj.reason ?? ""
692
+ ...peerFields,
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)
200
711
  };
201
712
  case "annotation":
202
713
  return {
203
714
  type: "annotation",
204
715
  seq,
205
716
  timestamp,
206
- label: obj.label ?? "",
207
- data: obj.data
717
+ ...peerFields,
718
+ label: 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(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: (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)
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 validatePreamble(bytes) {
221
- if (bytes.length < PREAMBLE_SIZE) {
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
- for (let i = 0; i < 8; i++) {
225
- if (bytes[i] !== MAGIC[i]) {
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 !== FORMAT_VERSION) {
232
- throw new Error(`Unsupported format version: ${version} (expected ${FORMAT_VERSION})`);
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 = options?.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
+ options?.onRecovered?.({ 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 readMoqtrace(bytes) {
260
- const { headerLength } = validatePreamble(bytes);
261
- const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
262
- const header = cborToHeader(decode(headerBytes));
263
- const eventBytes = bytes.slice(PREAMBLE_SIZE + headerLength);
264
- const events = [];
265
- if (eventBytes.length > 0) {
266
- decodeMultiple(eventBytes, (obj) => {
267
- events.push(cborToEvent(obj));
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;
269
902
  }
270
- return { header, events };
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");
909
+ }
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.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
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
+ 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
+
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 = options.maxEvents ?? 1e5;
319
1088
  const clock = options.clock ?? (() => Math.round(performance.now() * 1e3));
320
1089
  const messageTypeId = 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
- events.shift();
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
- addEvent({
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 = {
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
+ ...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);
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: [...events] };
1298
+ return { header, events: events.slice(head) };
492
1299
  },
493
1300
  get recording() {
494
1301
  return _recording;
@@ -496,10 +1303,21 @@ function createRecorder(options) {
496
1303
  };
497
1304
  }
498
1305
  export {
1306
+ FORMAT_VERSION,
1307
+ MAX_ERROR_RAW_BYTES,
1308
+ MalformedHeaderError,
1309
+ SUPPORTED_VERSIONS,
1310
+ TRACE_ID_LENGTH,
1311
+ TruncatedStreamError,
1312
+ TruncatedTraceError,
1313
+ controlMessageFields,
1314
+ createMoqtraceReader,
499
1315
  createMoqtraceWriter,
500
1316
  createRecorder,
501
1317
  readMoqtrace,
502
1318
  readMoqtraceHeader,
1319
+ readMoqtraceSegments,
503
1320
  traceToJSON,
504
- writeMoqtrace
1321
+ writeMoqtrace,
1322
+ writeMoqtraceSegments
505
1323
  };