pi-hashline-edit-pro 0.17.1 → 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 +3 -2
- package/src/constants.ts +1 -0
- package/src/hash-store.ts +195 -117
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": {
|
|
@@ -43,7 +43,8 @@
|
|
|
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",
|
package/src/constants.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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_BUSY_TIMEOUT = 1000;
|
|
8
9
|
export const HASH_STORE_VERSION = 2;
|
|
9
10
|
export const CONTENT_LINES_NOT_STRING_MSG =
|
|
10
11
|
`[E_BAD_SHAPE] "content_lines" must be a native JSON array of strings, not a JSON string.`
|
package/src/hash-store.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import initSqlJs, { type Database as DatabaseType, type SqlValue } from "sql.js";
|
|
2
1
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
2
|
import { readFile, rename, mkdir, stat } from "fs/promises";
|
|
4
3
|
import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
|
|
5
4
|
import { errCode } from "./utils";
|
|
6
5
|
import { initHasher, contentChecksum } from "./hashline/hasher";
|
|
7
|
-
import { HASH_STORE_VERSION } from "./constants";
|
|
6
|
+
import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
|
|
7
|
+
import initSqlJs from "sql.js";
|
|
8
8
|
|
|
9
|
-
type SqlParams =
|
|
9
|
+
type SqlParams = (string | number)[];
|
|
10
10
|
|
|
11
11
|
interface Prepared {
|
|
12
|
-
get: (...params: SqlParams) => Record<string,
|
|
13
|
-
allPaths: (...params: SqlParams) => Record<string,
|
|
12
|
+
get: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
13
|
+
allPaths: (...params: SqlParams) => Record<string, unknown>[];
|
|
14
14
|
deleteOne: (...params: SqlParams) => void;
|
|
15
15
|
upsert: (...params: SqlParams) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface HashStore {
|
|
19
|
-
readonly db: DatabaseType;
|
|
20
19
|
readonly stmts: Prepared;
|
|
20
|
+
readonly engine: "better-sqlite3" | "sql.js";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface LegacySnapshot {
|
|
@@ -36,60 +36,88 @@ function isValidSnapshot(value: unknown): value is LegacySnapshot {
|
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// ── Backend handle abstraction ──────────────────────────────────────
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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); },
|
|
67
94
|
};
|
|
68
|
-
}
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
db.
|
|
76
|
-
|
|
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
|
+
},
|
|
77
104
|
};
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
83
116
|
}
|
|
84
117
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const fileBuffer = readFileSync(storePath);
|
|
89
|
-
db = new DatabaseCtor(new Uint8Array(fileBuffer));
|
|
90
|
-
} else {
|
|
91
|
-
db = new DatabaseCtor();
|
|
92
|
-
}
|
|
118
|
+
function openSqlJsDb(storePath: string): BackendHandle {
|
|
119
|
+
const data = existsSync(storePath) ? new Uint8Array(readFileSync(storePath)) : undefined;
|
|
120
|
+
const db = new SqlJsDatabase(data);
|
|
93
121
|
|
|
94
122
|
db.run(
|
|
95
123
|
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
@@ -100,33 +128,124 @@ function openDatabase(storePath: string): HashStore {
|
|
|
100
128
|
"updated_at INTEGER NOT NULL" +
|
|
101
129
|
")"
|
|
102
130
|
);
|
|
103
|
-
saveDb(storePath, db);
|
|
104
131
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const upsert = makeRun(
|
|
109
|
-
db,
|
|
110
|
-
"INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) " +
|
|
111
|
-
"ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at"
|
|
112
|
-
);
|
|
132
|
+
function save() {
|
|
133
|
+
writeFileSync(storePath, Buffer.from(db.export()));
|
|
134
|
+
}
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
};
|
|
116
183
|
}
|
|
117
184
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
export async function loadHashStore(): Promise<HashStore> {
|
|
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();
|
|
211
|
+
|
|
212
|
+
const existed = existsSync(storePath);
|
|
213
|
+
|
|
214
|
+
let handle: BackendHandle;
|
|
215
|
+
if (BetterDatabase) {
|
|
216
|
+
handle = openBetterDb(storePath);
|
|
217
|
+
} else {
|
|
218
|
+
await ensureSqlJs();
|
|
219
|
+
handle = openSqlJsDb(storePath);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!existed) {
|
|
223
|
+
await migrateLegacy(handle);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cachedHandle = { path: storePath, handle };
|
|
227
|
+
return handle.store;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function shutdownHashStore(): void {
|
|
231
|
+
if (cachedHandle) {
|
|
232
|
+
cachedHandle.handle.close();
|
|
233
|
+
cachedHandle = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
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 {
|
|
121
244
|
fn();
|
|
122
|
-
db.run("COMMIT");
|
|
123
|
-
} catch (e) {
|
|
124
|
-
db.run("ROLLBACK");
|
|
125
|
-
throw e;
|
|
126
245
|
}
|
|
127
246
|
}
|
|
128
247
|
|
|
129
|
-
async function migrateLegacy(
|
|
248
|
+
async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
130
249
|
const legacyPath = legacyHashStorePath();
|
|
131
250
|
let content: string;
|
|
132
251
|
try {
|
|
@@ -161,16 +280,13 @@ async function migrateLegacy(store: HashStore, storePath: string): Promise<void>
|
|
|
161
280
|
}
|
|
162
281
|
|
|
163
282
|
if (rows.length > 0) {
|
|
164
|
-
|
|
165
|
-
const stmt =
|
|
283
|
+
handle.transact(() => {
|
|
284
|
+
const stmt = handle.prepare(
|
|
166
285
|
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
167
286
|
);
|
|
168
|
-
for (const row of rows)
|
|
169
|
-
stmt.run(row);
|
|
170
|
-
}
|
|
287
|
+
for (const row of rows) stmt.run(...row);
|
|
171
288
|
stmt.free();
|
|
172
289
|
});
|
|
173
|
-
saveDb(storePath, store.db);
|
|
174
290
|
}
|
|
175
291
|
|
|
176
292
|
try {
|
|
@@ -180,40 +296,7 @@ async function migrateLegacy(store: HashStore, storePath: string): Promise<void>
|
|
|
180
296
|
}
|
|
181
297
|
}
|
|
182
298
|
|
|
183
|
-
|
|
184
|
-
const storePath = hashStorePath();
|
|
185
|
-
if (cachedStore && cachedStore.path === storePath) {
|
|
186
|
-
return cachedStore.store;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
shutdownHashStore();
|
|
190
|
-
|
|
191
|
-
await initHasher();
|
|
192
|
-
await mkdir(hashStoreDir(), { recursive: true });
|
|
193
|
-
|
|
194
|
-
if (!sqlJsInit) {
|
|
195
|
-
sqlJsInit = initSqlJs().then((SQL) => {
|
|
196
|
-
DatabaseCtor = SQL.Database;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
await sqlJsInit;
|
|
200
|
-
|
|
201
|
-
const existed = existsSync(storePath);
|
|
202
|
-
const store = openDatabase(storePath);
|
|
203
|
-
if (!existed) {
|
|
204
|
-
await migrateLegacy(store, storePath);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
cachedStore = { path: storePath, store, filePath: storePath };
|
|
208
|
-
return store;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function shutdownHashStore(): void {
|
|
212
|
-
if (cachedStore) {
|
|
213
|
-
cachedStore.store.db.close();
|
|
214
|
-
cachedStore = null;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
299
|
+
// ── Snapshot operations ─────────────────────────────────────────────
|
|
217
300
|
|
|
218
301
|
export function getSnapshot(
|
|
219
302
|
store: HashStore,
|
|
@@ -234,17 +317,15 @@ export function upsertSnapshot(
|
|
|
234
317
|
hashes: string[],
|
|
235
318
|
): void {
|
|
236
319
|
const hashesJson = JSON.stringify(hashes);
|
|
237
|
-
|
|
320
|
+
withStore(store, () => {
|
|
238
321
|
store.stmts.upsert(path, checksum, lineCount, hashesJson, Date.now());
|
|
239
322
|
});
|
|
240
|
-
if (cachedStore) saveDb(cachedStore.filePath, store.db);
|
|
241
323
|
}
|
|
242
324
|
|
|
243
325
|
export function deleteSnapshot(store: HashStore, path: string): void {
|
|
244
|
-
|
|
326
|
+
withStore(store, () => {
|
|
245
327
|
store.stmts.deleteOne(path);
|
|
246
328
|
});
|
|
247
|
-
if (cachedStore) saveDb(cachedStore.filePath, store.db);
|
|
248
329
|
}
|
|
249
330
|
|
|
250
331
|
export async function pruneMissing(store: HashStore): Promise<void> {
|
|
@@ -258,12 +339,9 @@ export async function pruneMissing(store: HashStore): Promise<void> {
|
|
|
258
339
|
}
|
|
259
340
|
}
|
|
260
341
|
if (missing.length === 0) return;
|
|
261
|
-
|
|
262
|
-
for (const path of missing)
|
|
263
|
-
store.stmts.deleteOne(path);
|
|
264
|
-
}
|
|
342
|
+
withStore(store, () => {
|
|
343
|
+
for (const path of missing) store.stmts.deleteOne(path);
|
|
265
344
|
});
|
|
266
|
-
if (cachedStore) saveDb(cachedStore.filePath, store.db);
|
|
267
345
|
}
|
|
268
346
|
|
|
269
347
|
export { HASH_STORE_VERSION };
|