@moqtap/trace 0.1.1 → 0.1.3

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/src/binary.ts CHANGED
@@ -1,32 +1,46 @@
1
- import { decode, decodeMultiple, encode } from "cbor-x";
2
- import type { DetailLevel, Perspective, Trace, TraceEvent, TraceHeader } from "./types.js";
1
+ import { Encoder } from 'cbor-x'
2
+ import type { DetailLevel, Perspective, Trace, TraceEvent, TraceHeader } from './types.js'
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
+ }
3
17
 
4
- const MAGIC = new Uint8Array([0x4d, 0x4f, 0x51, 0x54, 0x52, 0x41, 0x43, 0x45]); // "MOQTRACE"
5
- const FORMAT_VERSION = 1;
6
- const PREAMBLE_SIZE = 16; // 8 magic + 4 version + 4 header length
18
+ const MAGIC = new Uint8Array([0x4d, 0x4f, 0x51, 0x54, 0x52, 0x41, 0x43, 0x45]) // "MOQTRACE"
19
+ const FORMAT_VERSION = 1
20
+ const PREAMBLE_SIZE = 16 // 8 magic + 4 version + 4 header length
7
21
 
8
22
  // Event type string ↔ integer mapping
9
- const EVENT_TYPE_TO_INT: Record<TraceEvent["type"], number> = {
23
+ const EVENT_TYPE_TO_INT: Record<TraceEvent['type'], number> = {
10
24
  control: 0,
11
- "stream-opened": 1,
12
- "stream-closed": 2,
13
- "object-header": 3,
14
- "object-payload": 4,
15
- "state-change": 5,
25
+ 'stream-opened': 1,
26
+ 'stream-closed': 2,
27
+ 'object-header': 3,
28
+ 'object-payload': 4,
29
+ 'state-change': 5,
16
30
  error: 6,
17
31
  annotation: 7,
18
- };
19
-
20
- const INT_TO_EVENT_TYPE: Record<number, TraceEvent["type"]> = {
21
- 0: "control",
22
- 1: "stream-opened",
23
- 2: "stream-closed",
24
- 3: "object-header",
25
- 4: "object-payload",
26
- 5: "state-change",
27
- 6: "error",
28
- 7: "annotation",
29
- };
32
+ }
33
+
34
+ const INT_TO_EVENT_TYPE: Record<number, TraceEvent['type']> = {
35
+ 0: 'control',
36
+ 1: 'stream-opened',
37
+ 2: 'stream-closed',
38
+ 3: 'object-header',
39
+ 4: 'object-payload',
40
+ 5: 'state-change',
41
+ 6: 'error',
42
+ 7: 'annotation',
43
+ }
30
44
 
31
45
  // --- Header serialization ---
32
46
 
@@ -36,14 +50,14 @@ function headerToCbor(header: TraceHeader): Record<string, unknown> {
36
50
  perspective: header.perspective,
37
51
  detail: header.detail,
38
52
  startTime: header.startTime,
39
- };
40
- if (header.endTime != null) map.endTime = header.endTime;
41
- if (header.transport != null) map.transport = header.transport;
42
- if (header.source != null) map.source = header.source;
43
- if (header.endpoint != null) map.endpoint = header.endpoint;
44
- if (header.sessionId != null) map.sessionId = header.sessionId;
45
- if (header.custom != null) map.custom = header.custom;
46
- return map;
53
+ }
54
+ if (header.endTime != null) map.endTime = header.endTime
55
+ if (header.transport != null) map.transport = header.transport
56
+ if (header.source != null) map.source = header.source
57
+ if (header.endpoint != null) map.endpoint = header.endpoint
58
+ if (header.sessionId != null) map.sessionId = header.sessionId
59
+ if (header.custom != null) map.custom = header.custom
60
+ return map
47
61
  }
48
62
 
49
63
  function cborToHeader(obj: Record<string, unknown>): TraceHeader {
@@ -58,7 +72,7 @@ function cborToHeader(obj: Record<string, unknown>): TraceHeader {
58
72
  ...(obj.endpoint != null ? { endpoint: obj.endpoint as string } : {}),
59
73
  ...(obj.sessionId != null ? { sessionId: obj.sessionId as string } : {}),
60
74
  ...(obj.custom != null ? { custom: obj.custom as Record<string, unknown> } : {}),
61
- };
75
+ }
62
76
  }
63
77
 
