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,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RRWEB_META = exports.RRWEB_FULL_SNAPSHOT = void 0;
|
|
4
|
+
exports.rrwebTypeOf = rrwebTypeOf;
|
|
5
|
+
exports.parseRecord = parseRecord;
|
|
6
|
+
exports.readRecords = readRecords;
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
/** rrweb's EventType.FullSnapshot. */
|
|
9
|
+
exports.RRWEB_FULL_SNAPSHOT = 2;
|
|
10
|
+
/** rrweb's EventType.Meta. */
|
|
11
|
+
exports.RRWEB_META = 4;
|
|
12
|
+
/** The rrweb event type of an rrweb record (2 = full snapshot, 4 = meta), or 0. */
|
|
13
|
+
function rrwebTypeOf(r) {
|
|
14
|
+
return r.type === 'rrweb' ? r.sum.rrwebType : 0;
|
|
15
|
+
}
|
|
16
|
+
function isPlainObject(v) {
|
|
17
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
18
|
+
}
|
|
19
|
+
/** Go's math.Round: halves away from zero. */
|
|
20
|
+
function roundHalfAway(n) {
|
|
21
|
+
return Math.sign(n) * Math.round(Math.abs(n));
|
|
22
|
+
}
|
|
23
|
+
// Non-strings and non-numbers deliberately count as absent.
|
|
24
|
+
const asString = (v) => (typeof v === 'string' ? v : '');
|
|
25
|
+
const asInt = (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.trunc(v) : 0);
|
|
26
|
+
/**
|
|
27
|
+
* Extracts index fields from a record's data. Best effort: data that does not
|
|
28
|
+
* match the expected shape is still stored, it just does not contribute to
|
|
29
|
+
* the counters.
|
|
30
|
+
*/
|
|
31
|
+
function summarize(data) {
|
|
32
|
+
if (!isPlainObject(data)) {
|
|
33
|
+
return { rrwebType: 0, name: '', url: '', title: '', level: '', status: 0, err: '', userId: '', kind: '', rating: '' };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
rrwebType: asInt(data.type),
|
|
37
|
+
name: asString(data.name),
|
|
38
|
+
url: asString(data.url),
|
|
39
|
+
title: asString(data.title),
|
|
40
|
+
level: asString(data.level),
|
|
41
|
+
status: asInt(data.status),
|
|
42
|
+
err: asString(data.error),
|
|
43
|
+
userId: asString(data.userId),
|
|
44
|
+
kind: asString(data.kind),
|
|
45
|
+
rating: asString(data.rating),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Validates a single NDJSON line. */
|
|
49
|
+
function parseRecord(input) {
|
|
50
|
+
const line = (typeof input === 'string' ? input : input.toString('utf8')).trim();
|
|
51
|
+
if (line === '' || line[0] !== '{')
|
|
52
|
+
throw new errors_1.RecordError('record must be a JSON object');
|
|
53
|
+
let v;
|
|
54
|
+
try {
|
|
55
|
+
v = JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new errors_1.RecordError(`invalid JSON: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
if (!isPlainObject(v))
|
|
61
|
+
throw new errors_1.RecordError('record must be a JSON object');
|
|
62
|
+
if (v.ts === undefined || v.ts === null)
|
|
63
|
+
throw new errors_1.RecordError('record has no "ts"');
|
|
64
|
+
if (typeof v.ts !== 'number' || !Number.isFinite(v.ts))
|
|
65
|
+
throw new errors_1.RecordError('decode record: "ts" must be a number');
|
|
66
|
+
if (v.type !== undefined && v.type !== null && typeof v.type !== 'string') {
|
|
67
|
+
throw new errors_1.RecordError('decode record: "type" must be a string');
|
|
68
|
+
}
|
|
69
|
+
if (!v.type)
|
|
70
|
+
throw new errors_1.RecordError('record has no "type"');
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(v, 'sid'))
|
|
72
|
+
throw new errors_1.RecordError('"sid" is set by the server');
|
|
73
|
+
return {
|
|
74
|
+
ts: roundHalfAway(v.ts),
|
|
75
|
+
type: v.type,
|
|
76
|
+
raw: JSON.stringify(v),
|
|
77
|
+
sum: summarize(v.data),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parses an NDJSON body. Blank lines are ignored; any invalid line rejects
|
|
82
|
+
* the whole batch.
|
|
83
|
+
*/
|
|
84
|
+
function readRecords(body) {
|
|
85
|
+
const text = typeof body === 'string' ? body : body.toString('utf8');
|
|
86
|
+
const recs = [];
|
|
87
|
+
const lines = text.split('\n');
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
if (lines[i].trim() === '')
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
recs.push(parseRecord(lines[i]));
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new errors_1.RecordError(`line ${i + 1}: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return recs;
|
|
99
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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.AbortedError = exports.TMP_EXT = exports.GZ_EXT = exports.SEG_EXT = void 0;
|
|
7
|
+
exports.segmentName = segmentName;
|
|
8
|
+
exports.parseSegmentName = parseSegmentName;
|
|
9
|
+
exports.readSegment = readSegment;
|
|
10
|
+
exports.readSegmentBytes = readSegmentBytes;
|
|
11
|
+
exports.gzipSizes = gzipSizes;
|
|
12
|
+
exports.compressFile = compressFile;
|
|
13
|
+
exports.syncDir = syncDir;
|
|
14
|
+
exports.removeQuiet = removeQuiet;
|
|
15
|
+
exports.repairTail = repairTail;
|
|
16
|
+
/**
|
|
17
|
+
* Segment files. A segment is written as plain JSONL; once closed it may be
|
|
18
|
+
* compressed to <seq>.jsonl.gz through <seq>.jsonl.gz.tmp. Readers address
|
|
19
|
+
* data by uncompressed offsets, so both forms read the same.
|
|
20
|
+
*/
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
23
|
+
const stream_1 = require("stream");
|
|
24
|
+
const promises_2 = require("stream/promises");
|
|
25
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
26
|
+
const errors_1 = require("../errors");
|
|
27
|
+
exports.SEG_EXT = '.jsonl';
|
|
28
|
+
exports.GZ_EXT = '.gz';
|
|
29
|
+
exports.TMP_EXT = '.tmp';
|
|
30
|
+
const READ_CHUNK = 256 * 1024;
|
|
31
|
+
function segmentName(seq) {
|
|
32
|
+
return String(seq).padStart(16, '0') + exports.SEG_EXT;
|
|
33
|
+
}
|
|
34
|
+
/** Recognizes "<seq>.jsonl", "<seq>.jsonl.gz" and "<seq>.jsonl.gz.tmp". */
|
|
35
|
+
function parseSegmentName(name) {
|
|
36
|
+
let base;
|
|
37
|
+
let kind;
|
|
38
|
+
if (name.endsWith(exports.SEG_EXT + exports.GZ_EXT + exports.TMP_EXT)) {
|
|
39
|
+
base = name.slice(0, -(exports.SEG_EXT + exports.GZ_EXT + exports.TMP_EXT).length);
|
|
40
|
+
kind = 'temp';
|
|
41
|
+
}
|
|
42
|
+
else if (name.endsWith(exports.SEG_EXT + exports.GZ_EXT)) {
|
|
43
|
+
base = name.slice(0, -(exports.SEG_EXT + exports.GZ_EXT).length);
|
|
44
|
+
kind = 'gzip';
|
|
45
|
+
}
|
|
46
|
+
else if (name.endsWith(exports.SEG_EXT)) {
|
|
47
|
+
base = name.slice(0, -exports.SEG_EXT.length);
|
|
48
|
+
kind = 'plain';
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!/^\d+$/.test(base))
|
|
54
|
+
return null;
|
|
55
|
+
const seq = Number(base);
|
|
56
|
+
return Number.isSafeInteger(seq) ? { seq, kind } : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reads up to `limit` uncompressed bytes of a segment, starting at the
|
|
60
|
+
* uncompressed offset `off`. `path` is a segment path as reported by a store
|
|
61
|
+
* snapshot: a segment compressed since then is found under its new name,
|
|
62
|
+
* because the .gz file is renamed into place before the plain file is
|
|
63
|
+
* removed. A reader that already opened the plain file keeps reading it.
|
|
64
|
+
* Throws an ENOENT error (see isNotExist) when the segment was deleted.
|
|
65
|
+
*/
|
|
66
|
+
async function* readSegment(path, off, limit) {
|
|
67
|
+
if (limit <= 0)
|
|
68
|
+
return;
|
|
69
|
+
if (!path.endsWith(exports.GZ_EXT)) {
|
|
70
|
+
let fh = null;
|
|
71
|
+
try {
|
|
72
|
+
fh = await promises_1.default.open(path, 'r');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (!(0, errors_1.isNotExist)(err))
|
|
76
|
+
throw err;
|
|
77
|
+
path += exports.GZ_EXT;
|
|
78
|
+
}
|
|
79
|
+
if (fh) {
|
|
80
|
+
try {
|
|
81
|
+
let pos = off;
|
|
82
|
+
let left = limit;
|
|
83
|
+
while (left > 0) {
|
|
84
|
+
const n = Math.min(left, READ_CHUNK);
|
|
85
|
+
const buf = Buffer.allocUnsafe(n);
|
|
86
|
+
const { bytesRead } = await fh.read(buf, 0, n, pos);
|
|
87
|
+
if (bytesRead === 0)
|
|
88
|
+
break;
|
|
89
|
+
pos += bytesRead;
|
|
90
|
+
left -= bytesRead;
|
|
91
|
+
yield bytesRead === n ? buf : buf.subarray(0, bytesRead);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await fh.close();
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
yield* readGzip(path, off, limit);
|
|
101
|
+
}
|
|
102
|
+
/** Opens a read stream, rejecting (ENOENT: deleted) before any data flows. */
|
|
103
|
+
function openReadStream(path, opts) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const s = fs_1.default.createReadStream(path, opts);
|
|
106
|
+
const onError = (err) => reject(err);
|
|
107
|
+
s.once('error', onError);
|
|
108
|
+
s.once('open', () => {
|
|
109
|
+
s.off('error', onError);
|
|
110
|
+
resolve(s);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function* readGzip(path, off, limit) {
|
|
115
|
+
const src = await openReadStream(path, { highWaterMark: 64 * 1024 });
|
|
116
|
+
const gunzip = zlib_1.default.createGunzip();
|
|
117
|
+
src.on('error', (err) => gunzip.destroy(err));
|
|
118
|
+
src.pipe(gunzip);
|
|
119
|
+
let skip = off;
|
|
120
|
+
let left = limit;
|
|
121
|
+
try {
|
|
122
|
+
for await (const piece of gunzip) {
|
|
123
|
+
let chunk = piece;
|
|
124
|
+
if (skip > 0) {
|
|
125
|
+
if (chunk.length <= skip) {
|
|
126
|
+
skip -= chunk.length;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
chunk = chunk.subarray(skip);
|
|
130
|
+
skip = 0;
|
|
131
|
+
}
|
|
132
|
+
if (chunk.length > left)
|
|
133
|
+
chunk = chunk.subarray(0, left);
|
|
134
|
+
left -= chunk.length;
|
|
135
|
+
yield chunk;
|
|
136
|
+
if (left <= 0)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
src.destroy();
|
|
142
|
+
gunzip.destroy();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** The first `limit` uncompressed bytes of a segment. */
|
|
146
|
+
async function readSegmentBytes(path, limit) {
|
|
147
|
+
const parts = [];
|
|
148
|
+
for await (const chunk of readSegment(path, 0, limit))
|
|
149
|
+
parts.push(chunk);
|
|
150
|
+
return Buffer.concat(parts);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A compressed segment's size on disk and its uncompressed size, read from
|
|
154
|
+
* the gzip trailer (ISIZE, the size modulo 2^32; segments are far smaller).
|
|
155
|
+
*/
|
|
156
|
+
function gzipSizes(path) {
|
|
157
|
+
const fd = fs_1.default.openSync(path, 'r');
|
|
158
|
+
try {
|
|
159
|
+
const disk = fs_1.default.fstatSync(fd).size;
|
|
160
|
+
const MIN_GZIP = 18; // 10-byte header + 8-byte trailer
|
|
161
|
+
if (disk < MIN_GZIP)
|
|
162
|
+
throw new Error('not a gzip file: too short');
|
|
163
|
+
const head = Buffer.alloc(2);
|
|
164
|
+
fs_1.default.readSync(fd, head, 0, 2, 0);
|
|
165
|
+
if (head[0] !== 0x1f || head[1] !== 0x8b)
|
|
166
|
+
throw new Error('not a gzip file');
|
|
167
|
+
const tail = Buffer.alloc(4);
|
|
168
|
+
fs_1.default.readSync(fd, tail, 0, 4, disk - 4);
|
|
169
|
+
return { disk, raw: tail.readUInt32LE(0) };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
fs_1.default.closeSync(fd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Stops a compression whose segment was deleted or whose store is closing. */
|
|
176
|
+
class AbortedError extends Error {
|
|
177
|
+
constructor() {
|
|
178
|
+
super('compression aborted');
|
|
179
|
+
this.name = 'AbortedError';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.AbortedError = AbortedError;
|
|
183
|
+
/**
|
|
184
|
+
* Writes the first `size` bytes of src to dst as gzip, syncs dst to disk and
|
|
185
|
+
* returns its size. `stop` is checked before every chunk.
|
|
186
|
+
*/
|
|
187
|
+
async function compressFile(src, dst, size, stop) {
|
|
188
|
+
const reader = size > 0 ? await openReadStream(src, { start: 0, end: size - 1, highWaterMark: READ_CHUNK }) : stream_1.Readable.from([]);
|
|
189
|
+
let copied = 0;
|
|
190
|
+
const guard = new stream_1.Transform({
|
|
191
|
+
transform(chunk, _enc, cb) {
|
|
192
|
+
if (stop()) {
|
|
193
|
+
cb(new AbortedError());
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
copied += chunk.length;
|
|
197
|
+
cb(null, chunk);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
await (0, promises_2.pipeline)(reader, guard, zlib_1.default.createGzip(), fs_1.default.createWriteStream(dst, { mode: 0o644 }));
|
|
201
|
+
if (copied !== size)
|
|
202
|
+
throw new Error(`segment is ${copied} bytes, expected ${size}`);
|
|
203
|
+
const out = await promises_1.default.open(dst, 'r+');
|
|
204
|
+
try {
|
|
205
|
+
await out.sync();
|
|
206
|
+
return (await out.stat()).size;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await out.close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Makes a rename in dir durable. Best effort: not every platform can sync a directory. */
|
|
213
|
+
function syncDir(dir) {
|
|
214
|
+
try {
|
|
215
|
+
const fd = fs_1.default.openSync(dir, 'r');
|
|
216
|
+
try {
|
|
217
|
+
fs_1.default.fsyncSync(fd);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
fs_1.default.closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// best effort, see above
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Removes a file that may not exist. */
|
|
228
|
+
function removeQuiet(path) {
|
|
229
|
+
try {
|
|
230
|
+
fs_1.default.unlinkSync(path);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// cleanup of a file that may not exist
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Truncates a partial last line left by an interrupted write and returns the resulting size. */
|
|
237
|
+
function repairTail(path) {
|
|
238
|
+
const fd = fs_1.default.openSync(path, 'r+');
|
|
239
|
+
try {
|
|
240
|
+
const size = fs_1.default.fstatSync(fd).size;
|
|
241
|
+
if (size === 0)
|
|
242
|
+
return 0;
|
|
243
|
+
const last = Buffer.alloc(1);
|
|
244
|
+
fs_1.default.readSync(fd, last, 0, 1, size - 1);
|
|
245
|
+
if (last[0] === 0x0a)
|
|
246
|
+
return size;
|
|
247
|
+
const CHUNK = 64 * 1024;
|
|
248
|
+
const buf = Buffer.alloc(CHUNK);
|
|
249
|
+
for (let end = size; end > 0;) {
|
|
250
|
+
const start = Math.max(0, end - CHUNK);
|
|
251
|
+
const n = end - start;
|
|
252
|
+
fs_1.default.readSync(fd, buf, 0, n, start);
|
|
253
|
+
const i = buf.subarray(0, n).lastIndexOf(0x0a);
|
|
254
|
+
if (i >= 0) {
|
|
255
|
+
const repaired = start + i + 1;
|
|
256
|
+
fs_1.default.ftruncateSync(fd, repaired);
|
|
257
|
+
return repaired;
|
|
258
|
+
}
|
|
259
|
+
end = start;
|
|
260
|
+
}
|
|
261
|
+
fs_1.default.ftruncateSync(fd, 0);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
fs_1.default.closeSync(fd);
|
|
266
|
+
}
|
|
267
|
+
}
|