@teampitch/mcpx 0.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/src/watcher.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { watch } from "node:fs";
2
+
3
+ import { connectBackends, type Backend } from "./backends.js";
4
+ import { loadConfig, type McpxConfig, type BackendConfig } from "./config.js";
5
+
6
+ export interface ReloadResult {
7
+ added: string[];
8
+ removed: string[];
9
+ changed: string[];
10
+ }
11
+
12
+ function diffBackends(
13
+ oldConfigs: Record<string, BackendConfig>,
14
+ newConfigs: Record<string, BackendConfig>,
15
+ ): ReloadResult {
16
+ const added: string[] = [];
17
+ const removed: string[] = [];
18
+ const changed: string[] = [];
19
+
20
+ for (const name of Object.keys(newConfigs)) {
21
+ if (!(name in oldConfigs)) {
22
+ added.push(name);
23
+ } else if (JSON.stringify(oldConfigs[name]) !== JSON.stringify(newConfigs[name])) {
24
+ changed.push(name);
25
+ }
26
+ }
27
+
28
+ for (const name of Object.keys(oldConfigs)) {
29
+ if (!(name in newConfigs)) {
30
+ removed.push(name);
31
+ }
32
+ }
33
+
34
+ return { added, removed, changed };
35
+ }
36
+
37
+ /** Watch a config file and call onReload when it changes */
38
+ export function watchConfig(
39
+ configPath: string,
40
+ backends: Map<string, Backend>,
41
+ onReload: (config: McpxConfig, result: ReloadResult) => void,
42
+ ): () => void {
43
+ let currentConfig = loadConfig(configPath);
44
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
45
+
46
+ const watcher = watch(configPath, () => {
47
+ if (debounceTimer) clearTimeout(debounceTimer);
48
+ debounceTimer = setTimeout(async () => {
49
+ try {
50
+ const newConfig = loadConfig(configPath);
51
+ const diff = diffBackends(currentConfig.backends, newConfig.backends);
52
+
53
+ if (!diff.added.length && !diff.removed.length && !diff.changed.length) {
54
+ return; // No backend changes
55
+ }
56
+
57
+ // Remove deleted backends
58
+ for (const name of diff.removed) {
59
+ const backend = backends.get(name);
60
+ if (backend) {
61
+ await backend.client.close().catch(() => {});
62
+ backends.delete(name);
63
+ }
64
+ }
65
+
66
+ // Disconnect changed backends
67
+ for (const name of diff.changed) {
68
+ const backend = backends.get(name);
69
+ if (backend) {
70
+ await backend.client.close().catch(() => {});
71
+ backends.delete(name);
72
+ }
73
+ }
74
+
75
+ // Connect new + changed backends
76
+ const toConnect: Record<string, BackendConfig> = {};
77
+ for (const name of [...diff.added, ...diff.changed]) {
78
+ toConnect[name] = newConfig.backends[name];
79
+ }
80
+
81
+ if (Object.keys(toConnect).length > 0) {
82
+ const newBackends = await connectBackends(toConnect);
83
+ for (const [name, backend] of newBackends) {
84
+ backends.set(name, backend);
85
+ }
86
+ }
87
+
88
+ currentConfig = newConfig;
89
+ onReload(newConfig, diff);
90
+ } catch (err) {
91
+ console.error("Config reload failed:", (err as Error).message);
92
+ }
93
+ }, 500);
94
+ });
95
+
96
+ return () => {
97
+ if (debounceTimer) clearTimeout(debounceTimer);
98
+ watcher.close();
99
+ };
100
+ }