64
78
  // --- Event serialization ---
@@ -68,113 +82,113 @@ function eventToCbor(event: TraceEvent): Record<string, unknown> {
68
82
  n: event.seq,
69
83
  t: event.timestamp,
70
84
  e: EVENT_TYPE_TO_INT[event.type],
71
- };
85
+ }
72
86
 
73
87
  switch (event.type) {
74
- case "control": {
75
- base.d = event.direction;
76
- base.mt = event.messageType;
77
- base.msg = event.message;
78
- if (event.raw != null) base.raw = event.raw;
79
- break;
88
+ case 'control': {
89
+ base.d = event.direction
90
+ base.mt = event.messageType
91
+ base.msg = event.message
92
+ if (event.raw != null) base.raw = event.raw
93
+ break
80
94
  }
81
- case "stream-opened": {
82
- base.sid = event.streamId;
83
- base.d = event.direction;
84
- base.st = event.streamType;
85
- break;
95
+ case 'stream-opened': {
96
+ base.sid = event.streamId
97
+ base.d = event.direction
98
+ base.st = event.streamType
99
+ break
86
100
  }
87
- case "stream-closed": {
88
- base.sid = event.streamId;
89
- base.ec = event.errorCode;
90
- break;
101
+ case 'stream-closed': {
102
+ base.sid = event.streamId
103
+ base.ec = event.errorCode
104
+ break
91
105
  }
92
- case "object-header": {
93
- base.sid = event.streamId;
94
- base.g = event.groupId;
95
- base.o = event.objectId;
96
- base.pp = event.publisherPriority;
97
- base.os = event.objectStatus;
98
- break;
106
+ case 'object-header': {
107
+ base.sid = event.streamId
108
+ base.g = event.groupId
109
+ base.o = event.objectId
110
+ base.pp = event.publisherPriority
111
+ base.os = event.objectStatus
112
+ break
99
113
  }
100
- case "object-payload": {
101
- base.sid = event.streamId;
102
- base.g = event.groupId;
103
- base.o = event.objectId;
104
- base.sz = event.size;
105
- if (event.payload != null) base.pl = event.payload;
106
- break;
114
+ case 'object-payload': {
115
+ base.sid = event.streamId
116
+ base.g = event.groupId
117
+ base.o = event.objectId
118
+ base.sz = event.size
119
+ if (event.payload != null) base.pl = event.payload
120
+ break
107
121
  }
108
- case "state-change": {
109
- base.from = event.from;
110
- base.to = event.to;
111
- break;
122
+ case 'state-change': {
123
+ base.from = event.from
124
+ base.to = event.to
125
+ break
112
126
  }
113
- case "error": {
114
- base.ec = event.errorCode;
115
- base.reason = event.reason;
116
- break;
127
+ case 'error': {
128
+ base.ec = event.errorCode
129
+ base.reason = event.reason
130
+ break
117
131
  }
118
- case "annotation": {
119
- base.label = event.label;
120
- if (event.data !== undefined) base.data = event.data;
121
- break;
132
+ case 'annotation': {
133
+ base.label = event.label
134
+ if (event.data !== undefined) base.data = event.data
135
+ break
122
136
  }
123
137
  }
124
138
 
125
- return base;
139
+ return base
126
140
  }
127
141
 
128
142
  function cborToEvent(obj: Record<string, unknown>): TraceEvent {
129
- const eventType = INT_TO_EVENT_TYPE[obj.e as number];
143
+ const eventType = INT_TO_EVENT_TYPE[obj.e as number]
130
144
  if (eventType == null) {
131
145
  // Unknown event type — skip per spec (return annotation as fallback)
132
146
  return {
133
- type: "annotation",
147
+ type: 'annotation',
134
148
  seq: Number(obj.n ?? 0),
135
149
  timestamp: Number(obj.t ?? 0),
136
150
  label: `unknown-event-${obj.e}`,
137
151
  data: obj,
138
- };
152
+ }
139
153
  }
140
154
 
141
- const seq = Number(obj.n ?? 0);
142
- const timestamp = Number(obj.t ?? 0);
155
+ const seq = Number(obj.n ?? 0)
156
+ const timestamp = Number(obj.t ?? 0)
143
157
 
144
158
  switch (eventType) {
145
- case "control":
159
+ case 'control':
146
160
  return {
147
- type: "control" as const,
161
+ type: 'control' as const,
148
162
  seq,
149
163
  timestamp,
150
164
  direction: obj.d as 0 | 1,
151
165
  messageType: Number(obj.mt ?? 0),
152
166
  message: (obj.msg ?? {}) as Record<string, unknown>,
153
167
  ...(obj.raw != null ? { raw: obj.raw as Uint8Array } : {}),
154
- };
168
+ }
155
169
 
156
- case "stream-opened":
170
+ case 'stream-opened':
157
171
  return {
158
- type: "stream-opened" as const,
172
+ type: 'stream-opened' as const,
159
173
  seq,
160
174
  timestamp,
161
175
  streamId: BigInt(obj.sid as bigint | number),
162
176
  direction: obj.d as 0 | 1,
163
177
  streamType: obj.st as 0 | 1 | 2,
164
- };
178
+ }
165
179
 
166
- case "stream-closed":
180
+ case 'stream-closed':
167
181
  return {
168
- type: "stream-closed" as const,
182
+ type: 'stream-closed' as const,
169
183
  seq,
170
184
  timestamp,
171
185
  streamId: BigInt(obj.sid as bigint | number),
172
186
  errorCode: Number(obj.ec ?? 0),
173
- };
187
+ }
174
188
 
175
- case "object-header":
189
+ case 'object-header':
176
190
  return {
177
- type: "object-header" as const,
191
+ type: 'object-header' as const,
178
192
  seq,
179
193
  timestamp,
180
194
  streamId: BigInt(obj.sid as bigint | number),
@@ -182,11 +196,11 @@ function cborToEvent(obj: Record<string, unknown>): TraceEvent {
182
196
  objectId: BigInt(obj.o as bigint | number),
183
197
  publisherPriority: Number(obj.pp ?? 0),
184
198
  objectStatus: Number(obj.os ?? 0),
185
- };
199
+ }
186
200
 
187
- case "object-payload":
201
+ case 'object-payload':
188
202
  return {
189
- type: "object-payload" as const,
203
+ type: 'object-payload' as const,
190
204
  seq,
191
205
  timestamp,
192
206
  streamId: BigInt(obj.sid as bigint | number),
@@ -194,72 +208,75 @@ function cborToEvent(obj: Record<string, unknown>): TraceEvent {
194
208
  objectId: BigInt(obj.o as bigint | number),
195
209
  size: Number(obj.sz ?? 0),
196
210
  ...(obj.pl != null ? { payload: obj.pl as Uint8Array } : {}),
197
- };
211
+ }
198
212
 
199
- case "state-change":
213
+ case 'state-change':
200
214
  return {
201
- type: "state-change" as const,
215
+ type: 'state-change' as const,
202
216
  seq,
203
217
  timestamp,
204
218
  from: obj.from as string,
205
219
  to: obj.to as string,
206
- };
220
+ }
207
221
 
208
- case "error":
222
+ case 'error':
209
223
  return {
210
- type: "error" as const,
224
+ type: 'error' as const,
211
225
  seq,
212
226
  timestamp,
213
227
  errorCode: Number(obj.ec ?? 0),
214
- reason: (obj.reason ?? "") as string,
215
- };
228
+ reason: (obj.reason ?? '') as string,
229
+ }
216
230
 
217
- case "annotation":
231
+ case 'annotation':
218
232
  return {
219
- type: "annotation" as const,
233
+ type: 'annotation' as const,
220
234
  seq,
221
235
  timestamp,
222
- label: (obj.label ?? "") as string,
236
+ label: (obj.label ?? '') as string,
223
237
  data: obj.data,
224
- };
238
+ }
225
239
  }
226
240
  }
