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