repoburg 1.3.255 → 1.3.256

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.
Files changed (75) hide show
  1. package/daemon/dist/index.js +61 -3
  2. package/daemon/dist/staticServing.js +2 -2
  3. package/daemon/dist/tabtape/api/follow.js +174 -0
  4. package/daemon/dist/tabtape/api/http.js +221 -0
  5. package/daemon/dist/tabtape/api/record.js +149 -0
  6. package/daemon/dist/tabtape/api/router.js +519 -0
  7. package/daemon/dist/tabtape/api/router.spec.js +604 -0
  8. package/daemon/dist/tabtape/api/static.js +70 -0
  9. package/daemon/dist/tabtape/api/ws.js +166 -0
  10. package/daemon/dist/tabtape/buffer/buffer.js +347 -0
  11. package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
  12. package/daemon/dist/tabtape/config.js +108 -0
  13. package/daemon/dist/tabtape/e2e-server.js +96 -0
  14. package/daemon/dist/tabtape/errors.js +77 -0
  15. package/daemon/dist/tabtape/export/export.js +331 -0
  16. package/daemon/dist/tabtape/export/export.spec.js +262 -0
  17. package/daemon/dist/tabtape/har/gostd.js +395 -0
  18. package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
  19. package/daemon/dist/tabtape/har/har.js +582 -0
  20. package/daemon/dist/tabtape/har/har.spec.js +325 -0
  21. package/daemon/dist/tabtape/index.js +134 -0
  22. package/daemon/dist/tabtape/index.spec.js +104 -0
  23. package/daemon/dist/tabtape/lock.js +67 -0
  24. package/daemon/dist/tabtape/log.js +32 -0
  25. package/daemon/dist/tabtape/mcp/definitions.js +362 -0
  26. package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
  27. package/daemon/dist/tabtape/mcp/params.js +345 -0
  28. package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
  29. package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
  30. package/daemon/dist/tabtape/mcp/server.js +411 -0
  31. package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
  32. package/daemon/dist/tabtape/mcp/tools.js +1085 -0
  33. package/daemon/dist/tabtape/query/format.js +195 -0
  34. package/daemon/dist/tabtape/query/network.js +133 -0
  35. package/daemon/dist/tabtape/query/perf.js +93 -0
  36. package/daemon/dist/tabtape/query/query.spec.js +233 -0
  37. package/daemon/dist/tabtape/query/reader.js +602 -0
  38. package/daemon/dist/tabtape/query/types.js +274 -0
  39. package/daemon/dist/tabtape/redact.js +37 -0
  40. package/daemon/dist/tabtape/repro/lang.js +95 -0
  41. package/daemon/dist/tabtape/repro/repro.js +422 -0
  42. package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
  43. package/daemon/dist/tabtape/sink.js +24 -0
  44. package/daemon/dist/tabtape/store/record.js +99 -0
  45. package/daemon/dist/tabtape/store/segment.js +267 -0
  46. package/daemon/dist/tabtape/store/store.js +846 -0
  47. package/daemon/dist/tabtape/store/store.spec.js +485 -0
  48. package/daemon/dist/tabtape/testing/querytest.js +119 -0
  49. package/daemon/dist/tabtape/testing/tmp.js +33 -0
  50. package/package.json +3 -2
  51. package/tabtape/extension/dist/background.js +8 -0
  52. package/tabtape/extension/dist/background.js.map +7 -0
  53. package/tabtape/extension/dist/bridge.js +3 -0
  54. package/tabtape/extension/dist/bridge.js.map +7 -0
  55. package/tabtape/extension/dist/icons/128.png +0 -0
  56. package/tabtape/extension/dist/icons/16.png +0 -0
  57. package/tabtape/extension/dist/icons/32.png +0 -0
  58. package/tabtape/extension/dist/icons/48.png +0 -0
  59. package/tabtape/extension/dist/manifest.json +28 -0
  60. package/tabtape/extension/dist/popup.css +62 -0
  61. package/tabtape/extension/dist/popup.html +34 -0
  62. package/tabtape/extension/dist/popup.js +4 -0
  63. package/tabtape/extension/dist/popup.js.map +7 -0
  64. package/tabtape/extension/dist/recorder.js +133 -0
  65. package/tabtape/extension/dist/recorder.js.map +1 -0
  66. package/tabtape/extension/dist/state.js +2 -0
  67. package/tabtape/extension/dist/state.js.map +1 -0
  68. package/tabtape/playwright/mcp.mjs +58 -0
  69. package/tabtape/playwright/tabtape.d.mts +37 -0
  70. package/tabtape/playwright/tabtape.mjs +146 -0
  71. package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
  72. package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
  73. package/tabtape/ui/dist/app/index.html +14 -0
  74. package/tabtape/ui/dist/export/viewer.css +2 -0
  75. package/tabtape/ui/dist/export/viewer.js +143 -0
