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,485 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
9
|
+
const errors_1 = require("../errors");
|
|
10
|
+
const log_1 = require("../log");
|
|
11
|
+
const tmp_1 = require("../testing/tmp");
|
|
12
|
+
const record_1 = require("./record");
|
|
13
|
+
const segment_1 = require("./segment");
|
|
14
|
+
const store_1 = require("./store");
|
|
15
|
+
const stores = [];
|
|
16
|
+
function openWith(opts) {
|
|
17
|
+
const s = store_1.Store.open({ ...opts, logger: log_1.silentLogger });
|
|
18
|
+
stores.push(s);
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
function openStore(dir, maxBytes, segmentBytes) {
|
|
22
|
+
return openWith({ dir, maxBytes, segmentBytes });
|
|
23
|
+
}
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
while (stores.length)
|
|
26
|
+
await stores.pop().close();
|
|
27
|
+
(0, tmp_1.cleanupTempDirs)();
|
|
28
|
+
});
|
|
29
|
+
function rec(ts, type, data) {
|
|
30
|
+
return (0, record_1.parseRecord)(`{"ts":${ts},"type":${JSON.stringify(type)},"data":${data}}`);
|
|
31
|
+
}
|
|
32
|
+
/** A record whose stored line is roughly n bytes long. */
|
|
33
|
+
function padded(ts, n) {
|
|
34
|
+
return rec(ts, 'rrweb', `{"type":3,"pad":${JSON.stringify('x'.repeat(n))}}`);
|
|
35
|
+
}
|
|
36
|
+
async function stream(s, id) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
await s.writeSession(id, (chunk) => {
|
|
39
|
+
parts.push(Buffer.from(chunk));
|
|
40
|
+
});
|
|
41
|
+
return Buffer.concat(parts);
|
|
42
|
+
}
|
|
43
|
+
async function readLines(s, id) {
|
|
44
|
+
const text = (await stream(s, id)).toString('utf8');
|
|
45
|
+
return text
|
|
46
|
+
.split('\n')
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((l) => JSON.parse(l));
|
|
49
|
+
}
|
|
50
|
+
const tsList = (lines) => lines.map((l) => l.ts);
|
|
51
|
+
function segmentFiles(dir, id) {
|
|
52
|
+
const out = { jsonl: [], gz: [], tmp: [] };
|
|
53
|
+
for (const name of fs_1.default.readdirSync(path_1.default.join(dir, 'sessions', id)).sort()) {
|
|
54
|
+
if (name.endsWith('.jsonl.gz.tmp'))
|
|
55
|
+
out.tmp.push(name);
|
|
56
|
+
else if (name.endsWith('.jsonl.gz'))
|
|
57
|
+
out.gz.push(name);
|
|
58
|
+
else if (name.endsWith('.jsonl'))
|
|
59
|
+
out.jsonl.push(name);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function fill(s, id, from, n, size) {
|
|
64
|
+
for (let i = 0; i < n; i++)
|
|
65
|
+
s.append(id, [padded(from + i, size)]);
|
|
66
|
+
}
|
|
67
|
+
describe('parseRecord', () => {
|
|
68
|
+
const cases = [
|
|
69
|
+
{ name: 'valid', in: '{"ts":1,"type":"rrweb","data":{"type":2}}', ts: 1, type: 'rrweb' },
|
|
70
|
+
{ name: 'fractional ts is rounded', in: '{"ts":1.6,"type":"network","data":{}}', ts: 2, type: 'network' },
|
|
71
|
+
{ name: 'surrounding whitespace', in: ' {"ts": 5, "type": "event"}\r\n', ts: 5, type: 'event' },
|
|
72
|
+
{ name: 'missing ts', in: '{"type":"rrweb"}', err: true },
|
|
73
|
+
{ name: 'missing type', in: '{"ts":1}', err: true },
|
|
74
|
+
{ name: 'sid is reserved', in: '{"sid":"x","ts":1,"type":"rrweb"}', err: true },
|
|
75
|
+
{ name: 'not an object', in: '[1,2]', err: true },
|
|
76
|
+
{ name: 'invalid json', in: '{"ts":1,"type":', err: true },
|
|
77
|
+
{ name: 'empty', in: '', err: true },
|
|
78
|
+
{ name: 'ts must be a number', in: '{"ts":"1","type":"x"}', err: true },
|
|
79
|
+
{
|
|
80
|
+
name: 'odd data shapes do not reject the record',
|
|
81
|
+
in: '{"ts":1,"type":"event","data":{"type":"x","status":"200","name":7}}',
|
|
82
|
+
ts: 1,
|
|
83
|
+
type: 'event',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
for (const c of cases) {
|
|
87
|
+
it(c.name, () => {
|
|
88
|
+
if (c.err) {
|
|
89
|
+
expect(() => (0, record_1.parseRecord)(c.in)).toThrow();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const r = (0, record_1.parseRecord)(c.in);
|
|
93
|
+
expect([r.ts, r.type]).toEqual([c.ts, c.type]);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
it('stores compact JSON', () => {
|
|
97
|
+
expect((0, record_1.parseRecord)('{ "ts" : 1, "type" : "x", "data" : { "a" : [1, 2] } }').raw).toBe('{"ts":1,"type":"x","data":{"a":[1,2]}}');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('readRecords', () => {
|
|
101
|
+
it('ignores blank lines and names the bad line', () => {
|
|
102
|
+
const recs = (0, record_1.readRecords)('{"ts":1,"type":"a"}\r\n\n{"ts":2,"type":"b"}');
|
|
103
|
+
expect(recs.map((r) => r.ts)).toEqual([1, 2]);
|
|
104
|
+
expect(() => (0, record_1.readRecords)('{"ts":1,"type":"a"}\nnope\n')).toThrow(/line 2/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('Store', () => {
|
|
108
|
+
it('appends and reads back', async () => {
|
|
109
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 16);
|
|
110
|
+
s.start({ id: 's1', startedAt: 100, url: 'https://example.com/', title: 'Example' });
|
|
111
|
+
s.append('s1', [rec(101, 'rrweb', '{"type":4,"data":{"width":800}}'), rec(102, 'network', '{"url":"https://example.com/api","status":200}')]);
|
|
112
|
+
s.append('s1', [rec(103, 'console', '{"level":"log","text":"hi"}')]);
|
|
113
|
+
const lines = await readLines(s, 's1');
|
|
114
|
+
expect(tsList(lines)).toEqual([101, 102, 103]);
|
|
115
|
+
for (const l of lines)
|
|
116
|
+
expect(l.sid).toBe('s1');
|
|
117
|
+
expect(JSON.stringify(lines[1].data)).toBe('{"url":"https://example.com/api","status":200}');
|
|
118
|
+
const sess = s.get('s1');
|
|
119
|
+
expect([sess.url, sess.title, sess.startedAt]).toEqual(['https://example.com/', 'Example', 100]);
|
|
120
|
+
expect([sess.counts.total, sess.firstTs, sess.lastTs]).toEqual([3, 101, 103]);
|
|
121
|
+
expect(sess.bytes).toBeGreaterThan(0);
|
|
122
|
+
expect(sess.segments).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
it('creates unknown sessions on append', () => {
|
|
125
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 16);
|
|
126
|
+
s.append('late', [rec(42, 'rrweb', '{"type":4}')]);
|
|
127
|
+
expect(s.get('late').startedAt).toBe(42);
|
|
128
|
+
});
|
|
129
|
+
it('rotates segments by size', async () => {
|
|
130
|
+
const dir = (0, tmp_1.tempDir)();
|
|
131
|
+
const s = openStore(dir, 1 << 20, 300);
|
|
132
|
+
for (let i = 0; i < 10; i++)
|
|
133
|
+
s.append('s1', [padded(i, 100)]);
|
|
134
|
+
expect(segmentFiles(dir, 's1').jsonl.length).toBeGreaterThanOrEqual(3);
|
|
135
|
+
expect(tsList(await readLines(s, 's1'))).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
136
|
+
});
|
|
137
|
+
it('evicts the oldest data first', async () => {
|
|
138
|
+
const maxBytes = 2000;
|
|
139
|
+
const s = openStore((0, tmp_1.tempDir)(), maxBytes, 400);
|
|
140
|
+
// ~160 bytes per line: each session fits the cap alone, both together do not.
|
|
141
|
+
for (let i = 0; i < 10; i++)
|
|
142
|
+
s.append('a', [padded(i, 100)]);
|
|
143
|
+
for (let i = 0; i < 10; i++)
|
|
144
|
+
s.append('b', [padded(100 + i, 100)]);
|
|
145
|
+
expect(s.stats().totalBytes).toBeLessThanOrEqual(maxBytes);
|
|
146
|
+
expect(s.get('b').truncated).toBeUndefined();
|
|
147
|
+
expect((await readLines(s, 'b')).length).toBe(10);
|
|
148
|
+
if (!s.has('a'))
|
|
149
|
+
return; // fully evicted is acceptable
|
|
150
|
+
expect(s.get('a').truncated).toBe(true);
|
|
151
|
+
const got = tsList(await readLines(s, 'a'));
|
|
152
|
+
expect(got.length).toBeGreaterThan(0);
|
|
153
|
+
expect(got.length).toBeLessThan(10);
|
|
154
|
+
expect(got[got.length - 1]).toBe(9);
|
|
155
|
+
});
|
|
156
|
+
it('removes fully evicted sessions', () => {
|
|
157
|
+
const dir = (0, tmp_1.tempDir)();
|
|
158
|
+
const s = openStore(dir, 1500, 300);
|
|
159
|
+
s.append('old', [padded(1, 100)]);
|
|
160
|
+
for (let i = 0; i < 20; i++)
|
|
161
|
+
s.append('new', [padded(10 + i, 100)]);
|
|
162
|
+
expect(() => s.get('old')).toThrow(errors_1.NotFoundError);
|
|
163
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'sessions', 'old'))).toBe(false);
|
|
164
|
+
expect(s.list().some((x) => x.id === 'old')).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
it('never evicts the segment being written', async () => {
|
|
167
|
+
const s = openStore((0, tmp_1.tempDir)(), 200, 100);
|
|
168
|
+
s.append('s1', [padded(1, 500)]);
|
|
169
|
+
expect(tsList(await readLines(s, 's1'))).toEqual([1]);
|
|
170
|
+
s.append('s1', [padded(2, 500)]);
|
|
171
|
+
expect(tsList(await readLines(s, 's1'))).toEqual([2]);
|
|
172
|
+
});
|
|
173
|
+
it('restores sessions on reopen', async () => {
|
|
174
|
+
const dir = (0, tmp_1.tempDir)();
|
|
175
|
+
const s1 = store_1.Store.open({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 16, logger: log_1.silentLogger });
|
|
176
|
+
s1.start({ id: 's1', startedAt: 5, url: 'https://a.test/' });
|
|
177
|
+
s1.append('s1', [rec(6, 'event', '{"name":"click"}'), rec(7, 'console', '{"level":"error"}')]);
|
|
178
|
+
s1.end('s1', 8);
|
|
179
|
+
const before = s1.get('s1');
|
|
180
|
+
await s1.close();
|
|
181
|
+
const s2 = openStore(dir, 1 << 20, 1 << 16);
|
|
182
|
+
const after = s2.get('s1');
|
|
183
|
+
expect(after.counts).toEqual(before.counts);
|
|
184
|
+
expect([after.endedAt, after.url, after.bytes]).toEqual([8, 'https://a.test/', before.bytes]);
|
|
185
|
+
expect(s2.stats().totalBytes).toBe(before.bytes);
|
|
186
|
+
s2.append('s1', [rec(9, 'event', '{"name":"click"}')]);
|
|
187
|
+
expect(segmentFiles(dir, 's1').jsonl.length).toBe(2);
|
|
188
|
+
expect(tsList(await readLines(s2, 's1'))).toEqual([6, 7, 9]);
|
|
189
|
+
});
|
|
190
|
+
it('repairs a partial tail on open', async () => {
|
|
191
|
+
const dir = (0, tmp_1.tempDir)();
|
|
192
|
+
const s1 = store_1.Store.open({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 16, logger: log_1.silentLogger });
|
|
193
|
+
s1.append('s1', [rec(1, 'rrweb', '{}'), rec(2, 'rrweb', '{}')]);
|
|
194
|
+
await s1.close();
|
|
195
|
+
const files = segmentFiles(dir, 's1').jsonl;
|
|
196
|
+
expect(files.length).toBe(1);
|
|
197
|
+
fs_1.default.appendFileSync(path_1.default.join(dir, 'sessions', 's1', files[0]), '{"sid":"s1","ts":3,"type":"rr');
|
|
198
|
+
const s2 = openStore(dir, 1 << 20, 1 << 16);
|
|
199
|
+
expect(tsList(await readLines(s2, 's1'))).toEqual([1, 2]);
|
|
200
|
+
});
|
|
201
|
+
it('enforces a smaller budget on open', async () => {
|
|
202
|
+
const dir = (0, tmp_1.tempDir)();
|
|
203
|
+
const s1 = store_1.Store.open({ dir, maxBytes: 1 << 20, segmentBytes: 200, logger: log_1.silentLogger });
|
|
204
|
+
for (let i = 0; i < 20; i++)
|
|
205
|
+
s1.append('s1', [padded(i, 100)]);
|
|
206
|
+
await s1.close();
|
|
207
|
+
const s2 = openStore(dir, 600, 200);
|
|
208
|
+
expect(s2.stats().totalBytes).toBeLessThanOrEqual(600);
|
|
209
|
+
const got = tsList(await readLines(s2, 's1'));
|
|
210
|
+
expect(got.length).toBeGreaterThan(0);
|
|
211
|
+
expect(got[got.length - 1]).toBe(19);
|
|
212
|
+
});
|
|
213
|
+
it('counts what a session contains', () => {
|
|
214
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 16);
|
|
215
|
+
s.append('s1', [
|
|
216
|
+
rec(1, 'rrweb', '{"type":4,"data":{}}'),
|
|
217
|
+
rec(1, 'rrweb', '{"type":2,"data":{}}'),
|
|
218
|
+
rec(2, 'rrweb', '{"type":3,"data":{}}'),
|
|
219
|
+
rec(3, 'event', '{"name":"page_view","url":"https://a.test/","title":"A"}'),
|
|
220
|
+
rec(4, 'event', '{"name":"click","url":"https://a.test/"}'),
|
|
221
|
+
rec(5, 'event', '{"name":"page_view","url":"https://a.test/b","title":"B"}'),
|
|
222
|
+
rec(6, 'console', '{"level":"error","text":"boom"}'),
|
|
223
|
+
rec(7, 'console', '{"level":"log","text":"ok"}'),
|
|
224
|
+
rec(8, 'network', '{"status":500}'),
|
|
225
|
+
rec(9, 'network', '{"status":200}'),
|
|
226
|
+
rec(10, 'network', '{"status":0,"error":"Failed to fetch"}'),
|
|
227
|
+
]);
|
|
228
|
+
const sess = s.get('s1');
|
|
229
|
+
expect(sess.counts).toEqual({
|
|
230
|
+
total: 11,
|
|
231
|
+
rrweb: 3,
|
|
232
|
+
network: 3,
|
|
233
|
+
console: 2,
|
|
234
|
+
events: 3,
|
|
235
|
+
snapshots: 1,
|
|
236
|
+
pages: 2,
|
|
237
|
+
clicks: 1,
|
|
238
|
+
errors: 1,
|
|
239
|
+
netErrors: 2,
|
|
240
|
+
});
|
|
241
|
+
expect([sess.url, sess.title, sess.lastUrl]).toEqual(['https://a.test/', 'A', 'https://a.test/b']);
|
|
242
|
+
});
|
|
243
|
+
it('ends and deletes sessions', async () => {
|
|
244
|
+
const dir = (0, tmp_1.tempDir)();
|
|
245
|
+
const s = openStore(dir, 1 << 20, 1 << 16);
|
|
246
|
+
s.append('s1', [rec(1, 'rrweb', '{}')]);
|
|
247
|
+
expect(s.end('s1', 99).endedAt).toBe(99);
|
|
248
|
+
s.delete('s1');
|
|
249
|
+
await expect(stream(s, 's1')).rejects.toThrow(errors_1.NotFoundError);
|
|
250
|
+
expect([s.stats().totalBytes, s.stats().sessions]).toEqual([0, 0]);
|
|
251
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'sessions', 's1'))).toBe(false);
|
|
252
|
+
expect(() => s.delete('s1')).toThrow(errors_1.NotFoundError);
|
|
253
|
+
expect(() => s.end('missing', 1)).toThrow(errors_1.NotFoundError);
|
|
254
|
+
});
|
|
255
|
+
it('rejects unsafe ids', () => {
|
|
256
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 16);
|
|
257
|
+
for (const id of ['', '../x', 'a/b', '.hidden', 'a'.repeat(65), 'a\n']) {
|
|
258
|
+
expect(() => s.append(id, [rec(1, 'rrweb', '{}')])).toThrow(errors_1.InvalidIDError);
|
|
259
|
+
expect(() => s.start({ id })).toThrow(errors_1.InvalidIDError);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
it('lists newest first', () => {
|
|
263
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 16);
|
|
264
|
+
for (const st of [
|
|
265
|
+
{ id: 'a', startedAt: 1 },
|
|
266
|
+
{ id: 'c', startedAt: 3 },
|
|
267
|
+
{ id: 'b', startedAt: 2 },
|
|
268
|
+
]) {
|
|
269
|
+
s.start(st);
|
|
270
|
+
}
|
|
271
|
+
expect(s.list().map((x) => x.id)).toEqual(['c', 'b', 'a']);
|
|
272
|
+
});
|
|
273
|
+
it('never yields torn lines while appends and evictions run', async () => {
|
|
274
|
+
const maxBytes = 20000;
|
|
275
|
+
const s = openStore((0, tmp_1.tempDir)(), maxBytes, 2000);
|
|
276
|
+
const recs = Array.from({ length: 50 }, (_, i) => padded(i, 80));
|
|
277
|
+
const writers = [0, 1, 2, 3].map(async (w) => {
|
|
278
|
+
for (const r of recs) {
|
|
279
|
+
s.append(`s${w}`, [r]);
|
|
280
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
const readers = [0, 1].map(async () => {
|
|
284
|
+
for (let i = 0; i < 20; i++) {
|
|
285
|
+
for (const sess of s.list()) {
|
|
286
|
+
let text;
|
|
287
|
+
try {
|
|
288
|
+
text = (await stream(s, sess.id)).toString('utf8');
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (err instanceof errors_1.NotFoundError)
|
|
292
|
+
continue;
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
for (const line of text.split('\n').filter(Boolean))
|
|
296
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
await Promise.all([...writers, ...readers]);
|
|
301
|
+
expect(s.stats().totalBytes).toBeLessThanOrEqual(maxBytes);
|
|
302
|
+
});
|
|
303
|
+
it('notifies watchers of appends, ends and deletes', async () => {
|
|
304
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 10);
|
|
305
|
+
expect(() => s.watch('nope')).toThrow(errors_1.NotFoundError);
|
|
306
|
+
s.append('a', [rec(1, 'event', '{}')]);
|
|
307
|
+
const settled = async (p) => {
|
|
308
|
+
let done = false;
|
|
309
|
+
void p.then(() => (done = true));
|
|
310
|
+
await new Promise((r) => setImmediate(r));
|
|
311
|
+
return done;
|
|
312
|
+
};
|
|
313
|
+
let w = s.watch('a');
|
|
314
|
+
expect(await settled(w)).toBe(false);
|
|
315
|
+
s.append('a', [rec(2, 'event', '{}')]);
|
|
316
|
+
expect(await settled(w)).toBe(true);
|
|
317
|
+
w = s.watch('a');
|
|
318
|
+
s.end('a', 0);
|
|
319
|
+
expect(await settled(w)).toBe(true);
|
|
320
|
+
w = s.watch('a');
|
|
321
|
+
s.delete('a');
|
|
322
|
+
expect(await settled(w)).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
it('keeps the user id and the worst vitals rating', () => {
|
|
325
|
+
const s = openStore((0, tmp_1.tempDir)(), 1 << 20, 1 << 10);
|
|
326
|
+
s.append('a', [
|
|
327
|
+
rec(1, 'event', '{"name":"identify","userId":"u_1"}'),
|
|
328
|
+
rec(2, 'perf', '{"kind":"vital","name":"LCP","rating":"needs-improvement"}'),
|
|
329
|
+
rec(3, 'perf', '{"kind":"vital","name":"CLS","rating":"good"}'),
|
|
330
|
+
rec(4, 'perf', '{"kind":"loaf","rating":"poor"}'),
|
|
331
|
+
rec(5, 'event', '{"name":"identify","userId":"u_2"}'),
|
|
332
|
+
rec(6, 'event', '{"name":"identify"}'),
|
|
333
|
+
]);
|
|
334
|
+
const sess = s.get('a');
|
|
335
|
+
expect([sess.userId, sess.perf]).toEqual(['u_2', 'needs-improvement']);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
describe('Store compression', () => {
|
|
339
|
+
it('is off by default', () => {
|
|
340
|
+
const dir = (0, tmp_1.tempDir)();
|
|
341
|
+
const s = openWith({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 10 });
|
|
342
|
+
fill(s, 'a', 0, 20, 300);
|
|
343
|
+
s.end('a', 0);
|
|
344
|
+
const files = segmentFiles(dir, 'a');
|
|
345
|
+
expect(files.gz.length).toBe(0);
|
|
346
|
+
expect(files.jsonl.length).toBeGreaterThanOrEqual(2);
|
|
347
|
+
const st = s.stats();
|
|
348
|
+
expect(st.compress).toBe(store_1.COMPRESS_OFF);
|
|
349
|
+
expect(st.totalBytes).toBe(st.rawBytes);
|
|
350
|
+
});
|
|
351
|
+
it('rejects unknown modes', () => {
|
|
352
|
+
expect(() => store_1.Store.open({ dir: (0, tmp_1.tempDir)(), maxBytes: 1 << 20, segmentBytes: 1 << 10, compress: 'zstd', logger: log_1.silentLogger })).toThrow();
|
|
353
|
+
});
|
|
354
|
+
it('compresses closed segments', async () => {
|
|
355
|
+
const dir = (0, tmp_1.tempDir)();
|
|
356
|
+
const s = openWith({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 10, compress: store_1.COMPRESS_GZIP });
|
|
357
|
+
fill(s, 'a', 0, 20, 300);
|
|
358
|
+
const before = await stream(s, 'a');
|
|
359
|
+
// Rotated segments are closed; the last one is still being written.
|
|
360
|
+
await (0, tmp_1.eventually)('rotated segments to be compressed', () => {
|
|
361
|
+
const f = segmentFiles(dir, 'a');
|
|
362
|
+
return f.jsonl.length === 1 && f.gz.length >= 2;
|
|
363
|
+
});
|
|
364
|
+
expect((await stream(s, 'a')).equals(before)).toBe(true);
|
|
365
|
+
s.end('a', 0);
|
|
366
|
+
await (0, tmp_1.eventually)("the ended session's last segment to be compressed", () => {
|
|
367
|
+
const f = segmentFiles(dir, 'a');
|
|
368
|
+
return f.jsonl.length === 0 && f.tmp.length === 0;
|
|
369
|
+
});
|
|
370
|
+
expect((await stream(s, 'a')).equals(before)).toBe(true);
|
|
371
|
+
const sess = s.get('a');
|
|
372
|
+
expect(sess.rawBytes).toBe(before.length);
|
|
373
|
+
expect(sess.bytes).toBeLessThan(sess.rawBytes);
|
|
374
|
+
const st = s.stats();
|
|
375
|
+
expect([st.rawBytes, st.totalBytes]).toEqual([before.length, sess.bytes]);
|
|
376
|
+
// Appending to an ended session opens a new plain segment.
|
|
377
|
+
s.append('a', [padded(100, 10)]);
|
|
378
|
+
expect(segmentFiles(dir, 'a').jsonl.length).toBe(1);
|
|
379
|
+
});
|
|
380
|
+
it('lets readers survive compression', async () => {
|
|
381
|
+
const dir = (0, tmp_1.tempDir)();
|
|
382
|
+
const s = openWith({ dir, maxBytes: 1 << 20, segmentBytes: 64 << 10, compress: store_1.COMPRESS_GZIP });
|
|
383
|
+
fill(s, 'a', 0, 10, 300); // the segment is compressed only once it closes
|
|
384
|
+
const { segments } = s.snapshot('a');
|
|
385
|
+
expect(segments.length).toBe(1);
|
|
386
|
+
const seg = segments[0];
|
|
387
|
+
// A reader that opened the plain file before compression keeps reading it.
|
|
388
|
+
const early = (0, segment_1.readSegment)(seg.path, 0, seg.size);
|
|
389
|
+
const first = await early.next();
|
|
390
|
+
expect(first.done).toBe(false);
|
|
391
|
+
s.end('a', 0);
|
|
392
|
+
await (0, tmp_1.eventually)('compression', () => segmentFiles(dir, 'a').gz.length === 1);
|
|
393
|
+
const parts = [first.value];
|
|
394
|
+
for await (const chunk of early)
|
|
395
|
+
parts.push(chunk);
|
|
396
|
+
const want = await stream(s, 'a');
|
|
397
|
+
expect(Buffer.concat(parts).equals(want)).toBe(true);
|
|
398
|
+
// The stale plain path from the snapshot still opens, at any offset.
|
|
399
|
+
for (const off of [0, 1, 500, seg.size - 1]) {
|
|
400
|
+
const got = [];
|
|
401
|
+
for await (const chunk of (0, segment_1.readSegment)(seg.path, off, seg.size - off))
|
|
402
|
+
got.push(chunk);
|
|
403
|
+
expect(Buffer.concat(got).equals(want.subarray(off))).toBe(true);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
it('finishes an interrupted compression on open', async () => {
|
|
407
|
+
const dir = (0, tmp_1.tempDir)();
|
|
408
|
+
const sdir = path_1.default.join(dir, 'sessions', 'a');
|
|
409
|
+
fs_1.default.mkdirSync(sdir, { recursive: true });
|
|
410
|
+
const seg1 = Buffer.from('{"sid":"a","ts":1,"type":"event","data":{}}\n');
|
|
411
|
+
const seg2 = Buffer.from('{"sid":"a","ts":2,"type":"event","data":{}}\n');
|
|
412
|
+
// Crash after the rename, before the plain file was deleted.
|
|
413
|
+
fs_1.default.writeFileSync(path_1.default.join(sdir, '0000000000000001.jsonl'), seg1);
|
|
414
|
+
fs_1.default.writeFileSync(path_1.default.join(sdir, '0000000000000001.jsonl.gz'), zlib_1.default.gzipSync(seg1));
|
|
415
|
+
// Crash while writing the temporary file.
|
|
416
|
+
fs_1.default.writeFileSync(path_1.default.join(sdir, '0000000000000002.jsonl'), seg2);
|
|
417
|
+
fs_1.default.writeFileSync(path_1.default.join(sdir, '0000000000000002.jsonl.gz.tmp'), zlib_1.default.gzipSync(seg2).subarray(0, 10));
|
|
418
|
+
const s = openWith({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 10 });
|
|
419
|
+
expect(segmentFiles(dir, 'a')).toEqual({ gz: ['0000000000000001.jsonl.gz'], jsonl: ['0000000000000002.jsonl'], tmp: [] });
|
|
420
|
+
expect((await stream(s, 'a')).equals(Buffer.concat([seg1, seg2]))).toBe(true);
|
|
421
|
+
expect(s.get('a').rawBytes).toBe(seg1.length + seg2.length);
|
|
422
|
+
});
|
|
423
|
+
it('can be turned on and off', async () => {
|
|
424
|
+
const dir = (0, tmp_1.tempDir)();
|
|
425
|
+
const base = { dir, maxBytes: 1 << 20, segmentBytes: 1 << 10, logger: log_1.silentLogger };
|
|
426
|
+
let s = store_1.Store.open(base);
|
|
427
|
+
fill(s, 'a', 0, 10, 300);
|
|
428
|
+
const want = await stream(s, 'a');
|
|
429
|
+
await s.close();
|
|
430
|
+
// Turned on: existing segments are compressed in the background.
|
|
431
|
+
s = store_1.Store.open({ ...base, compress: store_1.COMPRESS_GZIP });
|
|
432
|
+
await (0, tmp_1.eventually)('old segments to be compressed', () => {
|
|
433
|
+
const f = segmentFiles(dir, 'a');
|
|
434
|
+
return f.jsonl.length === 0 && f.gz.length > 0;
|
|
435
|
+
});
|
|
436
|
+
await s.close();
|
|
437
|
+
// Turned off: compressed segments are still read, new ones stay plain.
|
|
438
|
+
s = openWith({ dir, maxBytes: 1 << 20, segmentBytes: 1 << 10, compress: '' });
|
|
439
|
+
expect((await stream(s, 'a')).equals(want)).toBe(true);
|
|
440
|
+
s.append('a', [padded(100, 2000)]);
|
|
441
|
+
s.end('a', 0);
|
|
442
|
+
expect(segmentFiles(dir, 'a').jsonl.length).toBe(1);
|
|
443
|
+
});
|
|
444
|
+
it('counts the compressed size against the cap', async () => {
|
|
445
|
+
const run = async (compress) => {
|
|
446
|
+
const dir = (0, tmp_1.tempDir)();
|
|
447
|
+
const s = openWith({ dir, maxBytes: 20 << 10, segmentBytes: 2 << 10, compress });
|
|
448
|
+
for (let i = 0; i < 100; i++) {
|
|
449
|
+
const id = 's' + String.fromCharCode(97 + (i % 26)) + String.fromCharCode(97 + Math.floor(i / 26));
|
|
450
|
+
fill(s, id, i * 10, 4, 500);
|
|
451
|
+
s.end(id, 0);
|
|
452
|
+
if (compress) {
|
|
453
|
+
// Let the worker keep up, like a real recording that is slower than gzip.
|
|
454
|
+
await (0, tmp_1.eventually)('compression', () => {
|
|
455
|
+
if (!s.has(id))
|
|
456
|
+
return true;
|
|
457
|
+
const f = segmentFiles(dir, id);
|
|
458
|
+
return f.jsonl.length === 0 && f.tmp.length === 0;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const st = s.stats();
|
|
463
|
+
return { kept: s.list().length, total: st.totalBytes, max: st.maxBytes };
|
|
464
|
+
};
|
|
465
|
+
const plain = await run('');
|
|
466
|
+
const gz = await run(store_1.COMPRESS_GZIP);
|
|
467
|
+
expect(plain.total).toBeLessThanOrEqual(plain.max);
|
|
468
|
+
expect(gz.total).toBeLessThanOrEqual(gz.max);
|
|
469
|
+
expect(gz.kept).toBeGreaterThanOrEqual(3 * plain.kept);
|
|
470
|
+
});
|
|
471
|
+
it('abandons the compression of a deleted segment', async () => {
|
|
472
|
+
const s = openWith({ dir: (0, tmp_1.tempDir)(), maxBytes: 1 << 20, segmentBytes: 1 << 10 });
|
|
473
|
+
const recs = Array.from({ length: 5 }, (_, i) => rec(i + 1, 'event', `{"pad":"${'x'.repeat(100)}"}`));
|
|
474
|
+
s.append('a', recs);
|
|
475
|
+
s.end('a', 0);
|
|
476
|
+
// Reach into the store: evict the segment while its compression runs.
|
|
477
|
+
const internals = s;
|
|
478
|
+
const seg = internals.sessions.get('a').segs[0];
|
|
479
|
+
seg.dropped = true;
|
|
480
|
+
await internals.compressSegment(seg);
|
|
481
|
+
expect(fs_1.default.existsSync(seg.path + '.gz.tmp')).toBe(false);
|
|
482
|
+
expect(fs_1.default.existsSync(seg.path + '.gz')).toBe(false);
|
|
483
|
+
expect([seg.gz, seg.broken]).toEqual([false, false]);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stateRec = exports.ws = exports.loaf = exports.vital = exports.identify = exports.track = exports.consoleRec = exports.net = exports.submit = exports.change = exports.click = exports.pageView = exports.rrInc = exports.rrFull = exports.rrMeta = exports.UA = exports.T0 = void 0;
|
|
4
|
+
exports.testStore = testStore;
|
|
5
|
+
exports.closeTestStores = closeTestStores;
|
|
6
|
+
exports.append = append;
|
|
7
|
+
exports.start = start;
|
|
8
|
+
/** Builds recorded sessions for tests, the way the extension posts them. */
|
|
9
|
+
const log_1 = require("../log");
|
|
10
|
+
const record_1 = require("../store/record");
|
|
11
|
+
const store_1 = require("../store/store");
|
|
12
|
+
const tmp_1 = require("./tmp");
|
|
13
|
+
exports.T0 = 1759650432000;
|
|
14
|
+
exports.UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
|
|
15
|
+
const open = [];
|
|
16
|
+
/** An empty store in a temporary directory; closed by closeTestStores(). */
|
|
17
|
+
function testStore(opts = {}) {
|
|
18
|
+
const st = store_1.Store.open({ dir: (0, tmp_1.tempDir)(), maxBytes: 64 << 20, segmentBytes: 16 << 20, logger: log_1.silentLogger, ...opts });
|
|
19
|
+
open.push(st);
|
|
20
|
+
return st;
|
|
21
|
+
}
|
|
22
|
+
async function closeTestStores() {
|
|
23
|
+
while (open.length)
|
|
24
|
+
await open.pop().close();
|
|
25
|
+
}
|
|
26
|
+
/** Appends records like the extension posts them (key order ts, type, data). */
|
|
27
|
+
function append(st, id, ...recs) {
|
|
28
|
+
st.append(id, recs.map((r) => (0, record_1.parseRecord)(`{"ts":${JSON.stringify(r.ts)},"type":${JSON.stringify(r.type)},"data":${JSON.stringify(r.data)}}`)));
|
|
29
|
+
}
|
|
30
|
+
/** Announces a session like the extension does. */
|
|
31
|
+
function start(st, id, startedAt, url, title) {
|
|
32
|
+
st.start({ id, startedAt, url, title, userAgent: exports.UA });
|
|
33
|
+
}
|
|
34
|
+
const rrMeta = (ts, href, w, h) => ({
|
|
35
|
+
ts,
|
|
36
|
+
type: 'rrweb',
|
|
37
|
+
data: { type: 4, data: { href, width: w, height: h }, timestamp: ts },
|
|
38
|
+
});
|
|
39
|
+
exports.rrMeta = rrMeta;
|
|
40
|
+
/** A (tiny) rrweb full snapshot. */
|
|
41
|
+
const rrFull = (ts) => ({
|
|
42
|
+
ts,
|
|
43
|
+
type: 'rrweb',
|
|
44
|
+
data: { type: 2, data: { node: { type: 0, childNodes: [], id: 1 }, initialOffset: { left: 0, top: 0 } }, timestamp: ts },
|
|
45
|
+
});
|
|
46
|
+
exports.rrFull = rrFull;
|
|
47
|
+
/** An rrweb incremental snapshot (a mouse move). */
|
|
48
|
+
const rrInc = (ts) => ({
|
|
49
|
+
ts,
|
|
50
|
+
type: 'rrweb',
|
|
51
|
+
data: { type: 3, data: { source: 1, positions: [{ x: 1, y: 2, id: 1, timeOffset: 0 }] }, timestamp: ts },
|
|
52
|
+
});
|
|
53
|
+
exports.rrInc = rrInc;
|
|
54
|
+
const pageView = (ts, kind, url, title) => ({
|
|
55
|
+
ts,
|
|
56
|
+
type: 'event',
|
|
57
|
+
data: { name: 'page_view', kind, url, title, viewport: { w: 1440, h: 900 } },
|
|
58
|
+
});
|
|
59
|
+
exports.pageView = pageView;
|
|
60
|
+
const click = (ts, url, target) => ({
|
|
61
|
+
ts,
|
|
62
|
+
type: 'event',
|
|
63
|
+
data: { name: 'click', url, target, x: 10, y: 20 },
|
|
64
|
+
});
|
|
65
|
+
exports.click = click;
|
|
66
|
+
/** A change event; masked hides the value. */
|
|
67
|
+
const change = (ts, url, target, value, masked = false) => ({
|
|
68
|
+
ts,
|
|
69
|
+
type: 'event',
|
|
70
|
+
data: masked ? { name: 'change', url, target, masked: true } : { name: 'change', url, target, value },
|
|
71
|
+
});
|
|
72
|
+
exports.change = change;
|
|
73
|
+
const submit = (ts, url, target) => ({
|
|
74
|
+
ts,
|
|
75
|
+
type: 'event',
|
|
76
|
+
data: { name: 'submit', url, target, method: 'POST' },
|
|
77
|
+
});
|
|
78
|
+
exports.submit = submit;
|
|
79
|
+
const net = (ts, n) => ({ ts, type: 'network', data: n });
|
|
80
|
+
exports.net = net;
|
|
81
|
+
const consoleRec = (ts, c) => ({ ts, type: 'console', data: c });
|
|
82
|
+
exports.consoleRec = consoleRec;
|
|
83
|
+
/** A tabtape.track() event. */
|
|
84
|
+
const track = (ts, url, event, props) => ({
|
|
85
|
+
ts,
|
|
86
|
+
type: 'event',
|
|
87
|
+
data: props ? { name: 'custom', url, event, props } : { name: 'custom', url, event },
|
|
88
|
+
});
|
|
89
|
+
exports.track = track;
|
|
90
|
+
/** A tabtape.identify() event. */
|
|
91
|
+
const identify = (ts, url, userId) => ({
|
|
92
|
+
ts,
|
|
93
|
+
type: 'event',
|
|
94
|
+
data: { name: 'identify', url, userId },
|
|
95
|
+
});
|
|
96
|
+
exports.identify = identify;
|
|
97
|
+
/** A Web Vitals report. */
|
|
98
|
+
const vital = (ts, name, value, rating, attribution) => ({
|
|
99
|
+
ts,
|
|
100
|
+
type: 'perf',
|
|
101
|
+
data: { kind: 'vital', name, value, rating, id: `v5-${name}`, ...(attribution ? { attribution } : {}) },
|
|
102
|
+
});
|
|
103
|
+
exports.vital = vital;
|
|
104
|
+
/** A long animation frame starting at ts. */
|
|
105
|
+
const loaf = (ts, duration, ...scripts) => ({
|
|
106
|
+
ts,
|
|
107
|
+
type: 'perf',
|
|
108
|
+
data: { kind: 'loaf', duration, blockingDuration: Math.max(duration - 50, 0), ...(scripts.length ? { scripts } : {}) },
|
|
109
|
+
});
|
|
110
|
+
exports.loaf = loaf;
|
|
111
|
+
/** A WebSocket message of connection conn. */
|
|
112
|
+
const ws = (ts, conn, dir, data) => ({
|
|
113
|
+
ts,
|
|
114
|
+
type: 'ws',
|
|
115
|
+
data: { conn, dir, opcode: 1, size: Buffer.byteLength(data), data },
|
|
116
|
+
});
|
|
117
|
+
exports.ws = ws;
|
|
118
|
+
const stateRec = (ts, s) => ({ ts, type: 'state', data: s });
|
|
119
|
+
exports.stateRec = stateRec;
|
|
@@ -0,0 +1,33 @@
|
|
|
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.tempDir = tempDir;
|
|
7
|
+
exports.cleanupTempDirs = cleanupTempDirs;
|
|
8
|
+
exports.eventually = eventually;
|
|
9
|
+
/** Temporary directories for tests, removed by cleanupTempDirs(). */
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const dirs = [];
|
|
14
|
+
function tempDir(prefix = 'tabtape-test-') {
|
|
15
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), prefix));
|
|
16
|
+
dirs.push(dir);
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
function cleanupTempDirs() {
|
|
20
|
+
while (dirs.length) {
|
|
21
|
+
const dir = dirs.pop();
|
|
22
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Polls cond until it holds; background work (compression) needs a moment. */
|
|
26
|
+
async function eventually(what, cond, timeoutMs = 5000) {
|
|
27
|
+
const deadline = Date.now() + timeoutMs;
|
|
28
|
+
while (!(await cond())) {
|
|
29
|
+
if (Date.now() > deadline)
|
|
30
|
+
throw new Error(`timed out waiting for ${what}`);
|
|
31
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repoburg",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.256",
|
|
4
4
|
"description": "A local AI-powered software developer assistant that runs on your own machine.",
|
|
5
5
|
"author": "Celal Ertug",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
"dev:daemon": "npm run dev -w daemon",
|
|
37
37
|
"daemon:pane": "(cd agent-pane/backend && npm run build) && (cd agent-pane/frontend && npm run build) && npm run dev:daemon",
|
|
38
38
|
"dev:agent-pane": "concurrently -n backend,frontend -c blue,green \"cd agent-pane/backend && npm run dev\" \"cd agent-pane/frontend && npm run dev\"",
|
|
39
|
+
"dev:tabtape": "cd tabtape && npm run dev -w ui",
|
|
39
40
|
"jaeger:start": "docker run -d --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest",
|
|
40
41
|
"jaeger:stop": "docker stop jaeger",
|
|
41
|
-
"lo:build": "npm run build -w backend && npm run build -w website2 && (cd agent-pane/backend && npm run build) && (cd agent-pane/frontend && npm run build) && npm run build -w recorder && npm run build -w daemon && cd frontend && npm run build",
|
|
42
|
+
"lo:build": "npm run build -w backend && npm run build -w website2 && (cd agent-pane/backend && npm run build) && (cd agent-pane/frontend && npm run build) && npm run build -w recorder && (cd tabtape && npm run build) && npm run build -w daemon && cd frontend && npm run build",
|
|
42
43
|
"lo:publish": "npm publish",
|
|
43
44
|
"release": "npm version patch --no-git-tag-version && npm publish"
|
|
44
45
|
},
|