@moqtap/trace 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -5
- package/SPEC.md +707 -13
- package/dist/index.cjs +506 -95
- package/dist/index.d.cts +545 -6
- package/dist/index.d.ts +545 -6
- package/dist/index.js +504 -93
- package/package.json +5 -3
- package/src/binary.ts +776 -105
- package/src/index.ts +9 -4
- package/src/json.ts +7 -0
- package/src/reader.ts +286 -0
- package/src/recorder.ts +189 -12
- package/src/types.ts +263 -2
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ interface SegmentInfo {
|
|
|
32
32
|
readonly streamId?: string;
|
|
33
33
|
/** True if this segment continues a previous one with the same `streamId`. */
|
|
34
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>;
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Sampling and filtering metadata. Present only when events were dropped or
|
|
@@ -53,8 +61,24 @@ interface SamplingInfo {
|
|
|
53
61
|
readonly rule?: string;
|
|
54
62
|
/** Language the rule is written in ('prefix', 'glob', 'cel'). */
|
|
55
63
|
readonly ruleLang?: string;
|
|
56
|
-
/**
|
|
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
|
+
*/
|
|
57
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>;
|
|
58
82
|
}
|
|
59
83
|
interface TraceHeader {
|
|
60
84
|
readonly protocol: string;
|
|
@@ -74,7 +98,43 @@ interface TraceHeader {
|
|
|
74
98
|
readonly segment?: SegmentInfo;
|
|
75
99
|
/** Present only when events were dropped or filtered at the source. */
|
|
76
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
|
+
*/
|
|
77
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>;
|
|
78
138
|
}
|
|
79
139
|
interface BaseEvent {
|
|
80
140
|
/**
|
|
@@ -93,12 +153,53 @@ interface BaseEvent {
|
|
|
93
153
|
* not name the same peer.
|
|
94
154
|
*/
|
|
95
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>;
|
|
96
180
|
}
|
|
97
181
|
interface ControlMessageEvent extends BaseEvent {
|
|
98
182
|
readonly type: 'control';
|
|
99
183
|
readonly direction: 0 | 1;
|
|
100
184
|
readonly messageType: number;
|
|
101
|
-
|
|
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;
|
|
102
203
|
readonly raw?: Uint8Array;
|
|
103
204
|
/**
|
|
104
205
|
* QUIC stream the message travelled on.
|
|
@@ -110,11 +211,80 @@ interface ControlMessageEvent extends BaseEvent {
|
|
|
110
211
|
*/
|
|
111
212
|
readonly streamId?: bigint;
|
|
112
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;
|
|
113
226
|
interface StreamOpenedEvent extends BaseEvent {
|
|
114
227
|
readonly type: 'stream-opened';
|
|
115
228
|
readonly streamId: bigint;
|
|
116
229
|
readonly direction: 0 | 1;
|
|
117
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;
|
|
118
288
|
}
|
|
119
289
|
interface StreamClosedEvent extends BaseEvent {
|
|
120
290
|
readonly type: 'stream-closed';
|
|
@@ -142,10 +312,84 @@ interface StateChangeEvent extends BaseEvent {
|
|
|
142
312
|
readonly from: string;
|
|
143
313
|
readonly to: string;
|
|
144
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;
|
|
145
331
|
interface TraceErrorEvent extends BaseEvent {
|
|
146
332
|
readonly type: 'error';
|
|
147
333
|
readonly errorCode: number;
|
|
148
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;
|
|
149
393
|
}
|
|
150
394
|
interface AnnotationEvent extends BaseEvent {
|
|
151
395
|
readonly type: 'annotation';
|
|
@@ -261,6 +505,16 @@ interface RecorderOptions {
|
|
|
261
505
|
readonly source?: string;
|
|
262
506
|
readonly endpoint?: string;
|
|
263
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>;
|
|
264
518
|
readonly maxEvents?: number;
|
|
265
519
|
readonly clock?: () => number;
|
|
266
520
|
/** Map message type name (e.g. 'subscribe') to wire ID (e.g. 0x03). Required for session-layer recording. */
|
|
@@ -291,6 +545,43 @@ declare class TruncatedTraceError extends Error {
|
|
|
291
545
|
/** What decoded, flattened as {@link readMoqtrace} would return it. */
|
|
292
546
|
get trace(): Trace | undefined;
|
|
293
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
|
+
}
|
|
294
585
|
/** Options shared by the readers. */
|
|
295
586
|
interface ReadOptions {
|
|
296
587
|
/**
|
|
@@ -303,6 +594,31 @@ interface ReadOptions {
|
|
|
303
594
|
* segment, so a damaged one still ends the read, just without an error.
|
|
304
595
|
*/
|
|
305
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;
|
|
306
622
|
}
|
|
307
623
|
/**
|
|
308
624
|
* Write a complete trace to .moqtrace binary format.
|
|
@@ -328,6 +644,8 @@ declare function writeMoqtraceSegments(segments: Trace[]): Uint8Array;
|
|
|
328
644
|
*
|
|
329
645
|
* @throws {TruncatedTraceError} if the file stops part-way through an item.
|
|
330
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.
|
|
331
649
|
*/
|
|
332
650
|
declare function readMoqtrace(bytes: Uint8Array, options?: ReadOptions): Trace;
|
|
333
651
|
/**
|
|
@@ -335,12 +653,18 @@ declare function readMoqtrace(bytes: Uint8Array, options?: ReadOptions): Trace;
|
|
|
335
653
|
*
|
|
336
654
|
* A non-segmented file yields exactly one entry, whose header carries no
|
|
337
655
|
* `segment` field.
|
|
656
|
+
*
|
|
657
|
+
* @throws {MalformedHeaderError} as {@link readMoqtrace} does.
|
|
338
658
|
*/
|
|
339
659
|
declare function readMoqtraceSegments(bytes: Uint8Array, options?: ReadOptions): Trace[];
|
|
340
660
|
/**
|
|
341
661
|
* Read only the header from a .moqtrace file (fast metadata peek).
|
|
342
662
|
*
|
|
343
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.
|
|
344
668
|
*/
|
|
345
669
|
declare function readMoqtraceHeader(bytes: Uint8Array): TraceHeader;
|
|
346
670
|
/**
|
|
@@ -371,15 +695,223 @@ declare function createMoqtraceWriter(header: TraceHeader): MoqtraceWriter;
|
|
|
371
695
|
* This is a one-way export for human inspection and debugging.
|
|
372
696
|
* For lossless round-tripping, use the binary .moqtrace format
|
|
373
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.
|
|
374
705
|
*/
|
|
375
706
|
declare function traceToJSON(trace: Trace): string;
|
|
376
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
|
+
}
|
|
377
900
|
interface TraceRecorder {
|
|
378
901
|
/** Wrap a SessionState to auto-record control messages and state changes. */
|
|
379
902
|
wrapSession<M extends {
|
|
380
903
|
type: string;
|
|
381
904
|
}, T extends string>(session: SessionState<M, T>): SessionState<M, T>;
|
|
382
|
-
/**
|
|
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
|
+
*/
|
|
383
915
|
record(event: TraceEvent): void;
|
|
384
916
|
/** Record a stream-opened event. Ignored at 'control' detail level. */
|
|
385
917
|
recordStreamOpened(streamId: bigint, direction: 0 | 1, streamType: 0 | 1 | 2): void;
|
|
@@ -389,8 +921,15 @@ interface TraceRecorder {
|
|
|
389
921
|
recordObjectHeader(streamId: bigint, groupId: bigint, objectId: bigint, publisherPriority: number, objectStatus: number): void;
|
|
390
922
|
/** Record an object payload event. Ignored below 'headers+sizes' detail level. */
|
|
391
923
|
recordObjectPayload(streamId: bigint, groupId: bigint, objectId: bigint, size: number, payload?: Uint8Array): void;
|
|
392
|
-
/**
|
|
393
|
-
|
|
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;
|
|
394
933
|
/** Record a user-defined annotation. */
|
|
395
934
|
annotate(label: string, data?: unknown): void;
|
|
396
935
|
/** Finalize the trace. Stops recording and returns the trace. */
|
|
@@ -400,4 +939,4 @@ interface TraceRecorder {
|
|
|
400
939
|
}
|
|
401
940
|
declare function createRecorder(options: RecorderOptions): TraceRecorder;
|
|
402
941
|
|
|
403
|
-
export { type AnnotationEvent, type ControlMessageEvent, type DerivationKind, type DetailLevel, type DropPolicy, FORMAT_VERSION, type MoqtraceWriter, type ObjectHeaderEvent, type ObjectPayloadEvent, type PeerConnectedEvent, type PeerDisconnectedEvent, type PeerRole, type Perspective, type ReadOptions, type RecorderOptions, 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, TruncatedTraceError, type UnknownEvent, createMoqtraceWriter, createRecorder, readMoqtrace, readMoqtraceHeader, readMoqtraceSegments, traceToJSON, writeMoqtrace, writeMoqtraceSegments };
|
|
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 };
|