@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.d.cts CHANGED
@@ -1,28 +1,205 @@
1
1
  import { SessionState } from '@moqtap/codec/session';
2
2
 
3
- type DetailLevel = 'control' | 'headers' | 'headers+sizes' | 'headers+data' | 'full';
4
- type Perspective = 'client' | 'server' | 'observer';
3
+ /**
4
+ * Any string other than the ones named beside it.
5
+ *
6
+ * Unions built with this keep editor completion for the values this version
7
+ * knows, while still accepting a value it does not. Nothing in the format
8
+ * rejects an unrecognised string: new perspectives, detail levels and drop
9
+ * policies may be added without a version bump, and a reader that refused one
10
+ * would refuse a file it can otherwise read in full.
11
+ */
12
+ type OtherString = string & Record<never, never>;
13
+ type DetailLevel = 'control' | 'headers' | 'headers+sizes' | 'headers+data' | 'full' | OtherString;
14
+ type Perspective = 'client' | 'server' | 'observer' | 'relay-tap' | OtherString;
15
+ /** Drop strategy a sampled source applied when it could not keep up. */
16
+ type DropPolicy = 'head' | 'tail' | 'sampled' | OtherString;
17
+ /** Length of a trace ID, in bytes. Fixed by the format. */
18
+ declare const TRACE_ID_LENGTH = 16;
19
+ /**
20
+ * Per-segment metadata.
21
+ *
22
+ * Present only in segmented traces. A single-shot file must not carry it —
23
+ * its absence is what tells a reader that sequence numbers and timestamps are
24
+ * file-global rather than segment-local.
25
+ */
26
+ interface SegmentInfo {
27
+ /** 0-based sequence number of this segment within the stream. */
28
+ readonly sequence: number;
29
+ /** Nominal segment duration in milliseconds. A hint; the real one may differ. */
30
+ readonly durationMs?: number;
31
+ /** Opaque identifier shared by every segment of the same logical stream. */
32
+ readonly streamId?: string;
33
+ /** True if this segment continues a previous one with the same `streamId`. */
34
+ readonly continues?: boolean;
35
+ /**
36
+ * Keys in the `"segment"` map this version does not recognise, kept verbatim
37
+ * and written back inside that map rather than at the top level.
38
+ *
39
+ * See {@link TraceHeader.extra}, which this mirrors — including the part
40
+ * about a key this version *does* know whose value it cannot use.
41
+ */
42
+ readonly extra?: Record<string, unknown>;
43
+ }
44
+ /**
45
+ * Sampling and filtering metadata. Present only when events were dropped or
46
+ * filtered at the source; its absence means the trace is complete relative to
47
+ * its detail level.
48
+ */
49
+ interface SamplingInfo {
50
+ /** Fraction of source events retained, in (0, 1]. */
51
+ readonly effectiveRate?: number;
52
+ /** Per-segment cap that triggered drops, if rate-limited. */
53
+ readonly maxEventsPerSec?: number;
54
+ /** Drop strategy applied when the cap was exceeded. */
55
+ readonly dropPolicy?: DropPolicy;
56
+ /** Cumulative events dropped since `startTime`. */
57
+ readonly droppedTotal?: number;
58
+ /** Events dropped within this segment only. */
59
+ readonly droppedSegment?: number;
60
+ /** Source-side filter rule that selected events. */
61
+ readonly rule?: string;
62
+ /** Language the rule is written in ('prefix', 'glob', 'cel'). */
63
+ readonly ruleLang?: string;
64
+ /**
65
+ * Event type IDs the drop policy was applied to.
66
+ *
67
+ * All or nothing: an array with one element that is not an event type ID is
68
+ * unusable *entire* and stays in {@link extra}. Keeping the elements that
69
+ * read cleanly would not be a partial answer but a wrong one — this key
70
+ * names the types the policy touched, and every type absent from it may be
71
+ * treated as complete, so a shortened array reports a sampled type as fully
72
+ * recorded.
73
+ */
74
+ readonly appliesTo?: number[];
75
+ /**
76
+ * Keys in the `"sampling"` map this version does not recognise, kept
77
+ * verbatim and written back inside that map rather than at the top level.
78
+ *
79
+ * See {@link TraceHeader.extra}, which this mirrors.
80
+ */
81
+ readonly extra?: Record<string, unknown>;
82
+ }
5
83
  interface TraceHeader {
6
84
  readonly protocol: string;
7
85
  readonly perspective: Perspective;
8
86
  readonly detail: DetailLevel;
87
+ /**
88
+ * Recording start (Unix epoch milliseconds). In a segmented trace this is
89
+ * the segment's start, not the stream's.
90
+ */
9
91
  readonly startTime: number;
10
92
  readonly endTime?: number;
11
93
  readonly transport?: string;
12
94
  readonly source?: string;
13
95
  readonly endpoint?: string;
14
96
  readonly sessionId?: string;
97
+ /** Present only when this header begins one segment of a segmented stream. */
98
+ readonly segment?: SegmentInfo;
99
+ /** Present only when events were dropped or filtered at the source. */
100
+ readonly sampling?: SamplingInfo;
101
+ /**
102
+ * User-defined metadata, handed back key for key and value for value.
103
+ *
104
+ * A passthrough, and the one map in the header with no store of its own:
105
+ * every key in it belongs to whoever wrote the trace, so there is no such
106
+ * thing as an unrecognised key there. A `"custom"` this reader cannot hold
107
+ * exactly — one that is not a map at all — is therefore unusable as a whole
108
+ * and goes to {@link extra}, leaving this field absent rather than typed as
109
+ * a record it is not.
110
+ */
15
111
  readonly custom?: Record<string, unknown>;
112
+ /**
113
+ * Keys in the header map that this version of the package does not
114
+ * recognise, kept verbatim.
115
+ *
116
+ * The same guarantee a `TraceEvent`'s `extra` gives one level down, and for
117
+ * the same reason: "unknown keys MUST be ignored" is a rule about reading
118
+ * *past* them, not a licence to drop them. A tool that reads a trace and
119
+ * writes it back — a redaction pass, a filter, a re-segmentation, a download
120
+ * with annotations applied — would otherwise emit a valid file that looks as
121
+ * though it never carried them, and one tool's ignorance would become
122
+ * permanent for every reader downstream of it.
123
+ *
124
+ * A key this version *does* know counts as unrecognised when its value is of
125
+ * a type the field cannot hold. `"transport": 42` is not a transport; it is
126
+ * also not nothing. Such a value is kept here rather than coerced into the
127
+ * field or dropped, {@link transport} reads as absent, and the key is
128
+ * written back unchanged. Knowing more about a key must not mean preserving
129
+ * it less.
130
+ *
131
+ * Three maps in the header carry a store — this one, {@link SegmentInfo} and
132
+ * {@link SamplingInfo} — and each is written back into the map it came from.
133
+ * One store for the whole header would not do: a private key on `"segment"`
134
+ * and a key of the same name at the top level are different keys, and
135
+ * re-emitting either in the other's map changes what the file says.
136
+ */
137
+ readonly extra?: Record<string, unknown>;
16
138
  }
