@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 CHANGED
@@ -7,7 +7,10 @@ Record, serialize, and analyze MoQT (Media over QUIC Transport) sessions using t
7
7
  - Configurable detail levels from control-only to full wire captures
8
8
  - Session recorder that wraps `@moqtap/codec` session state machines
9
9
  - Human-readable JSON export for debugging
10
- - Zero-copy streaming writer for large traces
10
+ - Zero-copy streaming writer, and an incremental reader for a stream that
11
+ arrives in chunks
12
+ - Unrecognised keys, event types and enum values preserved through a
13
+ read-modify-write
11
14
 
12
15
  Writes format version 2 and reads versions 1 and 2.
13
16
 
@@ -50,6 +53,26 @@ const trace = recorder.finalize()
50
53
  const bytes = writeMoqtrace(trace) // → Uint8Array (.moqtrace binary)
51
54
  ```
52
55
 
56
+ ### Recording an error with the bytes behind it
57
+
58
+ A report saying "your SUBSCRIBE_OK did not parse" is an assertion. The same
59
+ report carrying the bytes is evidence the other party can run against their own
60
+ encoder.
61
+
62
+ ```typescript
63
+ recorder.recordError(0, 'SUBSCRIBE_OK did not parse', {
64
+ streamId: 4n,
65
+ errorKind: 'decode', // 'protocol' | 'transport' | 'decode' | any other string
66
+ raw: offendingBytes, // hand over everything held, untruncated
67
+ })
68
+ ```
69
+
70
+ Hand `raw` the full bytes. The recorder applies `MAX_ERROR_RAW_BYTES` (4096)
71
+ itself and reports the untruncated length separately, so a caller that
72
+ pre-truncates destroys the one signal saying the capture is partial. `rawLength`
73
+ is recorded from `'headers+sizes'` and `raw` only at `'full'`, and the bytes are
74
+ written once per flow.
75
+
53
76
  ### Reading a trace file
54
77
 
55
78
  ```typescript
@@ -81,6 +104,34 @@ for (const event of events) {
81
104
  }
82
105
  ```
83
106
 
107
+ ### Streaming reader (for a stream that arrives in chunks)
108
+
109
+ `readMoqtrace` and the functions beside it take a finished file. A live capture
110
+ does not have one: chunk boundaries fall wherever the transport put them,
111
+ almost never on an item boundary.
112
+
113
+ ```typescript
114
+ import { createMoqtraceReader, TruncatedStreamError } from '@moqtap/trace'
115
+
116
+ const reader = createMoqtraceReader()
117
+
118
+ for await (const chunk of incoming) {
119
+ // Returns whatever the chunk completed — often nothing, which is ordinary.
120
+ for (const item of reader.push(chunk)) {
121
+ if (item.kind === 'segment') console.log('segment', item.header.sessionId)
122
+ else console.log(item.event.type, item.event.timestamp)
123
+ }
124
+ }
125
+
126
+ // `end()` can still yield items, and throws if bytes are left over that do not
127
+ // form a whole one.
128
+ try {
129
+ for (const item of reader.end()) console.log(item.kind)
130
+ } catch (error) {
131
+ if (error instanceof TruncatedStreamError) console.warn('stream cut mid-item')
132
+ }
133
+ ```
134
+
84
135
  ### Segmented traces
85
136
 
86
137
  A segment is a complete `.moqtrace` blob — magic, version, header, events —
@@ -111,7 +162,8 @@ for (const segment of readMoqtraceSegments(bytes)) {
111
162
  A file that stops part-way through an event throws `TruncatedTraceError`, which
112
163
  carries everything that decoded before the cut — a truncated trace is still
113
164
  evidence. On a segmented trace, `{ recover: true }` skips a damaged region and
114
- resumes at the next segment instead of throwing.
165
+ resumes at the next segment instead of throwing; pass `onRecovered` alongside
166
+ it to be told what that cost, since the return value cannot say.
115
167
 
116
168
  ```typescript
117
169
  import { readMoqtrace, readMoqtraceSegments, TruncatedTraceError } from '@moqtap/trace'
@@ -121,11 +173,21 @@ try {
121
173
  } catch (error) {
122
174
  if (error instanceof TruncatedTraceError) {
123
175
  console.warn(`truncated at byte ${error.offset}`)
124
- console.log(error.trace?.events.length, 'events survived')
176
+ const survived = error.segments.reduce((n, segment) => n + segment.events.length, 0)
177
+ console.log(survived, 'events survived')
125
178
  }
126
179
  }
127
180
 
128
- const whatSurvived = readMoqtraceSegments(bytes, { recover: true })
181
+ const whatSurvived = readMoqtraceSegments(bytes, {
182
+ recover: true,
183
+ onRecovered: ({ offset, kind, resumedAt }) => {
184
+ console.warn(
185
+ resumedAt == null
186
+ ? `${kind} damage at ${offset}: nothing readable followed, rest discarded`
187
+ : `${kind} damage at ${offset}, resumed at ${resumedAt}`,
188
+ )
189
+ },
190
+ })
129
191
  ```
130
192
 
131
193
  ## Reading a trace you did not write
@@ -136,6 +198,12 @@ intact, so it survives a read-modify-write round trip rather than being dropped
136
198
  or relabelled; an unrecognised perspective, detail level, role, side, drop
137
199
  policy or derivation kind is kept verbatim.
138
200
 
201
+ Keys are kept on the same terms. One this version does not recognise — on the
202
+ header, on `segment`, on `sampling`, or on any event — is preserved verbatim in
203
+ an `extra` map beside the fields and written back unchanged. Ignoring a key is
204
+ permitted; dropping one is not, or a redaction pass, a filter or an annotated
205
+ download emits a file that looks as though it never carried the key.
206
+
139
207
  The one place that refuses is `createRecorder`, which cannot honour a detail
140
208
  level it does not implement: it would either capture less than you asked for
141
209
  while writing a header claiming otherwise, or — for the payload-bearing levels
@@ -166,7 +234,27 @@ const json = traceToJSON(trace)
166
234
  console.log(json)
167
235
  ```
168
236
 
169
- ## Upgrading from 0.2.0
237
+ ## Upgrading
238
+
239
+ ### To 0.4.0 (from 0.3.0)
240
+
241
+ Two changes to what a reader accepts:
242
+
243
+ - `ControlMessageEvent.message` is `unknown` rather than
244
+ `Record<string, unknown>`, so anything reading a key straight off it stops
245
+ compiling. Narrow with `controlMessageFields()` first. The old type was a
246
+ claim the runtime did not keep — every real capture carries a text rendering
247
+ there, so `event.message.request_id` typechecked and read `undefined`.
248
+ - A header with no usable value for a required key throws
249
+ `MalformedHeaderError` instead of returning a header with `undefined`, `NaN`
250
+ or `0` standing in for it. Pass `recover` to skip that segment instead. No
251
+ file either implementation writes is affected.
252
+
253
+ Everything else is additive: `extra` on the header and its two sub-maps, the
254
+ stream-header identifiers on Event 1, the error fields on Event 6, and
255
+ `createMoqtraceReader`.
256
+
257
+ ### To 0.3.0 (from 0.2.0)
170
258
 
171
259
  The package now writes format version 2. A 0.2.0 reader rejects those files at
172
260
  byte 8, which is the point of the bump: it turns what would have been silent