@moqtap/trace 0.1.2 → 0.2.0
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/SPEC.md +25 -6
- package/dist/index.cjs +15 -7
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -1
- package/package.json +2 -2
- package/src/binary.ts +17 -1
- package/src/types.ts +9 -0
package/SPEC.md
CHANGED
|
@@ -117,12 +117,27 @@ Short key names are used intentionally — traces can contain hundreds of thousa
|
|
|
117
117
|
|
|
118
118
|
#### Event 0: Control Message
|
|
119
119
|
|
|
120
|
-
| Key | Type | Detail Level | Description
|
|
121
|
-
| ------- | ----------- | ------------ |
|
|
122
|
-
| `"d"` | integer | `control`+ | Direction: `0` = sent (tx), `1` = received (rx)
|
|
123
|
-
| `"mt"` | integer | `control`+ | Wire message type ID (e.g., `0x03` for SUBSCRIBE)
|
|
124
|
-
| `"msg"` | map | `control`+ | Decoded message fields (draft-specific structure)
|
|
125
|
-
| `"
|
|
120
|
+
| Key | Type | Detail Level | Description |
|
|
121
|
+
| ------- | ----------- | ------------ | ---------------------------------------------------------------- |
|
|
122
|
+
| `"d"` | integer | `control`+ | Direction: `0` = sent (tx), `1` = received (rx) |
|
|
123
|
+
| `"mt"` | integer | `control`+ | Wire message type ID (e.g., `0x03` for SUBSCRIBE) |
|
|
124
|
+
| `"msg"` | map | `control`+ | Decoded message fields (draft-specific structure) |
|
|
125
|
+
| `"sid"` | integer | `control`+ | QUIC stream ID the message travelled on. Optional (see below). |
|
|
126
|
+
| `"raw"` | byte string | `full` only | Raw wire bytes (including type and length prefix) |
|
|
127
|
+
|
|
128
|
+
`"sid"` is optional: a recorder that sits at the session level rather than the
|
|
129
|
+
stream level has no stream to report, and omitting the key says so. Readers
|
|
130
|
+
MUST distinguish an absent `"sid"` from stream `0`.
|
|
131
|
+
|
|
132
|
+
Recorders SHOULD write it. From draft-17 the control plane is no longer a
|
|
133
|
+
single stream — the session control stream became a pair of unidirectional
|
|
134
|
+
streams, and every request (SUBSCRIBE, PUBLISH, FETCH, …) gets its own
|
|
135
|
+
bidirectional stream. Responses on those streams carry no request ID, because
|
|
136
|
+
the stream itself is the correlation, so a trace without `"sid"` cannot tie a
|
|
137
|
+
SUBSCRIBE_OK back to its SUBSCRIBE, and everything derived from that pairing —
|
|
138
|
+
track names, aliases, request outcomes — is lost to the reader. Through
|
|
139
|
+
draft-16 all control messages share one stream and the field is merely
|
|
140
|
+
informative.
|
|
126
141
|
|
|
127
142
|
#### Event 1: Stream Opened
|
|
128
143
|
|
|
@@ -215,6 +230,10 @@ Object header events and object payload events (event 4) for the same object sha
|
|
|
215
230
|
- **Version 1** is defined by this document.
|
|
216
231
|
- Readers MUST check the format version before parsing.
|
|
217
232
|
- Unknown keys in header maps or event maps MUST be ignored (forward-compatible).
|
|
233
|
+
- Optional keys MAY be added to an existing event type without a version bump —
|
|
234
|
+
`"sid"` on event 0 was added this way. A reader must therefore treat any
|
|
235
|
+
optional key as absent-by-default rather than assuming files of a given
|
|
236
|
+
version all carry the same keys.
|
|
218
237
|
- New event types (`"e"` values) MAY be added in future versions; readers SHOULD skip unknown event types.
|
|
219
238
|
- The detail level in the header is informational — readers MUST handle missing fields gracefully regardless of declared level.
|
|
220
239
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/binary.ts
|
|
2
2
|
var _cborx = require('cbor-x');
|
|
3
|
+
var codec = new (0, _cborx.Encoder)({ useRecords: false, mapsAsObjects: true });
|
|
4
|
+
var encode = (value) => codec.encode(value);
|
|
5
|
+
var decode = (bytes) => codec.decode(bytes);
|
|
6
|
+
var decodeMultiple = (bytes, cb) => {
|
|
7
|
+
codec.decodeMultiple(bytes, cb);
|
|
8
|
+
};
|
|
3
9
|
var MAGIC = new Uint8Array([77, 79, 81, 84, 82, 65, 67, 69]);
|
|
4
10
|
var FORMAT_VERSION = 1;
|
|
5
11
|
var PREAMBLE_SIZE = 16;
|
|
@@ -63,6 +69,7 @@ function eventToCbor(event) {
|
|
|
63
69
|
base.d = event.direction;
|
|
64
70
|
base.mt = event.messageType;
|
|
65
71
|
base.msg = event.message;
|
|
72
|
+
if (event.streamId != null) base.sid = event.streamId;
|
|
66
73
|
if (event.raw != null) base.raw = event.raw;
|
|
67
74
|
break;
|
|
68
75
|
}
|
|
@@ -133,6 +140,7 @@ function cborToEvent(obj) {
|
|
|
133
140
|
direction: obj.d,
|
|
134
141
|
messageType: Number(_nullishCoalesce(obj.mt, () => ( 0))),
|
|
135
142
|
message: _nullishCoalesce(obj.msg, () => ( {})),
|
|
143
|
+
...obj.sid != null ? { streamId: BigInt(obj.sid) } : {},
|
|
136
144
|
...obj.raw != null ? { raw: obj.raw } : {}
|
|
137
145
|
};
|
|
138
146
|
case "stream-opened":
|
|
@@ -230,12 +238,12 @@ function validatePreamble(bytes) {
|
|
|
230
238
|
return { version, headerLength };
|
|
231
239
|
}
|
|
232
240
|
function writeMoqtrace(trace) {
|
|
233
|
-
const headerCbor =
|
|
241
|
+
const headerCbor = encode(headerToCbor(trace.header));
|
|
234
242
|
const preamble = writePreamble(headerCbor);
|
|
235
243
|
const eventChunks = [];
|
|
236
244
|
let totalEventBytes = 0;
|
|
237
245
|
for (const event of trace.events) {
|
|
238
|
-
const chunk =
|
|
246
|
+
const chunk = encode(eventToCbor(event));
|
|
239
247
|
eventChunks.push(chunk);
|
|
240
248
|
totalEventBytes += chunk.length;
|
|
241
249
|
}
|
|
@@ -251,11 +259,11 @@ function writeMoqtrace(trace) {
|
|
|
251
259
|
function readMoqtrace(bytes) {
|
|
252
260
|
const { headerLength } = validatePreamble(bytes);
|
|
253
261
|
const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
|
|
254
|
-
const header = cborToHeader(
|
|
262
|
+
const header = cborToHeader(decode(headerBytes));
|
|
255
263
|
const eventBytes = bytes.slice(PREAMBLE_SIZE + headerLength);
|
|
256
264
|
const events = [];
|
|
257
265
|
if (eventBytes.length > 0) {
|
|
258
|
-
|
|
266
|
+
decodeMultiple(eventBytes, (obj) => {
|
|
259
267
|
events.push(cborToEvent(obj));
|
|
260
268
|
});
|
|
261
269
|
}
|
|
@@ -264,17 +272,17 @@ function readMoqtrace(bytes) {
|
|
|
264
272
|
function readMoqtraceHeader(bytes) {
|
|
265
273
|
const { headerLength } = validatePreamble(bytes);
|
|
266
274
|
const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
|
|
267
|
-
return cborToHeader(
|
|
275
|
+
return cborToHeader(decode(headerBytes));
|
|
268
276
|
}
|
|
269
277
|
function createMoqtraceWriter(header) {
|
|
270
|
-
const headerCbor =
|
|
278
|
+
const headerCbor = encode(headerToCbor(header));
|
|
271
279
|
const preambleBytes = writePreamble(headerCbor);
|
|
272
280
|
return {
|
|
273
281
|
preamble() {
|
|
274
282
|
return preambleBytes;
|
|
275
283
|
},
|
|
276
284
|
writeEvent(event) {
|
|
277
|
-
return
|
|
285
|
+
return encode(eventToCbor(event));
|
|
278
286
|
}
|
|
279
287
|
};
|
|
280
288
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -24,6 +24,15 @@ interface ControlMessageEvent extends BaseEvent {
|
|
|
24
24
|
readonly messageType: number;
|
|
25
25
|
readonly message: Record<string, unknown>;
|
|
26
26
|
readonly raw?: Uint8Array;
|
|
27
|
+
/**
|
|
28
|
+
* QUIC stream the message travelled on.
|
|
29
|
+
*
|
|
30
|
+
* Optional, and only meaningful for a recorder that knows it. It matters
|
|
31
|
+
* from draft-17, where the control plane is no longer one stream: each
|
|
32
|
+
* request gets its own bidirectional stream and responses carry no request
|
|
33
|
+
* id, so the stream is the only thing tying a response to its request.
|
|
34
|
+
*/
|
|
35
|
+
readonly streamId?: bigint;
|
|
27
36
|
}
|
|
28
37
|
interface StreamOpenedEvent extends BaseEvent {
|
|
29
38
|
readonly type: 'stream-opened';
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ interface ControlMessageEvent extends BaseEvent {
|
|
|
24
24
|
readonly messageType: number;
|
|
25
25
|
readonly message: Record<string, unknown>;
|
|
26
26
|
readonly raw?: Uint8Array;
|
|
27
|
+
/**
|
|
28
|
+
* QUIC stream the message travelled on.
|
|
29
|
+
*
|
|
30
|
+
* Optional, and only meaningful for a recorder that knows it. It matters
|
|
31
|
+
* from draft-17, where the control plane is no longer one stream: each
|
|
32
|
+
* request gets its own bidirectional stream and responses carry no request
|
|
33
|
+
* id, so the stream is the only thing tying a response to its request.
|
|
34
|
+
*/
|
|
35
|
+
readonly streamId?: bigint;
|
|
27
36
|
}
|
|
28
37
|
interface StreamOpenedEvent extends BaseEvent {
|
|
29
38
|
readonly type: 'stream-opened';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/binary.ts
|
|
2
|
-
import {
|
|
2
|
+
import { Encoder } from "cbor-x";
|
|
3
|
+
var codec = new Encoder({ useRecords: false, mapsAsObjects: true });
|
|
4
|
+
var encode = (value) => codec.encode(value);
|
|
5
|
+
var decode = (bytes) => codec.decode(bytes);
|
|
6
|
+
var decodeMultiple = (bytes, cb) => {
|
|
7
|
+
codec.decodeMultiple(bytes, cb);
|
|
8
|
+
};
|
|
3
9
|
var MAGIC = new Uint8Array([77, 79, 81, 84, 82, 65, 67, 69]);
|
|
4
10
|
var FORMAT_VERSION = 1;
|
|
5
11
|
var PREAMBLE_SIZE = 16;
|
|
@@ -63,6 +69,7 @@ function eventToCbor(event) {
|
|
|
63
69
|
base.d = event.direction;
|
|
64
70
|
base.mt = event.messageType;
|
|
65
71
|
base.msg = event.message;
|
|
72
|
+
if (event.streamId != null) base.sid = event.streamId;
|
|
66
73
|
if (event.raw != null) base.raw = event.raw;
|
|
67
74
|
break;
|
|
68
75
|
}
|
|
@@ -133,6 +140,7 @@ function cborToEvent(obj) {
|
|
|
133
140
|
direction: obj.d,
|
|
134
141
|
messageType: Number(obj.mt ?? 0),
|
|
135
142
|
message: obj.msg ?? {},
|
|
143
|
+
...obj.sid != null ? { streamId: BigInt(obj.sid) } : {},
|
|
136
144
|
...obj.raw != null ? { raw: obj.raw } : {}
|
|
137
145
|
};
|
|
138
146
|
case "stream-opened":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moqtap/trace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MoQT session trace format — record, serialize, and analyze .moqtrace files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test": "vitest run"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@moqtap/codec": "^0.
|
|
50
|
+
"@moqtap/codec": "^0.3.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"cbor-x": "^1.6.0"
|
package/src/binary.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Encoder } from 'cbor-x'
|
|
2
2
|
import type { DetailLevel, Perspective, Trace, TraceEvent, TraceHeader } from './types.js'
|
|
3
3
|
|
|
4
|
+
// Use a CBOR codec configured for cross-language interop.
|
|
5
|
+
// Why: cbor-x's default `useRecords: true` enables a proprietary "record
|
|
6
|
+
// extension" that encodes JS objects as tagged structures rather than standard
|
|
7
|
+
// CBOR maps — unreadable by ciborium (Rust) and other spec-compliant decoders.
|
|
8
|
+
// The .moqtrace spec requires standard CBOR, so we disable records and encode
|
|
9
|
+
// as plain maps. `mapsAsObjects` is set so decoded maps come back as plain
|
|
10
|
+
// JS objects (matching the code that accesses fields via `obj.protocol`).
|
|
11
|
+
const codec = new Encoder({ useRecords: false, mapsAsObjects: true })
|
|
12
|
+
const encode = (value: unknown): Uint8Array => codec.encode(value)
|
|
13
|
+
const decode = (bytes: Uint8Array): unknown => codec.decode(bytes)
|
|
14
|
+
const decodeMultiple = (bytes: Uint8Array, cb: (value: unknown) => void): void => {
|
|
15
|
+
codec.decodeMultiple(bytes, cb as (value: unknown) => boolean | undefined)
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
const MAGIC = new Uint8Array([0x4d, 0x4f, 0x51, 0x54, 0x52, 0x41, 0x43, 0x45]) // "MOQTRACE"
|
|
5
19
|
const FORMAT_VERSION = 1
|
|
6
20
|
const PREAMBLE_SIZE = 16 // 8 magic + 4 version + 4 header length
|
|
@@ -75,6 +89,7 @@ function eventToCbor(event: TraceEvent): Record<string, unknown> {
|
|
|
75
89
|
base.d = event.direction
|
|
76
90
|
base.mt = event.messageType
|
|
77
91
|
base.msg = event.message
|
|
92
|
+
if (event.streamId != null) base.sid = event.streamId
|
|
78
93
|
if (event.raw != null) base.raw = event.raw
|
|
79
94
|
break
|
|
80
95
|
}
|
|
@@ -150,6 +165,7 @@ function cborToEvent(obj: Record<string, unknown>): TraceEvent {
|
|
|
150
165
|
direction: obj.d as 0 | 1,
|
|
151
166
|
messageType: Number(obj.mt ?? 0),
|
|
152
167
|
message: (obj.msg ?? {}) as Record<string, unknown>,
|
|
168
|
+
...(obj.sid != null ? { streamId: BigInt(obj.sid as bigint | number) } : {}),
|
|
153
169
|
...(obj.raw != null ? { raw: obj.raw as Uint8Array } : {}),
|
|
154
170
|
}
|
|
155
171
|
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface ControlMessageEvent extends BaseEvent {
|
|
|
25
25
|
readonly messageType: number
|
|
26
26
|
readonly message: Record<string, unknown>
|
|
27
27
|
readonly raw?: Uint8Array
|
|
28
|
+
/**
|
|
29
|
+
* QUIC stream the message travelled on.
|
|
30
|
+
*
|
|
31
|
+
* Optional, and only meaningful for a recorder that knows it. It matters
|
|
32
|
+
* from draft-17, where the control plane is no longer one stream: each
|
|
33
|
+
* request gets its own bidirectional stream and responses carry no request
|
|
34
|
+
* id, so the stream is the only thing tying a response to its request.
|
|
35
|
+
*/
|
|
36
|
+
readonly streamId?: bigint
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
export interface StreamOpenedEvent extends BaseEvent {
|