apple-tools-mcp 1.1.4 → 1.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 +111 -6
- package/index.js +172 -193
- package/indexer.js +26 -21
- package/lib/audit.js +22 -14
- package/lib/config.js +312 -0
- package/lib/indexGate.js +49 -0
- package/lib/indexerLock.js +376 -0
- package/lib/indexerRuntime.js +142 -0
- package/lib/lancedbTables.js +307 -0
- package/lib/processMode.js +28 -0
- package/package.json +4 -2
- package/search.js +4 -18
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* indexer.lock acquire / release / heartbeat.
|
|
3
|
+
*
|
|
4
|
+
* A live holder is never displaced, even if the lock timestamp is old
|
|
5
|
+
* (blocked event loop, long index cycle). Dead-PID takeover never renames
|
|
6
|
+
* the live lock path. Waiters take a wx mutex, re-read, unlink only if the
|
|
7
|
+
* contents are still the expected dead lock, wx-create the new lock, then
|
|
8
|
+
* drop the mutex — so a peer's freshly created lock is never deleted.
|
|
9
|
+
* An orphaned takeover mutex is never stolen with compare-then-unlink of
|
|
10
|
+
* the live `.takeover` path: waiters wx a fence named by the dead PID.
|
|
11
|
+
* Empty or unparsable mutex/lock contents are unknown — do not fence or steal
|
|
12
|
+
* (covers wx open-then-write and heartbeat truncate-then-write windows).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_LOCK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
19
|
+
export const DEFAULT_LOCK_HEARTBEAT_MS = 60 * 1000;
|
|
20
|
+
export const TAKEOVER_MUTEX_SUFFIX = ".takeover";
|
|
21
|
+
export const TAKEOVER_FENCE_DEPTH = 8;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} lockFile
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function getTakeoverMutexPath(lockFile) {
|
|
28
|
+
return `${lockFile}${TAKEOVER_MUTEX_SUFFIX}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* wx fence used when indexer.lock.takeover is orphaned (dead PID).
|
|
33
|
+
* @param {string} lockFile
|
|
34
|
+
* @param {...number|string} deadIds
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function getTakeoverFencePath(lockFile, ...deadIds) {
|
|
38
|
+
let p = getTakeoverMutexPath(lockFile);
|
|
39
|
+
for (const id of deadIds) {
|
|
40
|
+
p = `${p}.${id}`;
|
|
41
|
+
}
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {unknown} pid
|
|
47
|
+
* @param {(pid: number, signal: number) => void} [killFn]
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function isProcessAlive(pid, killFn = (p, signal) => process.kill(p, signal)) {
|
|
51
|
+
try {
|
|
52
|
+
killFn(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} text
|
|
61
|
+
* @returns {{ pid: number, timestamp: number, raw: string } | null}
|
|
62
|
+
*/
|
|
63
|
+
export function parseLockData(text) {
|
|
64
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const [pidStr, timestampStr] = text.split(":");
|
|
68
|
+
const pid = parseInt(pidStr, 10);
|
|
69
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
70
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
pid,
|
|
75
|
+
timestamp: Number.isInteger(timestamp) ? timestamp : 0,
|
|
76
|
+
raw: text
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {number} pid
|
|
82
|
+
* @param {number} timestamp
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function formatLockData(pid, timestamp) {
|
|
86
|
+
return `${pid}:${timestamp}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {{
|
|
91
|
+
* lockFile: string,
|
|
92
|
+
* pid?: number,
|
|
93
|
+
* now?: () => number,
|
|
94
|
+
* timeoutMs?: number,
|
|
95
|
+
* isAlive?: (pid: number) => boolean,
|
|
96
|
+
* fsApi?: Pick<typeof fs, "existsSync" | "readFileSync" | "writeFileSync" | "unlinkSync" | "mkdirSync">,
|
|
97
|
+
* log?: (msg: string) => void,
|
|
98
|
+
* setIntervalFn?: typeof setInterval,
|
|
99
|
+
* clearIntervalFn?: typeof clearInterval
|
|
100
|
+
* }} options
|
|
101
|
+
*/
|
|
102
|
+
export function createIndexerLock(options) {
|
|
103
|
+
const lockFile = options.lockFile;
|
|
104
|
+
const mutexPath = getTakeoverMutexPath(lockFile);
|
|
105
|
+
const pid = options.pid ?? process.pid;
|
|
106
|
+
const now = options.now || (() => Date.now());
|
|
107
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
108
|
+
const isAlive = options.isAlive || ((holderPid) => isProcessAlive(holderPid));
|
|
109
|
+
const fsApi = options.fsApi || fs;
|
|
110
|
+
const log = options.log || ((msg) => console.error(msg));
|
|
111
|
+
const setIntervalFn = options.setIntervalFn || setInterval;
|
|
112
|
+
const clearIntervalFn = options.clearIntervalFn || clearInterval;
|
|
113
|
+
|
|
114
|
+
let ownsLock = false;
|
|
115
|
+
let heartbeatTimer = null;
|
|
116
|
+
let heldMutexPath = null;
|
|
117
|
+
|
|
118
|
+
function readLockFile() {
|
|
119
|
+
if (!fsApi.existsSync(lockFile)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return fsApi.readFileSync(lockFile, "utf8");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function skipLiveHolder(parsed) {
|
|
126
|
+
const lockAge = now() - parsed.timestamp;
|
|
127
|
+
if (lockAge > timeoutMs) {
|
|
128
|
+
log(
|
|
129
|
+
`Lock file is ${Math.round(lockAge / 60000)} minutes old, but PID ${parsed.pid} is still running. Not taking over.`
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
log(`Another indexing instance running (PID ${parsed.pid}). Skipping indexing.`);
|
|
133
|
+
}
|
|
134
|
+
ownsLock = false;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function refreshOwned() {
|
|
139
|
+
ownsLock = true;
|
|
140
|
+
try {
|
|
141
|
+
fsApi.writeFileSync(lockFile, formatLockData(pid, now()));
|
|
142
|
+
} catch {
|
|
143
|
+
// Heartbeat or the next cycle can retry the write.
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function tryAcquireMutex() {
|
|
149
|
+
const token = formatLockData(pid, now());
|
|
150
|
+
let candidate = mutexPath;
|
|
151
|
+
try {
|
|
152
|
+
for (let depth = 0; depth < TAKEOVER_FENCE_DEPTH; depth++) {
|
|
153
|
+
let exists = false;
|
|
154
|
+
for (let spin = 0; spin < 4; spin++) {
|
|
155
|
+
try {
|
|
156
|
+
fsApi.writeFileSync(candidate, token, { flag: "wx" });
|
|
157
|
+
heldMutexPath = candidate;
|
|
158
|
+
return true;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err && err.code === "ENOENT") {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!err || err.code !== "EEXIST") {
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
exists = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!exists) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
let data;
|
|
174
|
+
try {
|
|
175
|
+
data = fsApi.readFileSync(candidate, "utf8");
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err && err.code === "ENOENT") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
const parsed = parseLockData(data);
|
|
183
|
+
if (!parsed) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (isAlive(parsed.pid)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
candidate = `${candidate}.${parsed.pid}`;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err && err.code === "EEXIST") {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
log(`Takeover mutex error: ${err.message}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function dropMutex() {
|
|
202
|
+
const held = heldMutexPath;
|
|
203
|
+
heldMutexPath = null;
|
|
204
|
+
if (!held) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
if (!fsApi.existsSync(held)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const data = fsApi.readFileSync(held, "utf8");
|
|
212
|
+
const parsed = parseLockData(data);
|
|
213
|
+
if (parsed && parsed.pid === pid) {
|
|
214
|
+
fsApi.unlinkSync(held);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log(`Takeover mutex release error: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Under the wx mutex: re-read, unlink only the still-expected dead lock,
|
|
223
|
+
* then wx-create. Never rename the live path.
|
|
224
|
+
*/
|
|
225
|
+
function finishAcquireUnderMutex(expectedStale) {
|
|
226
|
+
const current = readLockFile();
|
|
227
|
+
|
|
228
|
+
if (current !== null) {
|
|
229
|
+
const parsed = parseLockData(current);
|
|
230
|
+
if (!parsed) {
|
|
231
|
+
ownsLock = false;
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (parsed.pid === pid) {
|
|
235
|
+
return refreshOwned();
|
|
236
|
+
}
|
|
237
|
+
if (isAlive(parsed.pid)) {
|
|
238
|
+
return skipLiveHolder(parsed);
|
|
239
|
+
}
|
|
240
|
+
if (expectedStale === null || current !== expectedStale) {
|
|
241
|
+
ownsLock = false;
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
// Re-read immediately before unlink. Never delete a peer's wx lock.
|
|
245
|
+
const again = readLockFile();
|
|
246
|
+
if (again !== expectedStale) {
|
|
247
|
+
ownsLock = false;
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
fsApi.unlinkSync(lockFile);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (!err || err.code !== "ENOENT") {
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
log(`Removing stale lock file (PID ${parsed ? parsed.pid : "unknown"} not running)`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
fsApi.writeFileSync(lockFile, formatLockData(pid, now()), { flag: "wx" });
|
|
262
|
+
ownsLock = true;
|
|
263
|
+
return true;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err.code === "EEXIST") {
|
|
266
|
+
log("Another process acquired lock during race. Skipping indexing.");
|
|
267
|
+
ownsLock = false;
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function acquire() {
|
|
275
|
+
try {
|
|
276
|
+
const lockDir = path.dirname(lockFile);
|
|
277
|
+
if (!fsApi.existsSync(lockDir)) {
|
|
278
|
+
fsApi.mkdirSync(lockDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const existing = readLockFile();
|
|
282
|
+
if (existing !== null) {
|
|
283
|
+
const parsed = parseLockData(existing);
|
|
284
|
+
if (!parsed) {
|
|
285
|
+
ownsLock = false;
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
if (parsed.pid === pid) {
|
|
289
|
+
return refreshOwned();
|
|
290
|
+
}
|
|
291
|
+
if (isAlive(parsed.pid)) {
|
|
292
|
+
return skipLiveHolder(parsed);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!tryAcquireMutex()) {
|
|
297
|
+
log("Another process acquired lock during race. Skipping indexing.");
|
|
298
|
+
ownsLock = false;
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return finishAcquireUnderMutex(existing);
|
|
303
|
+
} finally {
|
|
304
|
+
dropMutex();
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {
|
|
307
|
+
log(`Lock file error: ${e.message}`);
|
|
308
|
+
ownsLock = false;
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function release() {
|
|
314
|
+
try {
|
|
315
|
+
const lockData = readLockFile();
|
|
316
|
+
if (lockData) {
|
|
317
|
+
const parsed = parseLockData(lockData);
|
|
318
|
+
if (parsed && parsed.pid === pid) {
|
|
319
|
+
fsApi.unlinkSync(lockFile);
|
|
320
|
+
ownsLock = false;
|
|
321
|
+
log(`Released lock file (PID ${pid})`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
log(`Error releasing lock: ${err.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function refresh() {
|
|
330
|
+
try {
|
|
331
|
+
if (!ownsLock) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const lockData = readLockFile();
|
|
335
|
+
if (!lockData) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const parsed = parseLockData(lockData);
|
|
339
|
+
if (parsed && parsed.pid === pid) {
|
|
340
|
+
fsApi.writeFileSync(lockFile, formatLockData(pid, now()));
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
} catch (err) {
|
|
345
|
+
log(`Lock heartbeat error: ${err.message}`);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function startHeartbeat(intervalMs = DEFAULT_LOCK_HEARTBEAT_MS) {
|
|
351
|
+
if (heartbeatTimer) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
heartbeatTimer = setIntervalFn(() => {
|
|
355
|
+
refresh();
|
|
356
|
+
}, intervalMs);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function stopHeartbeat() {
|
|
360
|
+
if (heartbeatTimer) {
|
|
361
|
+
clearIntervalFn(heartbeatTimer);
|
|
362
|
+
heartbeatTimer = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
acquire,
|
|
368
|
+
release,
|
|
369
|
+
refresh,
|
|
370
|
+
startHeartbeat,
|
|
371
|
+
stopHeartbeat,
|
|
372
|
+
get ownsLock() {
|
|
373
|
+
return ownsLock;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexer daemon vs MCP stdio runtime helpers.
|
|
3
|
+
* Kept separate from index.js so daemon paths can be tested without
|
|
4
|
+
* starting MCP stdio or loading the embedding model.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {boolean} indexerMode
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function shouldConnectMcpStdio(indexerMode) {
|
|
12
|
+
return !indexerMode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {boolean} indexerMode
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function shouldExitOnStdinClose(indexerMode) {
|
|
20
|
+
return !indexerMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bind stdin `close` → callback only for MCP stdio clients.
|
|
25
|
+
* Indexer daemons must ignore stdin close (LaunchAgent often uses /dev/null).
|
|
26
|
+
*
|
|
27
|
+
* @param {{ on: (event: string, handler: () => void) => void }} stdin
|
|
28
|
+
* @param {boolean} indexerMode
|
|
29
|
+
* @param {() => void} onClose
|
|
30
|
+
* @returns {{ bound: boolean }}
|
|
31
|
+
*/
|
|
32
|
+
export function bindStdinCloseExit(stdin, indexerMode, onClose) {
|
|
33
|
+
if (!shouldExitOnStdinClose(indexerMode)) {
|
|
34
|
+
return { bound: false };
|
|
35
|
+
}
|
|
36
|
+
stdin.on("close", onClose);
|
|
37
|
+
return { bound: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start one index cycle, or skip if a cycle is already running.
|
|
42
|
+
*
|
|
43
|
+
* @param {boolean} indexingInProgress
|
|
44
|
+
* @param {(msg: string) => void} [log]
|
|
45
|
+
* @returns {{ started: boolean, indexingInProgress: boolean }}
|
|
46
|
+
*/
|
|
47
|
+
export function beginIndexCycle(indexingInProgress, log = (msg) => console.error(msg)) {
|
|
48
|
+
if (indexingInProgress) {
|
|
49
|
+
log("Indexing already in progress, skipping cycle");
|
|
50
|
+
return { started: false, indexingInProgress: true };
|
|
51
|
+
}
|
|
52
|
+
return { started: true, indexingInProgress: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* After a cycle: daemon keeps the lock; MCP local-fallback releases it.
|
|
57
|
+
*
|
|
58
|
+
* @param {{
|
|
59
|
+
* success: boolean,
|
|
60
|
+
* indexerMode: boolean,
|
|
61
|
+
* cycleEndFlags: (success: boolean) => {
|
|
62
|
+
* indexingInProgress: boolean,
|
|
63
|
+
* sessionIndexComplete: boolean,
|
|
64
|
+
* ownsIndexLock: boolean,
|
|
65
|
+
* isFirstEverRun?: boolean
|
|
66
|
+
* },
|
|
67
|
+
* releaseLock: () => void
|
|
68
|
+
* }} args
|
|
69
|
+
*/
|
|
70
|
+
export function applyIndexerCycleEnd({ success, indexerMode, cycleEndFlags, releaseLock }) {
|
|
71
|
+
const flags = cycleEndFlags(success);
|
|
72
|
+
if (indexerMode) {
|
|
73
|
+
return {
|
|
74
|
+
indexingInProgress: flags.indexingInProgress,
|
|
75
|
+
sessionIndexComplete: flags.sessionIndexComplete,
|
|
76
|
+
ownsIndexLock: true,
|
|
77
|
+
isFirstEverRun: flags.isFirstEverRun,
|
|
78
|
+
released: false
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
releaseLock();
|
|
82
|
+
return {
|
|
83
|
+
indexingInProgress: flags.indexingInProgress,
|
|
84
|
+
sessionIndexComplete: flags.sessionIndexComplete,
|
|
85
|
+
ownsIndexLock: flags.ownsIndexLock,
|
|
86
|
+
isFirstEverRun: flags.isFirstEverRun,
|
|
87
|
+
released: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* MCP stdio startup: index locally only when the lock is free.
|
|
93
|
+
*
|
|
94
|
+
* @param {() => boolean} acquireLock
|
|
95
|
+
* @returns {{ startBackground: boolean, ownsIndexLock: boolean, startHeartbeat: boolean, reason: "local-fallback" | "lock-held" }}
|
|
96
|
+
*/
|
|
97
|
+
export function mcpIndexingStartup(acquireLock) {
|
|
98
|
+
if (!acquireLock()) {
|
|
99
|
+
return { startBackground: false, ownsIndexLock: false, startHeartbeat: false, reason: "lock-held" };
|
|
100
|
+
}
|
|
101
|
+
return { startBackground: true, ownsIndexLock: true, startHeartbeat: true, reason: "local-fallback" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Shared path once this process owns indexer.lock: heartbeat + background cycles.
|
|
106
|
+
* MCP local-fallback must use this too, not only the daemon.
|
|
107
|
+
*
|
|
108
|
+
* @param {{ startHeartbeat: () => void, startBackground: () => void }} steps
|
|
109
|
+
*/
|
|
110
|
+
export function beginOwnedIndexing(steps) {
|
|
111
|
+
steps.startHeartbeat();
|
|
112
|
+
steps.startBackground();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Retry until the daemon owns indexer.lock, then run onAcquired.
|
|
117
|
+
*
|
|
118
|
+
* @param {() => boolean} acquireLock
|
|
119
|
+
* @param {{
|
|
120
|
+
* retryMs: number,
|
|
121
|
+
* onAcquired: () => void,
|
|
122
|
+
* log?: (msg: string) => void,
|
|
123
|
+
* setTimeoutFn?: typeof setTimeout
|
|
124
|
+
* }} options
|
|
125
|
+
*/
|
|
126
|
+
export function waitForIndexerLock(acquireLock, options) {
|
|
127
|
+
const retryMs = options.retryMs;
|
|
128
|
+
const onAcquired = options.onAcquired;
|
|
129
|
+
const log = options.log || ((msg) => console.error(msg));
|
|
130
|
+
const setTimeoutFn = options.setTimeoutFn || setTimeout;
|
|
131
|
+
|
|
132
|
+
const tryAcquire = () => {
|
|
133
|
+
if (acquireLock()) {
|
|
134
|
+
log("Indexer daemon acquired indexer.lock");
|
|
135
|
+
onAcquired();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
log("Indexer daemon waiting for indexer.lock...");
|
|
139
|
+
setTimeoutFn(tryAcquire, retryMs);
|
|
140
|
+
};
|
|
141
|
+
tryAcquire();
|
|
142
|
+
}
|