-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathreinit-watcher.js
More file actions
82 lines (75 loc) · 3.25 KB
/
Copy pathreinit-watcher.js
File metadata and controls
82 lines (75 loc) · 3.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
import { existsSync, mkdirSync, watch } from "node:fs";
import { dirname, join } from "node:path";
import { getConfigDir, getProfilePaths } from "./profiles.js";
/**
* Run a watcher callback so that neither a synchronous throw nor a rejected
* promise can escape.
*
* `try { callback(); } catch {}` looks like it covers this, but it does NOT:
* an async callback returns a promise immediately without throwing, so the
* catch never fires and the promise is discarded. If it later rejects, that is
* an unhandled rejection — and under Node >=15's default
* `--unhandled-rejections=throw` it HARD-EXITS the daemon. The daemon has no
* process-level rejection handler, so a single failed background reinit took
* down a healthy, listening HTTP server; every MCP client then saw
* ECONNREFUSED on a port VS Code still had cached (issue #10 follow-ups).
*
* Callers are expected to log their own failures — this is the structural
* backstop that stops any future async caller from re-introducing the crash.
*/
function runGuarded(callback) {
void Promise.resolve().then(callback).catch(() => {});
}
export function watchReinit(profileName, callback, opts = {}) {
const { debounceMs = 200 } = opts;
const target = getProfilePaths(profileName).reinit;
const parent = dirname(target);
if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
let timer = null;
const w = watch(parent, { persistent: false }, (event, filename) => {
if (!filename) return;
if (!String(filename).endsWith(".reinit")) return;
if (!existsSync(target)) return;
if (timer) clearTimeout(timer);
timer = setTimeout(() => { timer = null; runGuarded(callback); }, debounceMs);
});
return {
dispose() {
if (timer) { clearTimeout(timer); timer = null; }
try { w.close(); } catch {}
},
};
}
/**
* Watch the `<configDir>/active` pointer file for profile switches.
*
* The per-profile `watchReinit` is bound to a single profile's `.reinit` file
* captured at daemon startup; if the user switches the active profile, that
* watcher will never see anything (the new profile's `.reinit` is in a
* different directory). This second watcher fires whenever `setActive()`
* rewrites the active-pointer atomically, letting the daemon call
* `client.reinit()` on profile switches AND rebind its per-profile watcher
* to the newly-active profile so subsequent login events propagate.
*/
export function watchActiveProfile(configDirOverride, callback, opts = {}) {
const { debounceMs = 200 } = opts;
const configDir = configDirOverride ?? getConfigDir();
const target = join(configDir, "active");
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
let timer = null;
const w = watch(configDir, { persistent: false }, (event, filename) => {
if (!filename) return;
// setActive writes via `<active>.tmp` then renames; both events surface
// here, but only the final `active` rename is meaningful.
if (String(filename) !== "active") return;
if (!existsSync(target)) return;
if (timer) clearTimeout(timer);
timer = setTimeout(() => { timer = null; runGuarded(callback); }, debounceMs);
});
return {
dispose() {
if (timer) { clearTimeout(timer); timer = null; }
try { w.close(); } catch {}
},
};
}