omp-plugin-duplicate-detector 0.1.1 → 0.2.1
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 +1 -1
- package/dist/detector-worker.js +507 -276
- package/package.json +1 -1
- package/src/coordinator.ts +7 -3
- package/src/detector-worker.ts +156 -28
- package/src/disk-cache.ts +383 -314
- package/src/index.ts +134 -7
- package/src/project-state.ts +10 -4
- package/src/repo-context.ts +167 -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
|
+
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,8 +388,8 @@ 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;
|
|
392
|
+
#deleteByRelPathStmt: Statement | null = null;
|
|
415
393
|
#totalSizeStmt: Statement | null = null;
|
|
416
394
|
#oldestShardsStmt: Statement | null = null;
|
|
417
395
|
#deleteAllStmt: Statement | null = null;
|
|
@@ -419,13 +397,20 @@ export class DiskCacheManager {
|
|
|
419
397
|
|
|
420
398
|
constructor(options: DiskCacheOptions) {
|
|
421
399
|
this.rootDir = path.resolve(options.rootDir);
|
|
400
|
+
this.repositoryKey =
|
|
401
|
+
options.repositoryKey ??
|
|
402
|
+
crypto
|
|
403
|
+
.createHash("sha256")
|
|
404
|
+
.update(this.rootDir)
|
|
405
|
+
.digest("hex")
|
|
406
|
+
.slice(0, 16);
|
|
422
407
|
this.baseCacheDir = options.cacheDir
|
|
423
408
|
? path.resolve(options.cacheDir)
|
|
424
409
|
: getDefaultCacheDir();
|
|
425
410
|
this.configFingerprint = computeConfigFingerprint(options.config);
|
|
426
411
|
this.dbPath = computeWorkspaceCachePath(
|
|
427
412
|
this.baseCacheDir,
|
|
428
|
-
this.
|
|
413
|
+
this.repositoryKey,
|
|
429
414
|
this.configFingerprint,
|
|
430
415
|
);
|
|
431
416
|
this.workspaceCacheDir = this.baseCacheDir;
|
|
@@ -436,64 +421,78 @@ export class DiskCacheManager {
|
|
|
436
421
|
if (this.#closed) return null;
|
|
437
422
|
if (this.#db) return this.#db;
|
|
438
423
|
|
|
424
|
+
let db: Database | null = null;
|
|
439
425
|
try {
|
|
440
426
|
const dir = path.dirname(this.dbPath);
|
|
441
427
|
if (!fsSync.existsSync(dir)) {
|
|
442
428
|
fsSync.mkdirSync(dir, { recursive: true });
|
|
443
429
|
}
|
|
444
430
|
|
|
445
|
-
|
|
446
|
-
db.exec("PRAGMA
|
|
431
|
+
db = new Database(this.dbPath, { create: true });
|
|
432
|
+
db.exec("PRAGMA busy_timeout = 2000;");
|
|
433
|
+
const journalRow = db.query("PRAGMA journal_mode = WAL;").get() as
|
|
434
|
+
| { journal_mode?: string }
|
|
435
|
+
| undefined;
|
|
436
|
+
if (journalRow?.journal_mode?.toLowerCase() !== "wal") {
|
|
437
|
+
try {
|
|
438
|
+
db.close();
|
|
439
|
+
} catch {}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
447
442
|
db.exec("PRAGMA synchronous = NORMAL;");
|
|
448
443
|
db.exec("PRAGMA temp_store = MEMORY;");
|
|
449
|
-
|
|
450
|
-
// Clear cache and reset schema on version difference
|
|
451
444
|
const versionRow = db.query("PRAGMA user_version;").get() as
|
|
452
445
|
| { user_version: number }
|
|
453
446
|
| undefined;
|
|
454
447
|
const schemaVersion = versionRow?.user_version ?? 0;
|
|
455
448
|
if (schemaVersion !== CACHE_FORMAT_VERSION) {
|
|
456
|
-
|
|
449
|
+
try {
|
|
450
|
+
db.exec("DELETE FROM shards;");
|
|
451
|
+
} catch {}
|
|
457
452
|
db.exec(`PRAGMA user_version = ${CACHE_FORMAT_VERSION};`);
|
|
458
453
|
}
|
|
459
454
|
|
|
460
455
|
db.exec(`
|
|
461
456
|
CREATE TABLE IF NOT EXISTS shards (
|
|
462
|
-
rel_path TEXT NOT NULL
|
|
457
|
+
rel_path TEXT NOT NULL,
|
|
463
458
|
content_hash TEXT NOT NULL,
|
|
464
459
|
payload BLOB NOT NULL,
|
|
465
|
-
mtime REAL NOT NULL
|
|
460
|
+
mtime REAL NOT NULL,
|
|
461
|
+
PRIMARY KEY (rel_path, content_hash)
|
|
466
462
|
);
|
|
467
|
-
CREATE INDEX IF NOT EXISTS idx_shards_content_hash ON shards(content_hash);
|
|
468
463
|
CREATE INDEX IF NOT EXISTS idx_shards_mtime ON shards(mtime);
|
|
469
464
|
`);
|
|
470
|
-
|
|
471
465
|
this.#getStmt = db.prepare(
|
|
472
|
-
"SELECT payload
|
|
466
|
+
"SELECT payload FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
|
|
473
467
|
);
|
|
474
468
|
this.#saveStmt = db.prepare(`
|
|
475
469
|
INSERT INTO shards (rel_path, content_hash, payload, mtime)
|
|
476
470
|
VALUES (?1, ?2, ?3, ?4)
|
|
477
|
-
ON CONFLICT(rel_path) DO UPDATE SET
|
|
478
|
-
content_hash = excluded.content_hash,
|
|
479
|
-
payload = excluded.payload,
|
|
471
|
+
ON CONFLICT(rel_path, content_hash) DO UPDATE SET
|
|
480
472
|
mtime = excluded.mtime
|
|
481
473
|
`);
|
|
482
|
-
this.#
|
|
483
|
-
"
|
|
474
|
+
this.#deleteStmt = db.prepare(
|
|
475
|
+
"DELETE FROM shards WHERE rel_path = ?1 AND content_hash = ?2",
|
|
476
|
+
);
|
|
477
|
+
this.#deleteByRelPathStmt = db.prepare(
|
|
478
|
+
"DELETE FROM shards WHERE rel_path = ?1",
|
|
484
479
|
);
|
|
485
|
-
this.#deleteStmt = db.prepare("DELETE FROM shards WHERE rel_path = ?1");
|
|
486
480
|
this.#totalSizeStmt = db.prepare(
|
|
487
481
|
"SELECT COALESCE(SUM(LENGTH(payload)), 0) as total FROM shards",
|
|
488
482
|
);
|
|
489
483
|
this.#oldestShardsStmt = db.prepare(
|
|
490
|
-
"SELECT
|
|
484
|
+
"SELECT rowid, LENGTH(payload) as size FROM shards ORDER BY mtime ASC LIMIT ?1",
|
|
491
485
|
);
|
|
492
486
|
this.#deleteAllStmt = db.prepare("DELETE FROM shards");
|
|
493
487
|
|
|
494
488
|
this.#db = db;
|
|
495
489
|
return db;
|
|
496
490
|
} catch {
|
|
491
|
+
if (db) {
|
|
492
|
+
try {
|
|
493
|
+
db.close();
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
497
496
|
// Fail open on SQLite creation or permission errors
|
|
498
497
|
return null;
|
|
499
498
|
}
|
|
@@ -501,6 +500,7 @@ export class DiskCacheManager {
|
|
|
501
500
|
|
|
502
501
|
/**
|
|
503
502
|
* Retrieves a serialized shard from the SQLite cache if present and valid.
|
|
503
|
+
* Pure read-only operation: does not issue write transactions on cache hits.
|
|
504
504
|
* Returns null on cache miss or corrupted/invalid shard (fails open).
|
|
505
505
|
*/
|
|
506
506
|
async getShard(
|
|
@@ -512,15 +512,14 @@ export class DiskCacheManager {
|
|
|
512
512
|
if (!db || !this.#getStmt) return null;
|
|
513
513
|
|
|
514
514
|
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
515
|
-
const row = this.#getStmt.get(normalizedRelPath) as
|
|
515
|
+
const row = this.#getStmt.get(normalizedRelPath, contentHash) as
|
|
516
516
|
| {
|
|
517
517
|
payload: Uint8Array | Buffer;
|
|
518
|
-
content_hash: string;
|
|
519
518
|
}
|
|
520
519
|
| null
|
|
521
520
|
| undefined;
|
|
522
521
|
|
|
523
|
-
if (!row
|
|
522
|
+
if (!row) {
|
|
524
523
|
return null;
|
|
525
524
|
}
|
|
526
525
|
|
|
@@ -536,24 +535,11 @@ export class DiskCacheManager {
|
|
|
536
535
|
if (
|
|
537
536
|
shard &&
|
|
538
537
|
shard.contentHash === contentHash &&
|
|
539
|
-
typeof shard.sourceId === "string" &&
|
|
540
538
|
Array.isArray(shard.frames)
|
|
541
539
|
) {
|
|
542
|
-
try {
|
|
543
|
-
this.#updateMtimeStmt?.run(Date.now(), normalizedRelPath);
|
|
544
|
-
} catch {
|
|
545
|
-
// Non-fatal
|
|
546
|
-
}
|
|
547
540
|
return shard;
|
|
548
541
|
}
|
|
549
542
|
|
|
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
543
|
return null;
|
|
558
544
|
} catch {
|
|
559
545
|
// Fail open on any error
|
|
@@ -592,9 +578,84 @@ export class DiskCacheManager {
|
|
|
592
578
|
// Fail open: cache write failures should not disrupt indexing
|
|
593
579
|
}
|
|
594
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Deletes a cached shard by relPath and contentHash if present.
|
|
583
|
+
*/
|
|
584
|
+
async deleteShard(relPath: string, contentHash: string): Promise<void> {
|
|
585
|
+
try {
|
|
586
|
+
const db = this.#getDb();
|
|
587
|
+
if (!db || !this.#deleteStmt) return;
|
|
588
|
+
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
589
|
+
this.#deleteStmt.run(normalizedRelPath, contentHash);
|
|
590
|
+
} catch {
|
|
591
|
+
// Fail open
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Deletes all cached shards for a given relPath regardless of contentHash.
|
|
596
|
+
*/
|
|
597
|
+
async deleteByRelPath(relPath: string): Promise<void> {
|
|
598
|
+
try {
|
|
599
|
+
const db = this.#getDb();
|
|
600
|
+
if (!db || !this.#deleteByRelPathStmt) return;
|
|
601
|
+
const normalizedRelPath = relPath.replace(/\\/g, "/");
|
|
602
|
+
this.#deleteByRelPathStmt.run(normalizedRelPath);
|
|
603
|
+
} catch {
|
|
604
|
+
// Fail open
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Atomically saves a batch of pre-tokenized shards inside a single SQLite transaction.
|
|
610
|
+
*/
|
|
611
|
+
async saveShards(
|
|
612
|
+
items: Array<{ shard: SerializedSourceShard; relPath?: string }>,
|
|
613
|
+
): Promise<void> {
|
|
614
|
+
if (items.length === 0) return;
|
|
615
|
+
try {
|
|
616
|
+
const db = this.#getDb();
|
|
617
|
+
if (!db || !this.#saveStmt) return;
|
|
618
|
+
|
|
619
|
+
const stmt = this.#saveStmt;
|
|
620
|
+
const root = this.rootDir;
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
|
|
623
|
+
// Pre-pack payloads outside transaction to minimize write lock time
|
|
624
|
+
const prepared: Array<{
|
|
625
|
+
relPath: string;
|
|
626
|
+
hash: string;
|
|
627
|
+
payload: Buffer;
|
|
628
|
+
}> = [];
|
|
629
|
+
for (const item of items) {
|
|
630
|
+
const targetRelPath =
|
|
631
|
+
item.relPath ??
|
|
632
|
+
(path.isAbsolute(item.shard.sourceId)
|
|
633
|
+
? path.relative(root, item.shard.sourceId)
|
|
634
|
+
: item.shard.sourceId);
|
|
635
|
+
const normalizedRelPath = targetRelPath.replace(/\\/g, "/");
|
|
636
|
+
const payload = packBinaryShard(item.shard);
|
|
637
|
+
prepared.push({
|
|
638
|
+
relPath: normalizedRelPath,
|
|
639
|
+
hash: item.shard.contentHash,
|
|
640
|
+
payload,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const tx = db.transaction((entries: typeof prepared) => {
|
|
645
|
+
for (const entry of entries) {
|
|
646
|
+
stmt.run(entry.relPath, entry.hash, entry.payload, now);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
tx(prepared);
|
|
651
|
+
} catch {
|
|
652
|
+
// Fail open
|
|
653
|
+
}
|
|
654
|
+
}
|
|
595
655
|
|
|
596
656
|
/**
|
|
597
|
-
* Prunes
|
|
657
|
+
* Prunes oldest shards if total payload size exceeds budget using atomic windowed DELETE.
|
|
658
|
+
* Avoids concurrent VACUUM calls during active sessions.
|
|
598
659
|
*/
|
|
599
660
|
async prune(maxBytes?: number): Promise<void> {
|
|
600
661
|
const budget = maxBytes !== undefined ? maxBytes : this.maxBytes;
|
|
@@ -605,48 +666,44 @@ export class DiskCacheManager {
|
|
|
605
666
|
|
|
606
667
|
if (budget <= 0) {
|
|
607
668
|
this.#deleteAllStmt?.run();
|
|
608
|
-
try {
|
|
609
|
-
db.exec("VACUUM;");
|
|
610
|
-
} catch {
|
|
611
|
-
// Ignore vacuum errors
|
|
612
|
-
}
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const totalRow = this.#totalSizeStmt?.get() as
|
|
617
|
-
| { total: number }
|
|
618
|
-
| null
|
|
619
|
-
| undefined;
|
|
620
|
-
let totalSize = totalRow?.total ?? 0;
|
|
621
|
-
|
|
622
|
-
if (totalSize <= budget) {
|
|
623
669
|
return;
|
|
624
670
|
}
|
|
625
671
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
672
|
+
// Iteratively prune oldest shards in bounded windows
|
|
673
|
+
for (let iter = 0; iter < 10; iter++) {
|
|
674
|
+
const totalRow = this.#totalSizeStmt?.get() as
|
|
675
|
+
| { total: number }
|
|
676
|
+
| null
|
|
677
|
+
| undefined;
|
|
678
|
+
const totalSize = totalRow?.total ?? 0;
|
|
630
679
|
|
|
631
|
-
let deletedAny = false;
|
|
632
|
-
for (const entry of oldestShards) {
|
|
633
680
|
if (totalSize <= budget) {
|
|
634
681
|
break;
|
|
635
682
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
683
|
+
|
|
684
|
+
const excess = totalSize - budget;
|
|
685
|
+
const rows = (this.#oldestShardsStmt?.all(100) ?? []) as Array<{
|
|
686
|
+
rowid: number;
|
|
687
|
+
size: number;
|
|
688
|
+
}>;
|
|
689
|
+
|
|
690
|
+
if (rows.length === 0) break;
|
|
691
|
+
|
|
692
|
+
const rowidsToDelete: number[] = [];
|
|
693
|
+
let freed = 0;
|
|
694
|
+
for (const r of rows) {
|
|
695
|
+
rowidsToDelete.push(r.rowid);
|
|
696
|
+
freed += r.size;
|
|
697
|
+
if (freed >= excess) break;
|
|
642
698
|
}
|
|
643
|
-
}
|
|
644
699
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
700
|
+
if (rowidsToDelete.length > 0) {
|
|
701
|
+
const deleteBatchStmt = db.prepare(
|
|
702
|
+
`DELETE FROM shards WHERE rowid IN (${rowidsToDelete.join(",")})`,
|
|
703
|
+
);
|
|
704
|
+
deleteBatchStmt.run();
|
|
705
|
+
} else {
|
|
706
|
+
break;
|
|
650
707
|
}
|
|
651
708
|
}
|
|
652
709
|
} catch {
|
|
@@ -655,27 +712,14 @@ export class DiskCacheManager {
|
|
|
655
712
|
}
|
|
656
713
|
|
|
657
714
|
/**
|
|
658
|
-
* Clears all cached shards in the current workspace cache database.
|
|
715
|
+
* Clears all cached shards in the current workspace cache database non-destructively.
|
|
659
716
|
*/
|
|
660
717
|
async clear(): Promise<void> {
|
|
661
718
|
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;
|
|
719
|
+
const db = this.#getDb();
|
|
720
|
+
if (db && this.#deleteAllStmt) {
|
|
721
|
+
this.#deleteAllStmt.run();
|
|
674
722
|
}
|
|
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
723
|
} catch {
|
|
680
724
|
// Fail open
|
|
681
725
|
}
|
|
@@ -693,11 +737,36 @@ export class DiskCacheManager {
|
|
|
693
737
|
this.#db = null;
|
|
694
738
|
this.#getStmt = null;
|
|
695
739
|
this.#saveStmt = null;
|
|
696
|
-
this.#updateMtimeStmt = null;
|
|
697
740
|
this.#deleteStmt = null;
|
|
741
|
+
this.#deleteByRelPathStmt = null;
|
|
698
742
|
this.#totalSizeStmt = null;
|
|
699
743
|
this.#oldestShardsStmt = null;
|
|
700
744
|
this.#deleteAllStmt = null;
|
|
701
745
|
}
|
|
702
746
|
}
|
|
703
747
|
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Removes legacy pre-v4 cache files from the cache directory.
|
|
751
|
+
*/
|
|
752
|
+
export async function cleanupLegacyCacheFiles(
|
|
753
|
+
customCacheDir?: string,
|
|
754
|
+
): Promise<void> {
|
|
755
|
+
const baseDir = customCacheDir ?? getDefaultCacheDir();
|
|
756
|
+
try {
|
|
757
|
+
if (!fsSync.existsSync(baseDir)) return;
|
|
758
|
+
const entries = await fs.readdir(baseDir);
|
|
759
|
+
for (const entry of entries) {
|
|
760
|
+
if (
|
|
761
|
+
entry.endsWith(".sqlite") &&
|
|
762
|
+
!entry.includes(`_v${CACHE_FORMAT_VERSION}.sqlite`)
|
|
763
|
+
) {
|
|
764
|
+
await fs.unlink(path.join(baseDir, entry)).catch(() => {});
|
|
765
|
+
await fs.unlink(path.join(baseDir, `${entry}-wal`)).catch(() => {});
|
|
766
|
+
await fs.unlink(path.join(baseDir, `${entry}-shm`)).catch(() => {});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
// Fail open
|
|
771
|
+
}
|
|
772
|
+
}
|