@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/README.md +184 -1
- package/SPEC.md +1083 -122
- package/dist/index.cjs +915 -97
- package/dist/index.d.cts +794 -9
- package/dist/index.d.ts +794 -9
- package/dist/index.js +914 -96
- package/package.json +6 -4
- package/src/binary.ts +1178 -97
- package/src/cbor-scan.ts +185 -0
- package/src/index.ts +25 -3
- package/src/json.ts +7 -0
- package/src/reader.ts +286 -0
- package/src/recorder.ts +199 -12
- package/src/types.ts +460 -3
package/README.md
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
Record, serialize, and analyze MoQT (Media over QUIC Transport) sessions using the `.moqtrace` binary format.
|
|
4
4
|
|
|
5
5
|
- Binary `.moqtrace` format with CBOR encoding (compact, streamable, cross-language)
|
|
6
|
+
- Segmented traces for capture rotation, live carriage, and recovery from a damaged region
|
|
6
7
|
- Configurable detail levels from control-only to full wire captures
|
|
7
8
|
- Session recorder that wraps `@moqtap/codec` session state machines
|
|
8
9
|
- Human-readable JSON export for debugging
|
|
9
|
-
- 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
|
|
14
|
+
|
|
15
|
+
Writes format version 2 and reads versions 1 and 2.
|
|
10
16
|
|
|
11
17
|
## Install
|
|
12
18
|
|
|
@@ -47,6 +53,26 @@ const trace = recorder.finalize()
|
|
|
47
53
|
const bytes = writeMoqtrace(trace) // → Uint8Array (.moqtrace binary)
|
|
48
54
|
```
|
|
49
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
|
+
|
|
50
76
|
### Reading a trace file
|
|
51
77
|
|
|
52
78
|
```typescript
|
|
@@ -78,6 +104,111 @@ for (const event of events) {
|
|
|
78
104
|
}
|
|
79
105
|
```
|
|
80
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
|
+
|
|
135
|
+
### Segmented traces
|
|
136
|
+
|
|
137
|
+
A segment is a complete `.moqtrace` blob — magic, version, header, events —
|
|
138
|
+
appended after the one before it, so a reader can start at any segment without
|
|
139
|
+
the ones preceding it. That buys capture rotation without finalizing a file, a
|
|
140
|
+
natural cut point for live carriage, and recovery from a damaged region.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { readMoqtraceSegments, writeMoqtraceSegments } from '@moqtap/trace'
|
|
144
|
+
|
|
145
|
+
// Each segment header must carry `segment` metadata: it is what tells a reader
|
|
146
|
+
// that `seq` and `timestamp` restart at zero in every segment.
|
|
147
|
+
const bytes = writeMoqtraceSegments([
|
|
148
|
+
{ header: { ...header, segment: { sequence: 0 } }, events: firstBatch },
|
|
149
|
+
{ header: { ...header, segment: { sequence: 1, continues: true } }, events: secondBatch },
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
// Read the boundaries — you need them to order events across segments.
|
|
153
|
+
for (const segment of readMoqtraceSegments(bytes)) {
|
|
154
|
+
console.log(segment.header.segment?.sequence, segment.events.length)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Or ignore them: readMoqtrace flattens every segment's events in order.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Damaged and truncated files
|
|
161
|
+
|
|
162
|
+
A file that stops part-way through an event throws `TruncatedTraceError`, which
|
|
163
|
+
carries everything that decoded before the cut — a truncated trace is still
|
|
164
|
+
evidence. On a segmented trace, `{ recover: true }` skips a damaged region and
|
|
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.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { readMoqtrace, readMoqtraceSegments, TruncatedTraceError } from '@moqtap/trace'
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const trace = readMoqtrace(bytes)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof TruncatedTraceError) {
|
|
175
|
+
console.warn(`truncated at byte ${error.offset}`)
|
|
176
|
+
const survived = error.segments.reduce((n, segment) => n + segment.events.length, 0)
|
|
177
|
+
console.log(survived, 'events survived')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
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
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Reading a trace you did not write
|
|
194
|
+
|
|
195
|
+
Nothing here rejects a file for carrying something newer than this version
|
|
196
|
+
knows. An unrecognised event type arrives as an `UnknownEvent` with its fields
|
|
197
|
+
intact, so it survives a read-modify-write round trip rather than being dropped
|
|
198
|
+
or relabelled; an unrecognised perspective, detail level, role, side, drop
|
|
199
|
+
policy or derivation kind is kept verbatim.
|
|
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
|
+
|
|
207
|
+
The one place that refuses is `createRecorder`, which cannot honour a detail
|
|
208
|
+
level it does not implement: it would either capture less than you asked for
|
|
209
|
+
while writing a header claiming otherwise, or — for the payload-bearing levels
|
|
210
|
+
— capture more.
|
|
211
|
+
|
|
81
212
|
## Detail Levels
|
|
82
213
|
|
|
83
214
|
| Level | What's recorded |
|
|
@@ -88,6 +219,10 @@ for (const event of events) {
|
|
|
88
219
|
| `headers+data` | + full payload bytes |
|
|
89
220
|
| `full` | + raw wire bytes (pre-decode) |
|
|
90
221
|
|
|
222
|
+
`headers+data` and `full`, and the `raw` field on any event, carry user
|
|
223
|
+
content. Capturing them is optional for a conformant recorder and should be an
|
|
224
|
+
explicit choice, not a default.
|
|
225
|
+
|
|
91
226
|
## JSON Export
|
|
92
227
|
|
|
93
228
|
For human-readable debugging (not lossless — bigint/Uint8Array become strings):
|
|
@@ -99,6 +234,54 @@ const json = traceToJSON(trace)
|
|
|
99
234
|
console.log(json)
|
|
100
235
|
```
|
|
101
236
|
|
|
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)
|
|
258
|
+
|
|
259
|
+
The package now writes format version 2. A 0.2.0 reader rejects those files at
|
|
260
|
+
byte 8, which is the point of the bump: it turns what would have been silent
|
|
261
|
+
desynchronization on a segmented trace into a clear refusal. Reading is
|
|
262
|
+
unaffected in the other direction — 0.3.0 reads everything 0.2.0 wrote.
|
|
263
|
+
|
|
264
|
+
What changes for code already using it:
|
|
265
|
+
|
|
266
|
+
- `TraceEvent` gained four members (`peer-connected`, `peer-disconnected`,
|
|
267
|
+
`subscription-derivation`, `unknown`). A `switch` that handles only the
|
|
268
|
+
cases it cares about is unaffected; one that asserts exhaustiveness is not.
|
|
269
|
+
- `Perspective`, `DetailLevel` and the new string unions accept values they do
|
|
270
|
+
not name, so indexing a `Record<DetailLevel, T>` with one now yields
|
|
271
|
+
`T | undefined`.
|
|
272
|
+
- An unknown event type used to arrive as an annotation labelled
|
|
273
|
+
`unknown-event-<n>`. It now arrives as an `UnknownEvent` carrying its real
|
|
274
|
+
type and fields, so code matching that label should match `type === 'unknown'`
|
|
275
|
+
instead. The old shape was lossy in a way that mattered: writing the trace
|
|
276
|
+
back rewrote the event's type to 7.
|
|
277
|
+
- `createRecorder` throws on a detail level it does not implement, which the
|
|
278
|
+
types previously made unexpressible.
|
|
279
|
+
- The bytes changed in two ways that make files readable by the Rust
|
|
280
|
+
implementation for the first time: an integral value past 32 bits is now
|
|
281
|
+
written as a CBOR integer rather than a float64, and a byte string as major
|
|
282
|
+
type 2 rather than wrapped in the typed-array tag 64. Both readers accept
|
|
283
|
+
either form, so nothing already written becomes unreadable.
|
|
284
|
+
|
|
102
285
|
## File Format
|
|
103
286
|
|
|
104
287
|
See [SPEC.md](./SPEC.md) for the complete `.moqtrace` binary format specification.
|