@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 +68 -151
- 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 +6 -4
- 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
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# @moqtap/trace
|
|
2
2
|
|
|
3
|
-
Record, serialize
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
14
|
+
`@moqtap/codec` is a peer dependency — install it alongside.
|
|
21
15
|
|
|
22
16
|
## Quick Start
|
|
23
17
|
|
|
24
|
-
|
|
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 {
|
|
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: '
|
|
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
|
-
//
|
|
37
|
-
const
|
|
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
|
-
//
|
|
45
|
-
recorder.recordStreamOpened(4n, 0, 0)
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
Read one back:
|
|
54
45
|
|
|
55
46
|
```typescript
|
|
56
47
|
import { readMoqtrace, readMoqtraceHeader } from '@moqtap/trace'
|
|
57
48
|
|
|
58
|
-
//
|
|
59
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
```
|
|
98
|
+
## Reading a trace you did not write
|
|
108
99
|
|
|
109
|
-
|
|
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
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
[SPEC.md](./SPEC.md) is the complete `.moqtrace` binary specification.
|
|
145
113
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|