17
139
  interface BaseEvent {
140
+ /**
141
+ * Monotonically increasing sequence number. Segment-local in a segmented
142
+ * trace, so ordering across segments is `(segment.sequence, seq)`.
143
+ */
18
144
  readonly seq: number;
145
+ /** Microseconds since the containing segment's `startTime`. */
19
146
  readonly timestamp: number;
147
+ /**
148
+ * Which peer this event pertains to.
149
+ *
150
+ * Required when the perspective is `'relay-tap'`, where one trace covers
151
+ * many concurrent sessions; omitted otherwise. The identifier is
152
+ * source-local: the same string in two traces from different sources does
153
+ * not name the same peer.
154
+ */
155
+ readonly peer?: string;
156
+ /**
157
+ * Keys on this event that this version of the package does not recognise,
158
+ * kept verbatim.
159
+ *
160
+ * Optional keys may be added to an existing event type without a format
161
+ * version bump, so "unknown keys MUST be ignored" is a rule about *reading
162
+ * past* them. It is not a licence to drop them: a tool that reads a trace
163
+ * and writes it back — a redaction pass, a filter, an annotated download —
164
+ * would otherwise emit a valid file that looks like it never carried them,
165
+ * and one tool's ignorance would become permanent for every reader
166
+ * downstream of it.
167
+ *
168
+ * A key this version *does* know counts as unrecognised when its value is of
169
+ * a type the field cannot hold — a `"ta"` carrying text, say. It is kept
170
+ * here rather than coerced into the field or dropped, and written back
171
+ * unchanged, exactly as a key nobody had heard of would be. Knowing more
172
+ * about a key must not mean preserving it less.
173
+ *
174
+ * {@link UnknownEvent} already does this for an event type the package
175
+ * cannot name. This is the same guarantee one level down, for a key on a
176
+ * type it can. It is always absent on an `UnknownEvent`, whose `fields`
177
+ * hold every non-common key already.
178
+ */
179
+ readonly extra?: Record<string, unknown>;
20
180
  }
21
181
  interface ControlMessageEvent extends BaseEvent {
22
182
  readonly type: 'control';
23
183
  readonly direction: 0 | 1;
24
184
  readonly messageType: number;
25
- readonly message: Record<string, unknown>;
185
+ /**
186
+ * The message's decoded fields — the `"msg"` field of Event 0.
187
+ *
188
+ * A writer MUST produce a map here, keyed in snake_case (`request_id`,
189
+ * `track_alias`), and an empty one when it decoded nothing. A *reader* has
190
+ * to accept less, because files written before the format said so exist:
191
+ * every `capture-*` case in the conformance corpus carries a Rust `Debug`
192
+ * rendering of the message instead — `'Draft16(ClientSetup(ClientSetup {
193
+ * parameters: [] }))'` — and the format requires such a value to be handed
194
+ * back unchanged rather than rejected or replaced.
195
+ *
196
+ * So the type is `unknown`, not `Record<string, unknown>`. The `Record` was
197
+ * a lie the compiler could not catch: `event.message.request_id` typechecks
198
+ * against a string and reads `undefined` at runtime, on exactly the files
199
+ * this rule exists for. Narrow before reading keys — {@link
200
+ * controlMessageFields} does it correctly.
201
+ */
202
+ readonly message: unknown;
26
203
  readonly raw?: Uint8Array;
27
204
  /**
28
205
  * QUIC stream the message travelled on.
@@ -34,11 +211,80 @@ interface ControlMessageEvent extends BaseEvent {
34
211
  */
35
212
  readonly streamId?: bigint;
36
213
  }
