@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 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
- | `"raw"` | byte string | `full` only | Raw wire bytes (including type and length prefix) |
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 = _cborx.encode.call(void 0, headerToCbor(trace.header));
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 = _cborx.encode.call(void 0, eventToCbor(event));
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(_cborx.decode.call(void 0, headerBytes));
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
- _cborx.decodeMultiple.call(void 0, eventBytes, (obj) => {
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(_cborx.decode.call(void 0, headerBytes));
275
+ return cborToHeader(decode(headerBytes));
268
276
  }
269
277
  function createMoqtraceWriter(header) {
270
- const headerCbor = _cborx.encode.call(void 0, headerToCbor(header));
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 _cborx.encode.call(void 0, eventToCbor(event));
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 { decode, decodeMultiple, encode } from "cbor-x";
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.1.2",
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.1.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 { decode, decodeMultiple, encode } from 'cbor-x'
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 {