omp-plugin-duplicate-detector 0.1.0 → 0.2.0
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/README.md +5 -17
- package/dist/detector-worker.js +418 -272
- package/package.json +1 -1
- package/src/coordinator.ts +16 -4
- package/src/detector-worker.ts +63 -24
- package/src/disk-cache.ts +365 -314
- package/src/index.ts +134 -7
- package/src/project-state.ts +10 -4
- package/src/repo-context.ts +158 -0
- package/src/source-aware-index.ts +14 -1
package/src/disk-cache.ts
CHANGED
|
@@ -21,10 +21,13 @@ import {
|
|
|
21
21
|
import type { WorkspaceOptions } from "./worker-protocol";
|
|
22
22
|
|
|
23
23
|
const DEFAULT_MAX_CACHE_BYTES = 250 * 1024 * 1024; // 250 MB
|
|
24
|
+
export const TOKENIZER_CACHE_VERSION = "4.0";
|
|
24
25
|
|
|
25
26
|
export interface DiskCacheOptions {
|
|
26
27
|
/** Root directory of the workspace */
|
|
27
28
|
rootDir: string;
|
|
29
|
+
/** Stable repository key (from resolveRepositoryContext) */
|
|
30
|
+
repositoryKey?: string;
|
|
28
31
|
/** Custom base cache directory (defaults to OS user cache directory) */
|
|
29
32
|
cacheDir?: string;
|
|
30
33
|
/** Detector configuration used to compute configuration fingerprint */
|
|
@@ -65,7 +68,7 @@ export function getDefaultCacheDir(): string {
|
|
|
65
68
|
export function computeConfigFingerprint(
|
|
66
69
|
config?: WorkspaceOptions | SourceAwareIndexOptions,
|
|
67
70
|
): string {
|
|
68
|
-
if (!config) return
|
|
71
|
+
if (!config) return `default_${TOKENIZER_CACHE_VERSION}`;
|
|
69
72
|
|
|
70
73
|
let sortedFormats: Record<string, string[]> | undefined;
|
|
71
74
|
if (config.formatsExts) {
|
|
@@ -76,6 +79,7 @@ export function computeConfigFingerprint(
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
const canonical = {
|
|
82
|
+
version: TOKENIZER_CACHE_VERSION,
|
|
79
83
|
minTokens: config.minTokens ?? 40,
|
|
80
84
|
minLines: config.minLines ?? 5,
|
|
81
85
|
maxLines: config.maxLines ?? 500,
|
|
@@ -91,20 +95,26 @@ export function computeConfigFingerprint(
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
94
|
-
* Computes the workspace SQLite cache database path keyed by
|
|
98
|
+
* Computes the workspace SQLite cache database path keyed by repository identity and config fingerprint.
|
|
95
99
|
*/
|
|
96
100
|
export function computeWorkspaceCachePath(
|
|
97
101
|
baseDir: string,
|
|
98
|
-
|
|
102
|
+
repositoryKeyOrRootDir: string,
|
|
99
103
|
configFingerprint: string,
|
|
100
104
|
): string {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const isKey = /^[0-9a-f]{16}$/i.test(repositoryKeyOrRootDir);
|
|
106
|
+
const repoKey = isKey
|
|
107
|
+
? repositoryKeyOrRootDir
|
|
108
|
+
: crypto
|
|
109
|
+
.createHash("sha256")
|
|
110
|
+
.update(path.resolve(repositoryKeyOrRootDir))
|
|
111
|
+
.digest("hex")
|
|
112
|
+
.slice(0, 16);
|
|
113
|
+
|
|
114
|
+
return path.join(
|
|
115
|
+
baseDir,
|
|
116
|
+
`${repoKey}_${configFingerprint}_v${CACHE_FORMAT_VERSION}.sqlite`,
|
|
117
|
+
);
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/**
|
|
@@ -126,7 +136,7 @@ export function computeShardKey(
|
|
|
126
136
|
export const CACHE_FORMAT_MAGIC = "DUP3";
|
|
127
137
|
|
|
128
138
|
/** Current binary format & SQLite schema version */
|
|
129
|
-
export const CACHE_FORMAT_VERSION =
|
|
139
|
+
export const CACHE_FORMAT_VERSION = 4;
|
|
130
140
|
|
|
131
141
|
/**
|
|
132
142
|
* Encodes a SerializedSourceShard into a high-density, zlib-compressed binary buffer (DUP3 format).
|
|
@@ -143,132 +153,94 @@ function packBinaryShardV3(
|
|
|
143
153
|
shard: SerializedSourceShard,
|
|
144
154
|
tokens: SerializedToken[],
|
|
145
155
|
): Buffer {
|
|
146
|
-
const srcIdBuf = Buffer.from(shard.sourceId, "utf8");
|
|
147
|
-
const formatBuf = Buffer.from(shard.format, "utf8");
|
|
148
|
-
const hashBuf = Buffer.from(shard.contentHash, "utf8");
|
|
149
156
|
const tokenCount = tokens.length;
|
|
150
|
-
const
|
|
157
|
+
const dictionary: string[] = [];
|
|
158
|
+
const dictMap = new Map<string, number>();
|
|
151
159
|
|
|
152
|
-
// Build dictionary of unique 20-character token hashes
|
|
153
|
-
const dict = new Map<string, number>();
|
|
154
|
-
const tokenIndices = new Uint16Array(tokenCount);
|
|
155
160
|
for (let i = 0; i < tokenCount; i++) {
|
|
156
161
|
const h = tokens[i]!.hash;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
dict.set(h, idx);
|
|
162
|
+
if (!dictMap.has(h)) {
|
|
163
|
+
dictMap.set(h, dictionary.length);
|
|
164
|
+
dictionary.push(h);
|
|
161
165
|
}
|
|
162
|
-
tokenIndices[i] = idx;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
const dictCount =
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
2 + // version (3)
|
|
173
|
-
2 +
|
|
174
|
-
formatBuf.length +
|
|
175
|
-
2 +
|
|
176
|
-
hashBuf.length +
|
|
177
|
-
4 + // size
|
|
178
|
-
4 + // lines
|
|
179
|
-
4 + // tokenCount
|
|
180
|
-
8 + // updatedAt
|
|
181
|
-
2 + // minTokens
|
|
182
|
-
2 +
|
|
183
|
-
srcIdBuf.length +
|
|
184
|
-
2 + // dictCount
|
|
185
|
-
4; // tokenCount in payload
|
|
186
|
-
|
|
187
|
-
const buf = Buffer.allocUnsafe(
|
|
188
|
-
headerLen + dictPayloadLen + columnsPayloadLen,
|
|
189
|
-
);
|
|
168
|
+
const dictCount = dictionary.length;
|
|
169
|
+
const dictEntries: Buffer[] = new Array(dictCount);
|
|
170
|
+
for (let i = 0; i < dictCount; i++) {
|
|
171
|
+
const hex = dictionary[i]!;
|
|
172
|
+
dictEntries[i] = Buffer.from(hex, "hex");
|
|
173
|
+
}
|
|
174
|
+
const dictBuf = Buffer.concat(dictEntries);
|
|
190
175
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
pos += 2;
|
|
196
|
-
|
|
197
|
-
buf.writeUInt16LE(formatBuf.length, pos);
|
|
198
|
-
pos += 2;
|
|
199
|
-
formatBuf.copy(buf, pos);
|
|
200
|
-
pos += formatBuf.length;
|
|
201
|
-
|
|
202
|
-
buf.writeUInt16LE(hashBuf.length, pos);
|
|
203
|
-
pos += 2;
|
|
204
|
-
hashBuf.copy(buf, pos);
|
|
205
|
-
pos += hashBuf.length;
|
|
206
|
-
|
|
207
|
-
buf.writeUInt32LE(shard.size, pos);
|
|
208
|
-
pos += 4;
|
|
209
|
-
buf.writeUInt32LE(shard.lines, pos);
|
|
210
|
-
pos += 4;
|
|
211
|
-
buf.writeUInt32LE(shard.tokenCount, pos);
|
|
212
|
-
pos += 4;
|
|
213
|
-
buf.writeDoubleLE(shard.updatedAt ?? Date.now(), pos);
|
|
214
|
-
pos += 8;
|
|
215
|
-
|
|
216
|
-
buf.writeUInt16LE(minTokens, pos);
|
|
217
|
-
pos += 2;
|
|
218
|
-
|
|
219
|
-
buf.writeUInt16LE(srcIdBuf.length, pos);
|
|
220
|
-
pos += 2;
|
|
221
|
-
srcIdBuf.copy(buf, pos);
|
|
222
|
-
pos += srcIdBuf.length;
|
|
223
|
-
|
|
224
|
-
buf.writeUInt16LE(dictCount, pos);
|
|
225
|
-
pos += 2;
|
|
226
|
-
buf.writeUInt32LE(tokenCount, pos);
|
|
227
|
-
pos += 4;
|
|
228
|
-
|
|
229
|
-
// Write dictionary table
|
|
230
|
-
for (const h of dict.keys()) {
|
|
231
|
-
const hexHash = h.length === 20 ? h : h.padEnd(20, "0");
|
|
232
|
-
buf.write(hexHash, pos, 10, "hex");
|
|
233
|
-
pos += 10;
|
|
176
|
+
const indicesBuf = Buffer.allocUnsafe(tokenCount * 2);
|
|
177
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
178
|
+
const idx = dictMap.get(tokens[i]!.hash)!;
|
|
179
|
+
indicesBuf.writeUInt16LE(idx, i * 2);
|
|
234
180
|
}
|
|
235
181
|
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const lenOffset = deltaRangeOffset + tokenCount * 4;
|
|
182
|
+
const colBytes = tokenCount * 4;
|
|
183
|
+
const dLinesBuf = Buffer.allocUnsafe(colBytes);
|
|
184
|
+
const dColsBuf = Buffer.allocUnsafe(colBytes);
|
|
185
|
+
const dPosBuf = Buffer.allocUnsafe(colBytes);
|
|
186
|
+
const dLenBuf = Buffer.allocUnsafe(colBytes);
|
|
242
187
|
|
|
243
|
-
let prevLine =
|
|
244
|
-
let
|
|
188
|
+
let prevLine = 0;
|
|
189
|
+
let prevCol = 0;
|
|
190
|
+
let prevPos = 0;
|
|
245
191
|
|
|
246
192
|
for (let i = 0; i < tokenCount; i++) {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
buf.writeUInt16LE(Math.min(65535, tokLen), lenOffset + i * 2);
|
|
265
|
-
|
|
266
|
-
prevLine = curLine;
|
|
267
|
-
prevRangeStart = curRange0;
|
|
193
|
+
const tok = tokens[i]!;
|
|
194
|
+
const dLine = tok.line - prevLine;
|
|
195
|
+
const dCol = tok.column - prevCol;
|
|
196
|
+
const dPos = tok.position - prevPos;
|
|
197
|
+
const len =
|
|
198
|
+
Array.isArray(tok.range) && tok.range.length >= 2
|
|
199
|
+
? tok.range[1] - tok.range[0]
|
|
200
|
+
: 0;
|
|
201
|
+
|
|
202
|
+
dLinesBuf.writeInt32LE(dLine, i * 4);
|
|
203
|
+
dColsBuf.writeInt32LE(dCol, i * 4);
|
|
204
|
+
dPosBuf.writeInt32LE(dPos, i * 4);
|
|
205
|
+
dLenBuf.writeUInt32LE(len, i * 4);
|
|
206
|
+
|
|
207
|
+
prevLine = tok.line;
|
|
208
|
+
prevCol = tok.column;
|
|
209
|
+
prevPos = tok.position;
|
|
268
210
|
}
|
|
269
211
|
|
|
270
|
-
|
|
271
|
-
|
|
212
|
+
const meta = {
|
|
213
|
+
sourceId: shard.sourceId,
|
|
214
|
+
contentHash: shard.contentHash,
|
|
215
|
+
format: shard.format,
|
|
216
|
+
size: shard.size,
|
|
217
|
+
lines: shard.lines,
|
|
218
|
+
tokenCount: shard.tokenCount,
|
|
219
|
+
minTokens: shard.minTokens,
|
|
220
|
+
updatedAt: shard.updatedAt ?? Date.now(),
|
|
221
|
+
};
|
|
222
|
+
const metaJson = Buffer.from(JSON.stringify(meta), "utf-8");
|
|
223
|
+
|
|
224
|
+
const HEADER_SIZE = 16;
|
|
225
|
+
const header = Buffer.allocUnsafe(HEADER_SIZE);
|
|
226
|
+
header.write(CACHE_FORMAT_MAGIC, 0, 4, "ascii");
|
|
227
|
+
header.writeUInt16LE(CACHE_FORMAT_VERSION, 4);
|
|
228
|
+
header.writeUInt16LE(metaJson.length, 6);
|
|
229
|
+
header.writeUInt32LE(tokenCount, 8);
|
|
230
|
+
header.writeUInt32LE(dictCount, 12);
|
|
231
|
+
|
|
232
|
+
const rawUncompressed = Buffer.concat([
|
|
233
|
+
header,
|
|
234
|
+
metaJson,
|
|
235
|
+
dictBuf,
|
|
236
|
+
indicesBuf,
|
|
237
|
+
dLinesBuf,
|
|
238
|
+
dColsBuf,
|
|
239
|
+
dPosBuf,
|
|
240
|
+
dLenBuf,
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
return zlib.deflateSync(rawUncompressed, { level: 6 });
|
|
272
244
|
}
|
|
273
245
|
|
|
274
246
|
/**
|
|
@@ -279,121 +251,126 @@ export function unpackBinaryShard(
|
|
|
279
251
|
compressed: Buffer,
|
|
280
252
|
): SerializedSourceShard | null {
|
|
281
253
|
try {
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
254
|
+
const raw = zlib.inflateSync(compressed);
|
|
255
|
+
if (raw.length < 16) return null;
|
|
256
|
+
|
|
257
|
+
const magic = raw.toString("ascii", 0, 4);
|
|
258
|
+
if (magic === CACHE_FORMAT_MAGIC) {
|
|
259
|
+
return unpackBinaryShardV3(raw);
|
|
287
260
|
}
|
|
288
|
-
return
|
|
261
|
+
return null;
|
|
289
262
|
} catch {
|
|
290
263
|
return null;
|
|
291
264
|
}
|
|
292
265
|
}
|
|
293
266
|
|
|
294
267
|
function unpackBinaryShardV3(buf: Buffer): SerializedSourceShard | null {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const minTokens = buf.readUInt16LE(pos);
|
|
320
|
-
pos += 2;
|
|
321
|
-
|
|
322
|
-
const srcLen = buf.readUInt16LE(pos);
|
|
323
|
-
pos += 2;
|
|
324
|
-
const sourceId = buf.toString("utf8", pos, pos + srcLen);
|
|
325
|
-
pos += srcLen;
|
|
326
|
-
|
|
327
|
-
const dictCount = buf.readUInt16LE(pos);
|
|
328
|
-
pos += 2;
|
|
329
|
-
const tokensPayloadCount = buf.readUInt32LE(pos);
|
|
330
|
-
pos += 4;
|
|
331
|
-
|
|
332
|
-
// Read dictionary table
|
|
333
|
-
const dict = new Array<string>(dictCount);
|
|
334
|
-
for (let i = 0; i < dictCount; i++) {
|
|
335
|
-
dict[i] = buf.toString("hex", pos, pos + 10);
|
|
336
|
-
pos += 10;
|
|
337
|
-
}
|
|
268
|
+
try {
|
|
269
|
+
const version = buf.readUInt16LE(4);
|
|
270
|
+
if (version < 3 || version > CACHE_FORMAT_VERSION) return null;
|
|
271
|
+
|
|
272
|
+
const metaLen = buf.readUInt16LE(6);
|
|
273
|
+
const tokenCount = buf.readUInt32LE(8);
|
|
274
|
+
const dictCount = buf.readUInt32LE(12);
|
|
275
|
+
|
|
276
|
+
let offset = 16;
|
|
277
|
+
const metaJsonBuf = buf.subarray(offset, offset + metaLen);
|
|
278
|
+
offset += metaLen;
|
|
279
|
+
|
|
280
|
+
const meta = JSON.parse(metaJsonBuf.toString("utf-8"));
|
|
281
|
+
const TOKEN_HASH_RAW_BYTES = 10;
|
|
282
|
+
const dictByteLen = dictCount * TOKEN_HASH_RAW_BYTES;
|
|
283
|
+
if (buf.length < offset + dictByteLen) return null;
|
|
284
|
+
|
|
285
|
+
const dictionary: string[] = new Array(dictCount);
|
|
286
|
+
for (let i = 0; i < dictCount; i++) {
|
|
287
|
+
dictionary[i] = buf
|
|
288
|
+
.subarray(offset + i * 10, offset + (i + 1) * 10)
|
|
289
|
+
.toString("hex");
|
|
290
|
+
}
|
|
291
|
+
offset += dictByteLen;
|
|
338
292
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
let prevLine = 1;
|
|
347
|
-
let prevRangeStart = 0;
|
|
348
|
-
|
|
349
|
-
for (let i = 0; i < tokensPayloadCount; i++) {
|
|
350
|
-
const dictIdx = buf.readUInt16LE(dictIdxOffset + i * 2);
|
|
351
|
-
const hash = dict[dictIdx] || "";
|
|
352
|
-
const deltaLine = buf.readUInt16LE(deltaLineOffset + i * 2);
|
|
353
|
-
const col = buf.readUInt16LE(colOffset + i * 2);
|
|
354
|
-
const deltaRange = buf.readUInt32LE(deltaRangeOffset + i * 4);
|
|
355
|
-
const tokLen = buf.readUInt16LE(lenOffset + i * 2);
|
|
356
|
-
|
|
357
|
-
const line = prevLine + deltaLine;
|
|
358
|
-
const rangeStart = prevRangeStart + deltaRange;
|
|
359
|
-
const rangeEnd = rangeStart + tokLen;
|
|
360
|
-
|
|
361
|
-
tokens[i] = {
|
|
362
|
-
hash,
|
|
363
|
-
line,
|
|
364
|
-
column: col,
|
|
365
|
-
position: i,
|
|
366
|
-
range: [rangeStart, rangeEnd],
|
|
367
|
-
};
|
|
293
|
+
const indicesByteLen = tokenCount * 2;
|
|
294
|
+
if (buf.length < offset + indicesByteLen) return null;
|
|
295
|
+
const indices = new Uint16Array(tokenCount);
|
|
296
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
297
|
+
indices[i] = buf.readUInt16LE(offset + i * 2);
|
|
298
|
+
}
|
|
299
|
+
offset += indicesByteLen;
|
|
368
300
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
301
|
+
const colBytes = tokenCount * 4;
|
|
302
|
+
if (buf.length < offset + colBytes * 4) return null;
|
|
372
303
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
304
|
+
const dLines = new Int32Array(tokenCount);
|
|
305
|
+
const dCols = new Int32Array(tokenCount);
|
|
306
|
+
const dPos = new Int32Array(tokenCount);
|
|
307
|
+
const dLens = new Uint32Array(tokenCount);
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
310
|
+
dLines[i] = buf.readInt32LE(offset + i * 4);
|
|
311
|
+
}
|
|
312
|
+
offset += colBytes;
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
315
|
+
dCols[i] = buf.readInt32LE(offset + i * 4);
|
|
316
|
+
}
|
|
317
|
+
offset += colBytes;
|
|
318
|
+
|
|
319
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
320
|
+
dPos[i] = buf.readInt32LE(offset + i * 4);
|
|
321
|
+
}
|
|
322
|
+
offset += colBytes;
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
325
|
+
dLens[i] = buf.readUInt32LE(offset + i * 4);
|
|
326
|
+
}
|
|
327
|
+
offset += colBytes;
|
|
328
|
+
|
|
329
|
+
const tokens: SerializedToken[] = new Array(tokenCount);
|
|
330
|
+
let curLine = 0;
|
|
331
|
+
let curCol = 0;
|
|
332
|
+
let curPos = 0;
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
335
|
+
curLine += dLines[i]!;
|
|
336
|
+
curCol += dCols[i]!;
|
|
337
|
+
curPos += dPos[i]!;
|
|
338
|
+
const len = dLens[i]!;
|
|
339
|
+
const dictIdx = indices[i]!;
|
|
340
|
+
const hash = dictionary[dictIdx] ?? "";
|
|
341
|
+
|
|
342
|
+
tokens[i] = {
|
|
343
|
+
hash,
|
|
344
|
+
line: curLine,
|
|
345
|
+
column: curCol,
|
|
346
|
+
position: curPos,
|
|
347
|
+
range: [curPos, curPos + len],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const minTokens = meta.minTokens ?? 40;
|
|
352
|
+
const frames: SourceFrame[] = reconstructFramesFromTokens(
|
|
353
|
+
tokens,
|
|
354
|
+
meta.sourceId,
|
|
355
|
+
minTokens,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
version,
|
|
360
|
+
sourceId: meta.sourceId,
|
|
361
|
+
contentHash: meta.contentHash,
|
|
362
|
+
format: meta.format,
|
|
363
|
+
size: meta.size,
|
|
364
|
+
lines: meta.lines,
|
|
365
|
+
tokenCount,
|
|
366
|
+
minTokens,
|
|
367
|
+
updatedAt: meta.updatedAt,
|
|
368
|
+
tokens,
|
|
369
|
+
frames,
|
|
370
|
+
};
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
397
374
|
}
|
|
398
375
|
|
|
399
376
|
/**
|
|
@@ -401,6 +378,7 @@ function unpackBinaryShardV3(buf: Buffer): SerializedSourceShard | null {
|
|
|
401
378
|
*/
|
|
402
379
|
export class DiskCacheManager {
|
|
403
380
|
readonly rootDir: string;
|
|
381
|
+
readonly repositoryKey: string;
|
|
404
382
|
readonly baseCacheDir: string;
|
|
405
383
|
readonly dbPath: string;
|
|
406
384
|
readonly workspaceCacheDir: string;
|
|
@@ -410,7 +388,6 @@ export class DiskCacheManager {
|
|
|
410
388
|
#db: Database | null = null;
|
|
411
389
|
#getStmt: Statement | null = null;
|
|
412
390
|
#saveStmt: Statement | null = null;
|
|
413
|
-
#updateMtimeStmt: Statement | null = null;
|
|
414
391
|
#deleteStmt: Statement | null = null;
|
|
415
392
|
#totalSizeStmt: Statement | null = null;
|
|
416
393
|
#oldestShardsStmt: Statement | null = null;
|
|
@@ -419,13 +396,20 @@ export class DiskCacheManager {
|
|
|
419
396
|
|
|
420
397
|
constructor(options: DiskCacheOptions) {
|
|
421
398
|
this.rootDir = path.resolve(options.rootDir);
|
|
399
|
+
this.repositoryKey =
|
|
400
|
+
options.repositoryKey ??
|
|
401
|
+
crypto
|
|
402
|
+
.createHash("sha256")
|
|
403
|
+
.update(this.rootDir)
|
|
404
|
+
.digest("hex")
|
|
405
|
+
.slice(0, 16);
|
|
422
406
|
this.baseCacheDir = options.cacheDir
|
|
423
407
|
? path.resolve(options.cacheDir)
|
|
424
408
|
: getDefaultCacheDir();
|
|
425
409
|
this.configFingerprint = computeConfigFingerprint(options.config);
|
|
426
410
|
this.dbPath = computeWorkspaceCachePath(
|
|
427
411
|
this.baseCacheDir,
|
|
428
|
-
this.
|
|
412
|
+
this.repositoryKey,
|
|
429
413
|
this.configFingerprint,
|
|
430
414
|
);
|
|
431
415
|
this.workspaceCacheDir = this.baseCacheDir;
|
|
@@ -436,64 +420,75 @@ export class DiskCacheManager {
|
|
|
436
420
|
if (this.#closed) return null;
|
|
437
421
|
if (this.#db) return this.#db;
|
|
438
422
|
|
|
423
|
+
let db: Database | null = null;
|
|
439
424
|
try {
|
|
440
425
|
const dir = path.dirname(this.dbPath);
|
|
441
426
|
if (!fsSync.existsSync(dir)) {
|
|
442
427
|
fsSync.mkdirSync(dir, { recursive: true });
|
|
443
428
|
}
|
|
444
429
|
|
|
445
|
-
|
|
446
|
-
db.exec("PRAGMA
|
|
430
|
+
db = new Database(this.dbPath, { create: true });
|
|
431
|
+
db.exec("PRAGMA busy_timeout = 2000;");
|
|
432
|
+
const journalRow = db.query("PRAGMA journal_mode = WAL;").get() as
|
|
433
|
+
| { journal_mode?: string }
|
|
434
|
+
| undefined;
|
|
435
|
+
if (journalRow?.journal_mode?.toLowerCase() !== "wal") {
|
|
436
|
+
try {
|
|
437
|
+
db.close();
|
|
438
|
+
} catch {}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
447
441
|
db.exec("PRAGMA synchronous = NORMAL;");
|
|
448
442
|
db.exec("PRAGMA temp_store = MEMORY;");
|
|
449
|
-
|
|
450
|
-
// Clear cache and reset schema on version difference
|
|
451
443
|
const versionRow = db.query("PRAGMA user_version;").get() as
|
|
452
444
|
| { user_version: number }
|
|
453
445
|
| undefined;
|
|
454
446
|
const schemaVersion = versionRow?.user_version ?? 0;
|
|
455
447
|
if (schemaVersion !== CACHE_FORMAT_VERSION) {
|
|
456
|
-
|
|
448
|
+
try {
|
|
449
|
+
db.exec("DELETE FROM shards;");
|
|
450
|
+
} catch {}
|
|
457
451
|
db.exec(`PRAGMA user_version = ${CACHE_FORMAT_VERSION};`);
|
|
458
452
|
}
|
|
459
453
|
|
|
460
454
|
db.exec(`
|
|
461
455
|
CREATE TABLE IF NOT EXISTS shards (
|
|
462
|
-
rel_path TEXT NOT NULL
|
|
456
|
+
rel_path TEXT NOT NULL,
|
|
463
457
|
content_hash TEXT NOT NULL,
|
|
464
458
|
payload BLOB NOT NULL,
|
|
465
|
-
mtime REAL NOT NULL
|
|
459
|
+
mtime REAL NOT NULL,
|
|
460
|
+
PRIMARY KEY (rel_path, content_hash)
|
|
466
461
|
);
|
|
467
|
-
CREATE INDEX IF NOT EXISTS idx_shards_content_hash ON shards(content_hash);
|
|
468
462
|
CREATE INDEX IF NOT EXISTS idx_shards_mtime ON shards(mtime);
|
|
469
463
|
`);
|
|
470
|
-
|
|
471
464
|
this.#getStmt = db.prepare(
|
|
472
|
-
"SELECT payload
|
|
465
|
+
"SELECT payload FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
|
|
473
466
|
);
|
|
474
467
|
this.#saveStmt = db.prepare(`
|
|
475
468
|
INSERT INTO shards (rel_path, content_hash, payload, mtime)
|
|
476
469
|
VALUES (?1, ?2, ?3, ?4)
|
|
477
|
-
ON CONFLICT(rel_path) DO UPDATE SET
|
|
478
|
-
content_hash = excluded.content_hash,
|
|
479
|
-
payload = excluded.payload,
|
|
470
|
+
ON CONFLICT(rel_path, content_hash) DO UPDATE SET
|
|
480
471
|
mtime = excluded.mtime
|
|
481
472
|
`);
|
|
482
|
-
this.#
|
|
483
|
-
"
|
|
473
|
+
this.#deleteStmt = db.prepare(
|
|
474
|
+
"DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
|
|
484
475
|
);
|
|
485
|
-
this.#deleteStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1");
|
|
486
476
|
this.#totalSizeStmt = db.prepare(
|
|
487
477
|
"SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards",
|
|
488
478
|
);
|
|
489
479
|
this.#oldestShardsStmt = db.prepare(
|
|
490
|
-
"SELECT
|
|
480
|
+
"SELECT rowid, LENGTH(payload) as size FROM shards ORDER BY mtime ASC LIMIT ?1",
|
|
491
481
|
);
|
|
492
482
|
this.#deleteAllStmt = db.prepare("DELETE FROM shards");
|
|
493
483
|
|
|
494
484
|
this.#db = db;
|
|
495
485
|
return db;
|
|
496
486
|
} catch {
|
|
487
|
+
if (db) {
|
|
488
|
+
try {
|
|
489
|
+
db.close();
|
|
490
|
+
} catch {}
|
|
491
|
+
}
|
|
497
492
|
// Fail open on SQLite creation or permission errors
|
|
498
493
|
return null;
|
|
499
494
|
}
|
|
@@ -501,6 +496,7 @@ export class DiskCacheManager {
|
|
|
501
496
|
|
|
502
497
|
/**
|
|
503
498
|
* Retrieves a serialized shard from the SQLite cache if present and valid.
|
|
499
|
+
* Pure read-only operation: does not issue write transactions on cache hits.
|
|
504
500
|
* Returns null on cache miss or corrupted/invalid shard (fails open).
|
|
505
501
|
*/
|
|
506
502
|
async getShard(
|
|
@@ -512,15 +508,14 @@ export class DiskCacheManager {
|
|
|
512
508
|
if (!db || !this.#getStmt) return null;
|
|
513
509
|
|
|
514
510
|
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
515
|
-
const row = this.#getStmt.get(normalizedRelPath) as
|
|
511
|
+
const row = this.#getStmt.get(normalizedRelPath, contentHash) as
|
|
516
512
|
| {
|
|
517
513
|
payload: Uint8Array | Buffer;
|
|
518
|
-
content_hash: string;
|
|
519
514
|
}
|
|
520
515
|
| null
|
|
521
516
|
| undefined;
|
|
522
517
|
|
|
523
|
-
if (!row
|
|
518
|
+
if (!row) {
|
|
524
519
|
return null;
|
|
525
520
|
}
|
|
526
521
|
|
|
@@ -536,24 +531,11 @@ export class DiskCacheManager {
|
|
|
536
531
|
if (
|
|
537
532
|
shard &&
|
|
538
533
|
shard.contentHash === contentHash &&
|
|
539
|
-
typeof shard.sourceId === "string" &&
|
|
540
534
|
Array.isArray(shard.frames)
|
|
541
535
|
) {
|
|
542
|
-
try {
|
|
543
|
-
this.#updateMtimeStmt?.run(Date.now(), normalizedRelPath);
|
|
544
|
-
} catch {
|
|
545
|
-
// Non-fatal
|
|
546
|
-
}
|
|
547
536
|
return shard;
|
|
548
537
|
}
|
|
549
538
|
|
|
550
|
-
// If shard was corrupted or outdated version, clean up the invalid row
|
|
551
|
-
try {
|
|
552
|
-
this.#deleteStmt?.run(normalizedRelPath);
|
|
553
|
-
} catch {
|
|
554
|
-
// Non-fatal
|
|
555
|
-
}
|
|
556
|
-
|
|
557
539
|
return null;
|
|
558
540
|
} catch {
|
|
559
541
|
// Fail open on any error
|
|
@@ -592,9 +574,71 @@ export class DiskCacheManager {
|
|
|
592
574
|
// Fail open: cache write failures should not disrupt indexing
|
|
593
575
|
}
|
|
594
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Deletes a cached shard by relPath and contentHash if present.
|
|
579
|
+
*/
|
|
580
|
+
async deleteShard(relPath: string, contentHash: string): Promise<void> {
|
|
581
|
+
try {
|
|
582
|
+
const db = this.#getDb();
|
|
583
|
+
if (!db || !this.#deleteStmt) return;
|
|
584
|
+
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
585
|
+
this.#deleteStmt.run(normalizedRelPath, contentHash);
|
|
586
|
+
} catch {
|
|
587
|
+
// Fail open
|
|
588
|
+
}
|
|
589
|
+
}
|
|
595
590
|
|
|
596
591
|
/**
|
|
597
|
-
*
|
|
592
|
+
* Atomically saves a batch of pre-tokenized shards inside a single SQLite transaction.
|
|
593
|
+
*/
|
|
594
|
+
async saveShards(
|
|
595
|
+
items: Array<{ shard: SerializedSourceShard; relPath?: string }>,
|
|
596
|
+
): Promise<void> {
|
|
597
|
+
if (items.length === 0) return;
|
|
598
|
+
try {
|
|
599
|
+
const db = this.#getDb();
|
|
600
|
+
if (!db || !this.#saveStmt) return;
|
|
601
|
+
|
|
602
|
+
const stmt = this.#saveStmt;
|
|
603
|
+
const root = this.rootDir;
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
|
|
606
|
+
// Pre-pack payloads outside transaction to minimize write lock time
|
|
607
|
+
const prepared: Array<{
|
|
608
|
+
relPath: string;
|
|
609
|
+
hash: string;
|
|
610
|
+
payload: Buffer;
|
|
611
|
+
}> = [];
|
|
612
|
+
for (const item of items) {
|
|
613
|
+
const targetRelPath =
|
|
614
|
+
item.relPath ??
|
|
615
|
+
(path.isAbsolute(item.shard.sourceId)
|
|
616
|
+
? path.relative(root, item.shard.sourceId)
|
|
617
|
+
: item.shard.sourceId);
|
|
618
|
+
const normalizedRelPath = targetRelPath.replace(/\\/g, "/");
|
|
619
|
+
const payload = packBinaryShard(item.shard);
|
|
620
|
+
prepared.push({
|
|
621
|
+
relPath: normalizedRelPath,
|
|
622
|
+
hash: item.shard.contentHash,
|
|
623
|
+
payload,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const tx = db.transaction((entries: typeof prepared) => {
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
stmt.run(entry.relPath, entry.hash, entry.payload, now);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
tx(prepared);
|
|
634
|
+
} catch {
|
|
635
|
+
// Fail open
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Prunes oldest shards if total payload size exceeds budget using atomic windowed DELETE.
|
|
641
|
+
* Avoids concurrent VACUUM calls during active sessions.
|
|
598
642
|
*/
|
|
599
643
|
async prune(maxBytes?: number): Promise<void> {
|
|
600
644
|
const budget = maxBytes !== undefined ? maxBytes : this.maxBytes;
|
|
@@ -605,48 +649,44 @@ export class DiskCacheManager {
|
|
|
605
649
|
|
|
606
650
|
if (budget <= 0) {
|
|
607
651
|
this.#deleteAllStmt?.run();
|
|
608
|
-
try {
|
|
609
|
-
db.exec("VACUUM;");
|
|
610
|
-
} catch {
|
|
611
|
-
// Ignore vacuum errors
|
|
612
|
-
}
|
|
613
652
|
return;
|
|
614
653
|
}
|
|
615
654
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
655
|
+
// Iteratively prune oldest shards in bounded windows
|
|
656
|
+
for (let iter = 0; iter < 10; iter++) {
|
|
657
|
+
const totalRow = this.#totalSizeStmt?.get() as
|
|
658
|
+
| { total: number }
|
|
659
|
+
| null
|
|
660
|
+
| undefined;
|
|
661
|
+
const totalSize = totalRow?.total ?? 0;
|
|
625
662
|
|
|
626
|
-
const oldestShards = (this.#oldestShardsStmt?.all() ?? []) as Array<{
|
|
627
|
-
rel_path: string;
|
|
628
|
-
size: number;
|
|
629
|
-
}>;
|
|
630
|
-
|
|
631
|
-
let deletedAny = false;
|
|
632
|
-
for (const entry of oldestShards) {
|
|
633
663
|
if (totalSize <= budget) {
|
|
634
664
|
break;
|
|
635
665
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
666
|
+
|
|
667
|
+
const excess = totalSize - budget;
|
|
668
|
+
const rows = (this.#oldestShardsStmt?.all(100) ?? []) as Array<{
|
|
669
|
+
rowid: number;
|
|
670
|
+
size: number;
|
|
671
|
+
}>;
|
|
672
|
+
|
|
673
|
+
if (rows.length === 0) break;
|
|
674
|
+
|
|
675
|
+
const rowidsToDelete: number[] = [];
|
|
676
|
+
let freed = 0;
|
|
677
|
+
for (const r of rows) {
|
|
678
|
+
rowidsToDelete.push(r.rowid);
|
|
679
|
+
freed += r.size;
|
|
680
|
+
if (freed >= excess) break;
|
|
642
681
|
}
|
|
643
|
-
}
|
|
644
682
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
683
|
+
if (rowidsToDelete.length > 0) {
|
|
684
|
+
const deleteBatchStmt = db.prepare(
|
|
685
|
+
`DELETE FROM shards WHERE rowid IN (${rowidsToDelete.join(",")})`,
|
|
686
|
+
);
|
|
687
|
+
deleteBatchStmt.run();
|
|
688
|
+
} else {
|
|
689
|
+
break;
|
|
650
690
|
}
|
|
651
691
|
}
|
|
652
692
|
} catch {
|
|
@@ -655,27 +695,14 @@ export class DiskCacheManager {
|
|
|
655
695
|
}
|
|
656
696
|
|
|
657
697
|
/**
|
|
658
|
-
* Clears all cached shards in the current workspace cache database.
|
|
698
|
+
* Clears all cached shards in the current workspace cache database non-destructively.
|
|
659
699
|
*/
|
|
660
700
|
async clear(): Promise<void> {
|
|
661
701
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
} catch {}
|
|
666
|
-
this.#db = null;
|
|
667
|
-
this.#getStmt = null;
|
|
668
|
-
this.#saveStmt = null;
|
|
669
|
-
this.#updateMtimeStmt = null;
|
|
670
|
-
this.#deleteStmt = null;
|
|
671
|
-
this.#totalSizeStmt = null;
|
|
672
|
-
this.#oldestShardsStmt = null;
|
|
673
|
-
this.#deleteAllStmt = null;
|
|
702
|
+
const db = this.#getDb();
|
|
703
|
+
if (db && this.#deleteAllStmt) {
|
|
704
|
+
this.#deleteAllStmt.run();
|
|
674
705
|
}
|
|
675
|
-
|
|
676
|
-
await fs.unlink(this.dbPath).catch(() => {});
|
|
677
|
-
await fs.unlink(`${this.dbPath}-wal`).catch(() => {});
|
|
678
|
-
await fs.unlink(`${this.dbPath}-shm`).catch(() => {});
|
|
679
706
|
} catch {
|
|
680
707
|
// Fail open
|
|
681
708
|
}
|
|
@@ -693,7 +720,6 @@ export class DiskCacheManager {
|
|
|
693
720
|
this.#db = null;
|
|
694
721
|
this.#getStmt = null;
|
|
695
722
|
this.#saveStmt = null;
|
|
696
|
-
this.#updateMtimeStmt = null;
|
|
697
723
|
this.#deleteStmt = null;
|
|
698
724
|
this.#totalSizeStmt = null;
|
|
699
725
|
this.#oldestShardsStmt = null;
|
|
@@ -701,3 +727,28 @@ export class DiskCacheManager {
|
|
|
701
727
|
}
|
|
702
728
|
}
|
|
703
729
|
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Removes legacy pre-v4 cache files from the cache directory.
|
|
733
|
+
*/
|
|
734
|
+
export async function cleanupLegacyCacheFiles(
|
|
735
|
+
customCacheDir?: string,
|
|
736
|
+
): Promise<void> {
|
|
737
|
+
const baseDir = customCacheDir ?? getDefaultCacheDir();
|
|
738
|
+
try {
|
|
739
|
+
if (!fsSync.existsSync(baseDir)) return;
|
|
740
|
+
const entries = await fs.readdir(baseDir);
|
|
741
|
+
for (const entry of entries) {
|
|
742
|
+
if (
|
|
743
|
+
entry.endsWith(".sqlite") &&
|
|
744
|
+
!entry.includes(`_v${CACHE_FORMAT_VERSION}.sqlite`)
|
|
745
|
+
) {
|
|
746
|
+
await fs.unlink(path.join(baseDir, entry)).catch(() => {});
|
|
747
|
+
await fs.unlink(path.join(baseDir, `${entry}-wal`)).catch(() => {});
|
|
748
|
+
await fs.unlink(path.join(baseDir, `${entry}-shm`)).catch(() => {});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
// Fail open
|
|
753
|
+
}
|
|
754
|
+
}
|