214
+ /**
215
+ * The decoded fields of a control message, or `undefined` when its `"msg"` is
216
+ * not a map and there are no fields to address.
217
+ *
218
+ * Provided because the narrowing is easy to get wrong once per call site and
219
+ * silently: `typeof message === 'object'` also admits `null`, a CBOR array,
220
+ * and a byte string, each of which would then be typed as a field map and
221
+ * read back `undefined` for every key. A caller that skips the check entirely
222
+ * gets the bug this function exists to prevent — see
223
+ * {@link ControlMessageEvent.message}.
224
+ */
225
+ declare function controlMessageFields(message: unknown): Record<string, unknown> | undefined;
37
226
  interface StreamOpenedEvent extends BaseEvent {
38
227
  readonly type: 'stream-opened';
39
228
  readonly streamId: bigint;
40
229
  readonly direction: 0 | 1;
41
230
  readonly streamType: 0 | 1 | 2;
231
+ /**
232
+ * Track alias the stream carries. Written whenever the recorder knows it.
233
+ *
234
+ * The stream header carries the alias and no detail level records that
235
+ * header's bytes, so before this field a `'headers'` recording could not say
236
+ * which track a stream belonged to — which is most of what the level exists
237
+ * to answer, and there was nothing left to re-parse it from.
238
+ *
239
+ * `bigint`, and so are the three fields below, unlike this event's
240
+ * `direction` and `streamType`. Those two are small enumerations that a
241
+ * narrow type suits; these four are wire identifiers with the same range as
242
+ * {@link ObjectHeaderEvent}'s `groupId`, `objectId` and `streamId`, and
243
+ * readers compare the two events. Typed as `number` here, a match would read
244
+ * as a mismatch: `streamOpened.groupId === objectHeader.groupId` evaluates
245
+ * `42 === 42n`, which is `false`, with no error, for every pair of values
246
+ * that are in fact the same.
247
+ *
248
+ * Absent when the file's `"ta"` was not an unsigned integer — and the same
249
+ * for the three fields below. Such a value belongs to no field here, so it
250
+ * stays verbatim in {@link BaseEvent.extra} and is written back as it came.
251
+ * It is never coerced on the way in: `true` and the text `"4"` are not track
252
+ * alias 1 and track alias 4, however willingly `BigInt` converts them.
253
+ */
254
+ readonly trackAlias?: bigint;
255
+ /**
256
+ * Subgroup ID, on a subgroup stream (`streamType === 0`).
257
+ *
258
+ * A writer must not set it on another stream type, where nothing supplies
259
+ * it. A reader that meets one there keeps it rather than rejecting the
260
+ * event: an out-of-scope key makes one field meaningless, not the file
261
+ * unreadable, and discarding the event throws away every other field on it —
262
+ * all of which were fine. That is about the key being in the wrong place; a
263
+ * `"sg"` of the wrong *shape* is not held here at all, but in
264
+ * {@link BaseEvent.extra}.
265
+ */
266
+ readonly subgroupId?: bigint;
267
+ /**
268
+ * Fetch request ID, on a fetch stream (`streamType === 2`).
269
+ *
270
+ * A writer MUST set it there. It is the only correlation between the stream
271
+ * and the FETCH that asked for it, and without it a fetch stream cannot be
272
+ * attributed to a request at all. A reader must nevertheless accept a fetch
273
+ * stream that lacks one — every recording made before the key existed lacks
274
+ * it — for the same reason as the `'msg'` asymmetry on
275
+ * {@link ControlMessageEvent.message}: writers conform, readers tolerate.
276
+ */
277
+ readonly fetchRequestId?: bigint;
278
+ /**
279
+ * Group ID, on a datagram stream (`streamType === 1`).
280
+ *
281
+ * Scoped to datagrams because on a subgroup stream every object carries the
282
+ * same group by construction, so a copy here would be a second field with no
283
+ * independent source. Where an {@link ObjectHeaderEvent} for the same stream
284
+ * disagrees, that event wins and this is not corruption: this copy is a
285
+ * convenience for a reader that has not yet seen an object.
286
+ */
287
+ readonly groupId?: bigint;
42
288
  }
