repoburg 1.3.254 → 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.
- package/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
- package/agent-pane/frontend/dist/index.html +1 -1
- package/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
- package/visual-editor-proxy/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
+
}
|