227
241
 
228
242
  // --- Preamble helpers ---
229
243
 
230
244
  function writePreamble(headerCbor: Uint8Array): Uint8Array {
231
- const buf = new Uint8Array(PREAMBLE_SIZE + headerCbor.length);
232
- buf.set(MAGIC, 0);
233
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
234
- view.setUint32(8, FORMAT_VERSION, true);
235
- view.setUint32(12, headerCbor.length, true);
236
- buf.set(headerCbor, PREAMBLE_SIZE);
237
- return buf;
245
+ const buf = new Uint8Array(PREAMBLE_SIZE + headerCbor.length)
246
+ buf.set(MAGIC, 0)
247
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
248
+ view.setUint32(8, FORMAT_VERSION, true)
249
+ view.setUint32(12, headerCbor.length, true)
250
+ buf.set(headerCbor, PREAMBLE_SIZE)
251
+ return buf
238
252
  }
239
253
 
240
- function validatePreamble(bytes: Uint8Array): { version: number; headerLength: number } {
254
+ function validatePreamble(bytes: Uint8Array): {
255
+ version: number
256
+ headerLength: number
257
+ } {
241
258
  if (bytes.length < PREAMBLE_SIZE) {
242
- throw new Error("File too short: expected at least 16 bytes");
259
+ throw new Error('File too short: expected at least 16 bytes')
243
260
  }
244
261
 
245
262
  for (let i = 0; i < 8; i++) {
246
263
  if (bytes[i] !== MAGIC[i]) {
247
- throw new Error("Invalid magic bytes: not a .moqtrace file");
264
+ throw new Error('Invalid magic bytes: not a .moqtrace file')
248
265
  }
249
266
  }
250
267
 
251
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
252
- const version = view.getUint32(8, true);
268
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
269
+ const version = view.getUint32(8, true)
253
270
  if (version !== FORMAT_VERSION) {
254
- throw new Error(`Unsupported format version: ${version} (expected ${FORMAT_VERSION})`);
271
+ throw new Error(`Unsupported format version: ${version} (expected ${FORMAT_VERSION})`)
255
272
  }
256
273
 
257
- const headerLength = view.getUint32(12, true);
274
+ const headerLength = view.getUint32(12, true)
258
275
  if (PREAMBLE_SIZE + headerLength > bytes.length) {
259
- throw new Error("File truncated: header extends beyond file");
276
+ throw new Error('File truncated: header extends beyond file')
260
277
  }
261
278
 
262
- return { version, headerLength };
279
+ return { version, headerLength }
263
280
  }
264
281
 
265
282
  // --- Public API ---
@@ -268,56 +285,56 @@ function validatePreamble(bytes: Uint8Array): { version: number; headerLength: n
268
285
  * Write a complete trace to .moqtrace binary format.
269
286
  */
270
287
  export function writeMoqtrace(trace: Trace): Uint8Array {
271
- const headerCbor = encode(headerToCbor(trace.header));
272
- const preamble = writePreamble(headerCbor);
288
+ const headerCbor = encode(headerToCbor(trace.header))
289
+ const preamble = writePreamble(headerCbor)
273
290
 
274
- const eventChunks: Uint8Array[] = [];
275
- let totalEventBytes = 0;
291
+ const eventChunks: Uint8Array[] = []
292
+ let totalEventBytes = 0
276
293
  for (const event of trace.events) {
277
- const chunk = encode(eventToCbor(event));
278
- eventChunks.push(chunk);
279
- totalEventBytes += chunk.length;
294
+ const chunk = encode(eventToCbor(event))
295
+ eventChunks.push(chunk)
296
+ totalEventBytes += chunk.length
280
297
  }
281
298
 
282
- const result = new Uint8Array(preamble.length + totalEventBytes);
283
- result.set(preamble, 0);
284
- let offset = preamble.length;
299
+ const result = new Uint8Array(preamble.length + totalEventBytes)
300
+ result.set(preamble, 0)
301
+ let offset = preamble.length
285
302
  for (const chunk of eventChunks) {
286
- result.set(chunk, offset);
287
- offset += chunk.length;
303
+ result.set(chunk, offset)
304
+ offset += chunk.length
288
305
  }
289
306
 
290
- return result;
307
+ return result
291
308
  }
292
309
 
293
310
  /**
294
311
  * Read a complete .moqtrace file from bytes.
295
312
  */
296
313
  export function readMoqtrace(bytes: Uint8Array): Trace {
297
- const { headerLength } = validatePreamble(bytes);
314
+ const { headerLength } = validatePreamble(bytes)
298
315
 
299
- const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
300
- const header = cborToHeader(decode(headerBytes) as Record<string, unknown>);
316
+ const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength)
317
+ const header = cborToHeader(decode(headerBytes) as Record<string, unknown>)
301
318
 
302
- const eventBytes = bytes.slice(PREAMBLE_SIZE + headerLength);
303
- const events: TraceEvent[] = [];
319
+ const eventBytes = bytes.slice(PREAMBLE_SIZE + headerLength)
320
+ const events: TraceEvent[] = []
304
321
 
305
322
  if (eventBytes.length > 0) {
306
323
  decodeMultiple(eventBytes, (obj: unknown) => {
307
- events.push(cborToEvent(obj as Record<string, unknown>));
308
- });
324
+ events.push(cborToEvent(obj as Record<string, unknown>))
325
+ })
309
326
  }
