@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/src/types.ts CHANGED
@@ -41,6 +41,14 @@ export interface SegmentInfo {
41
41
  readonly streamId?: string
42
42
  /** True if this segment continues a previous one with the same `streamId`. */
43
43
  readonly continues?: boolean
44
+ /**
45
+ * Keys in the `"segment"` map this version does not recognise, kept verbatim
46
+ * and written back inside that map rather than at the top level.
47
+ *
48
+ * See {@link TraceHeader.extra}, which this mirrors — including the part
49
+ * about a key this version *does* know whose value it cannot use.
50
+ */
51
+ readonly extra?: Record<string, unknown>
44
52
  }
45
53
 
46
54
  /**
@@ -63,8 +71,24 @@ export interface SamplingInfo {
63
71
  readonly rule?: string
64
72
  /** Language the rule is written in ('prefix', 'glob', 'cel'). */
65
73
  readonly ruleLang?: string
66
- /** Event type IDs the drop policy was applied to. */
74
+ /**
75
+ * Event type IDs the drop policy was applied to.
76
+ *
77
+ * All or nothing: an array with one element that is not an event type ID is
78
+ * unusable *entire* and stays in {@link extra}. Keeping the elements that
79
+ * read cleanly would not be a partial answer but a wrong one — this key
80
+ * names the types the policy touched, and every type absent from it may be
81
+ * treated as complete, so a shortened array reports a sampled type as fully
82
+ * recorded.
83
+ */
67
84
  readonly appliesTo?: number[]
85
+ /**
86
+ * Keys in the `"sampling"` map this version does not recognise, kept
87
+ * verbatim and written back inside that map rather than at the top level.
88
+ *
89
+ * See {@link TraceHeader.extra}, which this mirrors.
90
+ */
91
+ readonly extra?: Record<string, unknown>
68
92
  }
69
93
 