@@ -0,0 +1,602 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.View = exports.Reader = exports.RR_META = exports.RR_FULL_SNAPSHOT = void 0;
7
+ exports.parseHead = parseHead;
8
+ /**
9
+ * Random access to recorded sessions for the features that need more than a
10
+ * raw stream: MCP tools, reproduction steps, HAR and HTML exports.
11
+ *
12
+ * A session is indexed once, line by line, and the index is extended as the
13
+ * session grows. rrweb lines (most of the bytes) are skipped after reading
14
+ * their first few bytes; the other records are decoded into compact
15
+ * summaries and kept in memory. Full records are re-read from disk on demand.
16
+ */
17
+ const promises_1 = __importDefault(require("fs/promises"));
18
+ const errors_1 = require("../errors");
19
+ const segment_1 = require("../store/segment");
20
+ const types_1 = require("./types");
21
+ /** rrweb event types (rrweb's EventType). */
22
+ exports.RR_FULL_SNAPSHOT = 2;
23
+ exports.RR_META = 4;
24
+ /** How many session indexes are kept in memory. */
25
+ const MAX_CACHED = 8;
26
+ /** Index copies of console text are clipped (bytes); full records are read from disk. */
27
+ const INDEX_TEXT_LIMIT = 8000;
28
+ /** Changed paths kept per state record in the index. */
29
+ const MAX_SUMMARY_PATHS = 20;
30
+ /** Bytes of a line read before deciding whether to keep it. */
31
+ const HEAD_PEEK = 256;
32
+ class Mutex {
33
+ constructor() {
34
+ this.tail = Promise.resolve();
35
+ }
36
+ run(fn) {
37
+ const result = this.tail.then(fn);
38
+ this.tail = result.then(() => undefined, () => undefined);
39
+ return result;
40
+ }
41
+ }
42
+ /** Indexes sessions of a store. */
43
+ class Reader {
44
+ constructor(st) {
45
+ this.st = st;
46
+ this.cache = new Map();
47
+ /** Most recently used last. */
48
+ this.lru = [];
49
+ }
50
+ /** Indexes any new data of the session and returns a view of it. */
51
+ async view(id) {
52
+ const { session, segments } = this.st.snapshot(id);
53
+ const idx = this.index(id);
54
+ return idx.lock.run(async () => {
55
+ const present = new Set(segments.map((s) => s.seq));
56
+ for (const seq of [...idx.segs.keys()]) {
57
+ if (!present.has(seq))
58
+ idx.segs.delete(seq); // evicted
59
+ }
60
+ const v = new View(session);
61
+ for (const sg of segments) {
62
+ let si = idx.segs.get(sg.seq);
63
+ if (!si) {
64
+ si = { path: sg.path, size: 0, lines: [] };
65
+ idx.segs.set(sg.seq, si);
66
+ }
67
+ si.path = sg.path; // changes when the segment is compressed
68
+ if (si.size < sg.size) {
69
+ try {
70
+ await extend(si, sg.seq, sg.size);
71
+ }
72
+ catch (err) {
73
+ if ((0, errors_1.isNotExist)(err)) {
74
+ idx.segs.delete(sg.seq); // evicted after the snapshot
75
+ continue;
76
+ }
77
+ throw new Error(`index segment ${sg.seq}: ${(0, errors_1.errorMessage)(err)}`);
78
+ }
79
+ }
80
+ v.paths.set(sg.seq, si.path);
81
+ v.sizes.set(sg.seq, si.size);
82
+ for (const l of si.lines)
83
+ v.lines.push(l);
84
+ }
85
+ v.lines.sort((a, b) => a.ts - b.ts); // stable: ties keep file order
86
+ v.computeRange();
87
+ return v;
88
+ });
89
+ }
90
+ index(id) {
91
+ let idx = this.cache.get(id);
92
+ if (!idx) {
93
+ idx = { lock: new Mutex(), segs: new Map() };
94
+ this.cache.set(id, idx);
95
+ }
96
+ this.lru = this.lru.filter((s) => s !== id);
97
+ this.lru.push(id);
98
+ while (this.lru.length > MAX_CACHED) {
99
+ this.cache.delete(this.lru[0]);
100
+ this.lru.shift();
101
+ }
102
+ return idx;
103
+ }
104
+ /** Drops the cached index of a session (after it was deleted). */
105
+ forget(id) {
106
+ this.cache.delete(id);
107
+ this.lru = this.lru.filter((s) => s !== id);
108
+ }
109
+ }
110
+ exports.Reader = Reader;
111
+ /**
112
+ * A consistent picture of one session: every line known when it was built,
113
+ * ordered like the replay UI orders them (by timestamp, ties in file order).
114
+ */
115
+ class View {
116
+ constructor(session) {
117
+ this.session = session;
118
+ this.lines = [];
119
+ /**
120
+ * The replay's time origin: the first rrweb Meta event that is followed by
121
+ * a full snapshot, else the first record. Offsets (and the UI's ?t= deep
122
+ * links) count from here.
123
+ */
124
+ this.start = 0;
125
+ this.end = 0;
126
+ this.replayable = false;
127
+ this.paths = new Map();
128
+ this.sizes = new Map();
129
+ /** The last compressed segment read by readLine, which cannot seek. */
130
+ this.gz = null;
131
+ this.byId = null;
132
+ }
133
+ /** @internal */
134
+ computeRange() {
135
+ if (this.lines.length === 0)
136
+ return;
137
+ const rr = this.lines.filter((l) => l.type === 'rrweb');
138
+ for (let i = 0; i + 1 < rr.length; i++) {
139
+ if (rr[i].rr === exports.RR_META && rr[i + 1].rr === exports.RR_FULL_SNAPSHOT) {
140
+ this.replayable = true;
141
+ this.start = rr[i].ts;
142
+ this.end = Math.max(rr[rr.length - 1].ts, this.start);
143
+ return;
144
+ }
145
+ }
146
+ this.start = this.lines[0].ts;
147
+ this.end = this.lines[this.lines.length - 1].ts;
148
+ }
149
+ /** A timestamp as milliseconds from the replay start, clamped to the replay's range like the UI does. */
150
+ offset(ts) {
151
+ return Math.min(Math.max(ts - this.start, 0), this.end - this.start);
152
+ }
153
+ /** The replay's length in milliseconds. */
154
+ duration() {
155
+ return this.end - this.start;
156
+ }
157
+ /** A line's raw bytes (without the newline). */
158
+ async readLine(l) {
159
+ let p = this.paths.get(l.seq);
160
+ if (p === undefined)
161
+ throw new errors_1.GoneError();
162
+ if (!p.endsWith('.gz')) {
163
+ try {
164
+ return await readAt(p, l.off, l.len);
165
+ }
166
+ catch (err) {
167
+ if (!(0, errors_1.isNotExist)(err))
168
+ throw err;
169
+ }
170
+ p += '.gz'; // compressed since the view was built
171
+ }
172
+ let data;
173
+ try {
174
+ data = await this.compressed(l.seq, p);
175
+ }
176
+ catch (err) {
177
+ if ((0, errors_1.isNotExist)(err))
178
+ throw new errors_1.GoneError();
179
+ throw err;
180
+ }
181
+ const end = l.off + l.len;
182
+ if (end > data.length)
183
+ throw new errors_1.GoneError();
184
+ return Buffer.from(data.subarray(l.off, end));
185
+ }
186
+ /** A compressed segment's content, keeping the last one: lines are usually read in order. */
187
+ compressed(seq, p) {
188
+ if (this.gz && this.gz.seq === seq)
189
+ return this.gz.data;
190
+ const data = (0, segment_1.readSegmentBytes)(p, this.sizes.get(seq) ?? 0);
191
+ this.gz = { seq, data };
192
+ data.catch(() => {
193
+ if (this.gz?.data === data)
194
+ this.gz = null;
195
+ });
196
+ return data;
197
+ }
198
+ async decodeData(l) {
199
+ const b = await this.readLine(l);
200
+ let v;
201
+ try {
202
+ v = JSON.parse(b.toString('utf8'));
203
+ }
204
+ catch (err) {
205
+ throw new Error(`decode record ${l.id}: ${(0, errors_1.errorMessage)(err)}`);
206
+ }
207
+ return (0, types_1.isObj)(v) ? v.data : undefined;
208
+ }
209
+ /** A network record with headers and bodies. */
210
+ async fullNetwork(l) {
211
+ return (0, types_1.decodeNetwork)(await this.decodeData(l));
212
+ }
213
+ /** A console record with its arguments and full text. */
214
+ async fullConsole(l) {
215
+ return (0, types_1.decodeConsole)(await this.decodeData(l));
216
+ }
217
+ /** A WebSocket message with its data. */
218
+ async fullWS(l) {
219
+ return (0, types_1.decodeWS)(await this.decodeData(l));
220
+ }
221
+ /** A state record with its payload, changes and state. */
222
+ async fullState(l) {
223
+ return (0, types_1.decodeState)(await this.decodeData(l));
224
+ }
225
+ /** The "data" field of an rrweb event (e.g. the Meta event's width and height). */
226
+ async rrwebData(l) {
227
+ const d = await this.decodeData(l);
228
+ return (0, types_1.isObj)(d) ? d.data : undefined;
229
+ }
230
+ /** The line with the given id. */
231
+ find(id) {
232
+ if (!this.byId) {
233
+ this.byId = new Map();
234
+ for (const l of this.lines)
235
+ this.byId.set(l.id, l);
236
+ }
237
+ return this.byId.get(id);
238
+ }
239
+ /**
240
+ * The last report of each Web Vitals metric of each page load. A metric is
241
+ * reported again whenever it changes, so every earlier report is
242
+ * superseded.
243
+ */
244
+ finalVitals() {
245
+ const last = new Map();
246
+ for (const l of this.lines) {
247
+ if (l.perf && l.perf.kind === 'vital')
248
+ last.set(`${l.perf.id}\u0000${l.perf.name}`, l);
249
+ }
250
+ return new Set(last.values());
251
+ }
252
+ /**
253
+ * Calls fn with every line of the view and its raw bytes (without the
254
+ * newline), in file order: segment by segment, oldest first. Segments
255
+ * deleted meanwhile are skipped.
256
+ */
257
+ async scan(fn) {
258
+ const bySeg = new Map();
259
+ for (const l of this.lines) {
260
+ let list = bySeg.get(l.seq);
261
+ if (!list)
262
+ bySeg.set(l.seq, (list = []));
263
+ list.push(l);
264
+ }
265
+ const seqs = [...bySeg.keys()].sort((a, b) => a - b);
266
+ for (const seq of seqs) {
267
+ const lines = bySeg.get(seq).sort((a, b) => a.off - b.off);
268
+ await this.scanSegment(seq, lines, fn);
269
+ }
270
+ }
271
+ async scanSegment(seq, lines, fn) {
272
+ const cursor = new Cursor((0, segment_1.readSegment)(this.paths.get(seq), 0, this.sizes.get(seq) ?? 0));
273
+ try {
274
+ let pos = 0;
275
+ for (const l of lines) {
276
+ if (l.off > pos) {
277
+ if (!(await cursor.skip(l.off - pos)))
278
+ return; // truncated meanwhile
279
+ pos = l.off;
280
+ }
281
+ const b = await cursor.read(l.len + 1);
282
+ if (!b)
283
+ return; // truncated meanwhile (deleted and recreated); stop here
284
+ pos += l.len + 1;
285
+ const pending = fn(l, b.subarray(0, l.len));
286
+ if (pending)
287
+ await pending;
288
+ }
289
+ }
290
+ catch (err) {
291
+ if ((0, errors_1.isNotExist)(err))
292
+ return; // evicted
293
+ throw err;
294
+ }
295
+ finally {
296
+ await cursor.close();
297
+ }
298
+ }
299
+ }
300
+ exports.View = View;
301
+ /** Sequential reads over a stream of chunks. */
302
+ class Cursor {
303
+ constructor(gen) {
304
+ this.parts = [];
305
+ this.avail = 0;
306
+ this.done = false;
307
+ this.it = gen[Symbol.asyncIterator]();
308
+ }
309
+ async pull() {
310
+ if (this.done)
311
+ return false;
312
+ const r = await this.it.next();
313
+ if (r.done) {
314
+ this.done = true;
315
+ return false;
316
+ }
317
+ this.parts.push(r.value);
318
+ this.avail += r.value.length;
319
+ return true;
320
+ }
321
+ async skip(n) {
322
+ while (n > 0) {
323
+ if (this.avail === 0 && !(await this.pull()))
324
+ return false;
325
+ const p = this.parts[0];
326
+ const k = Math.min(n, p.length);
327
+ if (k === p.length)
328
+ this.parts.shift();
329
+ else
330
+ this.parts[0] = p.subarray(k);
331
+ this.avail -= k;
332
+ n -= k;
333
+ }
334
+ return true;
335
+ }
336
+ async read(n) {
337
+ while (this.avail < n) {
338
+ if (!(await this.pull()))
339
+ return null;
340
+ }
341
+ if (this.parts[0].length >= n) {
342
+ const out = this.parts[0].subarray(0, n);
343
+ if (this.parts[0].length === n)
344
+ this.parts.shift();
345
+ else
346
+ this.parts[0] = this.parts[0].subarray(n);
347
+ this.avail -= n;
348
+ return out;
349
+ }
350
+ const out = Buffer.allocUnsafe(n);
351
+ let o = 0;
352
+ while (o < n) {
353
+ const p = this.parts[0];
354
+ const k = Math.min(p.length, n - o);
355
+ p.copy(out, o, 0, k);
356
+ o += k;
357
+ if (k === p.length)
358
+ this.parts.shift();
359
+ else
360
+ this.parts[0] = p.subarray(k);
361
+ }
362
+ this.avail -= n;
363
+ return out;
364
+ }
365
+ async close() {
366
+ await this.it.return?.(undefined);
367
+ }
368
+ }
369
+ async function readAt(p, off, n) {
370
+ const fh = await promises_1.default.open(p, 'r');
371
+ try {
372
+ const b = Buffer.alloc(n);
373
+ const { bytesRead } = await fh.read(b, 0, n, off);
374
+ if (bytesRead < n)
375
+ throw new Error('read record: EOF');
376
+ return b;
377
+ }
378
+ finally {
379
+ await fh.close();
380
+ }
381
+ }
382
+ function peek(parts, n) {
383
+ if (parts.length === 1)
384
+ return parts[0].subarray(0, n);
385
+ const out = [];
386
+ let len = 0;
387
+ for (const p of parts) {
388
+ if (len >= n)
389
+ break;
390
+ const piece = p.subarray(0, n - len);
391
+ out.push(piece);
392
+ len += piece.length;
393
+ }
394
+ return Buffer.concat(out, len);
395
+ }
396
+ /** Indexes the segment from si.size up to size. */
397
+ async function extend(si, seq, size) {
398
+ let off = si.size; // absolute offset of the next unread byte
399
+ let lineStart = off;
400
+ let parts = []; // the current line's bytes, while kept
401
+ let partsLen = 0;
402
+ // 0: undecided, 1: keep the whole line, 2: skip (an rrweb line whose header is known)
403
+ let mode = 0;
404
+ let head = NO_HEAD;
405
+ for await (const chunk of (0, segment_1.readSegment)(si.path, si.size, size - si.size)) {
406
+ let pos = 0;
407
+ while (pos < chunk.length) {
408
+ const nl = chunk.indexOf(0x0a, pos);
409
+ const end = nl < 0 ? chunk.length : nl;
410
+ if (mode !== 2 && end > pos) {
411
+ parts.push(chunk.subarray(pos, end));
412
+ partsLen += end - pos;
413
+ }
414
+ if (mode === 0 && (partsLen >= HEAD_PEEK || nl >= 0)) {
415
+ head = parseHead(peek(parts, HEAD_PEEK));
416
+ if (head.ok && head.type === 'rrweb' && head.rr >= 0) {
417
+ mode = 2;
418
+ parts = [];
419
+ partsLen = 0;
420
+ }
421
+ else {
422
+ mode = 1;
423
+ }
424
+ }
425
+ off += end - pos;
426
+ if (nl < 0)
427
+ break;
428
+ const l = { id: '', seq, n: si.lines.length, off: lineStart, len: off - lineStart, ts: 0, type: '', rr: -1 };
429
+ let ok = true;
430
+ if (mode === 2) {
431
+ l.ts = head.ts;
432
+ l.type = head.type;
433
+ l.rr = head.rr;
434
+ }
435
+ else {
436
+ try {
437
+ decodeLine(l, Buffer.concat(parts, partsLen).toString('utf8'));
438
+ }
439
+ catch {
440
+ ok = false; // not a record; the UI skips it too
441
+ }
442
+ }
443
+ if (ok) {
444
+ l.id = lineID(l.type, seq, l.n);
445
+ si.lines.push(l);
446
+ }
447
+ off += 1; // the newline
448
+ pos = nl + 1;
449
+ lineStart = off;
450
+ parts = [];
451
+ partsLen = 0;
452
+ mode = 0;
453
+ }
454
+ }
455
+ // Sizes only cover complete lines; a missing newline means the data
456
+ // changed under us: stop at the last complete line.
457
+ si.size = lineStart;
458
+ }
459
+ const clipBytes = (s, n) => {
460
+ const b = Buffer.from(s, 'utf8');
461
+ if (b.length <= n)
462
+ return s;
463
+ let cut = n;
464
+ while (cut > 0 && (b[cut] & 0xc0) === 0x80)
465
+ cut--;
466
+ return b.subarray(0, cut).toString('utf8') + '…';
467
+ };
468
+ function decodeLine(l, text) {
469
+ const v = JSON.parse(text);
470
+ if (!(0, types_1.isObj)(v))
471
+ throw new Error('not a record');
472
+ if (v.ts !== undefined && typeof v.ts !== 'number')
473
+ throw new Error('bad ts');
474
+ if (v.type !== undefined && typeof v.type !== 'string')
475
+ throw new Error('bad type');
476
+ l.ts = Math.trunc(v.ts ?? 0);
477
+ l.type = v.type ?? '';
478
+ const d = v.data;
479
+ switch (l.type) {
480
+ case 'rrweb': {
481
+ const t = (0, types_1.isObj)(d) ? d.type : undefined;
482
+ if (t !== undefined && typeof t !== 'number')
483
+ throw new Error('bad rrweb type');
484
+ l.rr = Math.trunc(t ?? 0);
485
+ break;
486
+ }
487
+ case 'network': {
488
+ const n = (0, types_1.decodeNetwork)(d);
489
+ delete n.reqBody;
490
+ delete n.resBody;
491
+ delete n.reqHeaders;
492
+ delete n.resHeaders;
493
+ l.net = n;
494
+ break;
495
+ }
496
+ case 'console': {
497
+ const c = (0, types_1.decodeConsole)(d);
498
+ delete c.args;
499
+ c.text = clipBytes(c.text, INDEX_TEXT_LIMIT);
500
+ c.stack = clipBytes(c.stack, INDEX_TEXT_LIMIT);
501
+ l.con = c;
502
+ break;
503
+ }
504
+ case 'event':
505
+ l.ev = (0, types_1.decodeEvent)(d);
506
+ break;
507
+ case 'perf':
508
+ l.perf = (0, types_1.decodePerf)(d);
509
+ break;
510
+ case 'ws': {
511
+ const f = (0, types_1.decodeWS)(d);
512
+ delete f.data;
513
+ l.ws = f;
514
+ break;
515
+ }
516
+ case 'state': {
517
+ const s = (0, types_1.decodeState)(d);
518
+ const changes = s.changes ?? [];
519
+ l.state = {
520
+ store: s.store,
521
+ kind: s.kind,
522
+ action: s.action,
523
+ paths: changes.slice(0, MAX_SUMMARY_PATHS).map((c) => c.path),
524
+ changes: changes.length,
525
+ full: s.state !== undefined,
526
+ burst: s.burst,
527
+ reason: s.reason,
528
+ };
529
+ break;
530
+ }
531
+ }
532
+ }
533
+ const PREFIXES = {
534
+ network: 'n',
535
+ console: 'c',
536
+ event: 'e',
537
+ rrweb: 'r',
538
+ perf: 'p',
539
+ ws: 'w',
540
+ state: 's',
541
+ };
542
+ function lineID(type, seq, n) {
543
+ const prefix = Object.prototype.hasOwnProperty.call(PREFIXES, type) ? PREFIXES[type] : 'x';
544
+ return `${prefix}${seq}-${n}`;
545
+ }
546
+ const NO_HEAD = { ok: false, ts: 0, type: '', rr: -1 };
547
+ const SID_PREFIX = Buffer.from('{"sid":"');
548
+ const TS_PREFIX = Buffer.from(',"ts":');
549
+ const TYPE_PREFIX = Buffer.from('"type":"');
550
+ const RR_PREFIX = Buffer.from(',"data":{"type":');
551
+ const NUMBER = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
552
+ function startsWith(b, prefix) {
553
+ return b.length >= prefix.length && b.subarray(0, prefix.length).equals(prefix);
554
+ }
555
+ /**
556
+ * Reads the start of a line as the store writes it:
557
+ *
558
+ * {"sid":"…","ts":123,"type":"rrweb","data":{"type":4,…
559
+ *
560
+ * ok is false for any other layout; callers then decode the line. rr is -1
561
+ * when the line is not rrweb or its event type is not where expected.
562
+ */
563
+ function parseHead(b) {
564
+ if (!startsWith(b, SID_PREFIX))
565
+ return NO_HEAD;
566
+ let rest = b.subarray(SID_PREFIX.length);
567
+ let i = rest.indexOf(0x22); // "
568
+ if (i < 0)
569
+ return NO_HEAD;
570
+ rest = rest.subarray(i + 1);
571
+ if (!startsWith(rest, TS_PREFIX))
572
+ return NO_HEAD;
573
+ rest = rest.subarray(TS_PREFIX.length);
574
+ i = rest.indexOf(0x2c); // ,
575
+ if (i < 0)
576
+ return NO_HEAD;
577
+ const tsText = rest.subarray(0, i).toString('latin1');
578
+ if (!NUMBER.test(tsText))
579
+ return NO_HEAD;
580
+ const ts = Math.trunc(Number(tsText));
581
+ rest = rest.subarray(i + 1);
582
+ if (!startsWith(rest, TYPE_PREFIX))
583
+ return NO_HEAD;
584
+ rest = rest.subarray(TYPE_PREFIX.length);
585
+ i = rest.indexOf(0x22);
586
+ if (i < 0)
587
+ return NO_HEAD;
588
+ const type = rest.subarray(0, i).toString('utf8');
589
+ if (type !== 'rrweb')
590
+ return { ok: true, ts, type, rr: -1 };
591
+ rest = rest.subarray(i + 1);
592
+ let rr = -1;
593
+ if (startsWith(rest, RR_PREFIX)) {
594
+ rest = rest.subarray(RR_PREFIX.length);
595
+ let j = 0;
596
+ while (j < rest.length && rest[j] >= 0x30 && rest[j] <= 0x39)
597
+ j++;
598
+ if (j > 0 && j < rest.length)
599
+ rr = Number(rest.subarray(0, j).toString('latin1'));
600
+ }
601
+ return { ok: true, ts, type, rr };
602
+ }