@moqtap/trace 0.3.0 → 0.4.2

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
@@ -1,15 +1,9 @@
1
1
  # @moqtap/trace
2
2
 
3
- Record, serialize, and analyze MoQT (Media over QUIC Transport) sessions using the `.moqtrace` binary format.
4
-
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
7
- - Configurable detail levels from control-only to full wire captures
8
- - Session recorder that wraps `@moqtap/codec` session state machines
9
- - Human-readable JSON export for debugging
10
- - Zero-copy streaming writer for large traces
11
-
12
- Writes format version 2 and reads versions 1 and 2.
3
+ Record, serialize and analyze MoQT (Media over QUIC Transport) sessions as
4
+ `.moqtrace` files — a compact, streamable CBOR format that reads the same across
5
+ implementations. Includes a recorder that wraps `@moqtap/codec` session state
6
+ machines, streaming read and write, and a lossy JSON export for debugging.
13
7
 
14
8
  ## Install
15
9
 
@@ -17,186 +11,109 @@ Writes format version 2 and reads versions 1 and 2.
17
11
  npm install @moqtap/trace @moqtap/codec
18
12
  ```
19
13
 
20
- `@moqtap/codec` is a peer dependency — install it alongside `@moqtap/trace`.
14
+ `@moqtap/codec` is a peer dependency — install it alongside.
21
15
 
22
16
  ## Quick Start
23
17
 
24
- ### Recording a session
18
+ Wrap a codec session state machine, then serialize what it recorded:
25
19
 
26
20
  ```typescript
27
21
  import { createRecorder, writeMoqtrace } from '@moqtap/trace'
28
- import { createDraft17SessionState } from '@moqtap/codec/draft17/session'
22
+ import { MESSAGE_ID_MAP } from '@moqtap/codec/draft21'
23
+ import { createDraft21SessionState } from '@moqtap/codec/draft21/session'
29
24
 
30
25
  const recorder = createRecorder({
31
- protocol: 'moq-transport-17',
26
+ protocol: 'moqt-21',
32
27
  perspective: 'client',
33
28
  detail: 'control',
29
+ messageTypeId: (name) => Number(MESSAGE_ID_MAP.get(name) ?? 0),
34
30
  })
35
31
 
36
- // Wrap a session to auto-capture control messages and state transitions
37
- const session = createDraft17SessionState('client')
38
- const traced = recorder.wrapSession(session)
39
-
40
- // Use `traced` instead of `session` — all send/receive calls are recorded
32
+ // Every send/receive on the wrapper is recorded.
33
+ const traced = recorder.wrapSession(createDraft21SessionState('client'))
41
34
  traced.send(setupMessage)
42
35
  traced.receive(setupMessage)
43
36
 
44
- // Manually record events the session layer doesn't see
45
- recorder.recordStreamOpened(4n, 0, 0) // outgoing subgroup stream
37
+ // Events the session layer never sees.
38
+ recorder.recordStreamOpened(4n, 0, 0)
46
39
  recorder.annotate('connected', { relay: 'cdn.example.com' })
47
40
 
48
- // Finalize and serialize
49
- const trace = recorder.finalize()
50
- const bytes = writeMoqtrace(trace) // → Uint8Array (.moqtrace binary)
41
+ const bytes = writeMoqtrace(recorder.finalize()) // Uint8Array
51
42
  ```
52
43
 
53
- ### Reading a trace file
44
+ Read one back:
54
45
 
55
46
  ```typescript
56
47
  import { readMoqtrace, readMoqtraceHeader } from '@moqtap/trace'
57
48
 
58
- // Quick metadata peek (no event parsing)
59
- const header = readMoqtraceHeader(bytes)
60
- console.log(header.protocol) // "moq-transport-17"
61
- console.log(header.perspective) // "client"
62
- console.log(header.detail) // "control"
49
+ const header = readMoqtraceHeader(bytes) // metadata only, no event parsing
50
+ console.log(header.protocol, header.perspective, header.detail)
63
51
 
64
- // Full parse
65
52
  const trace = readMoqtrace(bytes)
66
53
  for (const event of trace.events) {
67
54
  console.log(event.type, event.timestamp)
68
55
  }