43
289
  interface StreamClosedEvent extends BaseEvent {
44
290
  readonly type: 'stream-closed';
@@ -66,17 +312,187 @@ interface StateChangeEvent extends BaseEvent {
66
312
  readonly from: string;
67
313
  readonly to: string;
68
314
  }
315
+ /**
316
+ * What sort of failure an error event records.
317
+ *
318
+ * An open vocabulary: this revision names the three below, others may be added
319
+ * without a format version bump, and a reader must preserve a value it does not
320
+ * recognise rather than reject the event — the same contract as {@link
321
+ * Perspective} and {@link DetailLevel}, which is why all three widen with
322
+ * `OtherString` instead of being closed unions.
323
+ */
324
+ type ErrorKind =
325
+ /** The peer violated the protocol. */
326
+ 'protocol'
327
+ /** The QUIC or WebTransport layer failed. */
328
+ | 'transport'
329
+ /** Bytes that would not parse as any message this recorder knows. */
330
+ | 'decode' | OtherString;
69
331
  interface TraceErrorEvent extends BaseEvent {
70
332
  readonly type: 'error';
71
333
  readonly errorCode: number;
72
334
  readonly reason: string;
335
+ /**
336
+ * QUIC stream the error was observed on.
337
+ *
338
+ * Optional on the same terms as {@link ControlMessageEvent.streamId}: absent
339
+ * means there was no stream, or that the recorder knew of none, which a
340
+ * reader MUST distinguish from stream `0`. `bigint` for the reason given
341
+ * there — it is the same wire identifier, and `number` stops being exact at
342
+ * 2^53, which would make this reader disagree with the Rust one silently and
343
+ * only for large values.
344
+ *
345
+ * It may name a *data* stream, which is why {@link rawLength} is gated a
346
+ * level above this event rather than travelling with it: the length behind an
347
+ * error on a subgroup stream is the size of a run of media framing.
348
+ */
349
+ readonly streamId?: bigint;
350
+ /**
351
+ * What sort of failure this was.
352
+ *
353
+ * Spelt out rather than `kind` because {@link SubscriptionDerivationEvent}
354
+ * already owns that name with the unrelated vocabulary `'created'` /
355
+ * `'shared'`. Two vocabularies under one name survive in a typed reader and
356
+ * break in every flat projection of a trace — a JSONL export, a warehouse
357
+ * load, this package's own {@link traceToJSON} — where they become one column
358
+ * holding both. The wire key is `"ek"` for the same reason.
359
+ */
360
+ readonly errorKind?: ErrorKind;
361
+ /**
362
+ * How many bytes the recorder held for this error, before any truncation.
363
+ *
364
+ * Not the length of {@link raw}: where the two differ the capture is partial,
365
+ * and this is how a reader learns how much is missing. A `raw` of exactly
366
+ * `MAX_ERROR_RAW_BYTES` with no `rawLength` beside it must be treated as
367
+ * possibly truncated, that being the one length the cap makes ambiguous.
368
+ *
369
+ * Recorded from `'headers+sizes'` up — one level below {@link raw} and
370
+ * deliberately not with it. It is a size and carries no content, so it is
371
+ * available in every trace where the bytes themselves must not appear, and
372
+ * how large a malformed message was often separates a truncated message from
373
+ * a mistyped one on its own.
374
+ */
375
+ readonly rawLength?: number;
376
+ /**
377
+ * The bytes behind the error.
378
+ *
379
+ * Payload-bearing, and recorded only at `'full'`: an error naming a data
380
+ * stream has subgroup framing and object payload behind it, so inheriting
381
+ * this event's own `control`+ level would have put media into traces whose
382
+ * declared level excludes payloads entirely.
383
+ *
384
+ * The 4096-byte cap on this field binds a *recorder* — see
385
+ * `MAX_ERROR_RAW_BYTES` in `recorder.ts`, which is where it is applied and
386
+ * the only place it appears. It does not bind this type, the reader or the
387
+ * writer: a longer value read out of a file is handed over whole and written
388
+ * back whole. Re-truncating it would destroy evidence to make someone else's
389
+ * file conform to a rule that was never addressed to the tool doing the
390
+ * truncating.
391
+ */
392
+ readonly raw?: Uint8Array;
73
393
  }
74
394
  interface AnnotationEvent extends BaseEvent {
75
395
  readonly type: 'annotation';
76
396
  readonly label: string;
77
397
  readonly data: unknown;
78
398
  }
79
- type TraceEvent = ControlMessageEvent | StreamOpenedEvent | StreamClosedEvent | ObjectHeaderEvent | ObjectPayloadEvent | StateChangeEvent | TraceErrorEvent | AnnotationEvent;
399
+ /** Role a peer is acting in at connection time. */
400
+ type PeerRole = 'publisher' | 'subscriber' | 'both' | OtherString;
401
+ /**
402
+ * Which end of a relay a peer sits on.
403
+ *
404
+ * This is the direction the connection was made in, not the direction
405
+ * subscriptions flow. For subscription causality see
406
+ * {@link SubscriptionDerivationEvent}.
407
+ */
408
+ type Side = 'downstream' | 'upstream' | OtherString;
409
+ /**
410
+ * A new peer session was established. Emitted only by `'relay-tap'` sources;
411
+ * the peer it identifies is on the event's `peer` field.
412
+ */
413
+ interface PeerConnectedEvent extends BaseEvent {
414
+ readonly type: 'peer-connected';
415
+ readonly endpoint?: string;
416
+ readonly transport?: string;
417
+ readonly role?: PeerRole;
418
+ readonly side?: Side;
419
+ }
420
+ /** A peer session ended. Emitted only by `'relay-tap'` sources. */
421
+ interface PeerDisconnectedEvent extends BaseEvent {
422
+ readonly type: 'peer-disconnected';
423
+ readonly errorCode: number;
424
+ readonly reason?: string;
425
+ }
426
+ /**
427
+ * A `(peer, request id)` pair naming one subscription on one peer.
428
+ *
429
+ * Request ids are unique only within a peer's session, so neither half
430
+ * identifies a subscription on its own.
431
+ */
432
+ interface SubscriptionRef {
433
+ readonly peer: string;
434
+ readonly requestId: bigint;
435
+ }
436
+ /** How an upstream subscription came to serve a downstream one. */
437
+ type DerivationKind = 'created' | 'shared' | OtherString;
438
+ /**
439
+ * An upstream subscription was created or extended in causal response to
440
+ * downstream ones. Emitted only by `'relay-tap'` sources.
441
+ *
442
+ * This is the primitive multi-hop correlation is built from: a collector
443
+ * reconstructs an end-to-end tree from these links plus the trace ids
444
+ * propagated along them.
445
+ *
446
+ * The four timestamps share the `timestamp` field's timebase — the emitting
447
+ * source's own clock. Differences between them are meaningful with no
448
+ * cross-hop clock agreement, which is the point: they measure how long one
449
+ * hop took. Never subtract one source's timestamp from another's.
450
+ *
451
+ * A source emits the event as soon as the downstream SUBSCRIBE arrives,
452
+ * carrying whichever timestamps it has, and may emit it again for the same
453
+ * pair as the rest arrive. Treat a later event for the same pair as an update
454
+ * to the earlier one, not as a second derivation.
455
+ */
456
+ interface SubscriptionDerivationEvent extends BaseEvent {
457
+ readonly type: 'subscription-derivation';
458
+ readonly upstream: SubscriptionRef;
459
+ /** Every downstream subscription currently served by `upstream`. */
460
+ readonly downstream: SubscriptionRef[];
461
+ readonly kind: DerivationKind;
462
+ /**
463
+ * Trace id propagated along the subscription chain: exactly
464
+ * {@link TRACE_ID_LENGTH} raw bytes, never hex- or base64-encoded, so two
465
+ * implementations produce identical values for the same chain.
466
+ */
467
+ readonly traceId?: Uint8Array;
468
+ /** Track namespace the subscription targets, one entry per field. */
469
+ readonly namespace?: Uint8Array[];
470
+ readonly trackName?: Uint8Array;
471
+ /** When the downstream SUBSCRIBE was received. */
472
+ readonly tDownstreamReceived?: number;
473
+ /** When the upstream SUBSCRIBE was sent. Absent for a terminal subscription. */
474
+ readonly tUpstreamSent?: number;
475
+ /** When SUBSCRIBE_OK arrived from upstream. */
476
+ readonly tUpstreamOkReceived?: number;
477
+ /** When SUBSCRIBE_OK was sent downstream. */
478
+ readonly tDownstreamOkSent?: number;
479
+ }
480
+ /**
481
+ * An event whose type this version of the package does not know.
482
+ *
483
+ * New event types may be added without a format version bump, so a reader
484
+ * that dropped or relabelled them would make one tool's ignorance permanent
485
+ * for everything downstream of it. The fields are kept verbatim, so an
486
+ * unknown event survives a read-modify-write round trip intact.
487
+ */
488
+ interface UnknownEvent extends BaseEvent {
489
+ readonly type: 'unknown';
490
+ /** The `"e"` discriminant that was read. */
491
+ readonly eventType: number;
492
+ /** Every key from the event map other than `n`, `t`, `p` and `e`. */
493
+ readonly fields: Record<string, unknown>;
494
+ }
495
+ type TraceEvent = ControlMessageEvent | StreamOpenedEvent | StreamClosedEvent | ObjectHeaderEvent | ObjectPayloadEvent | StateChangeEvent | TraceErrorEvent | AnnotationEvent | PeerConnectedEvent | PeerDisconnectedEvent | SubscriptionDerivationEvent | UnknownEvent;
80
496
  interface Trace {
81
497
  readonly header: TraceHeader;
82
498
  readonly events: TraceEvent[];
@@ -89,22 +505,166 @@ interface RecorderOptions {
89
505
  readonly source?: string;
90
506
  readonly endpoint?: string;
91
507
  readonly sessionId?: string;
508
+ /**
509
+ * Extra header keys to write, for a recorder that has something to say the
510
+ * format has no key for.
511
+ *
512
+ * Goes to {@link TraceHeader.extra} on the finalized trace and is written
513
+ * into the header map after the keys above. Use the `"x-"` prefix the format
514
+ * reserves for private use; a key the format defines is dropped here rather
515
+ * than written twice, since the field always wins.
516
+ */
517
+ readonly extra?: Record<string, unknown>;
92
518
  readonly maxEvents?: number;
93
519
  readonly clock?: () => number;
94
520
  /** Map message type name (e.g. 'subscribe') to wire ID (e.g. 0x03). Required for session-layer recording. */
95
521
  readonly messageTypeId?: (name: string) => number;
96
522
  }
97
523
 
524
+ /** Format version this package writes. */
525
+ declare const FORMAT_VERSION = 2;
526
+ /**
527
+ * Format versions this package reads.
528
+ *
529
+ * Version 1 is a non-segmented version 2 trace carrying none of the keys
530
+ * version 2 added, so reading it costs nothing and keeps every capture taken
531
+ * before the bump openable.
532
+ */
533
+ declare const SUPPORTED_VERSIONS: readonly number[];
534
+ /** The stream ended part-way through an item — the file was truncated. */
535
+ declare class TruncatedTraceError extends Error {
536
+ /** Byte offset at which the incomplete item begins. */
537
+ readonly offset: number;
538
+ /**
539
+ * Everything that decoded before the cut, one entry per segment. A
540
+ * truncated trace is still evidence, and the events before the cut are
541
+ * exactly as valid as they were.
542
+ */
543
+ readonly segments: Trace[];
544
+ constructor(offset: number, segments: Trace[]);
545
+ /** What decoded, flattened as {@link readMoqtrace} would return it. */
546
+ get trace(): Trace | undefined;
547
+ }
548
+ /**
549
+ * A header carried no usable value for a key there is no header without.
550
+ *
551
+ * Five keys reach here: the four the format requires — `"protocol"`,
552
+ * `"perspective"`, `"detail"`, `"startTime"` — and `"segment.sequence"`, the
553
+ * sole ordering key of a segmented stream. Every other key in the header is
554
+ * optional, and an optional key's value never fails a file: an unusable one
555
+ * goes to the store its map keeps and the read continues.
556
+ *
557
+ * The alternative is worse than the error. A reader that fills a missing
558
+ * required key with `undefined`, an empty string or `0` returns a header the
559
+ * file never carried, and nothing downstream can tell it from a real one — a
560
+ * fabricated trace reported as evidence. `"segment.sequence"` is the sharpest
561
+ * case: the default `0` turns a segment whose place in the stream is unknown
562
+ * into one that claims to be first.
563
+ *
564
+ * Malformed here means malformed for *that segment*. {@link ReadOptions.recover}
565
+ * skips it and carries on at the next one, on the same terms as a malformed
566
+ * event.
567
+ *
568
+ * The writer throws the same error for the same key, before it emits anything.
569
+ * `"startTime"` and `"segment.sequence"` are `number` fields and TypeScript
570
+ * admits `-5` and `1.5` into both, so a header built by hand could be written
571
+ * to a file this package's own reader then refused — a writer emitting files
572
+ * only some other tool can open, out of a caller mistake nothing reported at
573
+ * the point it was made. One error type for both directions because it is one
574
+ * fault about one key, and a read-modify-write catches it once.
575
+ */
576
+ declare class MalformedHeaderError extends Error {
577
+ /** The key at fault, dotted for one inside `"segment"`. */
578
+ readonly key: string;
579
+ /**
580
+ * @param key the key at fault
581
+ * @param fault what was wrong with it, as a predicate on the key
582
+ */
583
+ constructor(key: string, fault: string);
584
+ }
585
+ /** Options shared by the readers. */
586
+ interface ReadOptions {
587
+ /**
588
+ * On a truncated or corrupt region, skip forward to the next segment and
589
+ * carry on instead of throwing.
590
+ *
591
+ * This is the recovery segmentation exists to offer: one damaged segment
592
+ * costs that segment rather than the rest of the capture. It can only help
593
+ * a segmented trace — there is nothing to resynchronize to in a single
594
+ * segment, so a damaged one still ends the read, just without an error.
595
+ */
596
+ readonly recover?: boolean;
597
+ /**
598
+ * Called once for each region `recover` skipped.
599
+ *
600
+ * Without it a recovered read cannot be told from a clean one: the return
601
+ * type carries no error channel, so a trace that lost a segment and a trace
602
+ * that never had one are the same value.
603
+ */
604
+ readonly onRecovered?: (region: RecoveredRegion) => void;
605
+ }
606
+ /** A region `recover` could not read. */
607
+ interface RecoveredRegion {
608
+ /** Byte offset the unreadable region began at. */
609
+ readonly offset: number;
610
+ /**
611
+ * What was lost: `header` drops the whole segment, `event` cuts the segment
612
+ * in progress short, `truncated` means the item ran past end of file.
613
+ */
614
+ readonly kind: 'header' | 'event' | 'truncated';
615
+ /**
616
+ * Offset the read resumed at, or `undefined` when no further segment
617
+ * followed — everything from `offset` on was then discarded.
618
+ */
619
+ readonly resumedAt: number | undefined;
620
+ /** The decode failure, absent when the region was merely truncated. */
621
+ readonly cause?: unknown;
622
+ }
98
623
  /**
99
624
  * Write a complete trace to .moqtrace binary format.
100
625
  */
101
626
  declare function writeMoqtrace(trace: Trace): Uint8Array;
627
+ /**
628
+ * Write several segments back to back as one segmented `.moqtrace` stream.
629
+ *
630
+ * Every segment header must carry {@link SegmentInfo}. That field is what
631
+ * tells a reader the sequence numbers and timestamps it is about to see
632
+ * restart at zero; a segmented stream written without it reads as a run of
633
+ * complete files whose timeline jumps backwards at every boundary, and
634
+ * nothing downstream can detect that it happened.
635
+ */
636
+ declare function writeMoqtraceSegments(segments: Trace[]): Uint8Array;
102
637
  /**
103
638
  * Read a complete .moqtrace file from bytes.
639
+ *
640
+ * A segmented file is flattened: the returned header is the first segment's,
641
+ * and the events of every segment follow in order. Use
642
+ * {@link readMoqtraceSegments} to see the boundaries — you need them to order
643
+ * events across segments, since `seq` and `timestamp` restart in each.
644
+ *
645
+ * @throws {TruncatedTraceError} if the file stops part-way through an item.
646
+ * The error carries everything that decoded before the cut.
647
+ * @throws {MalformedHeaderError} if a segment header carries no usable value
648
+ * for a required key. Pass `recover` to skip that segment instead.
104
649
  */
105
- declare function readMoqtrace(bytes: Uint8Array): Trace;
650
+ declare function readMoqtrace(bytes: Uint8Array, options?: ReadOptions): Trace;
651
+ /**
652
+ * Read a .moqtrace file as its segments, one entry per segment.
653
+ *
654
+ * A non-segmented file yields exactly one entry, whose header carries no
655
+ * `segment` field.
656
+ *
657
+ * @throws {MalformedHeaderError} as {@link readMoqtrace} does.
658
+ */
659
+ declare function readMoqtraceSegments(bytes: Uint8Array, options?: ReadOptions): Trace[];
106
660
  /**
107
661
  * Read only the header from a .moqtrace file (fast metadata peek).
662
+ *
663
+ * In a segmented file this is the first segment's header.
664
+ *
665
+ * @throws {MalformedHeaderError} if that header carries no usable value for a
666
+ * required key. There is no partial header to hand back and no later segment
667
+ * to fall forward to, so this one always throws.
108
668
  */
109
669
  declare function readMoqtraceHeader(bytes: Uint8Array): TraceHeader;
110
670
  /**
@@ -115,6 +675,16 @@ interface MoqtraceWriter {
115
675
  preamble(): Uint8Array;
116
676
  /** Encode a single event. Append the returned bytes after the preamble. */
117
677
  writeEvent(event: TraceEvent): Uint8Array;
678
+ /**
679
+ * Begin a new segment: returns a fresh preamble to append after the
680
+ * previous segment's last event.
681
+ *
682
+ * The header must carry {@link SegmentInfo} — see
683
+ * {@link writeMoqtraceSegments} for why. It should keep the same
684
+ * `protocol`, `perspective`, `detail` and `sessionId` as the segments
685
+ * before it, and increment `segment.sequence`.
686
+ */
687
+ startSegment(header: TraceHeader): Uint8Array;
118
688
  }
119
689
  declare function createMoqtraceWriter(header: TraceHeader): MoqtraceWriter;
120
690
 
@@ -125,15 +695,223 @@ declare function createMoqtraceWriter(header: TraceHeader): MoqtraceWriter;
125
695
  * This is a one-way export for human inspection and debugging.
126
696
  * For lossless round-tripping, use the binary .moqtrace format
127
697
  * (`writeMoqtrace` / `readMoqtrace`).
698
+ *
699
+ * The unrecognised-key stores are part of the rendering rather than filtered
700
+ * out of it: `extra` appears on the header, on `segment`, on `sampling` and on
701
+ * each event, nested values and all. A view that showed only the keys this
702
+ * version names would reintroduce, for anyone reading a trace through it,
703
+ * exactly the loss the binary reader stopped making — and it is the view
704
+ * someone reaches for precisely when they suspect a key went missing.
128
705
  */
129
706
  declare function traceToJSON(trace: Trace): string;
130
707
 
708
+ /**
709
+ * Incremental reader for the `.moqtrace` binary format.
710
+ *
711
+ * The three entry points in `binary.ts` all take the whole file at once, which
712
+ * a live capture does not have: the bytes arrive in chunks whose boundaries
713
+ * fall wherever the transport put them, almost never on an item boundary. This
714
+ * reader takes those chunks and hands back the items that have become whole,
715
+ * keeping the remainder for the next one.
716
+ *
717
+ * It is the read side of {@link createMoqtraceWriter}, which has been
718
+ * incremental from the start — the asymmetry this closes was one-sided.
719
+ *
720
+ * Decoding is not reimplemented here. The header and event decoders are
721
+ * imported from `binary.ts` so that a trace read in instalments and the same
722
+ * trace read whole produce the same values. A second decoder would be a second
723
+ * set of answers, and nothing tests two readers of one format against each
724
+ * other, so the disagreement would surface as a rendering bug somewhere far
725
+ * from here.
726
+ */
727
+
728
+ /**
729
+ * The stream ended part-way through an item.
730
+ *
731
+ * Deliberately not `TruncatedTraceError`, whose `segments` field promises
732
+ * "everything that decoded before the cut". An incremental reader has already
733
+ * handed those events to its caller and does not keep them, so it cannot fill
734
+ * that field — and filling it with an empty array would report a trace that
735
+ * decoded nothing, which is a wrong answer rather than a missing one. The
736
+ * caller holds what decoded; this says only where the bytes stopped.
737
+ */
738
+ declare class TruncatedStreamError extends Error {
739
+ /**
740
+ * Offset into the whole stream at which the incomplete item begins — not
741
+ * where the bytes ran out. It matches the Rust reader's
742
+ * `MoqTraceError::Truncated { offset }`, so the same cut is named the same
743
+ * number in both languages.
744
+ */
745
+ readonly offset: number;
746
+ /**
747
+ * How many events reached the caller before the cut.
748
+ *
749
+ * The events themselves are gone — that is what makes this error different
750
+ * from {@link TruncatedTraceError} — but their count is not, and it costs one
751
+ * integer to keep. It is the difference between "the stream was cut" and "the
752
+ * stream was cut after 4,812 events at byte 190,224", and only the second
753
+ * tells whoever is holding a damaged capture how much of it they still have.
754
+ */
755
+ readonly events: number;
756
+ constructor(offset: number, events: number);
757
+ }
758
+ /**
759
+ * One thing the reader has finished reading.
760
+ *
761
+ * The two kinds mirror the Rust crate's `ReadItem`, so a consumer that has read
762
+ * one implementation recognises the other. A `segment` arrives before any event
763
+ * that belongs to it, including the first one: unlike the Rust reader, which
764
+ * takes a blocking source and parses the opening header in its constructor,
765
+ * this one cannot have a header until bytes arrive, so every header reaches the
766
+ * caller the same way and there is no special case for the first.
767
+ */
768
+ type ReadItem = {
769
+ readonly kind: 'segment';
770
+ readonly header: TraceHeader;
771
+ } | {
772
+ readonly kind: 'event';
773
+ readonly event: TraceEvent;
774
+ };
775
+ /** Reads a `.moqtrace` stream in whatever instalments it arrives in. */
776
+ interface MoqtraceReader {
777
+ /**
778
+ * Add the next chunk and take everything it completed.
779
+ *
780
+ * Returns an empty array when the chunk did not finish an item, which is
781
+ * ordinary and not a signal of anything. Bytes left over are kept.
782
+ *
783
+ * @throws {MalformedHeaderError} if a segment header carries no usable value
784
+ * for a key the format requires.
785
+ * @throws {MalformedCborError} if the bytes at an item boundary are not a
786
+ * well-formed CBOR item.
787
+ */
788
+ push(chunk: Uint8Array): ReadItem[];
789
+ /**
790
+ * Declare the stream complete and take any last item.
791
+ *
792
+ * This can return items. An event shorter than the magic cannot be told from
793
+ * the start of a segment preamble while more bytes might still arrive, so
794
+ * such an event waits here until `end()` rules the preamble out.
795
+ *
796
+ * @throws {TruncatedStreamError} if bytes remain that do not form a whole
797
+ * item — the file stops mid-event, mid-preamble or mid-header.
798
+ */
799
+ end(): ReadItem[];
800
+ /**
801
+ * The header of the segment now being read, or `undefined` before the first
802
+ * one is whole. In a segmented stream this changes at every boundary.
803
+ */
804
+ readonly header: TraceHeader | undefined;
805
+ /** Bytes held back because they do not yet form a whole item. */
806
+ readonly pending: number;
807
+ }
808
+ /**
809
+ * A reader that takes the stream in chunks.
810
+ *
811
+ * ```ts
812
+ * const reader = createMoqtraceReader()
813
+ * for await (const chunk of stream) {
814
+ * for (const item of reader.push(chunk)) {
815
+ * if (item.kind === 'event') render(item.event)
816
+ * }
817
+ * }
818
+ * for (const item of reader.end()) { ... }
819
+ * ```
820
+ *
821
+ * `ReadOptions.recover` has no counterpart here **on purpose.** Its behaviour
822
+ * in the whole-buffer readers — drop the damaged segment, return the rest, say
823
+ * nothing — is under revision, because a caller cannot tell three segments from
824
+ * four with one unreadable. Guessing an answer here would make an incremental
825
+ * reader the place that shipped it, and a live view is where a silently
826
+ * dropped segment does the most harm. Until that is settled this reader stops
827
+ * at damage and says where.
828
+ */
829
+ declare function createMoqtraceReader(): MoqtraceReader;
830
+
831
+ /**
832
+ * The most bytes a recorder may put in an error event's `raw`.
833
+ *
834
+ * A MUST in the format, and a fixed number rather than a suggestion, because
835
+ * an error event is one of the types sampling must not drop: every other
836
+ * high-volume field is bounded by sampling, so if this one is not bounded here
837
+ * it is not bounded at all. A peer that opens ten thousand streams and sends
838
+ * garbage on each is what a fuzzer does to a relay, and the trace of it should
839
+ * not be larger than the attack.
840
+ *
841
+ * It lives here, in the recorder, and appears nowhere in `binary.ts`. The cap
842
+ * is addressed to the party deciding what to say about traffic it has just
843
+ * observed — {@link TraceRecorder.recordError}, and nothing else. A serializer
844
+ * cannot tell a freshly recorded event from one that arrived by being read, so
845
+ * a cap applied there would either truncate evidence on a rewrite or refuse a
846
+ * file the reader was required to accept. `writeMoqtrace` therefore writes a
847
+ * `raw` of any length, and {@link TraceRecorder.record} — which inserts an
848
+ * event a caller may well have read out of a file — does not apply it either.
849
+ */
850
+ declare const MAX_ERROR_RAW_BYTES = 4096;
851
+ /**
852
+ * What a recorder knows about an error beyond its code and reason.
853
+ *
854
+ * Every field is optional and each is gated at its own detail level, so one
855
+ * call site can hand over everything it has and let the recorder keep what its
856
+ * level allows.
857
+ */
858
+ interface ErrorDetails {
859
+ /**
860
+ * QUIC stream the error was observed on.
861
+ *
862
+ * Omit it when there was no stream or none is known — that is what its
863
+ * absence says, and a reader must not read it as stream `0`.
864
+ */
865
+ readonly streamId?: bigint;
866
+ /** What sort of failure this was. An open vocabulary; any string is legal. */
867
+ readonly errorKind?: ErrorKind;
868
+ /**
869
+ * The offending bytes, in full and untruncated.
870
+ *
871
+ * Hand over everything held: the recorder applies {@link
872
+ * MAX_ERROR_RAW_BYTES} and reports the untruncated length in `rawLength`,
873
+ * so a caller that pre-truncates loses the one signal saying the capture is
874
+ * partial. Recorded only at `'full'`, and only once per flow.
875
+ */
876
+ readonly raw?: Uint8Array;
877
+ /**
878
+ * The length to report, where the caller already holds less than it saw.
879
+ *
880
+ * Defaults to `raw.length`, which is right whenever `raw` is everything
881
+ * there was. Set it to state a length larger than the bytes on hand — a
882
+ * caller that hit a cap of its own upstream, or one that knows a message's
883
+ * declared length without having kept it.
884
+ *
885
+ * **Set it to `null` when the true length is unknown**, and the key is
886
+ * omitted. SPEC.md requires that rather than a guess, because the guess is
887
+ * not neutral: `rawlen` equal to the bytes present is this format's signal
888
+ * for *not truncated*, so defaulting it would assert a complete capture
889
+ * exactly where the caller knows least about whether the input was
890
+ * complete. Absent, a reader treats a `raw` of exactly
891
+ * {@link MAX_ERROR_RAW_BYTES} as possibly truncated, which is the truth.
892
+ *
893
+ * The caller this exists for is real rather than hypothetical: a recorder
894
+ * that stops *reading* a stream once it will not parse — the sensible thing
895
+ * to do with such a stream — never holds more than its own cap and cannot
896
+ * know what it did not read.
897
+ */
898
+ readonly rawLength?: number | null;
899
+ }
131
900
  interface TraceRecorder {
132
901
  /** Wrap a SessionState to auto-record control messages and state changes. */
133
902
  wrapSession<M extends {
134
903
  type: string;
135
904
  }, T extends string>(session: SessionState<M, T>): SessionState<M, T>;
136
- /** Record an arbitrary event manually. */
905
+ /**
906
+ * Record an arbitrary event manually.
907
+ *
908
+ * A passthrough: no detail-level gate, and no cap on an error event's
909
+ * `raw`, because an event handed here may have been read out of a file
910
+ * rather than built from observed bytes, and shortening that one would
911
+ * destroy evidence to enforce a rule addressed to whoever recorded it.
912
+ * Callers building an event from bytes they just saw want
913
+ * {@link recordError}.
914
+ */
137
915
  record(event: TraceEvent): void;
138
916
  /** Record a stream-opened event. Ignored at 'control' detail level. */
139
917
  recordStreamOpened(streamId: bigint, direction: 0 | 1, streamType: 0 | 1 | 2): void;
@@ -143,8 +921,15 @@ interface TraceRecorder {
143
921
  recordObjectHeader(streamId: bigint, groupId: bigint, objectId: bigint, publisherPriority: number, objectStatus: number): void;
144
922
  /** Record an object payload event. Ignored below 'headers+sizes' detail level. */
145
923
  recordObjectPayload(streamId: bigint, groupId: bigint, objectId: bigint, size: number, payload?: Uint8Array): void;
146
- /** Record a protocol error. */
147
- recordError(errorCode: number, reason: string): void;
924
+ /**
925
+ * Record a protocol error.
926
+ *
927
+ * Recorded at every detail level — an error event is non-droppable — but
928
+ * `details` is gated key by key: `streamId` and `errorKind` at every
929
+ * level, `rawLength` from `'headers+sizes'`, and `raw` at `'full'`
930
+ * alone, capped at {@link MAX_ERROR_RAW_BYTES} and written once per flow.
931
+ */
932
+ recordError(errorCode: number, reason: string, details?: ErrorDetails): void;
148
933
  /** Record a user-defined annotation. */
149
934
  annotate(label: string, data?: unknown): void;
150
935
  /** Finalize the trace. Stops recording and returns the trace. */
@@ -154,4 +939,4 @@ interface TraceRecorder {
154
939
  }
155
940
  declare function createRecorder(options: RecorderOptions): TraceRecorder;
156
941
 
157
- export { type AnnotationEvent, type ControlMessageEvent, type DetailLevel, type MoqtraceWriter, type ObjectHeaderEvent, type ObjectPayloadEvent, type Perspective, type RecorderOptions, type StateChangeEvent, type StreamClosedEvent, type StreamOpenedEvent, type Trace, type TraceErrorEvent, type TraceEvent, type TraceHeader, type TraceRecorder, createMoqtraceWriter, createRecorder, readMoqtrace, readMoqtraceHeader, traceToJSON, writeMoqtrace };
942
+ export { type AnnotationEvent, type ControlMessageEvent, type DerivationKind, type DetailLevel, type DropPolicy, type ErrorDetails, type ErrorKind, FORMAT_VERSION, MAX_ERROR_RAW_BYTES, MalformedHeaderError, type MoqtraceReader, type MoqtraceWriter, type ObjectHeaderEvent, type ObjectPayloadEvent, type PeerConnectedEvent, type PeerDisconnectedEvent, type PeerRole, type Perspective, type ReadItem, type ReadOptions, type RecorderOptions, type RecoveredRegion, SUPPORTED_VERSIONS, type SamplingInfo, type SegmentInfo, type Side, type StateChangeEvent, type StreamClosedEvent, type StreamOpenedEvent, type SubscriptionDerivationEvent, type SubscriptionRef, TRACE_ID_LENGTH, type Trace, type TraceErrorEvent, type TraceEvent, type TraceHeader, type TraceRecorder, TruncatedStreamError, TruncatedTraceError, type UnknownEvent, controlMessageFields, createMoqtraceReader, createMoqtraceWriter, createRecorder, readMoqtrace, readMoqtraceHeader, readMoqtraceSegments, traceToJSON, writeMoqtrace, writeMoqtraceSegments };