70
94
  export interface TraceHeader {
@@ -85,7 +109,43 @@ export interface TraceHeader {
85
109
  readonly segment?: SegmentInfo
86
110
  /** Present only when events were dropped or filtered at the source. */
87
111
  readonly sampling?: SamplingInfo
112
+ /**
113
+ * User-defined metadata, handed back key for key and value for value.
114
+ *
115
+ * A passthrough, and the one map in the header with no store of its own:
116
+ * every key in it belongs to whoever wrote the trace, so there is no such
117
+ * thing as an unrecognised key there. A `"custom"` this reader cannot hold
118
+ * exactly — one that is not a map at all — is therefore unusable as a whole
119
+ * and goes to {@link extra}, leaving this field absent rather than typed as
120
+ * a record it is not.
121
+ */
88
122
  readonly custom?: Record<string, unknown>
123
+ /**
124
+ * Keys in the header map that this version of the package does not
125
+ * recognise, kept verbatim.
126
+ *
127
+ * The same guarantee a `TraceEvent`'s `extra` gives one level down, and for
128
+ * the same reason: "unknown keys MUST be ignored" is a rule about reading
129
+ * *past* them, not a licence to drop them. A tool that reads a trace and
130
+ * writes it back — a redaction pass, a filter, a re-segmentation, a download
131
+ * with annotations applied — would otherwise emit a valid file that looks as
132
+ * though it never carried them, and one tool's ignorance would become
133
+ * permanent for every reader downstream of it.
134
+ *
135
+ * A key this version *does* know counts as unrecognised when its value is of
136
+ * a type the field cannot hold. `"transport": 42` is not a transport; it is
137
+ * also not nothing. Such a value is kept here rather than coerced into the
138
+ * field or dropped, {@link transport} reads as absent, and the key is
139
+ * written back unchanged. Knowing more about a key must not mean preserving
140
+ * it less.
141
+ *
142
+ * Three maps in the header carry a store — this one, {@link SegmentInfo} and
143
+ * {@link SamplingInfo} — and each is written back into the map it came from.
144
+ * One store for the whole header would not do: a private key on `"segment"`
145
+ * and a key of the same name at the top level are different keys, and
146
+ * re-emitting either in the other's map changes what the file says.
147
+ */
148
+ readonly extra?: Record<string, unknown>
89
149
  }
90
150
 
91
151
  interface BaseEvent {
@@ -105,13 +165,54 @@ interface BaseEvent {
105
165
  * not name the same peer.
106
166
  */
107
167
  readonly peer?: string
168
+ /**
169
+ * Keys on this event that this version of the package does not recognise,
170
+ * kept verbatim.
171
+ *
172
+ * Optional keys may be added to an existing event type without a format
173
+ * version bump, so "unknown keys MUST be ignored" is a rule about *reading
174
+ * past* them. It is not a licence to drop them: a tool that reads a trace
175
+ * and writes it back — a redaction pass, a filter, an annotated download —
176
+ * would otherwise emit a valid file that looks like it never carried them,
177
+ * and one tool's ignorance would become permanent for every reader
178
+ * downstream of it.
179
+ *
180
+ * A key this version *does* know counts as unrecognised when its value is of
181
+ * a type the field cannot hold — a `"ta"` carrying text, say. It is kept
182
+ * here rather than coerced into the field or dropped, and written back
183
+ * unchanged, exactly as a key nobody had heard of would be. Knowing more
184
+ * about a key must not mean preserving it less.
185
+ *
186
+ * {@link UnknownEvent} already does this for an event type the package
187
+ * cannot name. This is the same guarantee one level down, for a key on a
188
+ * type it can. It is always absent on an `UnknownEvent`, whose `fields`
189
+ * hold every non-common key already.
190
+ */
191
+ readonly extra?: Record<string, unknown>
108
192
  }
109
193
 
110
194
  export interface ControlMessageEvent extends BaseEvent {
111
195
  readonly type: 'control'
112
196
  readonly direction: 0 | 1
113
197
  readonly messageType: number
114
- readonly message: Record<string, unknown>
198
+ /**
199
+ * The message's decoded fields — the `"msg"` field of Event 0.
200
+ *
201
+ * A writer MUST produce a map here, keyed in snake_case (`request_id`,
202
+ * `track_alias`), and an empty one when it decoded nothing. A *reader* has
203
+ * to accept less, because files written before the format said so exist:
204
+ * every `capture-*` case in the conformance corpus carries a Rust `Debug`
205
+ * rendering of the message instead — `'Draft16(ClientSetup(ClientSetup {
206
+ * parameters: [] }))'` — and the format requires such a value to be handed
207
+ * back unchanged rather than rejected or replaced.
208
+ *
209
+ * So the type is `unknown`, not `Record<string, unknown>`. The `Record` was
210
+ * a lie the compiler could not catch: `event.message.request_id` typechecks
211
+ * against a string and reads `undefined` at runtime, on exactly the files
212
+ * this rule exists for. Narrow before reading keys — {@link
213
+ * controlMessageFields} does it correctly.
214
+ */
215
+ readonly message: unknown
115
216
  readonly raw?: Uint8Array
116
217
  /**
117
218
  * QUIC stream the message travelled on.
@@ -124,11 +225,85 @@ export interface ControlMessageEvent extends BaseEvent {
124
225
  readonly streamId?: bigint
125
226
  }
126
227
 
228
+ /**
229
+ * The decoded fields of a control message, or `undefined` when its `"msg"` is
230
+ * not a map and there are no fields to address.
231
+ *
232
+ * Provided because the narrowing is easy to get wrong once per call site and
233
+ * silently: `typeof message === 'object'` also admits `null`, a CBOR array,
234
+ * and a byte string, each of which would then be typed as a field map and
235
+ * read back `undefined` for every key. A caller that skips the check entirely
236
+ * gets the bug this function exists to prevent — see
237
+ * {@link ControlMessageEvent.message}.
238
+ */
239
+ export function controlMessageFields(message: unknown): Record<string, unknown> | undefined {
240
+ if (message === null || typeof message !== 'object') return undefined
241
+ if (Array.isArray(message) || ArrayBuffer.isView(message)) return undefined
242
+ return message as Record<string, unknown>
243
+ }
244
+
127
245
  export interface StreamOpenedEvent extends BaseEvent {
128
246
  readonly type: 'stream-opened'
129
247
  readonly streamId: bigint
130
248
  readonly direction: 0 | 1
131
249
  readonly streamType: 0 | 1 | 2
250
+ /**
251
+ * Track alias the stream carries. Written whenever the recorder knows it.
252
+ *
253
+ * The stream header carries the alias and no detail level records that
254
+ * header's bytes, so before this field a `'headers'` recording could not say
255
+ * which track a stream belonged to — which is most of what the level exists
256
+ * to answer, and there was nothing left to re-parse it from.
257
+ *
258
+ * `bigint`, and so are the three fields below, unlike this event's
259
+ * `direction` and `streamType`. Those two are small enumerations that a
260
+ * narrow type suits; these four are wire identifiers with the same range as
261
+ * {@link ObjectHeaderEvent}'s `groupId`, `objectId` and `streamId`, and
262
+ * readers compare the two events. Typed as `number` here, a match would read
263
+ * as a mismatch: `streamOpened.groupId === objectHeader.groupId` evaluates
264
+ * `42 === 42n`, which is `false`, with no error, for every pair of values
265
+ * that are in fact the same.
266
+ *
267
+ * Absent when the file's `"ta"` was not an unsigned integer — and the same
268
+ * for the three fields below. Such a value belongs to no field here, so it
269
+ * stays verbatim in {@link BaseEvent.extra} and is written back as it came.
270
+ * It is never coerced on the way in: `true` and the text `"4"` are not track
271
+ * alias 1 and track alias 4, however willingly `BigInt` converts them.
272
+ */
273
+ readonly trackAlias?: bigint
274
+ /**
275
+ * Subgroup ID, on a subgroup stream (`streamType === 0`).
276
+ *
277
+ * A writer must not set it on another stream type, where nothing supplies
278
+ * it. A reader that meets one there keeps it rather than rejecting the
279
+ * event: an out-of-scope key makes one field meaningless, not the file
280
+ * unreadable, and discarding the event throws away every other field on it —
281
+ * all of which were fine. That is about the key being in the wrong place; a
282
+ * `"sg"` of the wrong *shape* is not held here at all, but in
283
+ * {@link BaseEvent.extra}.
284
+ */
285
+ readonly subgroupId?: bigint
286
+ /**
287
+ * Fetch request ID, on a fetch stream (`streamType === 2`).
288
+ *
289
+ * A writer MUST set it there. It is the only correlation between the stream
290
+ * and the FETCH that asked for it, and without it a fetch stream cannot be
291
+ * attributed to a request at all. A reader must nevertheless accept a fetch
292
+ * stream that lacks one — every recording made before the key existed lacks
293
+ * it — for the same reason as the `'msg'` asymmetry on
294
+ * {@link ControlMessageEvent.message}: writers conform, readers tolerate.
295
+ */
296
+ readonly fetchRequestId?: bigint
297
+ /**
298
+ * Group ID, on a datagram stream (`streamType === 1`).
299
+ *
300
+ * Scoped to datagrams because on a subgroup stream every object carries the
301
+ * same group by construction, so a copy here would be a second field with no
302
+ * independent source. Where an {@link ObjectHeaderEvent} for the same stream
303
+ * disagrees, that event wins and this is not corruption: this copy is a
304
+ * convenience for a reader that has not yet seen an object.
305
+ */
306
+ readonly groupId?: bigint
132
307
  }
133
308
 
134
309
  export interface StreamClosedEvent extends BaseEvent {
@@ -161,10 +336,86 @@ export interface StateChangeEvent extends BaseEvent {
161
336
  readonly to: string
162
337
  }
163
338
 
339
+ /**
340
+ * What sort of failure an error event records.
341
+ *
342
+ * An open vocabulary: this revision names the three below, others may be added
343
+ * without a format version bump, and a reader must preserve a value it does not
344
+ * recognise rather than reject the event — the same contract as {@link
345
+ * Perspective} and {@link DetailLevel}, which is why all three widen with
346
+ * `OtherString` instead of being closed unions.
347
+ */
348
+ export type ErrorKind =
349
+ /** The peer violated the protocol. */
350
+ | 'protocol'
351
+ /** The QUIC or WebTransport layer failed. */
352
+ | 'transport'
353
+ /** Bytes that would not parse as any message this recorder knows. */
354
+ | 'decode'
355
+ | OtherString
356
+
164
357
  export interface TraceErrorEvent extends BaseEvent {
165
358
  readonly type: 'error'
166
359
  readonly errorCode: number
167
360
  readonly reason: string
361
+ /**
362
+ * QUIC stream the error was observed on.
363
+ *
364
+ * Optional on the same terms as {@link ControlMessageEvent.streamId}: absent
365
+ * means there was no stream, or that the recorder knew of none, which a
366
+ * reader MUST distinguish from stream `0`. `bigint` for the reason given
367
+ * there — it is the same wire identifier, and `number` stops being exact at
368
+ * 2^53, which would make this reader disagree with the Rust one silently and
369
+ * only for large values.
370
+ *
371
+ * It may name a *data* stream, which is why {@link rawLength} is gated a
372
+ * level above this event rather than travelling with it: the length behind an
373
+ * error on a subgroup stream is the size of a run of media framing.
374
+ */
375
+ readonly streamId?: bigint
376
+ /**
377
+ * What sort of failure this was.
378
+ *
379
+ * Spelt out rather than `kind` because {@link SubscriptionDerivationEvent}
380
+ * already owns that name with the unrelated vocabulary `'created'` /
381
+ * `'shared'`. Two vocabularies under one name survive in a typed reader and
382
+ * break in every flat projection of a trace — a JSONL export, a warehouse
383
+ * load, this package's own {@link traceToJSON} — where they become one column
384
+ * holding both. The wire key is `"ek"` for the same reason.
385
+ */
386
+ readonly errorKind?: ErrorKind
387
+ /**
388
+ * How many bytes the recorder held for this error, before any truncation.
389
+ *
390
+ * Not the length of {@link raw}: where the two differ the capture is partial,
391
+ * and this is how a reader learns how much is missing. A `raw` of exactly
392
+ * `MAX_ERROR_RAW_BYTES` with no `rawLength` beside it must be treated as
393
+ * possibly truncated, that being the one length the cap makes ambiguous.
394
+ *
395
+ * Recorded from `'headers+sizes'` up — one level below {@link raw} and
396
+ * deliberately not with it. It is a size and carries no content, so it is
397
+ * available in every trace where the bytes themselves must not appear, and
398
+ * how large a malformed message was often separates a truncated message from
399
+ * a mistyped one on its own.
400
+ */
401
+ readonly rawLength?: number
402
+ /**
403
+ * The bytes behind the error.
404
+ *
405
+ * Payload-bearing, and recorded only at `'full'`: an error naming a data
406
+ * stream has subgroup framing and object payload behind it, so inheriting
407
+ * this event's own `control`+ level would have put media into traces whose
408
+ * declared level excludes payloads entirely.
409
+ *
410
+ * The 4096-byte cap on this field binds a *recorder* — see
411
+ * `MAX_ERROR_RAW_BYTES` in `recorder.ts`, which is where it is applied and
412
+ * the only place it appears. It does not bind this type, the reader or the
413
+ * writer: a longer value read out of a file is handed over whole and written
414
+ * back whole. Re-truncating it would destroy evidence to make someone else's
415
+ * file conform to a rule that was never addressed to the tool doing the
416
+ * truncating.
417
+ */
418
+ readonly raw?: Uint8Array
168
419
  }
169
420
 
170
421
  export interface AnnotationEvent extends BaseEvent {
@@ -304,6 +555,16 @@ export interface RecorderOptions {
304
555
  readonly source?: string
305
556
  readonly endpoint?: string
306
557
  readonly sessionId?: string
558
+ /**
559
+ * Extra header keys to write, for a recorder that has something to say the
560
+ * format has no key for.
561
+ *
562
+ * Goes to {@link TraceHeader.extra} on the finalized trace and is written
563
+ * into the header map after the keys above. Use the `"x-"` prefix the format
564
+ * reserves for private use; a key the format defines is dropped here rather
565
+ * than written twice, since the field always wins.
566
+ */
567
+ readonly extra?: Record<string, unknown>
307
568
  readonly maxEvents?: number
308
569
  readonly clock?: () => number
309
570
  /** Map message type name (e.g. 'subscribe') to wire ID (e.g. 0x03). Required for session-layer recording. */