69
56
  ```
70
57
 
71
- ### Streaming writer (for large traces)
72
-
73
- ```typescript
74
- import { createMoqtraceWriter } from '@moqtap/trace'
75
-
76
- const writer = createMoqtraceWriter(header)
77
- outputStream.write(writer.preamble())
78
-
79
- for (const event of events) {
80
- outputStream.write(writer.writeEvent(event))
81
- }
82
- ```
83
-
84
- ### Segmented traces
85
-
86
- A segment is a complete `.moqtrace` blob — magic, version, header, events —
87
- appended after the one before it, so a reader can start at any segment without
88
- the ones preceding it. That buys capture rotation without finalizing a file, a
89
- natural cut point for live carriage, and recovery from a damaged region.
90
-
91
- ```typescript
92
- import { readMoqtraceSegments, writeMoqtraceSegments } from '@moqtap/trace'
93
-
94
- // Each segment header must carry `segment` metadata: it is what tells a reader
95
- // that `seq` and `timestamp` restart at zero in every segment.
96
- const bytes = writeMoqtraceSegments([
97
- { header: { ...header, segment: { sequence: 0 } }, events: firstBatch },
98
- { header: { ...header, segment: { sequence: 1, continues: true } }, events: secondBatch },
99
- ])
100
-
101
- // Read the boundaries — you need them to order events across segments.
102
- for (const segment of readMoqtraceSegments(bytes)) {
103
- console.log(segment.header.segment?.sequence, segment.events.length)
104
- }
58
+ ## API
59
+
60
+ | Function | Purpose |
61
+ | ---------------------------------------------------- | ------------------------------------------------------------ |
62
+ | `createRecorder(options)` | Session recorder; wraps a codec session state machine |
63
+ | `writeMoqtrace(trace)` | Serialize a complete trace to bytes |
64
+ | `readMoqtrace(bytes)` | Parse a complete file, flattening every segment in order |
65
+ | `readMoqtraceHeader(bytes)` | Parse the header only |
66
+ | `createMoqtraceWriter(header)` | Streaming writer: `preamble()`, then `writeEvent()` per event |
67
+ | `createMoqtraceReader()` | Incremental reader for a stream arriving in chunks |
68
+ | `writeMoqtraceSegments()` / `readMoqtraceSegments()` | Segmented traces, for rotation, live carriage and recovery |
69
+ | `traceToJSON(trace)` | Human-readable export; lossy, as bigints and bytes stringify |
70
+
71
+ `createRecorder` requires `protocol`, `perspective` and `detail`. Pass
72
+ `messageTypeId` to map a message name to its wire id, which session-layer
73
+ recording needs; `maxEvents` bounds the event buffer.
74
+
75
+ Beyond `wrapSession`, the recorder takes events the session layer cannot see:
76
+ `recordStreamOpened`, `recordStreamClosed`, `recordObjectHeader`,
77
+ `recordObjectPayload`, `recordError` and `annotate`. `finalize()` returns the
78
+ trace and stops recording.
79
+
80
+ ## Detail levels
81
+
82
+ | Level | What is recorded |
83
+ | --------------- | -------------------------------------- |
84
+ | `control` | Control messages only |
85
+ | `headers` | + data stream and object headers |
86
+ | `headers+sizes` | + payload byte lengths |
87
+ | `headers+data` | + full payload bytes |
88
+ | `full` | + raw wire bytes, pre-decode |
89
+
90
+ Errors and annotations are recorded at every level. `headers+data` and `full`,
91
+ and the `raw` field on any event, carry user content, so capturing them should
92
+ be an explicit choice rather than a default.
93
+
94
+ When recording an error, hand `recordError` the full bytes: it applies
95
+ `MAX_ERROR_RAW_BYTES` itself and reports the untruncated length separately, so a
96
+ caller that pre-truncates destroys the signal saying the capture is partial.
105
97
 
