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.
- 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
|
@@ -0,0 +1,846 @@
|
|
|
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.Store = exports.COMPRESS_GZIP = exports.COMPRESS_OFF = void 0;
|
|
7
|
+
exports.validID = validID;
|
|
8
|
+
/**
|
|
9
|
+
* Persists recorded browser sessions as JSONL files.
|
|
10
|
+
*
|
|
11
|
+
* Layout on disk:
|
|
12
|
+
*
|
|
13
|
+
* <dir>/sessions/<id>/meta.json session index entry
|
|
14
|
+
* <dir>/sessions/<id>/<seq>.jsonl event segments, one JSON object per line
|
|
15
|
+
* <dir>/sessions/<id>/<seq>.jsonl.gz closed segments, with compress "gzip"
|
|
16
|
+
*
|
|
17
|
+
* Segment numbers come from one global, monotonically increasing sequence.
|
|
18
|
+
* The combined size of all segments on disk is capped: when an append pushes
|
|
19
|
+
* the total over the cap, segments are deleted lowest sequence first — the
|
|
20
|
+
* oldest data across all sessions goes first. The data directory therefore
|
|
21
|
+
* behaves like a ring buffer that never splits a line.
|
|
22
|
+
*
|
|
23
|
+
* A segment is closed when it reaches the segment size, when its session ends
|
|
24
|
+
* or after two minutes without writes; a closed segment is never written
|
|
25
|
+
* again. With compression on, one background worker gzips closed segments.
|
|
26
|
+
*
|
|
27
|
+
* State changes run synchronously (one event loop: no locks needed); reads
|
|
28
|
+
* and compression stream asynchronously. Appends write complete lines in one
|
|
29
|
+
* synchronous call, so a segment's size always ends at a line boundary.
|
|
30
|
+
*/
|
|
31
|
+
const fs_1 = __importDefault(require("fs"));
|
|
32
|
+
const path_1 = __importDefault(require("path"));
|
|
33
|
+
const errors_1 = require("../errors");
|
|
34
|
+
const log_1 = require("../log");
|
|
35
|
+
const record_1 = require("./record");
|
|
36
|
+
const segment_1 = require("./segment");
|
|
37
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
38
|
+
/** Whether id is acceptable as a session id (and buffer key). */
|
|
39
|
+
function validID(id) {
|
|
40
|
+
return typeof id === 'string' && ID_PATTERN.test(id);
|
|
41
|
+
}
|
|
42
|
+
const SESSIONS_DIR = 'sessions';
|
|
43
|
+
const META_NAME = 'meta.json';
|
|
44
|
+
/** How long a session's segment stays open without writes. */
|
|
45
|
+
const IDLE_CLOSE_MS = 2 * 60000;
|
|
46
|
+
/** How often the compression worker closes idle segments. */
|
|
47
|
+
const IDLE_CHECK_MS = 30000;
|
|
48
|
+
exports.COMPRESS_OFF = 'off';
|
|
49
|
+
exports.COMPRESS_GZIP = 'gzip';
|
|
50
|
+
function emptyCounts() {
|
|
51
|
+
return { total: 0, rrweb: 0, network: 0, console: 0, events: 0, snapshots: 0, pages: 0, clicks: 0, errors: 0, netErrors: 0 };
|
|
52
|
+
}
|
|
53
|
+
function emptyMeta(id, startedAt = 0) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
startedAt,
|
|
57
|
+
endedAt: 0,
|
|
58
|
+
firstTs: 0,
|
|
59
|
+
lastTs: 0,
|
|
60
|
+
lastWriteAt: 0,
|
|
61
|
+
url: '',
|
|
62
|
+
title: '',
|
|
63
|
+
lastUrl: '',
|
|
64
|
+
userAgent: '',
|
|
65
|
+
source: '',
|
|
66
|
+
userId: '',
|
|
67
|
+
perf: '',
|
|
68
|
+
truncated: false,
|
|
69
|
+
counts: emptyCounts(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.trunc(v) : 0);
|
|
73
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
74
|
+
/** Reads a meta.json document, ignoring fields of the wrong type. */
|
|
75
|
+
function metaFromJSON(id, v) {
|
|
76
|
+
const m = emptyMeta(id);
|
|
77
|
+
if (typeof v !== 'object' || v === null)
|
|
78
|
+
return m;
|
|
79
|
+
const o = v;
|
|
80
|
+
m.startedAt = num(o.startedAt);
|
|
81
|
+
m.endedAt = num(o.endedAt);
|
|
82
|
+
m.firstTs = num(o.firstTs);
|
|
83
|
+
m.lastTs = num(o.lastTs);
|
|
84
|
+
m.lastWriteAt = num(o.lastWriteAt);
|
|
85
|
+
m.url = str(o.url);
|
|
86
|
+
m.title = str(o.title);
|
|
87
|
+
m.lastUrl = str(o.lastUrl);
|
|
88
|
+
m.userAgent = str(o.userAgent);
|
|
89
|
+
m.source = str(o.source);
|
|
90
|
+
m.userId = str(o.userId);
|
|
91
|
+
m.perf = str(o.perf);
|
|
92
|
+
m.truncated = o.truncated === true;
|
|
93
|
+
if (typeof o.counts === 'object' && o.counts !== null) {
|
|
94
|
+
const c = o.counts;
|
|
95
|
+
for (const key of Object.keys(m.counts))
|
|
96
|
+
m.counts[key] = num(c[key]);
|
|
97
|
+
}
|
|
98
|
+
return m;
|
|
99
|
+
}
|
|
100
|
+
/** The session as the API shows it: empty fields left out. */
|
|
101
|
+
function viewOf(sess) {
|
|
102
|
+
const m = sess.meta;
|
|
103
|
+
const out = { id: m.id, startedAt: m.startedAt };
|
|
104
|
+
if (m.endedAt)
|
|
105
|
+
out.endedAt = m.endedAt;
|
|
106
|
+
if (m.firstTs)
|
|
107
|
+
out.firstTs = m.firstTs;
|
|
108
|
+
if (m.lastTs)
|
|
109
|
+
out.lastTs = m.lastTs;
|
|
110
|
+
if (m.lastWriteAt)
|
|
111
|
+
out.lastWriteAt = m.lastWriteAt;
|
|
112
|
+
if (m.url)
|
|
113
|
+
out.url = m.url;
|
|
114
|
+
if (m.title)
|
|
115
|
+
out.title = m.title;
|
|
116
|
+
if (m.lastUrl)
|
|
117
|
+
out.lastUrl = m.lastUrl;
|
|
118
|
+
if (m.userAgent)
|
|
119
|
+
out.userAgent = m.userAgent;
|
|
120
|
+
if (m.source)
|
|
121
|
+
out.source = m.source;
|
|
122
|
+
if (m.userId)
|
|
123
|
+
out.userId = m.userId;
|
|
124
|
+
if (m.perf)
|
|
125
|
+
out.perf = m.perf;
|
|
126
|
+
if (m.truncated)
|
|
127
|
+
out.truncated = true;
|
|
128
|
+
out.counts = { ...m.counts };
|
|
129
|
+
let bytes = 0;
|
|
130
|
+
let raw = 0;
|
|
131
|
+
for (const seg of sess.segs) {
|
|
132
|
+
bytes += seg.disk;
|
|
133
|
+
raw += seg.size;
|
|
134
|
+
}
|
|
135
|
+
out.bytes = bytes;
|
|
136
|
+
out.rawBytes = raw;
|
|
137
|
+
out.segments = sess.segs.length;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Web Vitals ratings in order; unknown ratings rank lowest. */
|
|
141
|
+
function ratingRank(r) {
|
|
142
|
+
switch (r) {
|
|
143
|
+
case 'good':
|
|
144
|
+
return 1;
|
|
145
|
+
case 'needs-improvement':
|
|
146
|
+
return 2;
|
|
147
|
+
case 'poor':
|
|
148
|
+
return 3;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
function apply(m, r) {
|
|
153
|
+
const c = m.counts;
|
|
154
|
+
c.total++;
|
|
155
|
+
if (m.firstTs === 0 || r.ts < m.firstTs)
|
|
156
|
+
m.firstTs = r.ts;
|
|
157
|
+
m.lastTs = Math.max(m.lastTs, r.ts);
|
|
158
|
+
const s = r.sum;
|
|
159
|
+
switch (r.type) {
|
|
160
|
+
case 'rrweb':
|
|
161
|
+
c.rrweb++;
|
|
162
|
+
if (s.rrwebType === record_1.RRWEB_FULL_SNAPSHOT)
|
|
163
|
+
c.snapshots++;
|
|
164
|
+
break;
|
|
165
|
+
case 'network':
|
|
166
|
+
c.network++;
|
|
167
|
+
if (s.status >= 400 || s.err !== '')
|
|
168
|
+
c.netErrors++;
|
|
169
|
+
break;
|
|
170
|
+
case 'console':
|
|
171
|
+
c.console++;
|
|
172
|
+
if (s.level === 'error')
|
|
173
|
+
c.errors++;
|
|
174
|
+
break;
|
|
175
|
+
case 'event':
|
|
176
|
+
c.events++;
|
|
177
|
+
switch (s.name) {
|
|
178
|
+
case 'page_view':
|
|
179
|
+
c.pages++;
|
|
180
|
+
m.url = m.url || s.url;
|
|
181
|
+
m.title = m.title || s.title;
|
|
182
|
+
m.lastUrl = s.url;
|
|
183
|
+
break;
|
|
184
|
+
case 'click':
|
|
185
|
+
c.clicks++;
|
|
186
|
+
break;
|
|
187
|
+
case 'identify':
|
|
188
|
+
m.userId = s.userId || m.userId;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case 'perf':
|
|
193
|
+
if (s.kind === 'vital' && ratingRank(s.rating) > ratingRank(m.perf))
|
|
194
|
+
m.perf = s.rating;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Renders records as lines, injecting the session id as the first field.
|
|
200
|
+
* ids are validated, so they never need JSON escaping.
|
|
201
|
+
*/
|
|
202
|
+
function encode(id, recs) {
|
|
203
|
+
const prefix = `{"sid":"${id}",`;
|
|
204
|
+
let out = '';
|
|
205
|
+
for (const r of recs)
|
|
206
|
+
out += prefix + r.raw.slice(1) + '\n'; // raw is a non-empty object
|
|
207
|
+
return Buffer.from(out, 'utf8');
|
|
208
|
+
}
|
|
209
|
+
function minTS(recs) {
|
|
210
|
+
let m = recs[0].ts;
|
|
211
|
+
for (const r of recs)
|
|
212
|
+
m = Math.min(m, r.ts);
|
|
213
|
+
return m;
|
|
214
|
+
}
|
|
215
|
+
function deferred() {
|
|
216
|
+
let resolve;
|
|
217
|
+
const promise = new Promise((r) => {
|
|
218
|
+
resolve = r;
|
|
219
|
+
});
|
|
220
|
+
return { promise, resolve };
|
|
221
|
+
}
|
|
222
|
+
/** Writes all of buf; on failure the error carries the bytes written before it. */
|
|
223
|
+
function writeAll(fd, buf) {
|
|
224
|
+
let n = 0;
|
|
225
|
+
try {
|
|
226
|
+
while (n < buf.length)
|
|
227
|
+
n += fs_1.default.writeSync(fd, buf, n, buf.length - n);
|
|
228
|
+
return { n, err: null };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return { n, err };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
class Store {
|
|
235
|
+
constructor(opts) {
|
|
236
|
+
this.opts = opts;
|
|
237
|
+
this.closing = false;
|
|
238
|
+
this.wakeFn = null;
|
|
239
|
+
this.loop = null;
|
|
240
|
+
this.lastIdleCheck = Date.now();
|
|
241
|
+
this.sessions = new Map();
|
|
242
|
+
/** Every segment on disk, oldest (lowest seq) first. */
|
|
243
|
+
this.segments = [];
|
|
244
|
+
/** Bytes on disk. */
|
|
245
|
+
this.total = 0;
|
|
246
|
+
/** Uncompressed bytes. */
|
|
247
|
+
this.raw = 0;
|
|
248
|
+
this.nextSeq = 1;
|
|
249
|
+
/** Closed segments waiting for compression. */
|
|
250
|
+
this.queue = [];
|
|
251
|
+
this.root = path_1.default.join(opts.dir, SESSIONS_DIR);
|
|
252
|
+
this.log = opts.logger ?? (0, log_1.consoleLogger)();
|
|
253
|
+
this.now = opts.now ?? Date.now;
|
|
254
|
+
this.compress = opts.compress === exports.COMPRESS_GZIP;
|
|
255
|
+
this.idleCheckMs = opts.idleCheckMs ?? IDLE_CHECK_MS;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Loads the data directory, repairing partial trailing lines and enforcing
|
|
259
|
+
* the size cap, and starts the compression worker.
|
|
260
|
+
*/
|
|
261
|
+
static open(options) {
|
|
262
|
+
if (!options.dir)
|
|
263
|
+
throw new Error('store: dir is required');
|
|
264
|
+
if (!(options.maxBytes > 0))
|
|
265
|
+
throw new Error('store: maxBytes must be positive');
|
|
266
|
+
if (!(options.segmentBytes > 0) || options.segmentBytes > options.maxBytes) {
|
|
267
|
+
throw new Error('store: segmentBytes must be positive and no larger than maxBytes');
|
|
268
|
+
}
|
|
269
|
+
let compress = options.compress ?? '';
|
|
270
|
+
if (compress === '')
|
|
271
|
+
compress = exports.COMPRESS_OFF;
|
|
272
|
+
if (compress !== exports.COMPRESS_OFF && compress !== exports.COMPRESS_GZIP) {
|
|
273
|
+
throw new Error(`store: unknown compression "${compress}" (use "${exports.COMPRESS_OFF}" or "${exports.COMPRESS_GZIP}")`);
|
|
274
|
+
}
|
|
275
|
+
const s = new Store({ ...options, compress });
|
|
276
|
+
fs_1.default.mkdirSync(s.root, { recursive: true, mode: 0o755 });
|
|
277
|
+
s.load();
|
|
278
|
+
s.enforceBudget(null);
|
|
279
|
+
if (s.compress) {
|
|
280
|
+
// No segment is open yet: every plain one is closed.
|
|
281
|
+
for (const seg of s.segments)
|
|
282
|
+
s.enqueue(seg);
|
|
283
|
+
s.loop = s.compressLoop();
|
|
284
|
+
}
|
|
285
|
+
return s;
|
|
286
|
+
}
|
|
287
|
+
load() {
|
|
288
|
+
for (const e of fs_1.default.readdirSync(this.root, { withFileTypes: true })) {
|
|
289
|
+
if (!e.isDirectory() || !validID(e.name))
|
|
290
|
+
continue;
|
|
291
|
+
let sess;
|
|
292
|
+
try {
|
|
293
|
+
sess = this.loadSession(e.name);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.log.warn('skipping unreadable session', { session: e.name, err });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
this.sessions.set(sess.meta.id, sess);
|
|
300
|
+
this.segments.push(...sess.segs);
|
|
301
|
+
}
|
|
302
|
+
this.segments.sort((a, b) => a.seq - b.seq);
|
|
303
|
+
for (const seg of this.segments) {
|
|
304
|
+
this.total += seg.disk;
|
|
305
|
+
this.raw += seg.size;
|
|
306
|
+
this.nextSeq = Math.max(this.nextSeq, seg.seq + 1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
loadSession(id) {
|
|
310
|
+
const dir = path_1.default.join(this.root, id);
|
|
311
|
+
let meta = emptyMeta(id);
|
|
312
|
+
try {
|
|
313
|
+
const text = fs_1.default.readFileSync(path_1.default.join(dir, META_NAME), 'utf8');
|
|
314
|
+
try {
|
|
315
|
+
meta = metaFromJSON(id, JSON.parse(text));
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
this.log.warn('ignoring corrupt session meta', { session: id, err });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (!(0, errors_1.isNotExist)(err))
|
|
323
|
+
throw new Error(`read meta: ${(0, errors_1.errorMessage)(err)}`);
|
|
324
|
+
}
|
|
325
|
+
const sess = { meta, dir, segs: this.loadSegments(id, dir), cur: null, fd: null, lastWrite: 0, changed: null };
|
|
326
|
+
if (sess.meta.startedAt === 0) {
|
|
327
|
+
try {
|
|
328
|
+
sess.meta.startedAt = Math.trunc(fs_1.default.statSync(dir).mtimeMs);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// keep 0
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return sess;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Lists a session's segments. It finishes what a crash interrupted:
|
|
338
|
+
* leftover .tmp files are deleted, and when both forms of a segment exist
|
|
339
|
+
* the .gz is complete (it is renamed into place only after it is synced to
|
|
340
|
+
* disk), so the plain file is deleted.
|
|
341
|
+
*/
|
|
342
|
+
loadSegments(id, dir) {
|
|
343
|
+
const gz = new Map();
|
|
344
|
+
const plain = new Map();
|
|
345
|
+
for (const e of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
346
|
+
const parsed = (0, segment_1.parseSegmentName)(e.name);
|
|
347
|
+
if (!parsed || e.isDirectory())
|
|
348
|
+
continue;
|
|
349
|
+
const p = path_1.default.join(dir, e.name);
|
|
350
|
+
switch (parsed.kind) {
|
|
351
|
+
case 'temp':
|
|
352
|
+
fs_1.default.unlinkSync(p);
|
|
353
|
+
break;
|
|
354
|
+
case 'gzip':
|
|
355
|
+
try {
|
|
356
|
+
const { disk, raw } = (0, segment_1.gzipSizes)(p);
|
|
357
|
+
gz.set(parsed.seq, newSegment(parsed.seq, id, p, raw, disk, true));
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
this.log.warn('ignoring unreadable compressed segment', { path: p, err });
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
case 'plain':
|
|
364
|
+
plain.set(parsed.seq, p);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const segs = [...gz.values()];
|
|
369
|
+
for (const [seq, p] of plain) {
|
|
370
|
+
if (gz.has(seq)) {
|
|
371
|
+
fs_1.default.unlinkSync(p);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const size = (0, segment_1.repairTail)(p);
|
|
375
|
+
if (size === 0) {
|
|
376
|
+
fs_1.default.unlinkSync(p);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
segs.push(newSegment(seq, id, p, size, size, false));
|
|
380
|
+
}
|
|
381
|
+
return segs.sort((a, b) => a.seq - b.seq);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Creates the session if needed and records what the recorder knows about
|
|
385
|
+
* it. Calling it again for the same id only fills missing fields.
|
|
386
|
+
*/
|
|
387
|
+
start(info) {
|
|
388
|
+
if (!validID(info.id))
|
|
389
|
+
throw new errors_1.InvalidIDError();
|
|
390
|
+
const sess = this.sessionFor(info.id, info.startedAt ?? 0);
|
|
391
|
+
const m = sess.meta;
|
|
392
|
+
if (info.startedAt && info.startedAt > 0 && info.startedAt < m.startedAt)
|
|
393
|
+
m.startedAt = info.startedAt;
|
|
394
|
+
m.url = m.url || (info.url ?? '');
|
|
395
|
+
m.title = m.title || (info.title ?? '');
|
|
396
|
+
m.userAgent = m.userAgent || (info.userAgent ?? '');
|
|
397
|
+
m.source = m.source || (info.source ?? '');
|
|
398
|
+
this.saveMeta(sess);
|
|
399
|
+
return viewOf(sess);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Writes records to the session, creating it when it does not exist, then
|
|
403
|
+
* evicts the oldest segments if the size cap is exceeded. Returns the
|
|
404
|
+
* number of bytes written.
|
|
405
|
+
*/
|
|
406
|
+
append(id, recs) {
|
|
407
|
+
if (!validID(id))
|
|
408
|
+
throw new errors_1.InvalidIDError();
|
|
409
|
+
if (recs.length === 0)
|
|
410
|
+
return 0;
|
|
411
|
+
const buf = encode(id, recs);
|
|
412
|
+
const sess = this.sessionFor(id, minTS(recs));
|
|
413
|
+
const seg = this.openSegment(sess);
|
|
414
|
+
const { n, err } = writeAll(sess.fd, buf);
|
|
415
|
+
seg.size += n;
|
|
416
|
+
seg.disk += n;
|
|
417
|
+
this.total += n;
|
|
418
|
+
this.raw += n;
|
|
419
|
+
sess.lastWrite = this.now();
|
|
420
|
+
if (err) {
|
|
421
|
+
// A short write leaves a partial line behind. Stop appending to this
|
|
422
|
+
// segment; open() repairs the tail on the next start.
|
|
423
|
+
seg.broken = true;
|
|
424
|
+
this.closeFile(sess);
|
|
425
|
+
this.notify(sess);
|
|
426
|
+
throw new Error(`write segment: ${(0, errors_1.errorMessage)(err)}`);
|
|
427
|
+
}
|
|
428
|
+
for (const r of recs)
|
|
429
|
+
apply(sess.meta, r);
|
|
430
|
+
sess.meta.lastWriteAt = sess.lastWrite;
|
|
431
|
+
this.notify(sess);
|
|
432
|
+
if (seg.size >= this.opts.segmentBytes)
|
|
433
|
+
this.closeFile(sess);
|
|
434
|
+
this.enforceBudget(seg);
|
|
435
|
+
this.closeIdle();
|
|
436
|
+
// seg is never evicted, so sess is still indexed here.
|
|
437
|
+
this.saveMeta(sess);
|
|
438
|
+
return n;
|
|
439
|
+
}
|
|
440
|
+
/** Marks the session as finished. */
|
|
441
|
+
end(id, endedAt) {
|
|
442
|
+
const sess = this.sessions.get(id);
|
|
443
|
+
if (!sess)
|
|
444
|
+
throw new errors_1.NotFoundError();
|
|
445
|
+
sess.meta.endedAt = endedAt > 0 ? endedAt : this.now();
|
|
446
|
+
this.closeFile(sess);
|
|
447
|
+
this.notify(sess);
|
|
448
|
+
this.saveMeta(sess);
|
|
449
|
+
return viewOf(sess);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Resolves at the session's next change: an append, its end, or the ring
|
|
453
|
+
* buffer or a delete removing data. Take it before reading so that no
|
|
454
|
+
* change is missed.
|
|
455
|
+
*/
|
|
456
|
+
watch(id) {
|
|
457
|
+
const sess = this.sessions.get(id);
|
|
458
|
+
if (!sess)
|
|
459
|
+
throw new errors_1.NotFoundError();
|
|
460
|
+
if (!sess.changed)
|
|
461
|
+
sess.changed = deferred();
|
|
462
|
+
return sess.changed.promise;
|
|
463
|
+
}
|
|
464
|
+
notify(sess) {
|
|
465
|
+
if (sess.changed) {
|
|
466
|
+
sess.changed.resolve();
|
|
467
|
+
sess.changed = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** One session. */
|
|
471
|
+
get(id) {
|
|
472
|
+
const sess = this.sessions.get(id);
|
|
473
|
+
if (!sess)
|
|
474
|
+
throw new errors_1.NotFoundError();
|
|
475
|
+
return viewOf(sess);
|
|
476
|
+
}
|
|
477
|
+
/** Whether the session exists. */
|
|
478
|
+
has(id) {
|
|
479
|
+
return this.sessions.has(id);
|
|
480
|
+
}
|
|
481
|
+
/** All sessions, newest first. */
|
|
482
|
+
list() {
|
|
483
|
+
const out = [...this.sessions.values()].map(viewOf);
|
|
484
|
+
return out.sort((a, b) => b.startedAt - a.startedAt || (b.id < a.id ? -1 : b.id > a.id ? 1 : 0));
|
|
485
|
+
}
|
|
486
|
+
/** Removes a session and its files. */
|
|
487
|
+
delete(id) {
|
|
488
|
+
const sess = this.sessions.get(id);
|
|
489
|
+
if (!sess)
|
|
490
|
+
throw new errors_1.NotFoundError();
|
|
491
|
+
this.closeFile(sess);
|
|
492
|
+
for (const seg of sess.segs) {
|
|
493
|
+
seg.dropped = true;
|
|
494
|
+
this.total -= seg.disk;
|
|
495
|
+
this.raw -= seg.size;
|
|
496
|
+
}
|
|
497
|
+
this.segments = this.segments.filter((seg) => seg.session !== id);
|
|
498
|
+
this.sessions.delete(id);
|
|
499
|
+
this.notify(sess);
|
|
500
|
+
try {
|
|
501
|
+
fs_1.default.rmSync(sess.dir, { recursive: true, force: true });
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
throw new Error(`remove session: ${(0, errors_1.errorMessage)(err)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The session's index entry and its segments, oldest first. Segments can
|
|
509
|
+
* be evicted after the snapshot is taken; readers treat a missing file as
|
|
510
|
+
* empty.
|
|
511
|
+
*/
|
|
512
|
+
snapshot(id) {
|
|
513
|
+
const sess = this.sessions.get(id);
|
|
514
|
+
if (!sess)
|
|
515
|
+
throw new errors_1.NotFoundError();
|
|
516
|
+
return {
|
|
517
|
+
session: viewOf(sess),
|
|
518
|
+
segments: sess.segs.map((seg) => ({ seq: seg.seq, path: seg.path, size: seg.size })),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Streams the session's lines, oldest first, to sink. Throws NotFoundError
|
|
523
|
+
* before writing anything if the session does not exist. Segments evicted
|
|
524
|
+
* while streaming are skipped.
|
|
525
|
+
*/
|
|
526
|
+
async writeSession(id, sink) {
|
|
527
|
+
// Sizes only cover complete writes, so reading up to them never yields
|
|
528
|
+
// a torn line even while appends continue.
|
|
529
|
+
const { segments } = this.snapshot(id);
|
|
530
|
+
let total = 0;
|
|
531
|
+
for (const p of segments) {
|
|
532
|
+
try {
|
|
533
|
+
for await (const chunk of (0, segment_1.readSegment)(p.path, 0, p.size)) {
|
|
534
|
+
total += chunk.length;
|
|
535
|
+
const pending = sink(chunk);
|
|
536
|
+
if (pending)
|
|
537
|
+
await pending;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
if ((0, errors_1.isNotExist)(err))
|
|
542
|
+
continue; // evicted after the snapshot was taken
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return total;
|
|
547
|
+
}
|
|
548
|
+
/** Ring buffer usage. */
|
|
549
|
+
stats() {
|
|
550
|
+
return {
|
|
551
|
+
dir: this.opts.dir,
|
|
552
|
+
totalBytes: this.total,
|
|
553
|
+
rawBytes: this.raw,
|
|
554
|
+
maxBytes: this.opts.maxBytes,
|
|
555
|
+
segmentBytes: this.opts.segmentBytes,
|
|
556
|
+
compress: this.opts.compress,
|
|
557
|
+
sessions: this.sessions.size,
|
|
558
|
+
segments: this.segments.length,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Stops the compression worker (a compression in progress is abandoned and
|
|
563
|
+
* redone on the next start) and closes open segment files.
|
|
564
|
+
*/
|
|
565
|
+
async close() {
|
|
566
|
+
if (!this.closing) {
|
|
567
|
+
this.closing = true;
|
|
568
|
+
this.wakeFn?.();
|
|
569
|
+
}
|
|
570
|
+
await this.loop;
|
|
571
|
+
const errors = [];
|
|
572
|
+
for (const sess of this.sessions.values()) {
|
|
573
|
+
if (sess.fd === null)
|
|
574
|
+
continue;
|
|
575
|
+
try {
|
|
576
|
+
fs_1.default.closeSync(sess.fd);
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
errors.push(`close ${sess.cur?.path}: ${(0, errors_1.errorMessage)(err)}`);
|
|
580
|
+
}
|
|
581
|
+
sess.fd = null;
|
|
582
|
+
sess.cur = null;
|
|
583
|
+
}
|
|
584
|
+
if (errors.length)
|
|
585
|
+
throw new Error(errors.join('; '));
|
|
586
|
+
}
|
|
587
|
+
sessionFor(id, startedAt) {
|
|
588
|
+
const existing = this.sessions.get(id);
|
|
589
|
+
if (existing)
|
|
590
|
+
return existing;
|
|
591
|
+
const dir = path_1.default.join(this.root, id);
|
|
592
|
+
try {
|
|
593
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
throw new Error(`create session dir: ${(0, errors_1.errorMessage)(err)}`);
|
|
597
|
+
}
|
|
598
|
+
const sess = {
|
|
599
|
+
meta: emptyMeta(id, startedAt > 0 ? startedAt : this.now()),
|
|
600
|
+
dir,
|
|
601
|
+
segs: [],
|
|
602
|
+
cur: null,
|
|
603
|
+
fd: null,
|
|
604
|
+
lastWrite: 0,
|
|
605
|
+
changed: null,
|
|
606
|
+
};
|
|
607
|
+
this.sessions.set(id, sess);
|
|
608
|
+
return sess;
|
|
609
|
+
}
|
|
610
|
+
openSegment(sess) {
|
|
611
|
+
if (sess.fd !== null && sess.cur)
|
|
612
|
+
return sess.cur;
|
|
613
|
+
const seq = this.nextSeq;
|
|
614
|
+
const p = path_1.default.join(sess.dir, (0, segment_1.segmentName)(seq));
|
|
615
|
+
let fd;
|
|
616
|
+
try {
|
|
617
|
+
fd = fs_1.default.openSync(p, 'ax', 0o644);
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
throw new Error(`create segment: ${(0, errors_1.errorMessage)(err)}`);
|
|
621
|
+
}
|
|
622
|
+
this.nextSeq++;
|
|
623
|
+
const seg = newSegment(seq, sess.meta.id, p, 0, 0, false);
|
|
624
|
+
this.segments.push(seg); // seq only grows, order is kept
|
|
625
|
+
sess.segs.push(seg);
|
|
626
|
+
sess.cur = seg;
|
|
627
|
+
sess.fd = fd;
|
|
628
|
+
return seg;
|
|
629
|
+
}
|
|
630
|
+
/** Closes the session's open segment for good and queues it for compression. */
|
|
631
|
+
closeFile(sess) {
|
|
632
|
+
if (sess.fd === null)
|
|
633
|
+
return;
|
|
634
|
+
const seg = sess.cur;
|
|
635
|
+
try {
|
|
636
|
+
fs_1.default.closeSync(sess.fd);
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
this.log.warn('close segment', { path: seg.path, err });
|
|
640
|
+
seg.broken = true;
|
|
641
|
+
}
|
|
642
|
+
sess.fd = null;
|
|
643
|
+
sess.cur = null;
|
|
644
|
+
this.enqueue(seg);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Bounds the number of open files when sessions are left running without
|
|
648
|
+
* an explicit end (closed tab, crashed browser).
|
|
649
|
+
*/
|
|
650
|
+
closeIdle() {
|
|
651
|
+
const cutoff = this.now() - IDLE_CLOSE_MS;
|
|
652
|
+
for (const sess of this.sessions.values()) {
|
|
653
|
+
if (sess.fd !== null && sess.lastWrite < cutoff)
|
|
654
|
+
this.closeFile(sess);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/** Deletes the oldest segments until the total fits the cap. keep, the segment just written, is never deleted. */
|
|
658
|
+
enforceBudget(keep) {
|
|
659
|
+
while (this.total > this.opts.maxBytes) {
|
|
660
|
+
const i = this.segments.findIndex((seg) => seg !== keep);
|
|
661
|
+
if (i < 0)
|
|
662
|
+
return;
|
|
663
|
+
this.evict(i);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
evict(i) {
|
|
667
|
+
const seg = this.segments[i];
|
|
668
|
+
this.segments.splice(i, 1);
|
|
669
|
+
this.total -= seg.disk;
|
|
670
|
+
this.raw -= seg.size;
|
|
671
|
+
seg.dropped = true;
|
|
672
|
+
const sess = this.sessions.get(seg.session);
|
|
673
|
+
if (sess.cur === seg)
|
|
674
|
+
this.closeFile(sess);
|
|
675
|
+
try {
|
|
676
|
+
fs_1.default.unlinkSync(seg.path);
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
if (!(0, errors_1.isNotExist)(err))
|
|
680
|
+
this.log.warn('evict segment', { path: seg.path, err });
|
|
681
|
+
}
|
|
682
|
+
sess.segs = sess.segs.filter((x) => x !== seg);
|
|
683
|
+
this.log.debug('ring buffer evicted segment', { session: seg.session, seq: seg.seq, bytes: seg.size });
|
|
684
|
+
this.notify(sess);
|
|
685
|
+
if (sess.segs.length === 0) {
|
|
686
|
+
this.sessions.delete(seg.session);
|
|
687
|
+
try {
|
|
688
|
+
fs_1.default.rmSync(sess.dir, { recursive: true, force: true });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
this.log.warn('remove evicted session', { session: seg.session, err });
|
|
692
|
+
}
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
sess.meta.truncated = true;
|
|
696
|
+
try {
|
|
697
|
+
this.saveMeta(sess);
|
|
698
|
+
}
|
|
699
|
+
catch (err) {
|
|
700
|
+
this.log.warn('save session meta', { session: seg.session, err });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
saveMeta(sess) {
|
|
704
|
+
const tmp = path_1.default.join(sess.dir, META_NAME + '.tmp');
|
|
705
|
+
try {
|
|
706
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(viewOf(sess)), { mode: 0o644 });
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
throw new Error(`write meta: ${(0, errors_1.errorMessage)(err)}`);
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
fs_1.default.renameSync(tmp, path_1.default.join(sess.dir, META_NAME));
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
throw new Error(`commit meta: ${(0, errors_1.errorMessage)(err)}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// ---------- compression ----------
|
|
719
|
+
/**
|
|
720
|
+
* The single compression worker. It also closes idle segments, so a
|
|
721
|
+
* session that stops writing is compressed even when no other session
|
|
722
|
+
* writes. It runs until close().
|
|
723
|
+
*/
|
|
724
|
+
async compressLoop() {
|
|
725
|
+
while (!this.closing) {
|
|
726
|
+
const seg = this.nextJob();
|
|
727
|
+
if (seg) {
|
|
728
|
+
await this.compressSegment(seg);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
await this.sleep(this.idleCheckMs);
|
|
732
|
+
if (this.closing)
|
|
733
|
+
return;
|
|
734
|
+
if (Date.now() - this.lastIdleCheck >= this.idleCheckMs) {
|
|
735
|
+
this.lastIdleCheck = Date.now();
|
|
736
|
+
this.closeIdle();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/** Waits for a wake-up (a queued segment, close) or ms. */
|
|
741
|
+
sleep(ms) {
|
|
742
|
+
return new Promise((resolve) => {
|
|
743
|
+
const timer = setTimeout(() => {
|
|
744
|
+
this.wakeFn = null;
|
|
745
|
+
resolve();
|
|
746
|
+
}, ms);
|
|
747
|
+
timer.unref?.();
|
|
748
|
+
this.wakeFn = () => {
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
this.wakeFn = null;
|
|
751
|
+
resolve();
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
/** Queues a closed segment for compression. */
|
|
756
|
+
enqueue(seg) {
|
|
757
|
+
if (!this.compress || seg.gz || seg.queued || seg.broken || seg.dropped)
|
|
758
|
+
return;
|
|
759
|
+
seg.queued = true;
|
|
760
|
+
this.queue.push(seg);
|
|
761
|
+
this.wakeFn?.();
|
|
762
|
+
}
|
|
763
|
+
nextJob() {
|
|
764
|
+
if (this.closing)
|
|
765
|
+
return null;
|
|
766
|
+
while (this.queue.length > 0) {
|
|
767
|
+
const seg = this.queue.shift();
|
|
768
|
+
if (seg.dropped) {
|
|
769
|
+
seg.queued = false;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
return seg; // stays queued until compressSegment is done
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Replaces a closed plain segment with a gzip copy: write
|
|
778
|
+
* <seq>.jsonl.gz.tmp, sync it, rename it to <seq>.jsonl.gz, then delete the
|
|
779
|
+
* plain file. Appends are not blocked. A crash at any point leaves either
|
|
780
|
+
* the plain file, or both files (and the next open deletes the plain one).
|
|
781
|
+
*/
|
|
782
|
+
async compressSegment(seg) {
|
|
783
|
+
const src = seg.path;
|
|
784
|
+
const size = seg.size;
|
|
785
|
+
const dst = (src.endsWith(segment_1.SEG_EXT) ? src.slice(0, -segment_1.SEG_EXT.length) : src) + segment_1.SEG_EXT + segment_1.GZ_EXT;
|
|
786
|
+
const tmp = dst + segment_1.TMP_EXT;
|
|
787
|
+
const stop = () => seg.dropped || this.closing;
|
|
788
|
+
let disk = 0;
|
|
789
|
+
let err = null;
|
|
790
|
+
try {
|
|
791
|
+
disk = await (0, segment_1.compressFile)(src, tmp, size, stop);
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
err = e;
|
|
795
|
+
}
|
|
796
|
+
seg.queued = false;
|
|
797
|
+
if (!err && stop())
|
|
798
|
+
err = new segment_1.AbortedError();
|
|
799
|
+
if (!err) {
|
|
800
|
+
try {
|
|
801
|
+
fs_1.default.renameSync(tmp, dst);
|
|
802
|
+
}
|
|
803
|
+
catch (e) {
|
|
804
|
+
err = e;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (err) {
|
|
808
|
+
// A deleted segment or a closing store is not a failure.
|
|
809
|
+
const failed = !(err instanceof segment_1.AbortedError) && !(0, errors_1.isNotExist)(err);
|
|
810
|
+
if (failed)
|
|
811
|
+
seg.broken = true; // keep the plain file; do not retry
|
|
812
|
+
(0, segment_1.removeQuiet)(tmp);
|
|
813
|
+
if (failed)
|
|
814
|
+
this.log.warn('compress segment', { path: src, err });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
this.total += disk - seg.disk;
|
|
818
|
+
seg.path = dst;
|
|
819
|
+
seg.disk = disk;
|
|
820
|
+
seg.gz = true;
|
|
821
|
+
const sess = this.sessions.get(seg.session);
|
|
822
|
+
if (sess) {
|
|
823
|
+
try {
|
|
824
|
+
this.saveMeta(sess);
|
|
825
|
+
}
|
|
826
|
+
catch (e) {
|
|
827
|
+
this.log.warn('save session meta', { session: seg.session, err: e });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Readers that opened the plain file keep reading it; new readers find
|
|
831
|
+
// the .gz (see readSegment).
|
|
832
|
+
(0, segment_1.syncDir)(path_1.default.dirname(dst));
|
|
833
|
+
try {
|
|
834
|
+
fs_1.default.unlinkSync(src);
|
|
835
|
+
}
|
|
836
|
+
catch (e) {
|
|
837
|
+
if (!(0, errors_1.isNotExist)(e))
|
|
838
|
+
this.log.warn("remove compressed segment's plain copy", { path: src, err: e });
|
|
839
|
+
}
|
|
840
|
+
this.log.debug('compressed segment', { path: dst, bytes: size, disk });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
exports.Store = Store;
|
|
844
|
+
function newSegment(seq, session, p, size, disk, gz) {
|
|
845
|
+
return { seq, session, path: p, size, disk, gz, broken: false, queued: false, dropped: false };
|
|
846
|
+
}
|