310
327
 
311
- return { header, events };
328
+ return { header, events }
312
329
  }
313
330
 
314
331
  /**
315
332
  * Read only the header from a .moqtrace file (fast metadata peek).
316
333
  */
317
334
  export function readMoqtraceHeader(bytes: Uint8Array): TraceHeader {
318
- const { headerLength } = validatePreamble(bytes);
319
- const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
320
- return cborToHeader(decode(headerBytes) as Record<string, unknown>);
335
+ const { headerLength } = validatePreamble(bytes)
336
+ const headerBytes = bytes.slice(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength)
337
+ return cborToHeader(decode(headerBytes) as Record<string, unknown>)
321
338
  }
322
339
 
323
340
  /**
@@ -325,21 +342,21 @@ export function readMoqtraceHeader(bytes: Uint8Array): TraceHeader {
325
342
  */
326
343
  export interface MoqtraceWriter {
327
344
  /** Returns the file preamble (magic + version + header). Write this first. */
328
- preamble(): Uint8Array;
345
+ preamble(): Uint8Array
329
346
  /** Encode a single event. Append the returned bytes after the preamble. */
330
- writeEvent(event: TraceEvent): Uint8Array;
347
+ writeEvent(event: TraceEvent): Uint8Array
331
348
  }
332
349
 
