@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/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
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|