106
- // Or ignore them: readMoqtrace flattens every segment's events in order.
107
- ```
98
+ ## Reading a trace you did not write
108
99
 
109
- ### Damaged and truncated files
100
+ Nothing is rejected for carrying something newer than this version knows. An
101
+ unrecognised event type arrives as an `UnknownEvent` with its fields intact, and
102
+ an unrecognised key is preserved verbatim in an `extra` map beside the fields, so
103
+ neither is dropped by a read-modify-write round trip.
110
104
 
111
105
  A file that stops part-way through an event throws `TruncatedTraceError`, which
112
- carries everything that decoded before the cut — a truncated trace is still
113
- evidence. On a segmented trace, `{ recover: true }` skips a damaged region and
106
+ carries everything that decoded before the cut. On a segmented trace,
107
+ `readMoqtraceSegments(bytes, { recover: true })` skips a damaged region and
114
108
  resumes at the next segment instead of throwing.
115
109
 
116
- ```typescript
117
- import { readMoqtrace, readMoqtraceSegments, TruncatedTraceError } from '@moqtap/trace'
118
-
119
- try {
120
- const trace = readMoqtrace(bytes)
121
- } catch (error) {
122
- if (error instanceof TruncatedTraceError) {
123
- console.warn(`truncated at byte ${error.offset}`)
124
- console.log(error.trace?.events.length, 'events survived')
125
- }
126
- }
127
-
128
- const whatSurvived = readMoqtraceSegments(bytes, { recover: true })
129
- ```
130
-
131
- ## Reading a trace you did not write
132
-
133
- Nothing here rejects a file for carrying something newer than this version
134
- knows. An unrecognised event type arrives as an `UnknownEvent` with its fields
135
- intact, so it survives a read-modify-write round trip rather than being dropped
136
- or relabelled; an unrecognised perspective, detail level, role, side, drop
137
- policy or derivation kind is kept verbatim.
138
-
139
- The one place that refuses is `createRecorder`, which cannot honour a detail
140
- level it does not implement: it would either capture less than you asked for
141
- while writing a header claiming otherwise, or — for the payload-bearing levels
142
- — capture more.
110
+ ## File format
143
111
 
144
- ## Detail Levels
112
+ [SPEC.md](./SPEC.md) is the complete `.moqtrace` binary specification.
145
113
 
146
- | Level | What's recorded |
147
- | --------------- | --------------------------------------------------------- |
148
- | `control` | Control messages only (setup, subscribe, publish, goaway) |
149
- | `headers` | + data stream headers (subgroup/fetch, object metadata) |
150
- | `headers+sizes` | + payload byte lengths |
151
- | `headers+data` | + full payload bytes |
152
- | `full` | + raw wire bytes (pre-decode) |
153
-
154
- `headers+data` and `full`, and the `raw` field on any event, carry user
155
- content. Capturing them is optional for a conformant recorder and should be an
156
- explicit choice, not a default.
157
-
158
- ## JSON Export
159
-
160
- For human-readable debugging (not lossless — bigint/Uint8Array become strings):
161
-
162
- ```typescript
163
- import { traceToJSON } from '@moqtap/trace'
164
-
165
- const json = traceToJSON(trace)
166
- console.log(json)
167
- ```
114
+ ## Documentation
168
115
 
169
- ## Upgrading from 0.2.0
170
-
171
- The package now writes format version 2. A 0.2.0 reader rejects those files at
172
- byte 8, which is the point of the bump: it turns what would have been silent
173
- desynchronization on a segmented trace into a clear refusal. Reading is
174
- unaffected in the other direction — 0.3.0 reads everything 0.2.0 wrote.
175
-
176
- What changes for code already using it:
177
-
178
- - `TraceEvent` gained four members (`peer-connected`, `peer-disconnected`,
179
- `subscription-derivation`, `unknown`). A `switch` that handles only the
180
- cases it cares about is unaffected; one that asserts exhaustiveness is not.
181
- - `Perspective`, `DetailLevel` and the new string unions accept values they do
182
- not name, so indexing a `Record<DetailLevel, T>` with one now yields
183
- `T | undefined`.
184
- - An unknown event type used to arrive as an annotation labelled
185
- `unknown-event-<n>`. It now arrives as an `UnknownEvent` carrying its real
186
- type and fields, so code matching that label should match `type === 'unknown'`
187
- instead. The old shape was lossy in a way that mattered: writing the trace
188
- back rewrote the event's type to 7.
189
- - `createRecorder` throws on a detail level it does not implement, which the
190
- types previously made unexpressible.
191
- - The bytes changed in two ways that make files readable by the Rust
192
- implementation for the first time: an integral value past 32 bits is now
193
- written as a CBOR integer rather than a float64, and a byte string as major
194
- type 2 rather than wrapped in the typed-array tag 64. Both readers accept
195
- either form, so nothing already written becomes unreadable.
196
-
197
- ## File Format
198
-
199
- See [SPEC.md](./SPEC.md) for the complete `.moqtrace` binary format specification.
116
+ <https://moqtap.com/npm-packages/moqtap-trace/>
200
117
 
201
118
  ## License
202
119