333
350
  export function createMoqtraceWriter(header: TraceHeader): MoqtraceWriter {
334
- const headerCbor = encode(headerToCbor(header));
335
- const preambleBytes = writePreamble(headerCbor);
351
+ const headerCbor = encode(headerToCbor(header))
352
+ const preambleBytes = writePreamble(headerCbor)
336
353
 
337
354
  return {
338
355
  preamble(): Uint8Array {
339
- return preambleBytes;
356
+ return preambleBytes
340
357
  },
341
358
  writeEvent(event: TraceEvent): Uint8Array {
342
- return encode(eventToCbor(event));
359
+ return encode(eventToCbor(event))
343
360
  },
344
- };
361
+ }
345
362
  }
package/src/index.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  // Binary .moqtrace format
2
2
 
3
- export type { MoqtraceWriter } from "./binary.js";
3
+ export type { MoqtraceWriter } from './binary.js'
4
4
  export {
5
5
  createMoqtraceWriter,
6
6
  readMoqtrace,
7
7
  readMoqtraceHeader,
8
8
  writeMoqtrace,
9
- } from "./binary.js";
9
+ } from './binary.js'
10
10
  // JSON (convenience)
11
- export { traceToJSON } from "./json.js";
12
- export type { TraceRecorder } from "./recorder.js";
11
+ export { traceToJSON } from './json.js'
12
+ export type { TraceRecorder } from './recorder.js'
13
13
  // Recorder
14
- export { createRecorder } from "./recorder.js";
14
+ export { createRecorder } from './recorder.js'
15
15
 
16
16
  // Types
17
17
  export type {
@@ -29,4 +29,4 @@ export type {
29
29
  TraceErrorEvent,
30
30
  TraceEvent,
31
31
  TraceHeader,
32
- } from "./types.js";
32
+ } from './types.js'
package/src/json.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Trace } from "./types.js";
1
+ import type { Trace } from './types.js'
2
2
 
3
3
  /**
4
4
  * Serialize a trace to human-readable JSON.
@@ -12,16 +12,16 @@ export function traceToJSON(trace: Trace): string {
12
12
  return JSON.stringify(
13
13
  trace,
14
14
  (_key, value) => {
15
- if (typeof value === "bigint") {
16
- return `0x${value.toString(16)}`;
15
+ if (typeof value === 'bigint') {
16
+ return `0x${value.toString(16)}`
17
17
  }
18
18
  if (value instanceof Uint8Array) {
19
19
  return Array.from(value)
20
- .map((b) => b.toString(16).padStart(2, "0"))
21
- .join("");
20
+ .map((b) => b.toString(16).padStart(2, '0'))
21
+ .join('')
22
22
  }
23
- return value;
23
+ return value
24
24
  },
25
25
  2,
26
- );
26
+ )
27
27
  }