pi-hashline-edit-pro 0.17.0 → 0.17.2
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/package.json +5 -7
- package/src/constants.ts +1 -2
- package/src/hash-store.ts +308 -152
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"better-sqlite3": "^13.0.1",
|
|
35
34
|
"diff": "^8.0.2",
|
|
36
35
|
"file-type": "^21.3.0",
|
|
36
|
+
"sql.js": "1.11",
|
|
37
37
|
"xxhash-wasm": "^1.1.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -43,16 +43,14 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:watch": "vitest",
|
|
46
|
-
"typecheck": "tsc --noEmit"
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"postinstall": "node scripts/ensure-better-sqlite3.js"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
50
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
51
51
|
"@types/node": "^22.0.0",
|
|
52
|
+
"@types/sql.js": "^1.4.11",
|
|
52
53
|
"typescript": "^5.8.0",
|
|
53
54
|
"vitest": "^4.1.8"
|
|
54
|
-
},
|
|
55
|
-
"allowScripts": {
|
|
56
|
-
"better-sqlite3@13.0.1": true
|
|
57
55
|
}
|
|
58
56
|
}
|
package/src/constants.ts
CHANGED
|
@@ -5,9 +5,8 @@ export const MAX_BYTES = 100 * 1024 * 1024;
|
|
|
5
5
|
export const MAX_HASH_LINES = 1_000_000;
|
|
6
6
|
export const MAX_HASH_RETRIES = 262_144;
|
|
7
7
|
|
|
8
|
-
export const HASH_STORE_VERSION = 2;
|
|
9
8
|
export const HASH_STORE_BUSY_TIMEOUT = 1000;
|
|
10
|
-
|
|
9
|
+
export const HASH_STORE_VERSION = 2;
|
|
11
10
|
export const CONTENT_LINES_NOT_STRING_MSG =
|
|
12
11
|
`[E_BAD_SHAPE] "content_lines" must be a native JSON array of strings, not a JSON string.`
|
|
13
12
|
+ ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`;
|
package/src/hash-store.ts
CHANGED
|
@@ -1,191 +1,347 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { Database as DatabaseType, Statement } from "better-sqlite3";
|
|
3
|
-
import { existsSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
2
|
import { readFile, rename, mkdir, stat } from "fs/promises";
|
|
5
3
|
import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
|
|
6
4
|
import { errCode } from "./utils";
|
|
7
5
|
import { initHasher, contentChecksum } from "./hashline/hasher";
|
|
8
6
|
import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
|
|
7
|
+
import initSqlJs from "sql.js";
|
|
8
|
+
|
|
9
|
+
type SqlParams = (string | number)[];
|
|
9
10
|
|
|
10
11
|
interface Prepared {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
get: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
13
|
+
allPaths: (...params: SqlParams) => Record<string, unknown>[];
|
|
14
|
+
deleteOne: (...params: SqlParams) => void;
|
|
15
|
+
upsert: (...params: SqlParams) => void;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface HashStore {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
readonly stmts: Prepared;
|
|
20
|
+
readonly engine: "better-sqlite3" | "sql.js";
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface LegacySnapshot {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
content: string;
|
|
25
|
+
hashes: string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function isValidSnapshot(value: unknown): value is LegacySnapshot {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
29
|
+
if (typeof value !== "object" || value === null) return false;
|
|
30
|
+
const v = value as Record<string, unknown>;
|
|
31
|
+
if (typeof v.content !== "string") return false;
|
|
32
|
+
if (!Array.isArray(v.hashes)) return false;
|
|
33
|
+
for (const h of v.hashes) {
|
|
34
|
+
if (typeof h !== "string") return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Backend handle abstraction ──────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
interface BackendHandle {
|
|
42
|
+
store: HashStore;
|
|
43
|
+
close: () => void;
|
|
44
|
+
transact: (fn: () => void) => void;
|
|
45
|
+
prepare: (sql: string) => { run: (...params: unknown[]) => void; free: () => void };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let cachedHandle: { path: string; handle: BackendHandle } | null = null;
|
|
49
|
+
|
|
50
|
+
// ── better-sqlite3 backend ──────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
let BetterDatabase: any = undefined;
|
|
53
|
+
|
|
54
|
+
async function tryLoadBetter(): Promise<boolean> {
|
|
55
|
+
if (BetterDatabase !== undefined) return BetterDatabase !== null;
|
|
56
|
+
try {
|
|
57
|
+
const mod = await import("better-sqlite3");
|
|
58
|
+
BetterDatabase = mod.default || mod;
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
BetterDatabase = null;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function openBetterDb(storePath: string): BackendHandle {
|
|
67
|
+
const db = new BetterDatabase(storePath);
|
|
68
|
+
db.pragma("journal_mode = WAL");
|
|
69
|
+
db.pragma("synchronous = NORMAL");
|
|
70
|
+
db.pragma(`busy_timeout = ${HASH_STORE_BUSY_TIMEOUT}`);
|
|
71
|
+
db.exec(
|
|
72
|
+
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
73
|
+
"path TEXT PRIMARY KEY, " +
|
|
74
|
+
"checksum TEXT NOT NULL, " +
|
|
75
|
+
"line_count INTEGER NOT NULL, " +
|
|
76
|
+
"hashes TEXT NOT NULL, " +
|
|
77
|
+
"updated_at INTEGER NOT NULL" +
|
|
78
|
+
")"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
|
|
82
|
+
const allStmt = db.prepare("SELECT path FROM snapshots");
|
|
83
|
+
const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
|
|
84
|
+
const upsertStmt = db.prepare(
|
|
85
|
+
"INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) " +
|
|
86
|
+
"ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at"
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const stmts: Prepared = {
|
|
90
|
+
get: (...params) => getStmt.get(...params) as Record<string, unknown> | undefined,
|
|
91
|
+
allPaths: (...params) => allStmt.all(...params) as Record<string, unknown>[],
|
|
92
|
+
deleteOne: (...params) => { delStmt.run(...params); },
|
|
93
|
+
upsert: (...params) => { upsertStmt.run(...params); },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
store: { stmts, engine: "better-sqlite3" },
|
|
98
|
+
close: () => { db.close(); },
|
|
99
|
+
transact: (fn) => { db.transaction(fn).immediate(); },
|
|
100
|
+
prepare: (sql) => {
|
|
101
|
+
const stmt = db.prepare(sql);
|
|
102
|
+
return { run: (...params) => stmt.run(...params), free: () => {} };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── sql.js backend ──────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
let SqlJsDatabase: any = null;
|
|
110
|
+
let sqlJsPromise: Promise<void> | null = null;
|
|
111
|
+
|
|
112
|
+
async function ensureSqlJs(): Promise<void> {
|
|
113
|
+
if (sqlJsPromise) return sqlJsPromise;
|
|
114
|
+
sqlJsPromise = initSqlJs().then((SQL) => { SqlJsDatabase = SQL.Database; });
|
|
115
|
+
return sqlJsPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function openSqlJsDb(storePath: string): BackendHandle {
|
|
119
|
+
const data = existsSync(storePath) ? new Uint8Array(readFileSync(storePath)) : undefined;
|
|
120
|
+
const db = new SqlJsDatabase(data);
|
|
121
|
+
|
|
122
|
+
db.run(
|
|
123
|
+
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
124
|
+
"path TEXT PRIMARY KEY, " +
|
|
125
|
+
"checksum TEXT NOT NULL, " +
|
|
126
|
+
"line_count INTEGER NOT NULL, " +
|
|
127
|
+
"hashes TEXT NOT NULL, " +
|
|
128
|
+
"updated_at INTEGER NOT NULL" +
|
|
129
|
+
")"
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
function save() {
|
|
133
|
+
writeFileSync(storePath, Buffer.from(db.export()));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
save();
|
|
137
|
+
|
|
138
|
+
const stmts: Prepared = {
|
|
139
|
+
get: (...params) => {
|
|
140
|
+
const stmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
|
|
141
|
+
stmt.bind(params);
|
|
142
|
+
let result: Record<string, unknown> | undefined;
|
|
143
|
+
if (stmt.step()) result = stmt.getAsObject() as Record<string, unknown>;
|
|
144
|
+
stmt.free();
|
|
145
|
+
return result;
|
|
146
|
+
},
|
|
147
|
+
allPaths: (...params) => {
|
|
148
|
+
const stmt = db.prepare("SELECT path FROM snapshots");
|
|
149
|
+
if (params.length > 0) stmt.bind(params);
|
|
150
|
+
const results: Record<string, unknown>[] = [];
|
|
151
|
+
while (stmt.step()) results.push(stmt.getAsObject() as Record<string, unknown>);
|
|
152
|
+
stmt.free();
|
|
153
|
+
return results;
|
|
154
|
+
},
|
|
155
|
+
deleteOne: (...params) => {
|
|
156
|
+
if (params.length > 0) db.run("DELETE FROM snapshots WHERE path = ?", params);
|
|
157
|
+
else db.run("DELETE FROM snapshots WHERE path = ?");
|
|
158
|
+
},
|
|
159
|
+
upsert: (...params) => {
|
|
160
|
+
db.run(
|
|
161
|
+
"INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) " +
|
|
162
|
+
"ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at",
|
|
163
|
+
params
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
store: { stmts, engine: "sql.js" },
|
|
170
|
+
close: () => { db.close(); },
|
|
171
|
+
transact: (fn) => {
|
|
172
|
+
db.run("BEGIN IMMEDIATE");
|
|
173
|
+
try { fn(); db.run("COMMIT"); save(); } catch (e) { db.run("ROLLBACK"); throw e; }
|
|
174
|
+
},
|
|
175
|
+
prepare: (sql) => {
|
|
176
|
+
const stmt = db.prepare(sql);
|
|
177
|
+
return {
|
|
178
|
+
run: (...params) => { stmt.bind(params); stmt.step(); stmt.reset(); },
|
|
179
|
+
free: () => { stmt.free(); },
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Init ────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
let backendPromise: Promise<void> | null = null;
|
|
188
|
+
|
|
189
|
+
async function initBackend(): Promise<void> {
|
|
190
|
+
if (backendPromise) return backendPromise;
|
|
191
|
+
backendPromise = (async () => {
|
|
192
|
+
const hasBetter = await tryLoadBetter();
|
|
193
|
+
if (!hasBetter) await ensureSqlJs();
|
|
194
|
+
})();
|
|
195
|
+
return backendPromise;
|
|
116
196
|
}
|
|
117
197
|
|
|
198
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
199
|
+
|
|
118
200
|
export async function loadHashStore(): Promise<HashStore> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
201
|
+
const storePath = hashStorePath();
|
|
202
|
+
if (cachedHandle && cachedHandle.path === storePath) {
|
|
203
|
+
return cachedHandle.handle.store;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
shutdownHashStore();
|
|
207
|
+
|
|
208
|
+
await initHasher();
|
|
209
|
+
await mkdir(hashStoreDir(), { recursive: true });
|
|
210
|
+
await initBackend();
|
|
123
211
|
|
|
124
|
-
|
|
212
|
+
const existed = existsSync(storePath);
|
|
125
213
|
|
|
126
|
-
|
|
127
|
-
|
|
214
|
+
let handle: BackendHandle;
|
|
215
|
+
if (BetterDatabase) {
|
|
216
|
+
handle = openBetterDb(storePath);
|
|
217
|
+
} else {
|
|
218
|
+
await ensureSqlJs();
|
|
219
|
+
handle = openSqlJsDb(storePath);
|
|
220
|
+
}
|
|
128
221
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return store;
|
|
222
|
+
if (!existed) {
|
|
223
|
+
await migrateLegacy(handle);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cachedHandle = { path: storePath, handle };
|
|
227
|
+
return handle.store;
|
|
136
228
|
}
|
|
137
229
|
|
|
138
230
|
export function shutdownHashStore(): void {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
231
|
+
if (cachedHandle) {
|
|
232
|
+
cachedHandle.handle.close();
|
|
233
|
+
cachedHandle = null;
|
|
234
|
+
}
|
|
143
235
|
}
|
|
144
236
|
|
|
237
|
+
// ── Internal helpers ────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function withStore(store: HashStore, fn: () => void): void {
|
|
240
|
+
const h = cachedHandle?.handle;
|
|
241
|
+
if (h && h.store === store) {
|
|
242
|
+
h.transact(fn);
|
|
243
|
+
} else {
|
|
244
|
+
fn();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
249
|
+
const legacyPath = legacyHashStorePath();
|
|
250
|
+
let content: string;
|
|
251
|
+
try {
|
|
252
|
+
content = await readFile(legacyPath, "utf-8");
|
|
253
|
+
} catch (error: unknown) {
|
|
254
|
+
if (errCode(error) === "ENOENT") return;
|
|
255
|
+
console.error("Failed to read legacy hash store for migration:", error);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let parsed: { snapshots?: Record<string, unknown> };
|
|
260
|
+
try {
|
|
261
|
+
parsed = JSON.parse(content) as typeof parsed;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error("Failed to parse legacy hash store, skipping migration:", error);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const raw = parsed.snapshots;
|
|
268
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
269
|
+
|
|
270
|
+
const rows: [string, string, number, string, number][] = [];
|
|
271
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
272
|
+
if (!isValidSnapshot(value)) continue;
|
|
273
|
+
rows.push([
|
|
274
|
+
key,
|
|
275
|
+
contentChecksum(value.content),
|
|
276
|
+
value.content.split("\n").length,
|
|
277
|
+
JSON.stringify(value.hashes),
|
|
278
|
+
Date.now(),
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (rows.length > 0) {
|
|
283
|
+
handle.transact(() => {
|
|
284
|
+
const stmt = handle.prepare(
|
|
285
|
+
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
286
|
+
);
|
|
287
|
+
for (const row of rows) stmt.run(...row);
|
|
288
|
+
stmt.free();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await rename(legacyPath, `${legacyPath}.bak`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("Failed to rename legacy hash store after migration:", error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Snapshot operations ─────────────────────────────────────────────
|
|
300
|
+
|
|
145
301
|
export function getSnapshot(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
302
|
+
store: HashStore,
|
|
303
|
+
path: string,
|
|
304
|
+
content: string,
|
|
149
305
|
): string[] | undefined {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
306
|
+
const checksum = contentChecksum(content);
|
|
307
|
+
const lineCount = content.split("\n").length;
|
|
308
|
+
const row = store.stmts.get(path, checksum, lineCount);
|
|
309
|
+
return row ? (JSON.parse(row.hashes as string) as string[]) : undefined;
|
|
154
310
|
}
|
|
155
311
|
|
|
156
312
|
export function upsertSnapshot(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
313
|
+
store: HashStore,
|
|
314
|
+
path: string,
|
|
315
|
+
checksum: string,
|
|
316
|
+
lineCount: number,
|
|
317
|
+
hashes: string[],
|
|
162
318
|
): void {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
319
|
+
const hashesJson = JSON.stringify(hashes);
|
|
320
|
+
withStore(store, () => {
|
|
321
|
+
store.stmts.upsert(path, checksum, lineCount, hashesJson, Date.now());
|
|
322
|
+
});
|
|
167
323
|
}
|
|
168
324
|
|
|
169
325
|
export function deleteSnapshot(store: HashStore, path: string): void {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
326
|
+
withStore(store, () => {
|
|
327
|
+
store.stmts.deleteOne(path);
|
|
328
|
+
});
|
|
173
329
|
}
|
|
174
330
|
|
|
175
331
|
export async function pruneMissing(store: HashStore): Promise<void> {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export { HASH_STORE_VERSION };
|
|
332
|
+
const rows = store.stmts.allPaths() as { path: string }[];
|
|
333
|
+
const missing: string[] = [];
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
try {
|
|
336
|
+
await stat(row.path);
|
|
337
|
+
} catch {
|
|
338
|
+
missing.push(row.path);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (missing.length === 0) return;
|
|
342
|
+
withStore(store, () => {
|
|
343
|
+
for (const path of missing) store.stmts.deleteOne(path);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { HASH_STORE_VERSION };
|