@moqtap/trace 0.2.0 → 0.3.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/README.md +95 -0
- package/SPEC.md +378 -111
- package/dist/index.cjs +461 -54
- package/dist/index.d.cts +251 -5
- package/dist/index.d.ts +251 -5
- package/dist/index.js +460 -53
- package/package.json +2 -2
- package/src/binary.ts +467 -57
- package/src/cbor-scan.ts +185 -0
- package/src/index.ts +18 -1
- package/src/recorder.ts +10 -0
- package/src/types.ts +198 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,150 @@
|
|
|
1
1
|
// src/binary.ts
|
|
2
2
|
import { Encoder } from "cbor-x";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// src/cbor-scan.ts
|
|
5
|
+
var INCOMPLETE = null;
|
|
6
|
+
var MalformedCborError = class extends Error {
|
|
7
|
+
offset;
|
|
8
|
+
constructor(message, offset) {
|
|
9
|
+
super(`${message} at byte ${offset}`);
|
|
10
|
+
this.name = "MalformedCborError";
|
|
11
|
+
this.offset = offset;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var MAJOR_UNSIGNED = 0;
|
|
15
|
+
var MAJOR_NEGATIVE = 1;
|
|
16
|
+
var MAJOR_BYTES = 2;
|
|
17
|
+
var MAJOR_TEXT = 3;
|
|
18
|
+
var MAJOR_ARRAY = 4;
|
|
19
|
+
var MAJOR_MAP = 5;
|
|
20
|
+
var MAJOR_TAG = 6;
|
|
21
|
+
var MAJOR_SIMPLE = 7;
|
|
22
|
+
var AI_ONE_BYTE = 24;
|
|
23
|
+
var AI_TWO_BYTES = 25;
|
|
24
|
+
var AI_FOUR_BYTES = 26;
|
|
25
|
+
var AI_EIGHT_BYTES = 27;
|
|
26
|
+
var AI_INDEFINITE = 31;
|
|
27
|
+
var BREAK = 255;
|
|
28
|
+
function headerExtraBytes(additionalInfo, offset) {
|
|
29
|
+
if (additionalInfo < AI_ONE_BYTE) return 0;
|
|
30
|
+
switch (additionalInfo) {
|
|
31
|
+
case AI_ONE_BYTE:
|
|
32
|
+
return 1;
|
|
33
|
+
case AI_TWO_BYTES:
|
|
34
|
+
return 2;
|
|
35
|
+
case AI_FOUR_BYTES:
|
|
36
|
+
return 4;
|
|
37
|
+
case AI_EIGHT_BYTES:
|
|
38
|
+
return 8;
|
|
39
|
+
case AI_INDEFINITE:
|
|
40
|
+
return 0;
|
|
41
|
+
default:
|
|
42
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function readArgument(bytes, offset, additionalInfo) {
|
|
46
|
+
if (additionalInfo < AI_ONE_BYTE) return additionalInfo;
|
|
47
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
48
|
+
switch (additionalInfo) {
|
|
49
|
+
case AI_ONE_BYTE:
|
|
50
|
+
return view.getUint8(offset + 1);
|
|
51
|
+
case AI_TWO_BYTES:
|
|
52
|
+
return view.getUint16(offset + 1, false);
|
|
53
|
+
case AI_FOUR_BYTES:
|
|
54
|
+
return view.getUint32(offset + 1, false);
|
|
55
|
+
case AI_EIGHT_BYTES: {
|
|
56
|
+
const value = view.getBigUint64(offset + 1, false);
|
|
57
|
+
return Number(value);
|
|
58
|
+
}
|
|
59
|
+
default:
|
|
60
|
+
throw new MalformedCborError(`reserved additional information ${additionalInfo}`, offset);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function skipItem(bytes, offset, allowBreak) {
|
|
64
|
+
if (offset >= bytes.length) return INCOMPLETE;
|
|
65
|
+
const initial = bytes[offset];
|
|
66
|
+
if (initial === BREAK) {
|
|
67
|
+
if (!allowBreak) {
|
|
68
|
+
throw new MalformedCborError("break outside an indefinite-length item", offset);
|
|
69
|
+
}
|
|
70
|
+
return offset + 1;
|
|
71
|
+
}
|
|
72
|
+
const major = initial >> 5;
|
|
73
|
+
const additionalInfo = initial & 31;
|
|
74
|
+
const extra = headerExtraBytes(additionalInfo, offset);
|
|
75
|
+
if (offset + 1 + extra > bytes.length) return INCOMPLETE;
|
|
76
|
+
const indefinite = additionalInfo === AI_INDEFINITE;
|
|
77
|
+
if (indefinite && (major === MAJOR_UNSIGNED || major === MAJOR_NEGATIVE || major === MAJOR_TAG)) {
|
|
78
|
+
throw new MalformedCborError(`major type ${major} cannot be indefinite-length`, offset);
|
|
79
|
+
}
|
|
80
|
+
const afterHeader = offset + 1 + extra;
|
|
81
|
+
switch (major) {
|
|
82
|
+
case MAJOR_UNSIGNED:
|
|
83
|
+
case MAJOR_NEGATIVE:
|
|
84
|
+
return afterHeader;
|
|
85
|
+
case MAJOR_BYTES:
|
|
86
|
+
case MAJOR_TEXT: {
|
|
87
|
+
if (!indefinite) {
|
|
88
|
+
const length = readArgument(bytes, offset, additionalInfo);
|
|
89
|
+
const end = afterHeader + length;
|
|
90
|
+
return end > bytes.length ? INCOMPLETE : end;
|
|
91
|
+
}
|
|
92
|
+
let cursor = afterHeader;
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
if (cursor >= bytes.length) return INCOMPLETE;
|
|
95
|
+
if (bytes[cursor] === BREAK) return cursor + 1;
|
|
96
|
+
const next = skipItem(bytes, cursor, false);
|
|
97
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
98
|
+
cursor = next;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
case MAJOR_ARRAY:
|
|
102
|
+
case MAJOR_MAP: {
|
|
103
|
+
const perEntry = major === MAJOR_MAP ? 2 : 1;
|
|
104
|
+
if (!indefinite) {
|
|
105
|
+
const count = readArgument(bytes, offset, additionalInfo) * perEntry;
|
|
106
|
+
let cursor2 = afterHeader;
|
|
107
|
+
for (let i = 0; i < count; i++) {
|
|
108
|
+
const next = skipItem(bytes, cursor2, false);
|
|
109
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
110
|
+
cursor2 = next;
|
|
111
|
+
}
|
|
112
|
+
return cursor2;
|
|
113
|
+
}
|
|
114
|
+
let cursor = afterHeader;
|
|
115
|
+
for (; ; ) {
|
|
116
|
+
if (cursor >= bytes.length) return INCOMPLETE;
|
|
117
|
+
if (bytes[cursor] === BREAK) return cursor + 1;
|
|
118
|
+
for (let i = 0; i < perEntry; i++) {
|
|
119
|
+
const next = skipItem(bytes, cursor, false);
|
|
120
|
+
if (next === INCOMPLETE) return INCOMPLETE;
|
|
121
|
+
cursor = next;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
case MAJOR_TAG:
|
|
126
|
+
return skipItem(bytes, afterHeader, false);
|
|
127
|
+
case MAJOR_SIMPLE:
|
|
128
|
+
return afterHeader;
|
|
129
|
+
default:
|
|
130
|
+
throw new MalformedCborError(`unknown major type ${major}`, offset);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function cborItemLength(bytes, start) {
|
|
134
|
+
const end = skipItem(bytes, start, false);
|
|
135
|
+
return end === INCOMPLETE ? INCOMPLETE : end - start;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/types.ts
|
|
139
|
+
var TRACE_ID_LENGTH = 16;
|
|
140
|
+
|
|
141
|
+
// src/binary.ts
|
|
142
|
+
var codec = new Encoder({ useRecords: false, mapsAsObjects: true, tagUint8Array: false });
|
|
4
143
|
var encode = (value) => codec.encode(value);
|
|
5
144
|
var decode = (bytes) => codec.decode(bytes);
|
|
6
|
-
var decodeMultiple = (bytes, cb) => {
|
|
7
|
-
codec.decodeMultiple(bytes, cb);
|
|
8
|
-
};
|
|
9
145
|
var MAGIC = new Uint8Array([77, 79, 81, 84, 82, 65, 67, 69]);
|
|
10
|
-
var FORMAT_VERSION =
|
|
146
|
+
var FORMAT_VERSION = 2;
|
|
147
|
+
var SUPPORTED_VERSIONS = [1, FORMAT_VERSION];
|
|
11
148
|
var PREAMBLE_SIZE = 16;
|
|
12
149
|
var EVENT_TYPE_TO_INT = {
|
|
13
150
|
control: 0,
|
|
@@ -17,7 +154,10 @@ var EVENT_TYPE_TO_INT = {
|
|
|
17
154
|
"object-payload": 4,
|
|
18
155
|
"state-change": 5,
|
|
19
156
|
error: 6,
|
|
20
|
-
annotation: 7
|
|
157
|
+
annotation: 7,
|
|
158
|
+
"peer-connected": 8,
|
|
159
|
+
"peer-disconnected": 9,
|
|
160
|
+
"subscription-derivation": 10
|
|
21
161
|
};
|
|
22
162
|
var INT_TO_EVENT_TYPE = {
|
|
23
163
|
0: "control",
|
|
@@ -27,20 +167,85 @@ var INT_TO_EVENT_TYPE = {
|
|
|
27
167
|
4: "object-payload",
|
|
28
168
|
5: "state-change",
|
|
29
169
|
6: "error",
|
|
30
|
-
7: "annotation"
|
|
170
|
+
7: "annotation",
|
|
171
|
+
8: "peer-connected",
|
|
172
|
+
9: "peer-disconnected",
|
|
173
|
+
10: "subscription-derivation"
|
|
31
174
|
};
|
|
175
|
+
var COMMON_EVENT_KEYS = /* @__PURE__ */ new Set(["n", "t", "p", "e"]);
|
|
176
|
+
var TruncatedTraceError = class extends Error {
|
|
177
|
+
/** Byte offset at which the incomplete item begins. */
|
|
178
|
+
offset;
|
|
179
|
+
/**
|
|
180
|
+
* Everything that decoded before the cut, one entry per segment. A
|
|
181
|
+
* truncated trace is still evidence, and the events before the cut are
|
|
182
|
+
* exactly as valid as they were.
|
|
183
|
+
*/
|
|
184
|
+
segments;
|
|
185
|
+
constructor(offset, segments) {
|
|
186
|
+
super(`Trace truncated: the item starting at byte ${offset} is incomplete`);
|
|
187
|
+
this.name = "TruncatedTraceError";
|
|
188
|
+
this.offset = offset;
|
|
189
|
+
this.segments = segments;
|
|
190
|
+
}
|
|
191
|
+
/** What decoded, flattened as {@link readMoqtrace} would return it. */
|
|
192
|
+
get trace() {
|
|
193
|
+
return flatten(this.segments);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function segmentToCbor(segment) {
|
|
197
|
+
const map = { sequence: int(segment.sequence) };
|
|
198
|
+
if (segment.durationMs != null) map.durationMs = int(segment.durationMs);
|
|
199
|
+
if (segment.streamId != null) map.streamId = segment.streamId;
|
|
200
|
+
if (segment.continues != null) map.continues = segment.continues;
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
function cborToSegment(obj) {
|
|
204
|
+
return {
|
|
205
|
+
sequence: Number(obj.sequence ?? 0),
|
|
206
|
+
...obj.durationMs != null ? { durationMs: Number(obj.durationMs) } : {},
|
|
207
|
+
...obj.streamId != null ? { streamId: obj.streamId } : {},
|
|
208
|
+
...obj.continues != null ? { continues: obj.continues } : {}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function samplingToCbor(sampling) {
|
|
212
|
+
const map = {};
|
|
213
|
+
if (sampling.effectiveRate != null) map.effectiveRate = sampling.effectiveRate;
|
|
214
|
+
if (sampling.maxEventsPerSec != null) map.maxEventsPerSec = int(sampling.maxEventsPerSec);
|
|
215
|
+
if (sampling.dropPolicy != null) map.dropPolicy = sampling.dropPolicy;
|
|
216
|
+
if (sampling.droppedTotal != null) map.droppedTotal = int(sampling.droppedTotal);
|
|
217
|
+
if (sampling.droppedSegment != null) map.droppedSegment = int(sampling.droppedSegment);
|
|
218
|
+
if (sampling.rule != null) map.rule = sampling.rule;
|
|
219
|
+
if (sampling.ruleLang != null) map.ruleLang = sampling.ruleLang;
|
|
220
|
+
if (sampling.appliesTo != null) map.appliesTo = sampling.appliesTo.map(int);
|
|
221
|
+
return map;
|
|
222
|
+
}
|
|
223
|
+
function cborToSampling(obj) {
|
|
224
|
+
return {
|
|
225
|
+
...obj.effectiveRate != null ? { effectiveRate: Number(obj.effectiveRate) } : {},
|
|
226
|
+
...obj.maxEventsPerSec != null ? { maxEventsPerSec: Number(obj.maxEventsPerSec) } : {},
|
|
227
|
+
...obj.dropPolicy != null ? { dropPolicy: obj.dropPolicy } : {},
|
|
228
|
+
...obj.droppedTotal != null ? { droppedTotal: Number(obj.droppedTotal) } : {},
|
|
229
|
+
...obj.droppedSegment != null ? { droppedSegment: Number(obj.droppedSegment) } : {},
|
|
230
|
+
...obj.rule != null ? { rule: obj.rule } : {},
|
|
231
|
+
...obj.ruleLang != null ? { ruleLang: obj.ruleLang } : {},
|
|
232
|
+
...obj.appliesTo != null ? { appliesTo: obj.appliesTo.map(Number) } : {}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
32
235
|
function headerToCbor(header) {
|
|
33
236
|
const map = {
|
|
34
237
|
protocol: header.protocol,
|
|
35
238
|
perspective: header.perspective,
|
|
36
239
|
detail: header.detail,
|
|
37
|
-
startTime: header.startTime
|
|
240
|
+
startTime: int(header.startTime)
|
|
38
241
|
};
|
|
39
|
-
if (header.endTime != null) map.endTime = header.endTime;
|
|
242
|
+
if (header.endTime != null) map.endTime = int(header.endTime);
|
|
40
243
|
if (header.transport != null) map.transport = header.transport;
|
|
41
244
|
if (header.source != null) map.source = header.source;
|
|
42
245
|
if (header.endpoint != null) map.endpoint = header.endpoint;
|
|
43
246
|
if (header.sessionId != null) map.sessionId = header.sessionId;
|
|
247
|
+
if (header.segment != null) map.segment = segmentToCbor(header.segment);
|
|
248
|
+
if (header.sampling != null) map.sampling = samplingToCbor(header.sampling);
|
|
44
249
|
if (header.custom != null) map.custom = header.custom;
|
|
45
250
|
return map;
|
|
46
251
|
}
|
|
@@ -49,25 +254,51 @@ function cborToHeader(obj) {
|
|
|
49
254
|
protocol: obj.protocol,
|
|
50
255
|
perspective: obj.perspective,
|
|
51
256
|
detail: obj.detail,
|
|
52
|
-
startTime: obj.startTime,
|
|
53
|
-
...obj.endTime != null ? { endTime: obj.endTime } : {},
|
|
257
|
+
startTime: Number(obj.startTime),
|
|
258
|
+
...obj.endTime != null ? { endTime: Number(obj.endTime) } : {},
|
|
54
259
|
...obj.transport != null ? { transport: obj.transport } : {},
|
|
55
260
|
...obj.source != null ? { source: obj.source } : {},
|
|
56
261
|
...obj.endpoint != null ? { endpoint: obj.endpoint } : {},
|
|
57
262
|
...obj.sessionId != null ? { sessionId: obj.sessionId } : {},
|
|
263
|
+
...obj.segment != null ? { segment: cborToSegment(obj.segment) } : {},
|
|
264
|
+
...obj.sampling != null ? { sampling: cborToSampling(obj.sampling) } : {},
|
|
58
265
|
...obj.custom != null ? { custom: obj.custom } : {}
|
|
59
266
|
};
|
|
60
267
|
}
|
|
268
|
+
function subscriptionRefToCbor(ref) {
|
|
269
|
+
return [ref.peer, ref.requestId];
|
|
270
|
+
}
|
|
271
|
+
function cborToSubscriptionRef(value) {
|
|
272
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
273
|
+
throw new Error("Subscription reference must be [peer, requestId]");
|
|
274
|
+
}
|
|
275
|
+
return { peer: value[0], requestId: BigInt(value[1]) };
|
|
276
|
+
}
|
|
277
|
+
function toBytes(value) {
|
|
278
|
+
return value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
|
|
279
|
+
}
|
|
280
|
+
function int(value) {
|
|
281
|
+
const outsideInt32 = value > 4294967295 || value < -2147483648;
|
|
282
|
+
return Number.isInteger(value) && outsideInt32 ? BigInt(value) : value;
|
|
283
|
+
}
|
|
284
|
+
function checkTraceId(value) {
|
|
285
|
+
const bytes = toBytes(value);
|
|
286
|
+
if (bytes.length !== TRACE_ID_LENGTH) {
|
|
287
|
+
throw new Error(`traceId is ${bytes.length} bytes, must be ${TRACE_ID_LENGTH}`);
|
|
288
|
+
}
|
|
289
|
+
return bytes;
|
|
290
|
+
}
|
|
61
291
|
function eventToCbor(event) {
|
|
62
292
|
const base = {
|
|
63
|
-
n: event.seq,
|
|
64
|
-
t: event.timestamp
|
|
65
|
-
e: EVENT_TYPE_TO_INT[event.type]
|
|
293
|
+
n: int(event.seq),
|
|
294
|
+
t: int(event.timestamp)
|
|
66
295
|
};
|
|
296
|
+
if (event.peer != null) base.p = event.peer;
|
|
297
|
+
if (event.type !== "unknown") base.e = EVENT_TYPE_TO_INT[event.type];
|
|
67
298
|
switch (event.type) {
|
|
68
299
|
case "control": {
|
|
69
300
|
base.d = event.direction;
|
|
70
|
-
base.mt = event.messageType;
|
|
301
|
+
base.mt = int(event.messageType);
|
|
71
302
|
base.msg = event.message;
|
|
72
303
|
if (event.streamId != null) base.sid = event.streamId;
|
|
73
304
|
if (event.raw != null) base.raw = event.raw;
|
|
@@ -81,22 +312,22 @@ function eventToCbor(event) {
|
|
|
81
312
|
}
|
|
82
313
|
case "stream-closed": {
|
|
83
314
|
base.sid = event.streamId;
|
|
84
|
-
base.ec = event.errorCode;
|
|
315
|
+
base.ec = int(event.errorCode);
|
|
85
316
|
break;
|
|
86
317
|
}
|
|
87
318
|
case "object-header": {
|
|
88
319
|
base.sid = event.streamId;
|
|
89
320
|
base.g = event.groupId;
|
|
90
321
|
base.o = event.objectId;
|
|
91
|
-
base.pp = event.publisherPriority;
|
|
92
|
-
base.os = event.objectStatus;
|
|
322
|
+
base.pp = int(event.publisherPriority);
|
|
323
|
+
base.os = int(event.objectStatus);
|
|
93
324
|
break;
|
|
94
325
|
}
|
|
95
326
|
case "object-payload": {
|
|
96
327
|
base.sid = event.streamId;
|
|
97
328
|
base.g = event.groupId;
|
|
98
329
|
base.o = event.objectId;
|
|
99
|
-
base.sz = event.size;
|
|
330
|
+
base.sz = int(event.size);
|
|
100
331
|
if (event.payload != null) base.pl = event.payload;
|
|
101
332
|
break;
|
|
102
333
|
}
|
|
@@ -106,7 +337,7 @@ function eventToCbor(event) {
|
|
|
106
337
|
break;
|
|
107
338
|
}
|
|
108
339
|
case "error": {
|
|
109
|
-
base.ec = event.errorCode;
|
|
340
|
+
base.ec = int(event.errorCode);
|
|
110
341
|
base.reason = event.reason;
|
|
111
342
|
break;
|
|
112
343
|
}
|
|
@@ -115,39 +346,79 @@ function eventToCbor(event) {
|
|
|
115
346
|
if (event.data !== void 0) base.data = event.data;
|
|
116
347
|
break;
|
|
117
348
|
}
|
|
349
|
+
case "peer-connected": {
|
|
350
|
+
if (event.endpoint != null) base.endpoint = event.endpoint;
|
|
351
|
+
if (event.transport != null) base.transport = event.transport;
|
|
352
|
+
if (event.role != null) base.role = event.role;
|
|
353
|
+
if (event.side != null) base.side = event.side;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "peer-disconnected": {
|
|
357
|
+
base.ec = int(event.errorCode);
|
|
358
|
+
if (event.reason != null) base.reason = event.reason;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "subscription-derivation": {
|
|
362
|
+
base.u = subscriptionRefToCbor(event.upstream);
|
|
363
|
+
base.d = event.downstream.map(subscriptionRefToCbor);
|
|
364
|
+
base.kind = event.kind;
|
|
365
|
+
if (event.traceId != null) base.traceId = checkTraceId(event.traceId);
|
|
366
|
+
if (event.namespace != null) base.ns = event.namespace;
|
|
367
|
+
if (event.trackName != null) base.tn = event.trackName;
|
|
368
|
+
if (event.tDownstreamReceived != null) base.tdr = int(event.tDownstreamReceived);
|
|
369
|
+
if (event.tUpstreamSent != null) base.tus = int(event.tUpstreamSent);
|
|
370
|
+
if (event.tUpstreamOkReceived != null) base.tuo = int(event.tUpstreamOkReceived);
|
|
371
|
+
if (event.tDownstreamOkSent != null) base.tdo = int(event.tDownstreamOkSent);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "unknown": {
|
|
375
|
+
base.e = event.eventType;
|
|
376
|
+
for (const [key, value] of Object.entries(event.fields)) {
|
|
377
|
+
base[key] = value;
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
118
381
|
}
|
|
119
382
|
return base;
|
|
120
383
|
}
|
|
121
384
|
function cborToEvent(obj) {
|
|
385
|
+
const seq = Number(obj.n ?? 0);
|
|
386
|
+
const timestamp = Number(obj.t ?? 0);
|
|
387
|
+
const peerFields = obj.p != null ? { peer: obj.p } : {};
|
|
122
388
|
const eventType = INT_TO_EVENT_TYPE[obj.e];
|
|
123
389
|
if (eventType == null) {
|
|
390
|
+
const fields = {};
|
|
391
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
392
|
+
if (!COMMON_EVENT_KEYS.has(key)) fields[key] = value;
|
|
393
|
+
}
|
|
124
394
|
return {
|
|
125
|
-
type: "
|
|
126
|
-
seq
|
|
127
|
-
timestamp
|
|
128
|
-
|
|
129
|
-
|
|
395
|
+
type: "unknown",
|
|
396
|
+
seq,
|
|
397
|
+
timestamp,
|
|
398
|
+
...peerFields,
|
|
399
|
+
eventType: Number(obj.e ?? -1),
|
|
400
|
+
fields
|
|
130
401
|
};
|
|
131
402
|
}
|
|
132
|
-
const seq = Number(obj.n ?? 0);
|
|
133
|
-
const timestamp = Number(obj.t ?? 0);
|
|
134
403
|
switch (eventType) {
|
|
135
404
|
case "control":
|
|
136
405
|
return {
|
|
137
406
|
type: "control",
|
|
138
407
|
seq,
|
|
139
408
|
timestamp,
|
|
409
|
+
...peerFields,
|
|
140
410
|
direction: obj.d,
|
|
141
411
|
messageType: Number(obj.mt ?? 0),
|
|
142
412
|
message: obj.msg ?? {},
|
|
143
413
|
...obj.sid != null ? { streamId: BigInt(obj.sid) } : {},
|
|
144
|
-
...obj.raw != null ? { raw: obj.raw } : {}
|
|
414
|
+
...obj.raw != null ? { raw: toBytes(obj.raw) } : {}
|
|
145
415
|
};
|
|
146
416
|
case "stream-opened":
|
|
147
417
|
return {
|
|
148
418
|
type: "stream-opened",
|
|
149
419
|
seq,
|
|
150
420
|
timestamp,
|
|
421
|
+
...peerFields,
|
|
151
422
|
streamId: BigInt(obj.sid),
|
|
152
423
|
direction: obj.d,
|
|
153
424
|
streamType: obj.st
|
|
@@ -157,6 +428,7 @@ function cborToEvent(obj) {
|
|
|
157
428
|
type: "stream-closed",
|
|
158
429
|
seq,
|
|
159
430
|
timestamp,
|
|
431
|
+
...peerFields,
|
|
160
432
|
streamId: BigInt(obj.sid),
|
|
161
433
|
errorCode: Number(obj.ec ?? 0)
|
|
162
434
|
};
|
|
@@ -165,6 +437,7 @@ function cborToEvent(obj) {
|
|
|
165
437
|
type: "object-header",
|
|
166
438
|
seq,
|
|
167
439
|
timestamp,
|
|
440
|
+
...peerFields,
|
|
168
441
|
streamId: BigInt(obj.sid),
|
|
169
442
|
groupId: BigInt(obj.g),
|
|
170
443
|
objectId: BigInt(obj.o),
|
|
@@ -176,17 +449,19 @@ function cborToEvent(obj) {
|
|
|
176
449
|
type: "object-payload",
|
|
177
450
|
seq,
|
|
178
451
|
timestamp,
|
|
452
|
+
...peerFields,
|
|
179
453
|
streamId: BigInt(obj.sid),
|
|
180
454
|
groupId: BigInt(obj.g),
|
|
181
455
|
objectId: BigInt(obj.o),
|
|
182
456
|
size: Number(obj.sz ?? 0),
|
|
183
|
-
...obj.pl != null ? { payload: obj.pl } : {}
|
|
457
|
+
...obj.pl != null ? { payload: toBytes(obj.pl) } : {}
|
|
184
458
|
};
|
|
185
459
|
case "state-change":
|
|
186
460
|
return {
|
|
187
461
|
type: "state-change",
|
|
188
462
|
seq,
|
|
189
463
|
timestamp,
|
|
464
|
+
...peerFields,
|
|
190
465
|
from: obj.from,
|
|
191
466
|
to: obj.to
|
|
192
467
|
};
|
|
@@ -195,6 +470,7 @@ function cborToEvent(obj) {
|
|
|
195
470
|
type: "error",
|
|
196
471
|
seq,
|
|
197
472
|
timestamp,
|
|
473
|
+
...peerFields,
|
|
198
474
|
errorCode: Number(obj.ec ?? 0),
|
|
199
475
|
reason: obj.reason ?? ""
|
|
200
476
|
};
|
|
@@ -203,9 +479,47 @@ function cborToEvent(obj) {
|
|
|
203
479
|
type: "annotation",
|
|
204
480
|
seq,
|
|
205
481
|
timestamp,
|
|
482
|
+
...peerFields,
|
|
206
483
|
label: obj.label ?? "",
|
|
207
484
|
data: obj.data
|
|
208
485
|
};
|
|
486
|
+
case "peer-connected":
|
|
487
|
+
return {
|
|
488
|
+
type: "peer-connected",
|
|
489
|
+
seq,
|
|
490
|
+
timestamp,
|
|
491
|
+
...peerFields,
|
|
492
|
+
...obj.endpoint != null ? { endpoint: obj.endpoint } : {},
|
|
493
|
+
...obj.transport != null ? { transport: obj.transport } : {},
|
|
494
|
+
...obj.role != null ? { role: obj.role } : {},
|
|
495
|
+
...obj.side != null ? { side: obj.side } : {}
|
|
496
|
+
};
|
|
497
|
+
case "peer-disconnected":
|
|
498
|
+
return {
|
|
499
|
+
type: "peer-disconnected",
|
|
500
|
+
seq,
|
|
501
|
+
timestamp,
|
|
502
|
+
...peerFields,
|
|
503
|
+
errorCode: Number(obj.ec ?? 0),
|
|
504
|
+
...obj.reason != null ? { reason: obj.reason } : {}
|
|
505
|
+
};
|
|
506
|
+
case "subscription-derivation":
|
|
507
|
+
return {
|
|
508
|
+
type: "subscription-derivation",
|
|
509
|
+
seq,
|
|
510
|
+
timestamp,
|
|
511
|
+
...peerFields,
|
|
512
|
+
upstream: cborToSubscriptionRef(obj.u),
|
|
513
|
+
downstream: (obj.d ?? []).map(cborToSubscriptionRef),
|
|
514
|
+
kind: obj.kind ?? "",
|
|
515
|
+
...obj.traceId != null ? { traceId: checkTraceId(obj.traceId) } : {},
|
|
516
|
+
...obj.ns != null ? { namespace: obj.ns.map(toBytes) } : {},
|
|
517
|
+
...obj.tn != null ? { trackName: toBytes(obj.tn) } : {},
|
|
518
|
+
...obj.tdr != null ? { tDownstreamReceived: Number(obj.tdr) } : {},
|
|
519
|
+
...obj.tus != null ? { tUpstreamSent: Number(obj.tus) } : {},
|
|
520
|
+
...obj.tuo != null ? { tUpstreamOkReceived: Number(obj.tuo) } : {},
|
|
521
|
+
...obj.tdo != null ? { tDownstreamOkSent: Number(obj.tdo) } : {}
|
|
522
|
+
};
|
|
209
523
|
}
|
|
210
524
|
}
|
|
211
525
|
function writePreamble(headerCbor) {
|
|
@@ -217,26 +531,89 @@ function writePreamble(headerCbor) {
|
|
|
217
531
|
buf.set(headerCbor, PREAMBLE_SIZE);
|
|
218
532
|
return buf;
|
|
219
533
|
}
|
|
220
|
-
function
|
|
221
|
-
if (
|
|
534
|
+
function startsWithMagic(bytes, offset) {
|
|
535
|
+
if (offset + MAGIC.length > bytes.length) return false;
|
|
536
|
+
for (let i = 0; i < MAGIC.length; i++) {
|
|
537
|
+
if (bytes[offset + i] !== MAGIC[i]) return false;
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
function validatePreamble(bytes, offset = 0) {
|
|
542
|
+
if (bytes.length - offset < PREAMBLE_SIZE) {
|
|
222
543
|
throw new Error("File too short: expected at least 16 bytes");
|
|
223
544
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
227
|
-
}
|
|
545
|
+
if (!startsWithMagic(bytes, offset)) {
|
|
546
|
+
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
228
547
|
}
|
|
229
548
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
230
|
-
const version = view.getUint32(8, true);
|
|
231
|
-
if (version
|
|
232
|
-
throw new Error(
|
|
549
|
+
const version = view.getUint32(offset + 8, true);
|
|
550
|
+
if (!SUPPORTED_VERSIONS.includes(version)) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`Unsupported format version: ${version} (supported: ${SUPPORTED_VERSIONS.join(", ")})`
|
|
553
|
+
);
|
|
233
554
|
}
|
|
234
|
-
const headerLength = view.getUint32(12, true);
|
|
235
|
-
if (PREAMBLE_SIZE + headerLength > bytes.length) {
|
|
555
|
+
const headerLength = view.getUint32(offset + 12, true);
|
|
556
|
+
if (offset + PREAMBLE_SIZE + headerLength > bytes.length) {
|
|
236
557
|
throw new Error("File truncated: header extends beyond file");
|
|
237
558
|
}
|
|
238
559
|
return { version, headerLength };
|
|
239
560
|
}
|
|
561
|
+
function flatten(segments) {
|
|
562
|
+
const first = segments[0];
|
|
563
|
+
if (first == null) return void 0;
|
|
564
|
+
if (segments.length === 1) return first;
|
|
565
|
+
return { header: first.header, events: segments.flatMap((segment) => segment.events) };
|
|
566
|
+
}
|
|
567
|
+
function findNextSegment(bytes, from) {
|
|
568
|
+
for (let offset = from; offset + MAGIC.length <= bytes.length; offset++) {
|
|
569
|
+
if (startsWithMagic(bytes, offset)) return offset;
|
|
570
|
+
}
|
|
571
|
+
return -1;
|
|
572
|
+
}
|
|
573
|
+
function readSegments(bytes, options) {
|
|
574
|
+
const recover = options?.recover ?? false;
|
|
575
|
+
const segments = [];
|
|
576
|
+
let events = [];
|
|
577
|
+
let offset = 0;
|
|
578
|
+
validatePreamble(bytes, 0);
|
|
579
|
+
while (offset < bytes.length) {
|
|
580
|
+
if (startsWithMagic(bytes, offset)) {
|
|
581
|
+
const { headerLength } = validatePreamble(bytes, offset);
|
|
582
|
+
const start = offset + PREAMBLE_SIZE;
|
|
583
|
+
const headerBytes = bytes.subarray(start, start + headerLength);
|
|
584
|
+
events = [];
|
|
585
|
+
segments.push({
|
|
586
|
+
header: cborToHeader(decode(headerBytes)),
|
|
587
|
+
events
|
|
588
|
+
});
|
|
589
|
+
offset = start + headerLength;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
let length;
|
|
593
|
+
try {
|
|
594
|
+
length = cborItemLength(bytes, offset);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (!recover || !(error instanceof MalformedCborError)) throw error;
|
|
597
|
+
length = null;
|
|
598
|
+
}
|
|
599
|
+
if (length != null) {
|
|
600
|
+
try {
|
|
601
|
+
const item = decode(bytes.subarray(offset, offset + length));
|
|
602
|
+
events.push(cborToEvent(item));
|
|
603
|
+
offset += length;
|
|
604
|
+
continue;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (!recover) throw error;
|
|
607
|
+
}
|
|
608
|
+
} else if (!recover) {
|
|
609
|
+
throw new TruncatedTraceError(offset, segments);
|
|
610
|
+
}
|
|
611
|
+
const next = findNextSegment(bytes, offset + 1);
|
|
612
|
+
if (next < 0) return segments;
|
|
613
|
+
offset = next;
|
|
614
|
+
}
|
|
615
|
+
return segments;
|
|
616
|
+
}
|
|
240
617
|
function writeMoqtrace(trace) {
|
|
241
618
|
const headerCbor = encode(headerToCbor(trace.header));
|
|
242
619
|
const preamble = writePreamble(headerCbor);
|
|
@@ -256,22 +633,35 @@ function writeMoqtrace(trace) {
|
|
|
256
633
|
}
|
|
257
634
|
return result;
|
|
258
635
|
}
|
|
259
|
-
function
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
636
|
+
function writeMoqtraceSegments(segments) {
|
|
637
|
+
const chunks = segments.map((segment, index) => {
|
|
638
|
+
if (segment.header.segment == null) {
|
|
639
|
+
throw new Error(`Segment ${index} has no 'segment' metadata in its header`);
|
|
640
|
+
}
|
|
641
|
+
return writeMoqtrace(segment);
|
|
642
|
+
});
|
|
643
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
644
|
+
const result = new Uint8Array(total);
|
|
645
|
+
let offset = 0;
|
|
646
|
+
for (const chunk of chunks) {
|
|
647
|
+
result.set(chunk, offset);
|
|
648
|
+
offset += chunk.length;
|
|
649
|
+
}
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
function readMoqtrace(bytes, options) {
|
|
653
|
+
const trace = flatten(readSegments(bytes, options));
|
|
654
|
+
if (trace == null) {
|
|
655
|
+
throw new Error("Invalid magic bytes: not a .moqtrace file");
|
|
269
656
|
}
|
|
270
|
-
return
|
|
657
|
+
return trace;
|
|
658
|
+
}
|
|
659
|
+
function readMoqtraceSegments(bytes, options) {
|
|
660
|
+
return readSegments(bytes, options);
|
|
271
661
|
}
|
|
272
662
|
function readMoqtraceHeader(bytes) {
|
|
273
663
|
const { headerLength } = validatePreamble(bytes);
|
|
274
|
-
const headerBytes = bytes.
|
|
664
|
+
const headerBytes = bytes.subarray(PREAMBLE_SIZE, PREAMBLE_SIZE + headerLength);
|
|
275
665
|
return cborToHeader(decode(headerBytes));
|
|
276
666
|
}
|
|
277
667
|
function createMoqtraceWriter(header) {
|
|
@@ -283,6 +673,12 @@ function createMoqtraceWriter(header) {
|
|
|
283
673
|
},
|
|
284
674
|
writeEvent(event) {
|
|
285
675
|
return encode(eventToCbor(event));
|
|
676
|
+
},
|
|
677
|
+
startSegment(next) {
|
|
678
|
+
if (next.segment == null) {
|
|
679
|
+
throw new Error("A segment header must carry 'segment' metadata");
|
|
680
|
+
}
|
|
681
|
+
return writePreamble(encode(headerToCbor(next)));
|
|
286
682
|
}
|
|
287
683
|
};
|
|
288
684
|
}
|
|
@@ -315,6 +711,11 @@ var DETAIL_RANK = {
|
|
|
315
711
|
function createRecorder(options) {
|
|
316
712
|
const detail = options.detail;
|
|
317
713
|
const detailRank = DETAIL_RANK[detail];
|
|
714
|
+
if (detailRank === void 0) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`Unknown detail level '${detail}': cannot record at a level this build does not implement`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
318
719
|
const maxEvents = options.maxEvents ?? 1e5;
|
|
319
720
|
const clock = options.clock ?? (() => Math.round(performance.now() * 1e3));
|
|
320
721
|
const messageTypeId = options.messageTypeId ?? (() => 0);
|
|
@@ -496,10 +897,16 @@ function createRecorder(options) {
|
|
|
496
897
|
};
|
|
497
898
|
}
|
|
498
899
|
export {
|
|
900
|
+
FORMAT_VERSION,
|
|
901
|
+
SUPPORTED_VERSIONS,
|
|
902
|
+
TRACE_ID_LENGTH,
|
|
903
|
+
TruncatedTraceError,
|
|
499
904
|
createMoqtraceWriter,
|
|
500
905
|
createRecorder,
|
|
501
906
|
readMoqtrace,
|
|
502
907
|
readMoqtraceHeader,
|
|
908
|
+
readMoqtraceSegments,
|
|
503
909
|
traceToJSON,
|
|
504
|
-
writeMoqtrace
|
|
910
|
+
writeMoqtrace,
|
|
911
|
+
writeMoqtraceSegments
|
|
505
912
|
};
|