@moqtap/trace 0.4.1 → 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 +69 -240
- package/dist/index.cjs +4 -4
- package/dist/index.js +4 -4
- package/package.json +2 -2
- package/src/binary.ts +15 -15
- package/src/recorder.ts +4 -4
package/README.md
CHANGED
|
@@ -1,18 +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, 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.
|
|
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.
|
|
16
7
|
|
|
17
8
|
## Install
|
|
18
9
|
|
|
@@ -20,271 +11,109 @@ Writes format version 2 and reads versions 1 and 2.
|
|
|
20
11
|
npm install @moqtap/trace @moqtap/codec
|
|
21
12
|
```
|
|
22
13
|
|
|
23
|
-
`@moqtap/codec` is a peer dependency — install it alongside
|
|
14
|
+
`@moqtap/codec` is a peer dependency — install it alongside.
|
|
24
15
|
|
|
25
16
|
## Quick Start
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
Wrap a codec session state machine, then serialize what it recorded:
|
|
28
19
|
|
|
29
20
|
```typescript
|
|
30
21
|
import { createRecorder, writeMoqtrace } from '@moqtap/trace'
|
|
31
|
-
import {
|
|
22
|
+
import { MESSAGE_ID_MAP } from '@moqtap/codec/draft21'
|
|
23
|
+
import { createDraft21SessionState } from '@moqtap/codec/draft21/session'
|
|
32
24
|
|
|
33
25
|
const recorder = createRecorder({
|
|
34
|
-
protocol: '
|
|
26
|
+
protocol: 'moqt-21',
|
|
35
27
|
perspective: 'client',
|
|
36
28
|
detail: 'control',
|
|
29
|
+
messageTypeId: (name) => Number(MESSAGE_ID_MAP.get(name) ?? 0),
|
|
37
30
|
})
|
|
38
31
|
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
const traced = recorder.wrapSession(session)
|
|
42
|
-
|
|
43
|
-
// 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'))
|
|
44
34
|
traced.send(setupMessage)
|
|
45
35
|
traced.receive(setupMessage)
|
|
46
36
|
|
|
47
|
-
//
|
|
48
|
-
recorder.recordStreamOpened(4n, 0, 0)
|
|
37
|
+
// Events the session layer never sees.
|
|
38
|
+
recorder.recordStreamOpened(4n, 0, 0)
|
|
49
39
|
recorder.annotate('connected', { relay: 'cdn.example.com' })
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
const trace = recorder.finalize()
|
|
53
|
-
const bytes = writeMoqtrace(trace) // → Uint8Array (.moqtrace binary)
|
|
41
|
+
const bytes = writeMoqtrace(recorder.finalize()) // Uint8Array
|
|
54
42
|
```
|
|
55
43
|
|
|
56
|
-
|
|
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
|
-
|
|
76
|
-
### Reading a trace file
|
|
44
|
+
Read one back:
|
|
77
45
|
|
|
78
46
|
```typescript
|
|
79
47
|
import { readMoqtrace, readMoqtraceHeader } from '@moqtap/trace'
|
|
80
48
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
console.log(header.protocol) // "moq-transport-17"
|
|
84
|
-
console.log(header.perspective) // "client"
|
|
85
|
-
console.log(header.detail) // "control"
|
|
49
|
+
const header = readMoqtraceHeader(bytes) // metadata only, no event parsing
|
|
50
|
+
console.log(header.protocol, header.perspective, header.detail)
|
|
86
51
|
|
|
87
|
-
// Full parse
|
|
88
52
|
const trace = readMoqtrace(bytes)
|
|
89
53
|
for (const event of trace.events) {
|
|
90
54
|
console.log(event.type, event.timestamp)
|
|
91
55
|
}
|
|
92
56
|
```
|
|
93
57
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
```
|
|
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.
|
|
192
97
|
|
|
193
98
|
## Reading a trace you did not write
|
|
194
99
|
|
|
195
|
-
Nothing
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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.
|
|
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.
|
|
211
104
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
| `control` | Control messages only (setup, subscribe, publish, goaway) |
|
|
217
|
-
| `headers` | + data stream headers (subgroup/fetch, object metadata) |
|
|
218
|
-
| `headers+sizes` | + payload byte lengths |
|
|
219
|
-
| `headers+data` | + full payload bytes |
|
|
220
|
-
| `full` | + raw wire bytes (pre-decode) |
|
|
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
|
-
|
|
226
|
-
## JSON Export
|
|
105
|
+
A file that stops part-way through an event throws `TruncatedTraceError`, which
|
|
106
|
+
carries everything that decoded before the cut. On a segmented trace,
|
|
107
|
+
`readMoqtraceSegments(bytes, { recover: true })` skips a damaged region and
|
|
108
|
+
resumes at the next segment instead of throwing.
|
|
227
109
|
|
|
228
|
-
|
|
110
|
+
## File format
|
|
229
111
|
|
|
230
|
-
|
|
231
|
-
import { traceToJSON } from '@moqtap/trace'
|
|
112
|
+
[SPEC.md](./SPEC.md) is the complete `.moqtrace` binary specification.
|
|
232
113
|
|
|
233
|
-
|
|
234
|
-
console.log(json)
|
|
235
|
-
```
|
|
114
|
+
## Documentation
|
|
236
115
|
|
|
237
|
-
|
|
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
|
-
|
|
285
|
-
## File Format
|
|
286
|
-
|
|
287
|
-
See [SPEC.md](./SPEC.md) for the complete `.moqtrace` binary format specification.
|
|
116
|
+
<https://moqtap.com/npm-packages/moqtap-trace/>
|
|
288
117
|
|
|
289
118
|
## License
|
|
290
119
|
|
package/dist/index.cjs
CHANGED
|
@@ -329,10 +329,10 @@ function cborToHeader(obj) {
|
|
|
329
329
|
...optional(src, "sessionId", "sessionId", asText),
|
|
330
330
|
...optional(src, "segment", "segment", cborToSegment),
|
|
331
331
|
...optional(src, "sampling", "sampling", cborToSampling),
|
|
332
|
-
// Not `Record<string, unknown>` by assertion but by test.
|
|
333
|
-
// that is not a map
|
|
334
|
-
// first caller to iterate its keys
|
|
335
|
-
// occur. The bytes survive either way; only
|
|
332
|
+
// Not `Record<string, unknown>` by assertion but by test. Asserted, a
|
|
333
|
+
// `"custom"` that is not a map would reach this field wearing that type,
|
|
334
|
+
// and the first caller to iterate its keys would meet a shape it was told
|
|
335
|
+
// could not occur. The bytes survive either way; only the test keeps the
|
|
336
336
|
// field's type true.
|
|
337
337
|
...optional(src, "custom", "custom", asMap)
|
|
338
338
|
};
|
package/dist/index.js
CHANGED
|
@@ -329,10 +329,10 @@ function cborToHeader(obj) {
|
|
|
329
329
|
...optional(src, "sessionId", "sessionId", asText),
|
|
330
330
|
...optional(src, "segment", "segment", cborToSegment),
|
|
331
331
|
...optional(src, "sampling", "sampling", cborToSampling),
|
|
332
|
-
// Not `Record<string, unknown>` by assertion but by test.
|
|
333
|
-
// that is not a map
|
|
334
|
-
// first caller to iterate its keys
|
|
335
|
-
// occur. The bytes survive either way; only
|
|
332
|
+
// Not `Record<string, unknown>` by assertion but by test. Asserted, a
|
|
333
|
+
// `"custom"` that is not a map would reach this field wearing that type,
|
|
334
|
+
// and the first caller to iterate its keys would meet a shape it was told
|
|
335
|
+
// could not occur. The bytes survive either way; only the test keeps the
|
|
336
336
|
// field's type true.
|
|
337
337
|
...optional(src, "custom", "custom", asMap)
|
|
338
338
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moqtap/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "MoQT session trace format — record, serialize, and analyze .moqtrace files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"corpus": "bun run src/__tests__/corpus/generate.ts && bun run src/__tests__/corpus/manifest.ts"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@moqtap/codec": "
|
|
51
|
+
"@moqtap/codec": ">=0.11.0"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"cbor-x": "^1.6.0"
|
package/src/binary.ts
CHANGED
|
@@ -110,11 +110,11 @@ const COMMON_EVENT_KEYS = new Set(['n', 't', 'p', 'e'])
|
|
|
110
110
|
* "A defined key whose value has an unusable type is treated as unrecognised."
|
|
111
111
|
*
|
|
112
112
|
* Only the decode writes to the set, so there is no separate list of owned keys
|
|
113
|
-
* to keep in step with it.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
113
|
+
* to keep in step with it. A list would make adding a key to an event *reduce*
|
|
114
|
+
* what this reader preserves: a listed key counts as owned whatever it
|
|
115
|
+
* carries, so a wrong-typed one would be kept out of `extra` while the decode
|
|
116
|
+
* either threw on it, losing the whole file, or coerced it into a value the
|
|
117
|
+
* file never carried.
|
|
118
118
|
*/
|
|
119
119
|
interface Decoding {
|
|
120
120
|
readonly obj: Record<string, unknown>
|
|
@@ -315,8 +315,8 @@ function segmentToCbor(segment: SegmentInfo): Record<string, unknown> {
|
|
|
315
315
|
*
|
|
316
316
|
* `"sequence"` is the exception, and the only key inside here that can fail a
|
|
317
317
|
* header: it is the sole ordering key of a segmented stream, so a reader that
|
|
318
|
-
* cannot read it cannot place the segment, and
|
|
319
|
-
*
|
|
318
|
+
* cannot read it cannot place the segment, and defaulting it to `0` would
|
|
319
|
+
* invent an order the file never had.
|
|
320
320
|
*/
|
|
321
321
|
function cborToSegment(value: unknown): SegmentInfo | undefined {
|
|
322
322
|
const obj = asMap(value)
|
|
@@ -415,10 +415,10 @@ export function cborToHeader(obj: Record<string, unknown>): TraceHeader {
|
|
|
415
415
|
...optional(src, 'sessionId', 'sessionId', asText),
|
|
416
416
|
...optional(src, 'segment', 'segment', cborToSegment),
|
|
417
417
|
...optional(src, 'sampling', 'sampling', cborToSampling),
|
|
418
|
-
// Not `Record<string, unknown>` by assertion but by test.
|
|
419
|
-
// that is not a map
|
|
420
|
-
// first caller to iterate its keys
|
|
421
|
-
// occur. The bytes survive either way; only
|
|
418
|
+
// Not `Record<string, unknown>` by assertion but by test. Asserted, a
|
|
419
|
+
// `"custom"` that is not a map would reach this field wearing that type,
|
|
420
|
+
// and the first caller to iterate its keys would meet a shape it was told
|
|
421
|
+
// could not occur. The bytes survive either way; only the test keeps the
|
|
422
422
|
// field's type true.
|
|
423
423
|
...optional(src, 'custom', 'custom', asMap),
|
|
424
424
|
}
|
|
@@ -808,10 +808,10 @@ function requireUint(src: Decoding, key: string): bigint {
|
|
|
808
808
|
/**
|
|
809
809
|
* Take a required header text key, or fail the header.
|
|
810
810
|
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
811
|
+
* Asserting the type instead — `obj.protocol as string` — would let a header
|
|
812
|
+
* with no `"protocol"` reach a caller with `undefined` sitting in a field
|
|
813
|
+
* declared `string`, and re-encode as CBOR `undefined`, a shape the format
|
|
814
|
+
* tells writers never to produce.
|
|
815
815
|
*/
|
|
816
816
|
function requireHeaderText(src: Decoding, key: string): string {
|
|
817
817
|
const present = Object.hasOwn(src.obj, key)
|
package/src/recorder.ts
CHANGED
|
@@ -171,10 +171,10 @@ export function createRecorder(options: RecorderOptions): TraceRecorder {
|
|
|
171
171
|
/**
|
|
172
172
|
* Index of the oldest event still live in {@link events}.
|
|
173
173
|
*
|
|
174
|
-
* Dropping
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
174
|
+
* Dropping is not `events.shift()`, which moves the whole backlog on every
|
|
175
|
+
* call; at a hundred thousand retained events that is O(n) per recorded
|
|
176
|
+
* event, and it bites hardest exactly when the recorder is already under
|
|
177
|
+
* pressure. The head advances instead, and the dead prefix is spliced
|
|
178
178
|
* away in one pass every few thousand drops, which is O(1) amortised.
|
|
179
179
|
*/
|
|
180
180
|
let head = 0
|