sensemaking 0.24.4 → 0.24.6
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 +16 -12
- package/dist/cjs/cli/status.js +14 -24
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/commands/search.js +22 -0
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/lib/watch-notification.d.cts +1 -0
- package/dist/cjs/lib/watch-notification.d.ts +1 -0
- package/dist/cjs/lib/watch-notification.js +15 -0
- package/dist/cjs/lib/watch-notification.js.map +1 -0
- package/dist/cjs/lib/worker-file.d.cts +1 -0
- package/dist/cjs/lib/worker-file.d.ts +1 -0
- package/dist/cjs/lib/worker-file.js +34 -0
- package/dist/cjs/lib/worker-file.js.map +1 -0
- package/dist/cjs/output/search-error.js +38 -0
- package/dist/cjs/output/search-error.js.map +1 -1
- package/dist/cjs/scan/pool.js +2 -23
- package/dist/cjs/scan/pool.js.map +1 -1
- package/dist/cjs/store/builder.d.cts +2 -1
- package/dist/cjs/store/builder.d.ts +2 -1
- package/dist/cjs/store/builder.js +4 -4
- package/dist/cjs/store/builder.js.map +1 -1
- package/dist/cjs/store/duckdb/lexical.d.cts +1 -0
- package/dist/cjs/store/duckdb/lexical.d.ts +1 -0
- package/dist/cjs/store/duckdb/lexical.js +28 -9
- package/dist/cjs/store/duckdb/lexical.js.map +1 -1
- package/dist/cjs/store/duckdb/native.d.cts +2 -0
- package/dist/cjs/store/duckdb/native.d.ts +2 -0
- package/dist/cjs/store/duckdb/native.js +11 -1
- package/dist/cjs/store/duckdb/native.js.map +1 -1
- package/dist/cjs/store/duckdb/ordered-bm25.d.cts +17 -0
- package/dist/cjs/store/duckdb/ordered-bm25.d.ts +17 -0
- package/dist/cjs/store/duckdb/ordered-bm25.js +238 -0
- package/dist/cjs/store/duckdb/ordered-bm25.js.map +1 -0
- package/dist/cjs/store/lock-wait.d.cts +2 -2
- package/dist/cjs/store/lock-wait.d.ts +2 -2
- package/dist/cjs/store/lock-wait.js +14 -11
- package/dist/cjs/store/lock-wait.js.map +1 -1
- package/dist/cjs/store/native.d.cts +4 -0
- package/dist/cjs/store/native.d.ts +4 -0
- package/dist/cjs/store/native.js +64 -9
- package/dist/cjs/store/native.js.map +1 -1
- package/dist/cjs/store/open.js +11 -7
- package/dist/cjs/store/open.js.map +1 -1
- package/dist/cjs/store/reconcile.d.cts +5 -1
- package/dist/cjs/store/reconcile.d.ts +5 -1
- package/dist/cjs/store/reconcile.js +5 -4
- package/dist/cjs/store/reconcile.js.map +1 -1
- package/dist/cjs/store/turso/lexical.js +5 -1
- package/dist/cjs/store/turso/lexical.js.map +1 -1
- package/dist/cjs/text/segment.js +168 -3
- package/dist/cjs/text/segment.js.map +1 -1
- package/dist/cjs/watch-claim.d.cts +39 -0
- package/dist/cjs/watch-claim.d.ts +39 -0
- package/dist/cjs/watch-claim.js +446 -0
- package/dist/cjs/watch-claim.js.map +1 -0
- package/dist/cjs/watch.js +246 -147
- package/dist/cjs/watch.js.map +1 -1
- package/dist/cjs/workers/watch-heartbeat.d.cts +1 -0
- package/dist/cjs/workers/watch-heartbeat.d.ts +1 -0
- package/dist/cjs/workers/watch-heartbeat.js +308 -0
- package/dist/cjs/workers/watch-heartbeat.js.map +1 -0
- package/dist/esm/cli/status.js +6 -5
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/search.js +21 -0
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/lib/watch-notification.d.ts +1 -0
- package/dist/esm/lib/watch-notification.js +4 -0
- package/dist/esm/lib/watch-notification.js.map +1 -0
- package/dist/esm/lib/worker-file.d.ts +1 -0
- package/dist/esm/lib/worker-file.js +24 -0
- package/dist/esm/lib/worker-file.js.map +1 -0
- package/dist/esm/output/search-error.js +21 -0
- package/dist/esm/output/search-error.js.map +1 -1
- package/dist/esm/scan/pool.js +2 -22
- package/dist/esm/scan/pool.js.map +1 -1
- package/dist/esm/store/builder.d.ts +2 -1
- package/dist/esm/store/builder.js +4 -4
- package/dist/esm/store/builder.js.map +1 -1
- package/dist/esm/store/duckdb/lexical.d.ts +1 -0
- package/dist/esm/store/duckdb/lexical.js +16 -7
- package/dist/esm/store/duckdb/lexical.js.map +1 -1
- package/dist/esm/store/duckdb/native.d.ts +2 -0
- package/dist/esm/store/duckdb/native.js +5 -1
- package/dist/esm/store/duckdb/native.js.map +1 -1
- package/dist/esm/store/duckdb/ordered-bm25.d.ts +17 -0
- package/dist/esm/store/duckdb/ordered-bm25.js +56 -0
- package/dist/esm/store/duckdb/ordered-bm25.js.map +1 -0
- package/dist/esm/store/lock-wait.d.ts +2 -2
- package/dist/esm/store/lock-wait.js +17 -14
- package/dist/esm/store/lock-wait.js.map +1 -1
- package/dist/esm/store/native.d.ts +4 -0
- package/dist/esm/store/native.js +60 -6
- package/dist/esm/store/native.js.map +1 -1
- package/dist/esm/store/open.js +9 -6
- package/dist/esm/store/open.js.map +1 -1
- package/dist/esm/store/reconcile.d.ts +5 -1
- package/dist/esm/store/reconcile.js +4 -3
- package/dist/esm/store/reconcile.js.map +1 -1
- package/dist/esm/store/turso/lexical.js +5 -1
- package/dist/esm/store/turso/lexical.js.map +1 -1
- package/dist/esm/text/segment.js +130 -4
- package/dist/esm/text/segment.js.map +1 -1
- package/dist/esm/watch-claim.d.ts +39 -0
- package/dist/esm/watch-claim.js +176 -0
- package/dist/esm/watch-claim.js.map +1 -0
- package/dist/esm/watch.js +169 -118
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workers/watch-heartbeat.d.ts +1 -0
- package/dist/esm/workers/watch-heartbeat.js +106 -0
- package/dist/esm/workers/watch-heartbeat.js.map +1 -0
- package/package.json +2 -2
- package/skills/sense/SKILL.md +105 -104
- package/skills/sense/references/search.md +47 -0
- package/skills/sense/references/sql.md +120 -0
- package/skills/sense/references/stores/duckdb.md +35 -0
- package/skills/sense/references/stores/sqlite.md +66 -0
- package/skills/sense/references/stores/turso.md +36 -0
- package/skills/sense-setup/SKILL.md +93 -34
- package/skills/sense-setup/references/embeddings.md +51 -0
- package/skills/sense-setup/references/store-benchmarks.md +16 -0
- package/skills/sense-setup/references/stores/duckdb.md +28 -0
- package/skills/sense-setup/references/stores/sqlite.md +21 -0
- package/skills/sense-setup/references/stores/turso.md +25 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type WorkerErrorPayload } from './scan/worker-error.js';
|
|
2
|
+
export declare const WATCH_CLAIM_FILENAME = ".sense-watch.db";
|
|
3
|
+
export declare const WATCH_HEARTBEAT_INTERVAL_MS = 5000;
|
|
4
|
+
export declare const WATCH_STALE_HEARTBEAT_MS = 15000;
|
|
5
|
+
export interface WatchClaimRecord {
|
|
6
|
+
token: string;
|
|
7
|
+
pid: number;
|
|
8
|
+
heartbeatMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface WatchClaimWorkerData {
|
|
11
|
+
configDir: string;
|
|
12
|
+
force: boolean;
|
|
13
|
+
heartbeatIntervalMs: number;
|
|
14
|
+
pid: number;
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
17
|
+
export type WatchClaimWorkerMessage = {
|
|
18
|
+
type: 'acquired';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'stopped';
|
|
21
|
+
} | {
|
|
22
|
+
type: 'failure';
|
|
23
|
+
error: WorkerErrorPayload;
|
|
24
|
+
};
|
|
25
|
+
export interface WatchClaimController {
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class WatchClaimDatabase {
|
|
29
|
+
private readonly db;
|
|
30
|
+
private readonly conn;
|
|
31
|
+
constructor(configDir: string);
|
|
32
|
+
acquire(token: string, pid: number, force: boolean): Promise<void>;
|
|
33
|
+
renew(token: string): boolean;
|
|
34
|
+
release(token: string): boolean;
|
|
35
|
+
read(): WatchClaimRecord | null;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare function readWatchClaim(configDir: string): WatchClaimRecord | null;
|
|
39
|
+
export declare function startWatchClaim(configDir: string, force: boolean, heartbeatIntervalMs: number, onFailure: (err: Error) => void): Promise<WatchClaimController>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { Worker } from 'node:worker_threads';
|
|
6
|
+
import { SenseError } from './errors.js';
|
|
7
|
+
import { resolveWorkerFile } from './lib/worker-file.js';
|
|
8
|
+
import { reviveError } from './scan/worker-error.js';
|
|
9
|
+
import { createConnection } from './store/sqlite/connection.js';
|
|
10
|
+
import { BEGIN_WRITE, withTransaction } from './store/transaction.js';
|
|
11
|
+
export const WATCH_CLAIM_FILENAME = '.sense-watch.db';
|
|
12
|
+
export const WATCH_HEARTBEAT_INTERVAL_MS = 5000;
|
|
13
|
+
export const WATCH_STALE_HEARTBEAT_MS = 15000;
|
|
14
|
+
const CLAIM_BUSY_TIMEOUT_MS = 30000;
|
|
15
|
+
function claimPath(configDir) {
|
|
16
|
+
return join(configDir, WATCH_CLAIM_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
function changedOne(changes) {
|
|
19
|
+
return changes === 1 || changes === BigInt(1);
|
|
20
|
+
}
|
|
21
|
+
// This database is separate from the selected search store so its heartbeat remains writable while
|
|
22
|
+
// a DuckDB, Turso, or SQLite reconciliation owns the cache connection or blocks the main thread.
|
|
23
|
+
export class WatchClaimDatabase {
|
|
24
|
+
async acquire(token, pid, force) {
|
|
25
|
+
await withTransaction(this.conn, async ()=>{
|
|
26
|
+
const current = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
27
|
+
if (current && !force) {
|
|
28
|
+
const age = Date.now() - current.heartbeat_ms;
|
|
29
|
+
if (age >= 0 && age < WATCH_STALE_HEARTBEAT_MS) throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);
|
|
30
|
+
}
|
|
31
|
+
this.db.prepare('INSERT INTO watch_claim (singleton, token, pid, heartbeat_ms) VALUES (1, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET token = excluded.token, pid = excluded.pid, heartbeat_ms = excluded.heartbeat_ms').run(token, pid, Date.now());
|
|
32
|
+
}, BEGIN_WRITE);
|
|
33
|
+
}
|
|
34
|
+
renew(token) {
|
|
35
|
+
return changedOne(this.db.prepare('UPDATE watch_claim SET heartbeat_ms = ? WHERE singleton = 1 AND token = ?').run(Date.now(), token).changes);
|
|
36
|
+
}
|
|
37
|
+
release(token) {
|
|
38
|
+
return changedOne(this.db.prepare('DELETE FROM watch_claim WHERE singleton = 1 AND token = ?').run(token).changes);
|
|
39
|
+
}
|
|
40
|
+
read() {
|
|
41
|
+
const row = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
42
|
+
return row ? {
|
|
43
|
+
token: row.token,
|
|
44
|
+
pid: row.pid,
|
|
45
|
+
heartbeatMs: row.heartbeat_ms
|
|
46
|
+
} : null;
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
this.db.close();
|
|
50
|
+
}
|
|
51
|
+
constructor(configDir){
|
|
52
|
+
const db = new DatabaseSync(claimPath(configDir));
|
|
53
|
+
try {
|
|
54
|
+
db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);
|
|
55
|
+
db.exec('CREATE TABLE IF NOT EXISTS watch_claim (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), token TEXT NOT NULL, pid INTEGER NOT NULL, heartbeat_ms INTEGER NOT NULL)');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
db.close();
|
|
59
|
+
} catch (closeError) {
|
|
60
|
+
throw new AggregateError([
|
|
61
|
+
err,
|
|
62
|
+
closeError
|
|
63
|
+
], 'watch claim setup and database cleanup both failed');
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
this.db = db;
|
|
68
|
+
this.conn = createConnection(db);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Status opens the coordinator read-only and therefore cannot create either the file or its schema.
|
|
72
|
+
export function readWatchClaim(configDir) {
|
|
73
|
+
const path = claimPath(configDir);
|
|
74
|
+
if (!existsSync(path)) return null;
|
|
75
|
+
let db;
|
|
76
|
+
try {
|
|
77
|
+
db = new DatabaseSync(path, {
|
|
78
|
+
readOnly: true
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (!existsSync(path)) return null;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);
|
|
86
|
+
const row = db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
87
|
+
return row ? {
|
|
88
|
+
token: row.token,
|
|
89
|
+
pid: row.pid,
|
|
90
|
+
heartbeatMs: row.heartbeat_ms
|
|
91
|
+
} : null;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (/no such table: watch_claim/.test(err.message)) return null;
|
|
94
|
+
throw err;
|
|
95
|
+
} finally{
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function startWatchClaim(configDir, force, heartbeatIntervalMs, onFailure) {
|
|
100
|
+
const workerData = {
|
|
101
|
+
configDir,
|
|
102
|
+
force,
|
|
103
|
+
heartbeatIntervalMs,
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
token: randomUUID()
|
|
106
|
+
};
|
|
107
|
+
const worker = new Worker(resolveWorkerFile('watch-heartbeat'), {
|
|
108
|
+
workerData
|
|
109
|
+
});
|
|
110
|
+
let acquired = false;
|
|
111
|
+
let closing = false;
|
|
112
|
+
let exited = false;
|
|
113
|
+
let failureReported = false;
|
|
114
|
+
let closeError = null;
|
|
115
|
+
let closePromise;
|
|
116
|
+
let resolveExit;
|
|
117
|
+
const exit = new Promise((resolve)=>{
|
|
118
|
+
resolveExit = resolve;
|
|
119
|
+
});
|
|
120
|
+
let resolveStart;
|
|
121
|
+
let rejectStart;
|
|
122
|
+
const start = new Promise((resolve, reject)=>{
|
|
123
|
+
resolveStart = resolve;
|
|
124
|
+
rejectStart = reject;
|
|
125
|
+
});
|
|
126
|
+
const fail = (err)=>{
|
|
127
|
+
if (failureReported) return;
|
|
128
|
+
failureReported = true;
|
|
129
|
+
if (!acquired) rejectStart(err);
|
|
130
|
+
else if (closing) closeError !== null && closeError !== void 0 ? closeError : closeError = err;
|
|
131
|
+
else onFailure(err);
|
|
132
|
+
};
|
|
133
|
+
worker.on('message', (message)=>{
|
|
134
|
+
if (message.type === 'acquired') {
|
|
135
|
+
acquired = true;
|
|
136
|
+
resolveStart();
|
|
137
|
+
} else if (message.type === 'failure') {
|
|
138
|
+
fail(reviveError(message.error));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
worker.on('error', fail);
|
|
142
|
+
worker.on('exit', (code)=>{
|
|
143
|
+
exited = true;
|
|
144
|
+
resolveExit();
|
|
145
|
+
if (closing && code !== 0) closeError !== null && closeError !== void 0 ? closeError : closeError = new Error(`watch heartbeat worker stopped with exit code ${code}`);
|
|
146
|
+
else if (!closing && !failureReported) fail(new Error(`watch heartbeat worker stopped unexpectedly with exit code ${code}`));
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
await start;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
closing = true;
|
|
152
|
+
if (!exited) await worker.terminate();
|
|
153
|
+
await exit;
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
close () {
|
|
158
|
+
closePromise !== null && closePromise !== void 0 ? closePromise : closePromise = (async ()=>{
|
|
159
|
+
closing = true;
|
|
160
|
+
if (!exited) {
|
|
161
|
+
try {
|
|
162
|
+
worker.postMessage({
|
|
163
|
+
type: 'stop'
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
closeError !== null && closeError !== void 0 ? closeError : closeError = err instanceof Error ? err : new Error(String(err));
|
|
167
|
+
await worker.terminate();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await exit;
|
|
171
|
+
if (closeError) throw closeError;
|
|
172
|
+
})();
|
|
173
|
+
return closePromise;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch-claim.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport { Worker } from 'node:worker_threads';\nimport { SenseError } from './errors.ts';\nimport { resolveWorkerFile } from './lib/worker-file.ts';\nimport { reviveError, type WorkerErrorPayload } from './scan/worker-error.ts';\nimport { createConnection } from './store/sqlite/connection.ts';\nimport { BEGIN_WRITE, withTransaction } from './store/transaction.ts';\n\nexport const WATCH_CLAIM_FILENAME = '.sense-watch.db';\nexport const WATCH_HEARTBEAT_INTERVAL_MS = 5_000;\nexport const WATCH_STALE_HEARTBEAT_MS = 15_000;\nconst CLAIM_BUSY_TIMEOUT_MS = 30_000;\n\nexport interface WatchClaimRecord {\n token: string;\n pid: number;\n heartbeatMs: number;\n}\n\ninterface ClaimRow {\n token: string;\n pid: number;\n heartbeat_ms: number;\n}\n\nexport interface WatchClaimWorkerData {\n configDir: string;\n force: boolean;\n heartbeatIntervalMs: number;\n pid: number;\n token: string;\n}\n\nexport type WatchClaimWorkerMessage = { type: 'acquired' } | { type: 'stopped' } | { type: 'failure'; error: WorkerErrorPayload };\n\nexport interface WatchClaimController {\n close(): Promise<void>;\n}\n\nfunction claimPath(configDir: string): string {\n return join(configDir, WATCH_CLAIM_FILENAME);\n}\n\nfunction changedOne(changes: number | bigint): boolean {\n return changes === 1 || changes === BigInt(1);\n}\n\n// This database is separate from the selected search store so its heartbeat remains writable while\n// a DuckDB, Turso, or SQLite reconciliation owns the cache connection or blocks the main thread.\nexport class WatchClaimDatabase {\n private readonly db: DatabaseSync;\n private readonly conn: ReturnType<typeof createConnection>;\n\n constructor(configDir: string) {\n const db = new DatabaseSync(claimPath(configDir));\n try {\n db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);\n db.exec('CREATE TABLE IF NOT EXISTS watch_claim (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), token TEXT NOT NULL, pid INTEGER NOT NULL, heartbeat_ms INTEGER NOT NULL)');\n } catch (err) {\n try {\n db.close();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch claim setup and database cleanup both failed');\n }\n throw err;\n }\n this.db = db;\n this.conn = createConnection(db);\n }\n\n async acquire(token: string, pid: number, force: boolean): Promise<void> {\n await withTransaction(\n this.conn,\n async () => {\n const current = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n if (current && !force) {\n const age = Date.now() - current.heartbeat_ms;\n if (age >= 0 && age < WATCH_STALE_HEARTBEAT_MS) throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n this.db.prepare('INSERT INTO watch_claim (singleton, token, pid, heartbeat_ms) VALUES (1, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET token = excluded.token, pid = excluded.pid, heartbeat_ms = excluded.heartbeat_ms').run(token, pid, Date.now());\n },\n BEGIN_WRITE\n );\n }\n\n renew(token: string): boolean {\n return changedOne(this.db.prepare('UPDATE watch_claim SET heartbeat_ms = ? WHERE singleton = 1 AND token = ?').run(Date.now(), token).changes);\n }\n\n release(token: string): boolean {\n return changedOne(this.db.prepare('DELETE FROM watch_claim WHERE singleton = 1 AND token = ?').run(token).changes);\n }\n\n read(): WatchClaimRecord | null {\n const row = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n return row ? { token: row.token, pid: row.pid, heartbeatMs: row.heartbeat_ms } : null;\n }\n\n close(): void {\n this.db.close();\n }\n}\n\n// Status opens the coordinator read-only and therefore cannot create either the file or its schema.\nexport function readWatchClaim(configDir: string): WatchClaimRecord | null {\n const path = claimPath(configDir);\n if (!existsSync(path)) return null;\n let db: DatabaseSync;\n try {\n db = new DatabaseSync(path, { readOnly: true });\n } catch (err) {\n if (!existsSync(path)) return null;\n throw err;\n }\n try {\n db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);\n const row = db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n return row ? { token: row.token, pid: row.pid, heartbeatMs: row.heartbeat_ms } : null;\n } catch (err) {\n if (/no such table: watch_claim/.test((err as Error).message)) return null;\n throw err;\n } finally {\n db.close();\n }\n}\n\nexport async function startWatchClaim(configDir: string, force: boolean, heartbeatIntervalMs: number, onFailure: (err: Error) => void): Promise<WatchClaimController> {\n const workerData: WatchClaimWorkerData = { configDir, force, heartbeatIntervalMs, pid: process.pid, token: randomUUID() };\n const worker = new Worker(resolveWorkerFile('watch-heartbeat'), { workerData });\n let acquired = false;\n let closing = false;\n let exited = false;\n let failureReported = false;\n let closeError: Error | null = null;\n let closePromise: Promise<void> | undefined;\n let resolveExit!: () => void;\n const exit = new Promise<void>((resolve) => {\n resolveExit = resolve;\n });\n let resolveStart!: () => void;\n let rejectStart!: (err: Error) => void;\n const start = new Promise<void>((resolve, reject) => {\n resolveStart = resolve;\n rejectStart = reject;\n });\n\n const fail = (err: Error): void => {\n if (failureReported) return;\n failureReported = true;\n if (!acquired) rejectStart(err);\n else if (closing) closeError ??= err;\n else onFailure(err);\n };\n worker.on('message', (message: WatchClaimWorkerMessage) => {\n if (message.type === 'acquired') {\n acquired = true;\n resolveStart();\n } else if (message.type === 'failure') {\n fail(reviveError(message.error));\n }\n });\n worker.on('error', fail);\n worker.on('exit', (code) => {\n exited = true;\n resolveExit();\n if (closing && code !== 0) closeError ??= new Error(`watch heartbeat worker stopped with exit code ${code}`);\n else if (!closing && !failureReported) fail(new Error(`watch heartbeat worker stopped unexpectedly with exit code ${code}`));\n });\n\n try {\n await start;\n } catch (err) {\n closing = true;\n if (!exited) await worker.terminate();\n await exit;\n throw err;\n }\n\n return {\n close(): Promise<void> {\n closePromise ??= (async () => {\n closing = true;\n if (!exited) {\n try {\n worker.postMessage({ type: 'stop' });\n } catch (err) {\n closeError ??= err instanceof Error ? err : new Error(String(err));\n await worker.terminate();\n }\n }\n await exit;\n if (closeError) throw closeError;\n })();\n return closePromise;\n },\n };\n}\n"],"names":["randomUUID","existsSync","join","DatabaseSync","Worker","SenseError","resolveWorkerFile","reviveError","createConnection","BEGIN_WRITE","withTransaction","WATCH_CLAIM_FILENAME","WATCH_HEARTBEAT_INTERVAL_MS","WATCH_STALE_HEARTBEAT_MS","CLAIM_BUSY_TIMEOUT_MS","claimPath","configDir","changedOne","changes","BigInt","WatchClaimDatabase","acquire","token","pid","force","conn","current","db","prepare","get","age","Date","now","heartbeat_ms","Math","round","run","renew","release","read","row","heartbeatMs","close","exec","err","closeError","AggregateError","readWatchClaim","path","readOnly","test","message","startWatchClaim","heartbeatIntervalMs","onFailure","workerData","process","worker","acquired","closing","exited","failureReported","closePromise","resolveExit","exit","Promise","resolve","resolveStart","rejectStart","start","reject","fail","on","type","error","code","Error","terminate","postMessage","String"],"mappings":"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"}
|
package/dist/esm/watch.js
CHANGED
|
@@ -1,138 +1,189 @@
|
|
|
1
1
|
import { watch as fsWatch } from 'node:fs';
|
|
2
|
-
import { STATE_DIR } from './config/index.js';
|
|
3
2
|
import { SenseError } from './errors.js';
|
|
4
3
|
import { guardedTick } from './lib/guarded-tick.js';
|
|
5
|
-
import {
|
|
4
|
+
import { shouldReconcileWatchNotification } from './lib/watch-notification.js';
|
|
5
|
+
import { docCount, openStore } from './store/index.js';
|
|
6
|
+
import { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.js';
|
|
6
7
|
// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.
|
|
7
8
|
const DEBOUNCE_MS = 200;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to
|
|
11
|
-
// one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.
|
|
9
|
+
// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to
|
|
10
|
+
// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.
|
|
12
11
|
export async function runWatch(cfg, opts = {}) {
|
|
13
|
-
var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs, _cfg_rootDir;
|
|
12
|
+
var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs, _cfg_rootDir, _cfg_configDir, _opts_force;
|
|
14
13
|
const onEvent = (_opts_onEvent = opts.onEvent) !== null && _opts_onEvent !== void 0 ? _opts_onEvent : ()=>{};
|
|
15
14
|
const debounceMs = (_opts_debounceMs = opts.debounceMs) !== null && _opts_debounceMs !== void 0 ? _opts_debounceMs : DEBOUNCE_MS;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
//
|
|
15
|
+
const reconcileIntervalMs = (_opts_heartbeatIntervalMs = opts.heartbeatIntervalMs) !== null && _opts_heartbeatIntervalMs !== void 0 ? _opts_heartbeatIntervalMs : WATCH_HEARTBEAT_INTERVAL_MS;
|
|
16
|
+
if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');
|
|
17
|
+
// Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.
|
|
18
|
+
const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);
|
|
19
19
|
const rootDir = (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// total is read before close: an event fires only once nothing is held open again.
|
|
29
|
-
const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;
|
|
30
|
-
await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());
|
|
31
|
-
await setMeta(initialStore, 'watch_pid', String(process.pid));
|
|
32
|
-
await initialStore.close();
|
|
33
|
-
onEvent({
|
|
34
|
-
type: 'started',
|
|
35
|
-
rootDir,
|
|
36
|
-
dbPath
|
|
20
|
+
const configDir = (_cfg_configDir = cfg.configDir) !== null && _cfg_configDir !== void 0 ? _cfg_configDir : rootDir;
|
|
21
|
+
let claimFailure = null;
|
|
22
|
+
let requestShutdown;
|
|
23
|
+
const claim = await startWatchClaim(configDir, (_opts_force = opts.force) !== null && _opts_force !== void 0 ? _opts_force : false, claimIntervalMs, (err)=>{
|
|
24
|
+
claimFailure !== null && claimFailure !== void 0 ? claimFailure : claimFailure = err;
|
|
25
|
+
requestShutdown === null || requestShutdown === void 0 ? void 0 : requestShutdown(err);
|
|
37
26
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
let stopping = false;
|
|
47
|
-
// At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is
|
|
48
|
-
// coalesced into a single rerun instead of opening a second, overlapping connection.
|
|
49
|
-
let running = null;
|
|
50
|
-
let queuedAlways = null;
|
|
51
|
-
const tick = async (alwaysEmit)=>{
|
|
27
|
+
let claimCloseStarted = false;
|
|
28
|
+
const closeClaim = ()=>{
|
|
29
|
+
claimCloseStarted = true;
|
|
30
|
+
return claim.close();
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
|
|
34
|
+
let initialTotal = 0;
|
|
52
35
|
try {
|
|
53
|
-
|
|
36
|
+
// total is read before close: an event fires only once nothing is held open again.
|
|
37
|
+
initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;
|
|
38
|
+
} finally{
|
|
39
|
+
await initialStore.close();
|
|
40
|
+
}
|
|
41
|
+
if (claimFailure) throw claimFailure;
|
|
42
|
+
let stopping = false;
|
|
43
|
+
// At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is
|
|
44
|
+
// coalesced into a single rerun instead of opening a second, overlapping connection.
|
|
45
|
+
let running = null;
|
|
46
|
+
let queuedAlways = null;
|
|
47
|
+
const tick = async (alwaysEmit)=>{
|
|
54
48
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
const { store, parsed, warnings } = await openStore(cfg);
|
|
50
|
+
try {
|
|
51
|
+
if (alwaysEmit || parsed > 0 || warnings.length > 0) {
|
|
52
|
+
onEvent({
|
|
53
|
+
type: 'reconciled',
|
|
54
|
+
parsed,
|
|
55
|
+
total: await docCount(store),
|
|
56
|
+
warnings
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} finally{
|
|
60
|
+
await store.close();
|
|
64
61
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
62
|
+
} catch (err) {
|
|
63
|
+
onEvent({
|
|
64
|
+
type: 'reconcile-error',
|
|
65
|
+
message: err.message
|
|
66
|
+
});
|
|
67
67
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
};
|
|
69
|
+
const startCycle = (alwaysEmit)=>{
|
|
70
|
+
running = tick(alwaysEmit).then(()=>{
|
|
71
|
+
running = null;
|
|
72
|
+
if (queuedAlways !== null && !stopping) {
|
|
73
|
+
const again = queuedAlways;
|
|
74
|
+
queuedAlways = null;
|
|
75
|
+
startCycle(again);
|
|
76
|
+
}
|
|
72
77
|
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const startCycle = (alwaysEmit)=>{
|
|
76
|
-
running = tick(alwaysEmit).then(()=>{
|
|
77
|
-
running = null;
|
|
78
|
-
if (queuedAlways !== null && !stopping) {
|
|
79
|
-
const again = queuedAlways;
|
|
80
|
-
queuedAlways = null;
|
|
81
|
-
startCycle(again);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
const requestCycle = (alwaysEmit)=>{
|
|
86
|
-
if (stopping) return;
|
|
87
|
-
if (running) {
|
|
88
|
-
queuedAlways = queuedAlways || alwaysEmit;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
startCycle(alwaysEmit);
|
|
92
|
-
};
|
|
93
|
-
let debounceTimer = null;
|
|
94
|
-
const scheduleReconcile = ()=>{
|
|
95
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
96
|
-
debounceTimer = setTimeout(()=>{
|
|
97
|
-
debounceTimer = null;
|
|
98
|
-
requestCycle(true);
|
|
99
|
-
}, debounceMs);
|
|
100
|
-
};
|
|
101
|
-
// Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with
|
|
102
|
-
// an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.
|
|
103
|
-
const watcher = fsWatch(rootDir, {
|
|
104
|
-
recursive: true
|
|
105
|
-
}, (_event, filename)=>{
|
|
106
|
-
if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
|
|
107
|
-
scheduleReconcile();
|
|
108
|
-
});
|
|
109
|
-
const heartbeatTimer = setInterval(guardedTick(async ()=>requestCycle(false), ()=>stopping), heartbeatIntervalMs);
|
|
110
|
-
return new Promise((resolveShutdown)=>{
|
|
111
|
-
var _opts_signal, _opts_signal1;
|
|
112
|
-
// SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is
|
|
113
|
-
// unregistered here too so a second runWatch call in the same process starts clean.
|
|
114
|
-
const shutdown = async ()=>{
|
|
115
|
-
var _opts_signal;
|
|
78
|
+
};
|
|
79
|
+
const requestCycle = (alwaysEmit)=>{
|
|
116
80
|
if (stopping) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
81
|
+
if (running) {
|
|
82
|
+
queuedAlways = queuedAlways || alwaysEmit;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
startCycle(alwaysEmit);
|
|
86
|
+
};
|
|
87
|
+
let debounceTimer = null;
|
|
88
|
+
const scheduleReconcile = ()=>{
|
|
122
89
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await setMeta(finalStore, 'watch_pid', null);
|
|
128
|
-
await finalStore.close();
|
|
129
|
-
resolveShutdown();
|
|
90
|
+
debounceTimer = setTimeout(()=>{
|
|
91
|
+
debounceTimer = null;
|
|
92
|
+
requestCycle(true);
|
|
93
|
+
}, debounceMs);
|
|
130
94
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
95
|
+
// Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.
|
|
96
|
+
// An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.
|
|
97
|
+
const watcher = fsWatch(rootDir, {
|
|
98
|
+
recursive: true
|
|
99
|
+
}, (_event, filename)=>{
|
|
100
|
+
if (!shouldReconcileWatchNotification(filename)) return;
|
|
101
|
+
scheduleReconcile();
|
|
136
102
|
});
|
|
137
|
-
|
|
103
|
+
const reconcileTimer = setInterval(guardedTick(async ()=>requestCycle(false), ()=>stopping), reconcileIntervalMs);
|
|
104
|
+
return await new Promise((resolveShutdown, rejectShutdown)=>{
|
|
105
|
+
var _opts_signal;
|
|
106
|
+
let shutdownReason = null;
|
|
107
|
+
const shutdown = (reason)=>{
|
|
108
|
+
shutdownReason !== null && shutdownReason !== void 0 ? shutdownReason : shutdownReason = reason !== null && reason !== void 0 ? reason : null;
|
|
109
|
+
if (stopping) return;
|
|
110
|
+
stopping = true;
|
|
111
|
+
const shutdownTask = (async ()=>{
|
|
112
|
+
var _opts_signal;
|
|
113
|
+
process.off('SIGINT', stopNormally);
|
|
114
|
+
process.off('SIGTERM', stopNormally);
|
|
115
|
+
(_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.removeEventListener('abort', stopNormally);
|
|
116
|
+
clearInterval(reconcileTimer);
|
|
117
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
118
|
+
watcher.close();
|
|
119
|
+
await running;
|
|
120
|
+
let finalError;
|
|
121
|
+
try {
|
|
122
|
+
// A normal stop performs one final freshness pass. Ownership loss drains existing work
|
|
123
|
+
// but starts nothing new because the replacement watcher now owns that responsibility.
|
|
124
|
+
if (!shutdownReason) {
|
|
125
|
+
const { store: finalStore } = await openStore(cfg);
|
|
126
|
+
await finalStore.close();
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
finalError = err;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await closeClaim();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (finalError) throw new AggregateError([
|
|
135
|
+
finalError,
|
|
136
|
+
err
|
|
137
|
+
], 'watch shutdown and claim cleanup both failed');
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
if (finalError) throw finalError;
|
|
141
|
+
if (shutdownReason) throw shutdownReason;
|
|
142
|
+
})();
|
|
143
|
+
shutdownTask.then(resolveShutdown, rejectShutdown);
|
|
144
|
+
};
|
|
145
|
+
const stopNormally = ()=>shutdown();
|
|
146
|
+
requestShutdown = shutdown;
|
|
147
|
+
process.once('SIGINT', stopNormally);
|
|
148
|
+
process.once('SIGTERM', stopNormally);
|
|
149
|
+
if (claimFailure) shutdown(claimFailure);
|
|
150
|
+
else if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) shutdown();
|
|
151
|
+
else {
|
|
152
|
+
var _opts_signal1;
|
|
153
|
+
(_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.addEventListener('abort', stopNormally, {
|
|
154
|
+
once: true
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
onEvent({
|
|
158
|
+
type: 'started',
|
|
159
|
+
rootDir,
|
|
160
|
+
dbPath
|
|
161
|
+
});
|
|
162
|
+
if (!stopping && (initialWarnings.length > 0 || initialParsed > 0)) {
|
|
163
|
+
onEvent({
|
|
164
|
+
type: 'reconciled',
|
|
165
|
+
parsed: initialParsed,
|
|
166
|
+
total: initialTotal,
|
|
167
|
+
warnings: initialWarnings
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
shutdown(err instanceof Error ? err : new Error(String(err)));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (claimCloseStarted) throw err;
|
|
177
|
+
try {
|
|
178
|
+
await closeClaim();
|
|
179
|
+
} catch (closeError) {
|
|
180
|
+
throw new AggregateError([
|
|
181
|
+
err,
|
|
182
|
+
closeError
|
|
183
|
+
], 'watch setup and claim cleanup both failed');
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
} finally{
|
|
187
|
+
requestShutdown = undefined;
|
|
188
|
+
}
|
|
138
189
|
}
|
package/dist/esm/watch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, getMeta, openStore, setMeta } from './store/index.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to\n// one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n // openStore normalizes the legacy direct-API baseDir before a watcher touches the filesystem.\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n await initialStore.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n // total is read before close: an event fires only once nothing is held open again.\n const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());\n await setMeta(initialStore, 'watch_pid', String(process.pid));\n await initialStore.close();\n\n onEvent({ type: 'started', rootDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n await setMeta(store, 'watch_heartbeat', new Date().toISOString());\n await setMeta(store, 'watch_pid', String(process.pid));\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with\n // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n heartbeatIntervalMs\n );\n\n return new Promise<void>((resolveShutdown) => {\n // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is\n // unregistered here too so a second runWatch call in the same process starts clean.\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n opts.signal?.removeEventListener('abort', shutdown);\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n const { store: finalStore } = await openStore(cfg);\n await setMeta(finalStore, 'watch_heartbeat', null);\n await setMeta(finalStore, 'watch_pid', null);\n await finalStore.close();\n resolveShutdown();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n if (opts.signal?.aborted) shutdown();\n else opts.signal?.addEventListener('abort', shutdown, { once: true });\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","getMeta","openStore","setMeta","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","debounceMs","heartbeatIntervalMs","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","rootDir","baseDir","existingHeartbeat","force","age","Date","now","parse","close","Math","round","initialTotal","length","toISOString","String","process","pid","type","total","stopping","running","queuedAlways","tick","alwaysEmit","err","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","shutdown","off","signal","removeEventListener","clearInterval","finalStore","once","aborted","addEventListener"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { shouldReconcileWatchNotification } from './lib/watch-notification.ts';\nimport { docCount, openStore } from './store/index.ts';\nimport { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to\n// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const reconcileIntervalMs = opts.heartbeatIntervalMs ?? WATCH_HEARTBEAT_INTERVAL_MS;\n if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');\n // Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.\n const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const configDir = cfg.configDir ?? rootDir;\n let claimFailure: Error | null = null;\n let requestShutdown: ((reason: Error) => void) | undefined;\n const claim = await startWatchClaim(configDir, opts.force ?? false, claimIntervalMs, (err) => {\n claimFailure ??= err;\n requestShutdown?.(err);\n });\n let claimCloseStarted = false;\n const closeClaim = (): Promise<void> => {\n claimCloseStarted = true;\n return claim.close();\n };\n try {\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n let initialTotal = 0;\n try {\n // total is read before close: an event fires only once nothing is held open again.\n initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n } finally {\n await initialStore.close();\n }\n if (claimFailure) throw claimFailure;\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.\n // An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (!shouldReconcileWatchNotification(filename)) return;\n scheduleReconcile();\n });\n const reconcileTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n reconcileIntervalMs\n );\n\n return await new Promise<void>((resolveShutdown, rejectShutdown) => {\n let shutdownReason: Error | null = null;\n const shutdown = (reason?: Error) => {\n shutdownReason ??= reason ?? null;\n if (stopping) return;\n stopping = true;\n const shutdownTask = (async () => {\n process.off('SIGINT', stopNormally);\n process.off('SIGTERM', stopNormally);\n opts.signal?.removeEventListener('abort', stopNormally);\n clearInterval(reconcileTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n let finalError: unknown;\n try {\n // A normal stop performs one final freshness pass. Ownership loss drains existing work\n // but starts nothing new because the replacement watcher now owns that responsibility.\n if (!shutdownReason) {\n const { store: finalStore } = await openStore(cfg);\n await finalStore.close();\n }\n } catch (err) {\n finalError = err;\n }\n try {\n await closeClaim();\n } catch (err) {\n if (finalError) throw new AggregateError([finalError, err], 'watch shutdown and claim cleanup both failed');\n throw err;\n }\n if (finalError) throw finalError;\n if (shutdownReason) throw shutdownReason;\n })();\n shutdownTask.then(resolveShutdown, rejectShutdown);\n };\n const stopNormally = () => shutdown();\n requestShutdown = shutdown;\n process.once('SIGINT', stopNormally);\n process.once('SIGTERM', stopNormally);\n if (claimFailure) shutdown(claimFailure);\n else if (opts.signal?.aborted) shutdown();\n else {\n opts.signal?.addEventListener('abort', stopNormally, { once: true });\n try {\n onEvent({ type: 'started', rootDir, dbPath });\n if (!stopping && (initialWarnings.length > 0 || initialParsed > 0)) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n } catch (err) {\n shutdown(err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n } catch (err) {\n if (claimCloseStarted) throw err;\n try {\n await closeClaim();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch setup and claim cleanup both failed');\n }\n throw err;\n } finally {\n requestShutdown = undefined;\n }\n}\n"],"names":["watch","fsWatch","SenseError","guardedTick","shouldReconcileWatchNotification","docCount","openStore","startWatchClaim","WATCH_HEARTBEAT_INTERVAL_MS","DEBOUNCE_MS","runWatch","cfg","opts","onEvent","debounceMs","reconcileIntervalMs","heartbeatIntervalMs","Number","isFinite","claimIntervalMs","Math","min","rootDir","baseDir","configDir","claimFailure","requestShutdown","claim","force","err","claimCloseStarted","closeClaim","close","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","initialTotal","length","stopping","running","queuedAlways","tick","alwaysEmit","type","total","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","reconcileTimer","setInterval","Promise","resolveShutdown","rejectShutdown","shutdownReason","shutdown","reason","shutdownTask","process","off","stopNormally","signal","removeEventListener","clearInterval","finalError","finalStore","AggregateError","once","aborted","addEventListener","Error","String","closeError","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|