@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/dist/index.cjs 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 (0, _cborx.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
 
@@ -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
+
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 = { sequence: int(segment.sequence) };
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(obj) {
204
- return {
205
- sequence: Number(_nullishCoalesce(obj.sequence, () => ( 0))),
206
- ...obj.durationMs != null ? { durationMs: Number(obj.durationMs) } : {},
207
- ...obj.streamId != null ? { streamId: obj.streamId } : {},
208
- ...obj.continues != null ? { continues: obj.continues } : {}
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(obj) {
224
- return {
225
- ...obj.effectiveRate != null ? { effectiveRate: Number(obj.effectiveRate) } : {},
226
- ...obj.maxEventsPerSec != null ? { maxEventsPerSec: Number(obj.maxEventsPerSec) } : {},
227
- ...obj.dropPolicy != null ? { dropPolicy: obj.dropPolicy } : {},
228
- ...obj.droppedTotal != null ? { droppedTotal: Number(obj.droppedTotal) } : {},
229
- ...obj.droppedSegment != null ? { droppedSegment: Number(obj.droppedSegment) } : {},
230
- ...obj.rule != null ? { rule: obj.rule } : {},
231
- ...obj.ruleLang != null ? { ruleLang: obj.ruleLang } : {},
232
- ...obj.appliesTo != null ? { appliesTo: obj.appliesTo.map(Number) } : {}
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: int(header.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
- return {
254
- protocol: obj.protocol,
255
- perspective: obj.perspective,
256
- detail: obj.detail,
257
- startTime: Number(obj.startTime),
258
- ...obj.endTime != null ? { endTime: Number(obj.endTime) } : {},
259
- ...obj.transport != null ? { transport: obj.transport } : {},
260
- ...obj.source != null ? { source: obj.source } : {},
261
- ...obj.endpoint != null ? { endpoint: obj.endpoint } : {},
262
- ...obj.sessionId != null ? { sessionId: obj.sessionId } : {},
263
- ...obj.segment != null ? { segment: cborToSegment(obj.segment) } : {},
264
- ...obj.sampling != null ? { sampling: cborToSampling(obj.sampling) } : {},
265
- ...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)
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
- return { peer: value[0], requestId: BigInt(value[1]) };
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[key] = value;
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 _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
+ }
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(_nullishCoalesce(obj.n, () => ( 0)));
386
578
  const timestamp = Number(_nullishCoalesce(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[key] = value;
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: obj.d,
411
- messageType: Number(_nullishCoalesce(obj.mt, () => ( 0))),
412
- message: _nullishCoalesce(obj.msg, () => ( {})),
413
- ...obj.sid != null ? { streamId: BigInt(obj.sid) } : {},
414
- ...obj.raw != null ? { raw: toBytes(obj.raw) } : {}
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)
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: BigInt(obj.sid),
423
- direction: obj.d,
424
- streamType: obj.st
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: BigInt(obj.sid),
433
- errorCode: Number(_nullishCoalesce(obj.ec, () => ( 0)))
651
+ streamId: requireUint(src, "sid"),
652
+ errorCode: Number(_nullishCoalesce(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: BigInt(obj.sid),
442
- groupId: BigInt(obj.g),
443
- objectId: BigInt(obj.o),
444
- publisherPriority: Number(_nullishCoalesce(obj.pp, () => ( 0))),
445
- objectStatus: Number(_nullishCoalesce(obj.os, () => ( 0)))
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)))
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: BigInt(obj.sid),
454
- groupId: BigInt(obj.g),
455
- objectId: BigInt(obj.o),
456
- size: Number(_nullishCoalesce(obj.sz, () => ( 0))),
457
- ...obj.pl != null ? { payload: toBytes(obj.pl) } : {}
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)
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: obj.from,
466
- to: obj.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(_nullishCoalesce(obj.ec, () => ( 0))),
475
- reason: _nullishCoalesce(obj.reason, () => ( ""))
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)
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: _nullishCoalesce(obj.label, () => ( "")),
484
- data: obj.data
718
+ label: _nullishCoalesce(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
- ...obj.endpoint != null ? { endpoint: obj.endpoint } : {},
493
- ...obj.transport != null ? { transport: obj.transport } : {},
494
- ...obj.role != null ? { role: obj.role } : {},
495
- ...obj.side != null ? { side: obj.side } : {}
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(_nullishCoalesce(obj.ec, () => ( 0))),
504
- ...obj.reason != null ? { reason: obj.reason } : {}
741
+ errorCode: Number(_nullishCoalesce(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(obj.u),
513
- downstream: (_nullishCoalesce(obj.d, () => ( []))).map(cborToSubscriptionRef),
514
- kind: _nullishCoalesce(obj.kind, () => ( "")),
515
- ...obj.traceId != null ? { traceId: checkTraceId(obj.traceId) } : {},
516
- ...obj.ns != null ? { namespace: obj.ns.map(toBytes) } : {},
517
- ...obj.tn != null ? { trackName: toBytes(obj.tn) } : {},
518
- ...obj.tdr != null ? { tDownstreamReceived: Number(obj.tdr) } : {},
519
- ...obj.tus != null ? { tUpstreamSent: Number(obj.tus) } : {},
520
- ...obj.tuo != null ? { tUpstreamOkReceived: Number(obj.tuo) } : {},
521
- ...obj.tdo != null ? { tDownstreamOkSent: Number(obj.tdo) } : {}
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)
522
760
  };
523
761
  }
524
762
  }
@@ -571,30 +809,44 @@ function findNextSegment(bytes, from) {
571
809
  return -1;
572
810
  }
573
811
  function readSegments(bytes, options) {
574
- const recover = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _ => _.recover]), () => ( false));
812
+ const recover = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _2 => _2.recover]), () => ( false));
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
+ _optionalChain([options, 'optionalAccess', _3 => _3.onRecovered, 'optionalCall', _4 => _4({ 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 = findNextSegment(bytes, offset + 1);
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
+
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
+
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 = _nullishCoalesce(options.clock, () => ( (() => Math.round(performance.now() * 1e3))));
721
1089
  const messageTypeId = _nullishCoalesce(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
- 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
+ }
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
- addEvent({
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 = {
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
+ ..._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);
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: [...events] };
1298
+ return { header, events: events.slice(head) };
893
1299
  },
894
1300
  get recording() {
895
1301
  return _recording;
@@ -909,4 +1315,9 @@ function createRecorder(options) {
909
1315
 
910
1316
 
911
1317
 
912
- exports.FORMAT_VERSION = FORMAT_VERSION; exports.SUPPORTED_VERSIONS = SUPPORTED_VERSIONS; exports.TRACE_ID_LENGTH = TRACE_ID_LENGTH; exports.TruncatedTraceError = TruncatedTraceError; exports.createMoqtraceWriter = createMoqtraceWriter; exports.createRecorder = createRecorder; exports.readMoqtrace = readMoqtrace; exports.readMoqtraceHeader = readMoqtraceHeader; exports.readMoqtraceSegments = readMoqtraceSegments; exports.traceToJSON = traceToJSON; exports.writeMoqtrace = writeMoqtrace; exports.writeMoqtraceSegments = writeMoqtraceSegments;
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;