@schoolai/shipyard 3.5.0-rc.20260504.0 → 3.5.0
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/dist/{auth-LS3NBD42.js → auth-SS7LV5XK.js} +4 -3
- package/dist/{chunk-GLH3V7NG.js → chunk-2J3WSIAF.js} +5 -3
- package/dist/{chunk-GLH3V7NG.js.map → chunk-2J3WSIAF.js.map} +1 -1
- package/dist/{chunk-YUG27SAR.js → chunk-2UN5AR7V.js} +2 -2
- package/dist/{chunk-ODCN6W33.js → chunk-3CAEALVL.js} +7 -5
- package/dist/{chunk-ODCN6W33.js.map → chunk-3CAEALVL.js.map} +1 -1
- package/dist/chunk-3MNPDCO5.js +1011 -0
- package/dist/chunk-3MNPDCO5.js.map +1 -0
- package/dist/{chunk-JQ7HCEFS.js → chunk-BNEE7ZPW.js} +8 -6
- package/dist/{chunk-JQ7HCEFS.js.map → chunk-BNEE7ZPW.js.map} +1 -1
- package/dist/{chunk-5LIPEC7P.js → chunk-GIFN3IPT.js} +4 -4
- package/dist/{chunk-3TB4VNFG.js → chunk-IISLTKYY.js} +2 -2
- package/dist/chunk-PI77CUEP.js +49 -0
- package/dist/chunk-PI77CUEP.js.map +1 -0
- package/dist/chunk-SNYEQHUK.js +64 -0
- package/dist/chunk-SNYEQHUK.js.map +1 -0
- package/dist/{chunk-XXTIKBCU.js → chunk-VBPHGPBR.js} +2 -2
- package/dist/{chunk-M5M6VC5F.js → chunk-VPMN47TL.js} +31 -72
- package/dist/chunk-VPMN47TL.js.map +1 -0
- package/dist/{git-repo-CNIKBYPB.js → git-repo-364VANDM.js} +5 -4
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/{logger-7XW3I4XN.js → logger-GQCSLSZH.js} +4 -3
- package/dist/{login-RHZDNC74.js → login-D6USDG5M.js} +7 -6
- package/dist/{logout-CUAAF5IK.js → logout-VUNCW5B2.js} +6 -5
- package/dist/{logout-CUAAF5IK.js.map → logout-VUNCW5B2.js.map} +1 -1
- package/dist/mcp-servers-FZV2P2ZO.js +16 -0
- package/dist/{roi-LN7MMRH7.js → roi-Y3MX5UW4.js} +4 -3
- package/dist/{roi-LN7MMRH7.js.map → roi-Y3MX5UW4.js.map} +1 -1
- package/dist/{serve-E7CHPJD4.js → serve-IVUGCBEE.js} +73 -462
- package/dist/{serve-E7CHPJD4.js.map → serve-IVUGCBEE.js.map} +1 -1
- package/dist/services/watcher-worker/worker.d.ts +49 -0
- package/dist/services/watcher-worker/worker.js +157 -0
- package/dist/services/watcher-worker/worker.js.map +1 -0
- package/dist/{skills-OMDIMU7D.js → skills-GPGRNV4R.js} +2 -2
- package/dist/start-I7ZONWK7.js +285 -0
- package/dist/start-I7ZONWK7.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-M5M6VC5F.js.map +0 -1
- package/dist/mcp-servers-3SHS2PEJ.js +0 -15
- package/dist/start-HQ42GOYF.js +0 -36
- package/dist/start-HQ42GOYF.js.map +0 -1
- /package/dist/{auth-LS3NBD42.js.map → auth-SS7LV5XK.js.map} +0 -0
- /package/dist/{chunk-YUG27SAR.js.map → chunk-2UN5AR7V.js.map} +0 -0
- /package/dist/{chunk-5LIPEC7P.js.map → chunk-GIFN3IPT.js.map} +0 -0
- /package/dist/{chunk-3TB4VNFG.js.map → chunk-IISLTKYY.js.map} +0 -0
- /package/dist/{chunk-XXTIKBCU.js.map → chunk-VBPHGPBR.js.map} +0 -0
- /package/dist/{git-repo-CNIKBYPB.js.map → git-repo-364VANDM.js.map} +0 -0
- /package/dist/{logger-7XW3I4XN.js.map → logger-GQCSLSZH.js.map} +0 -0
- /package/dist/{login-RHZDNC74.js.map → login-D6USDG5M.js.map} +0 -0
- /package/dist/{mcp-servers-3SHS2PEJ.js.map → mcp-servers-FZV2P2ZO.js.map} +0 -0
- /package/dist/{skills-OMDIMU7D.js.map → skills-GPGRNV4R.js.map} +0 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertNever,
|
|
4
|
+
decodeLine,
|
|
5
|
+
encodeLine
|
|
6
|
+
} from "./chunk-SNYEQHUK.js";
|
|
7
|
+
|
|
8
|
+
// src/services/metrics/metrics-collector.ts
|
|
9
|
+
var NOOP_METRICS = {
|
|
10
|
+
capture() {
|
|
11
|
+
},
|
|
12
|
+
dispose() {
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var MetricsCollector = class {
|
|
16
|
+
#workerUrl;
|
|
17
|
+
#authToken;
|
|
18
|
+
#maxBatchSize;
|
|
19
|
+
#buffer = [];
|
|
20
|
+
#timer = null;
|
|
21
|
+
#disposed = false;
|
|
22
|
+
constructor(workerUrl, authToken, opts) {
|
|
23
|
+
this.#workerUrl = workerUrl.replace(/\/$/, "");
|
|
24
|
+
this.#authToken = authToken;
|
|
25
|
+
this.#maxBatchSize = opts?.maxBatchSize ?? 50;
|
|
26
|
+
const intervalMs = opts?.flushIntervalMs ?? 3e4;
|
|
27
|
+
this.#timer = setInterval(() => {
|
|
28
|
+
this.flush();
|
|
29
|
+
}, intervalMs);
|
|
30
|
+
this.#timer.unref();
|
|
31
|
+
}
|
|
32
|
+
capture(eventType, properties) {
|
|
33
|
+
if (this.#disposed) return;
|
|
34
|
+
const taskId = typeof properties?.taskId === "string" ? properties.taskId : void 0;
|
|
35
|
+
this.#buffer.push({
|
|
36
|
+
eventType,
|
|
37
|
+
taskId,
|
|
38
|
+
payload: properties ?? {},
|
|
39
|
+
clientTimestamp: Date.now()
|
|
40
|
+
});
|
|
41
|
+
if (this.#buffer.length >= this.#maxBatchSize) {
|
|
42
|
+
this.flush();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
flush() {
|
|
46
|
+
if (this.#buffer.length === 0) return;
|
|
47
|
+
const events = this.#buffer;
|
|
48
|
+
this.#buffer = [];
|
|
49
|
+
const body = { events };
|
|
50
|
+
fetch(`${this.#workerUrl}/ingest`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Authorization: `Bearer ${this.#authToken}`
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(body)
|
|
57
|
+
}).catch(() => {
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
dispose() {
|
|
61
|
+
this.#disposed = true;
|
|
62
|
+
if (this.#timer) {
|
|
63
|
+
clearInterval(this.#timer);
|
|
64
|
+
this.#timer = null;
|
|
65
|
+
}
|
|
66
|
+
this.flush();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
function createMetricsCollector(workerUrl, authToken, telemetryEnabled) {
|
|
70
|
+
if (!telemetryEnabled || !workerUrl || !authToken) return NOOP_METRICS;
|
|
71
|
+
return new MetricsCollector(workerUrl, authToken);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/shared/file-watcher-guard.ts
|
|
75
|
+
import { fork as childProcessFork } from "child_process";
|
|
76
|
+
|
|
77
|
+
// src/services/watcher-worker/worker-supervisor.ts
|
|
78
|
+
import { randomUUID } from "crypto";
|
|
79
|
+
var ABNORMAL_SIGNALS = /* @__PURE__ */ new Set(["SIGABRT", "SIGSEGV", "SIGBUS", "SIGKILL"]);
|
|
80
|
+
var SHUTDOWN_GRACEFUL_MS = 2e3;
|
|
81
|
+
var SHUTDOWN_SIGTERM_MS = 500;
|
|
82
|
+
function createWatcherWorkerSupervisor(deps) {
|
|
83
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
84
|
+
let child = null;
|
|
85
|
+
let restartState = makeInitialAttemptState();
|
|
86
|
+
let nextGeneration = 1;
|
|
87
|
+
let shutdownInitiated = false;
|
|
88
|
+
let restartTimer = null;
|
|
89
|
+
let pendingRespawnGeneration = null;
|
|
90
|
+
function log(entry) {
|
|
91
|
+
deps.log(entry);
|
|
92
|
+
}
|
|
93
|
+
function captureMetric(eventType, properties) {
|
|
94
|
+
deps.metrics?.capture(eventType, properties);
|
|
95
|
+
}
|
|
96
|
+
function sendCommand(holder, cmd) {
|
|
97
|
+
const stdin = holder.process.stdin;
|
|
98
|
+
if (!stdin || stdin.destroyed || holder.exited) return false;
|
|
99
|
+
try {
|
|
100
|
+
stdin.write(`${encodeLine(cmd)}
|
|
101
|
+
`);
|
|
102
|
+
return true;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log({
|
|
105
|
+
event: "watcher_worker_stdin_write_failed",
|
|
106
|
+
err: err instanceof Error ? err.message : String(err)
|
|
107
|
+
});
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function dispatchReply(reply, fromGeneration) {
|
|
112
|
+
switch (reply.type) {
|
|
113
|
+
case "subscribed":
|
|
114
|
+
handleSubscribedReply(reply.id, fromGeneration);
|
|
115
|
+
return;
|
|
116
|
+
case "subscribe_failed":
|
|
117
|
+
handleSubscribeFailedReply(reply.id, reply.error);
|
|
118
|
+
return;
|
|
119
|
+
case "unsubscribed":
|
|
120
|
+
handleUnsubscribedReply(reply.id);
|
|
121
|
+
return;
|
|
122
|
+
case "events":
|
|
123
|
+
handleEventsReply(reply.id, reply.events);
|
|
124
|
+
return;
|
|
125
|
+
default:
|
|
126
|
+
assertNever(reply);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function handleSubscribedReply(id, fromGeneration) {
|
|
130
|
+
const entry = subscriptions.get(id);
|
|
131
|
+
if (!entry) return;
|
|
132
|
+
if (pendingRespawnGeneration !== null && fromGeneration === pendingRespawnGeneration) {
|
|
133
|
+
pendingRespawnGeneration = null;
|
|
134
|
+
const successDecision = decideAction(restartState, { kind: "subscribe_success" }, deps.now());
|
|
135
|
+
restartState = successDecision.state;
|
|
136
|
+
for (const sig of successDecision.signals) {
|
|
137
|
+
if (sig.kind === "circuit_closed") {
|
|
138
|
+
log({ event: "watcher_worker_circuit_closed" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (entry.status === "pending" && entry.resolveSubscribed) {
|
|
143
|
+
entry.status = "subscribed";
|
|
144
|
+
entry.resolveSubscribed(makeHandle(entry));
|
|
145
|
+
entry.resolveSubscribed = void 0;
|
|
146
|
+
entry.rejectSubscribed = void 0;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
entry.status = "subscribed";
|
|
150
|
+
}
|
|
151
|
+
function handleSubscribeFailedReply(id, error) {
|
|
152
|
+
const entry = subscriptions.get(id);
|
|
153
|
+
if (!entry) return;
|
|
154
|
+
subscriptions.delete(id);
|
|
155
|
+
entry.rejectSubscribed?.(new Error(error));
|
|
156
|
+
}
|
|
157
|
+
function handleUnsubscribedReply(id) {
|
|
158
|
+
const entry = subscriptions.get(id);
|
|
159
|
+
if (!entry) return;
|
|
160
|
+
subscriptions.delete(id);
|
|
161
|
+
entry.resolveUnsubscribed?.();
|
|
162
|
+
}
|
|
163
|
+
function handleEventsReply(id, events) {
|
|
164
|
+
const entry = subscriptions.get(id);
|
|
165
|
+
if (!entry || entry.status === "unsubscribing") return;
|
|
166
|
+
entry.callback(null, toCallbackEvents(events));
|
|
167
|
+
}
|
|
168
|
+
function attachChildIo(holder) {
|
|
169
|
+
const proc = holder.process;
|
|
170
|
+
const { stdout, stderr } = proc;
|
|
171
|
+
if (!stdout) {
|
|
172
|
+
log({ event: "watcher_worker_no_stdout" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let stdoutBuffer = "";
|
|
176
|
+
const fromGeneration = holder.generation;
|
|
177
|
+
stdout.setEncoding("utf8");
|
|
178
|
+
stdout.on("data", (chunk) => {
|
|
179
|
+
stdoutBuffer += chunk;
|
|
180
|
+
let nl = stdoutBuffer.indexOf("\n");
|
|
181
|
+
while (nl !== -1) {
|
|
182
|
+
const line = stdoutBuffer.slice(0, nl);
|
|
183
|
+
stdoutBuffer = stdoutBuffer.slice(nl + 1);
|
|
184
|
+
nl = stdoutBuffer.indexOf("\n");
|
|
185
|
+
if (line.length === 0) continue;
|
|
186
|
+
const decoded = decodeLine(line);
|
|
187
|
+
if (decoded === null) {
|
|
188
|
+
log({ event: "watcher_worker_decode_failed", line });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!("type" in decoded)) {
|
|
192
|
+
log({ event: "watcher_worker_unexpected_command", cmd: decoded.cmd });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
dispatchReply(decoded, fromGeneration);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (stderr) {
|
|
199
|
+
stderr.setEncoding("utf8");
|
|
200
|
+
let stderrBuffer = "";
|
|
201
|
+
stderr.on("data", (chunk) => {
|
|
202
|
+
stderrBuffer += chunk;
|
|
203
|
+
let nl = stderrBuffer.indexOf("\n");
|
|
204
|
+
while (nl !== -1) {
|
|
205
|
+
const line = stderrBuffer.slice(0, nl);
|
|
206
|
+
stderrBuffer = stderrBuffer.slice(nl + 1);
|
|
207
|
+
nl = stderrBuffer.indexOf("\n");
|
|
208
|
+
if (line.length > 0) {
|
|
209
|
+
log({ event: "watcher_worker_stderr", line });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
proc.on("exit", (code, signal) => {
|
|
215
|
+
handleChildExit(holder, code, signal);
|
|
216
|
+
});
|
|
217
|
+
proc.on("error", (err) => {
|
|
218
|
+
log({ event: "watcher_worker_error", err: err.message });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function handleChildExit(holder, code, signal) {
|
|
222
|
+
holder.exited = true;
|
|
223
|
+
if (child !== holder) return;
|
|
224
|
+
child = null;
|
|
225
|
+
if (pendingRespawnGeneration === holder.generation) {
|
|
226
|
+
pendingRespawnGeneration = null;
|
|
227
|
+
}
|
|
228
|
+
const replayCount = subscriptions.size;
|
|
229
|
+
const uptimeMs = deps.now() - holder.spawnedAt;
|
|
230
|
+
if (shutdownInitiated) {
|
|
231
|
+
log({ event: "watcher_worker_exited_during_shutdown", code, signal, uptimeMs });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const abnormal = isAbnormalExit(code, signal) || replayCount > 0;
|
|
235
|
+
if (!abnormal) {
|
|
236
|
+
log({ event: "watcher_worker_exited_clean", code, signal, uptimeMs });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
log({ event: "watcher_worker_died", code, signal, replayCount, uptimeMs });
|
|
240
|
+
captureMetric("watcher_worker_died", {
|
|
241
|
+
code,
|
|
242
|
+
signal,
|
|
243
|
+
replayCount,
|
|
244
|
+
uptimeMs
|
|
245
|
+
});
|
|
246
|
+
recordRestartFailure(holder);
|
|
247
|
+
fireSyntheticEvictionToAll();
|
|
248
|
+
scheduleRespawn();
|
|
249
|
+
}
|
|
250
|
+
function recordRestartFailure(holder) {
|
|
251
|
+
const failureDecision = decideAction(restartState, { kind: "subscribe_failure" }, deps.now());
|
|
252
|
+
restartState = failureDecision.state;
|
|
253
|
+
for (const sig of failureDecision.signals) {
|
|
254
|
+
if (sig.kind === "circuit_opened") {
|
|
255
|
+
log({ event: "watcher_worker_circuit_open" });
|
|
256
|
+
captureMetric("watcher_worker_circuit_open", {
|
|
257
|
+
deathCount: restartState.failureCount,
|
|
258
|
+
windowMs: deps.now() - holder.spawnedAt
|
|
259
|
+
});
|
|
260
|
+
} else if (sig.kind === "circuit_closed") {
|
|
261
|
+
log({ event: "watcher_worker_circuit_closed" });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function dispatchSyntheticEviction(entry) {
|
|
266
|
+
if (!subscriptions.has(entry.id)) return;
|
|
267
|
+
try {
|
|
268
|
+
entry.callback(null, [{ type: "evicted", path: entry.path }]);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
log({
|
|
271
|
+
event: "watcher_worker_synthetic_dispatch_failed",
|
|
272
|
+
id: entry.id,
|
|
273
|
+
err: err instanceof Error ? err.message : String(err)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function fireSyntheticEvictionToAll() {
|
|
278
|
+
const snapshot = Array.from(subscriptions.values());
|
|
279
|
+
queueMicrotask(() => {
|
|
280
|
+
for (const entry of snapshot) {
|
|
281
|
+
dispatchSyntheticEviction(entry);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function scheduleRespawn() {
|
|
286
|
+
if (shutdownInitiated) return;
|
|
287
|
+
if (restartTimer !== null) return;
|
|
288
|
+
const requestDecision = decideAction(
|
|
289
|
+
restartState,
|
|
290
|
+
{ kind: "request_subscribe", reason: "rescan", activeCount: 0 },
|
|
291
|
+
deps.now()
|
|
292
|
+
);
|
|
293
|
+
restartState = requestDecision.state;
|
|
294
|
+
switch (requestDecision.action.kind) {
|
|
295
|
+
case "reject_stub": {
|
|
296
|
+
const openedAt = restartState.circuitOpenedAt;
|
|
297
|
+
if (openedAt === null) {
|
|
298
|
+
attemptRespawn();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const cooldownEnd = openedAt + restartState.circuitCooldownMs;
|
|
302
|
+
const waitMs = Math.max(0, cooldownEnd - deps.now());
|
|
303
|
+
restartTimer = deps.setTimeout(() => {
|
|
304
|
+
restartTimer = null;
|
|
305
|
+
scheduleRespawn();
|
|
306
|
+
}, waitMs);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
case "wait": {
|
|
310
|
+
restartTimer = deps.setTimeout(() => {
|
|
311
|
+
restartTimer = null;
|
|
312
|
+
attemptRespawn();
|
|
313
|
+
}, requestDecision.action.ms);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
case "subscribe":
|
|
317
|
+
case "evict_and_subscribe":
|
|
318
|
+
attemptRespawn();
|
|
319
|
+
return;
|
|
320
|
+
case "noop":
|
|
321
|
+
attemptRespawn();
|
|
322
|
+
return;
|
|
323
|
+
default:
|
|
324
|
+
assertNever(requestDecision.action);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function attemptRespawn() {
|
|
328
|
+
if (shutdownInitiated) return;
|
|
329
|
+
if (restartState.circuitOpenedAt !== null) {
|
|
330
|
+
log({ event: "watcher_worker_probe_attempt" });
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
forkAndReplay();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
log({
|
|
336
|
+
event: "watcher_worker_fork_failed",
|
|
337
|
+
err: err instanceof Error ? err.message : String(err)
|
|
338
|
+
});
|
|
339
|
+
const failureDecision = decideAction(restartState, { kind: "subscribe_failure" }, deps.now());
|
|
340
|
+
restartState = failureDecision.state;
|
|
341
|
+
scheduleRespawn();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function forkAndReplay() {
|
|
345
|
+
const isRespawn = nextGeneration > 1;
|
|
346
|
+
const replayStart = deps.now();
|
|
347
|
+
const holder = spawnChildHolder();
|
|
348
|
+
if (isRespawn && subscriptions.size > 0) {
|
|
349
|
+
pendingRespawnGeneration = holder.generation;
|
|
350
|
+
}
|
|
351
|
+
const replayResult = replaySubscriptions(holder);
|
|
352
|
+
if (replayResult.kind === "pipe_failed") {
|
|
353
|
+
handleReplayPipeFailure(holder, replayResult.replayedCount);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (replayResult.replayedCount > 0) {
|
|
357
|
+
log({ event: "watcher_worker_replayed", replayCount: replayResult.replayedCount });
|
|
358
|
+
}
|
|
359
|
+
if (isRespawn) {
|
|
360
|
+
recordRespawnTelemetry(holder, replayResult.replayedCount, deps.now() - replayStart);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function spawnChildHolder() {
|
|
364
|
+
const proc = deps.fork(deps.workerPath, [], {
|
|
365
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
366
|
+
});
|
|
367
|
+
const holder = {
|
|
368
|
+
process: proc,
|
|
369
|
+
spawnedAt: deps.now(),
|
|
370
|
+
generation: nextGeneration++,
|
|
371
|
+
exited: false
|
|
372
|
+
};
|
|
373
|
+
child = holder;
|
|
374
|
+
log({ event: "watcher_worker_started", pid: proc.pid ?? null });
|
|
375
|
+
captureMetric("watcher_worker_started", { pid: proc.pid ?? null });
|
|
376
|
+
attachChildIo(holder);
|
|
377
|
+
return holder;
|
|
378
|
+
}
|
|
379
|
+
function replaySubscriptions(holder) {
|
|
380
|
+
let replayedCount = 0;
|
|
381
|
+
for (const entry of subscriptions.values()) {
|
|
382
|
+
entry.status = "pending";
|
|
383
|
+
const ok = sendCommand(holder, {
|
|
384
|
+
cmd: "subscribe",
|
|
385
|
+
id: entry.id,
|
|
386
|
+
path: entry.path,
|
|
387
|
+
opts: entry.opts
|
|
388
|
+
});
|
|
389
|
+
if (!ok) {
|
|
390
|
+
return { kind: "pipe_failed", replayedCount };
|
|
391
|
+
}
|
|
392
|
+
replayedCount++;
|
|
393
|
+
}
|
|
394
|
+
return { kind: "completed", replayedCount };
|
|
395
|
+
}
|
|
396
|
+
function handleReplayPipeFailure(holder, replayedCount) {
|
|
397
|
+
log({
|
|
398
|
+
event: "watcher_worker_replay_pipe_failed",
|
|
399
|
+
pid: holder.process.pid ?? null,
|
|
400
|
+
replayedCount,
|
|
401
|
+
pendingCount: subscriptions.size - replayedCount
|
|
402
|
+
});
|
|
403
|
+
recordRestartFailure(holder);
|
|
404
|
+
if (!holder.exited) {
|
|
405
|
+
try {
|
|
406
|
+
holder.process.kill("SIGKILL");
|
|
407
|
+
} catch (err) {
|
|
408
|
+
log({
|
|
409
|
+
event: "watcher_worker_kill_failed",
|
|
410
|
+
signal: "SIGKILL",
|
|
411
|
+
err: err instanceof Error ? err.message : String(err)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
scheduleRespawn();
|
|
416
|
+
}
|
|
417
|
+
function recordRespawnTelemetry(holder, replayCount, replayDurationMs) {
|
|
418
|
+
log({
|
|
419
|
+
event: "watcher_worker_respawned",
|
|
420
|
+
pid: holder.process.pid ?? null,
|
|
421
|
+
replayCount,
|
|
422
|
+
replayDurationMs
|
|
423
|
+
});
|
|
424
|
+
captureMetric("watcher_worker_respawned", {
|
|
425
|
+
newPid: holder.process.pid ?? null,
|
|
426
|
+
replayCount,
|
|
427
|
+
replayDurationMs
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function makeHandle(entry) {
|
|
431
|
+
return {
|
|
432
|
+
unsubscribe: () => unsubscribeEntry(entry)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function unsubscribeEntry(entry) {
|
|
436
|
+
if (!subscriptions.has(entry.id)) return Promise.resolve();
|
|
437
|
+
entry.status = "unsubscribing";
|
|
438
|
+
if (!child) {
|
|
439
|
+
subscriptions.delete(entry.id);
|
|
440
|
+
return Promise.resolve();
|
|
441
|
+
}
|
|
442
|
+
return new Promise((resolve) => {
|
|
443
|
+
entry.resolveUnsubscribed = resolve;
|
|
444
|
+
const sent = child !== null && sendCommand(child, { cmd: "unsubscribe", id: entry.id });
|
|
445
|
+
if (!sent) {
|
|
446
|
+
subscriptions.delete(entry.id);
|
|
447
|
+
resolve();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function makeStubHandle() {
|
|
452
|
+
return { unsubscribe: async () => {
|
|
453
|
+
} };
|
|
454
|
+
}
|
|
455
|
+
async function start() {
|
|
456
|
+
if (child !== null) return;
|
|
457
|
+
if (shutdownInitiated) {
|
|
458
|
+
throw new Error("WatcherWorkerSupervisor: cannot start after shutdown");
|
|
459
|
+
}
|
|
460
|
+
forkAndReplay();
|
|
461
|
+
}
|
|
462
|
+
async function subscribe(path, callback, opts) {
|
|
463
|
+
if (shutdownInitiated) {
|
|
464
|
+
throw new Error("WatcherWorkerSupervisor: cannot subscribe after shutdown");
|
|
465
|
+
}
|
|
466
|
+
if (restartState.circuitOpenedAt !== null) {
|
|
467
|
+
const cooldownEnd = restartState.circuitOpenedAt + restartState.circuitCooldownMs;
|
|
468
|
+
if (deps.now() < cooldownEnd) {
|
|
469
|
+
log({ event: "watcher_worker_subscribe_stubbed", path });
|
|
470
|
+
return makeStubHandle();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const id = randomUUID();
|
|
474
|
+
const entry = {
|
|
475
|
+
id,
|
|
476
|
+
path,
|
|
477
|
+
opts,
|
|
478
|
+
callback,
|
|
479
|
+
status: "pending"
|
|
480
|
+
};
|
|
481
|
+
const promise = new Promise((resolve, reject) => {
|
|
482
|
+
entry.resolveSubscribed = resolve;
|
|
483
|
+
entry.rejectSubscribed = reject;
|
|
484
|
+
});
|
|
485
|
+
subscriptions.set(id, entry);
|
|
486
|
+
if (child === null) {
|
|
487
|
+
await start();
|
|
488
|
+
} else {
|
|
489
|
+
sendCommand(child, { cmd: "subscribe", id, path, opts });
|
|
490
|
+
}
|
|
491
|
+
return promise;
|
|
492
|
+
}
|
|
493
|
+
async function shutdown() {
|
|
494
|
+
if (shutdownInitiated) return;
|
|
495
|
+
shutdownInitiated = true;
|
|
496
|
+
if (restartTimer !== null) {
|
|
497
|
+
deps.clearTimeout(restartTimer);
|
|
498
|
+
restartTimer = null;
|
|
499
|
+
}
|
|
500
|
+
for (const entry of subscriptions.values()) {
|
|
501
|
+
entry.rejectSubscribed?.(new Error("WatcherWorkerSupervisor: shutdown"));
|
|
502
|
+
entry.rejectSubscribed = void 0;
|
|
503
|
+
entry.resolveSubscribed = void 0;
|
|
504
|
+
entry.resolveUnsubscribed?.();
|
|
505
|
+
entry.resolveUnsubscribed = void 0;
|
|
506
|
+
}
|
|
507
|
+
subscriptions.clear();
|
|
508
|
+
const holder = child;
|
|
509
|
+
if (holder === null) return;
|
|
510
|
+
sendCommand(holder, { cmd: "shutdown" });
|
|
511
|
+
await escalateShutdown(holder);
|
|
512
|
+
}
|
|
513
|
+
function killWithSignal(holder, signal) {
|
|
514
|
+
log({
|
|
515
|
+
event: signal === "SIGTERM" ? "watcher_worker_shutdown_sigterm" : "watcher_worker_shutdown_sigkill"
|
|
516
|
+
});
|
|
517
|
+
try {
|
|
518
|
+
holder.process.kill(signal);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
log({
|
|
521
|
+
event: "watcher_worker_kill_failed",
|
|
522
|
+
signal,
|
|
523
|
+
err: err instanceof Error ? err.message : String(err)
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function escalateShutdown(holder) {
|
|
528
|
+
return new Promise((resolve) => {
|
|
529
|
+
const ctx = { done: false };
|
|
530
|
+
const finish = () => {
|
|
531
|
+
if (ctx.done) return;
|
|
532
|
+
ctx.done = true;
|
|
533
|
+
resolve();
|
|
534
|
+
};
|
|
535
|
+
holder.process.once("exit", finish);
|
|
536
|
+
if (holder.exited) {
|
|
537
|
+
finish();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
deps.setTimeout(() => onSigtermDeadline(holder, ctx, finish), SHUTDOWN_GRACEFUL_MS);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function onSigtermDeadline(holder, ctx, finish) {
|
|
544
|
+
if (ctx.done || holder.exited) return;
|
|
545
|
+
killWithSignal(holder, "SIGTERM");
|
|
546
|
+
deps.setTimeout(() => onSigkillDeadline(holder, ctx, finish), SHUTDOWN_SIGTERM_MS);
|
|
547
|
+
}
|
|
548
|
+
function onSigkillDeadline(holder, ctx, finish) {
|
|
549
|
+
if (ctx.done || holder.exited) return;
|
|
550
|
+
killWithSignal(holder, "SIGKILL");
|
|
551
|
+
finish();
|
|
552
|
+
}
|
|
553
|
+
return { start, subscribe, shutdown };
|
|
554
|
+
}
|
|
555
|
+
function isAbnormalExit(code, signal) {
|
|
556
|
+
if (signal !== null && ABNORMAL_SIGNALS.has(signal)) return true;
|
|
557
|
+
if (code !== null && code !== 0) return true;
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
function toCallbackEvents(events) {
|
|
561
|
+
return events.map((e) => ({ type: e.type, path: e.path }));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/shared/file-watcher-guard.ts
|
|
565
|
+
var DEFAULT_MAX_ACTIVE_WATCHERS = 450;
|
|
566
|
+
var ENV_MAX = process.env.SHIPYARD_FILE_WATCHER_MAX;
|
|
567
|
+
var PARSED_ENV_MAX = ENV_MAX ? Number.parseInt(ENV_MAX, 10) : Number.NaN;
|
|
568
|
+
var MAX_ACTIVE_WATCHERS = Number.isFinite(PARSED_ENV_MAX) && PARSED_ENV_MAX > 0 ? PARSED_ENV_MAX : DEFAULT_MAX_ACTIVE_WATCHERS;
|
|
569
|
+
var STARTING_BACKOFF_MS_INITIAL = 250;
|
|
570
|
+
var STARTING_BACKOFF_MS_ESCALATION = 1e3;
|
|
571
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
572
|
+
var BACKOFF_RESET_AFTER_MS = 5 * 6e4;
|
|
573
|
+
var CIRCUIT_FAILURES = 5;
|
|
574
|
+
var CIRCUIT_WINDOW_MS = 6e4;
|
|
575
|
+
var CIRCUIT_OPEN_MS = 5 * 6e4;
|
|
576
|
+
var CIRCUIT_OPEN_MAX_MS = 30 * 6e4;
|
|
577
|
+
var ESSENTIAL_RESUBSCRIBE_DELAY_MS = 1e3;
|
|
578
|
+
function makeInitialAttemptState() {
|
|
579
|
+
return {
|
|
580
|
+
failureCount: 0,
|
|
581
|
+
backoffMs: 0,
|
|
582
|
+
lastFailureAt: 0,
|
|
583
|
+
circuitOpenedAt: null,
|
|
584
|
+
circuitCooldownMs: CIRCUIT_OPEN_MS,
|
|
585
|
+
recentFailures: [],
|
|
586
|
+
halfOpenAt: null
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function isCleanAttemptState(state) {
|
|
590
|
+
return state.circuitOpenedAt === null && state.failureCount === 0 && state.backoffMs === 0 && state.recentFailures.length === 0 && state.halfOpenAt === null;
|
|
591
|
+
}
|
|
592
|
+
function decideAction(state, event, now) {
|
|
593
|
+
switch (event.kind) {
|
|
594
|
+
case "request_subscribe":
|
|
595
|
+
return decideRequest(state, event, now);
|
|
596
|
+
case "subscribe_success":
|
|
597
|
+
return decideSuccess(state, now);
|
|
598
|
+
case "subscribe_failure":
|
|
599
|
+
return decideFailure(state, now);
|
|
600
|
+
default:
|
|
601
|
+
return assertNever(event);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function decideRequest(state, event, now) {
|
|
605
|
+
const decayed = decayBackoff(state, now);
|
|
606
|
+
if (decayed.circuitOpenedAt !== null) {
|
|
607
|
+
const cooldownEnd = decayed.circuitOpenedAt + decayed.circuitCooldownMs;
|
|
608
|
+
if (now < cooldownEnd) {
|
|
609
|
+
return {
|
|
610
|
+
state: decayed,
|
|
611
|
+
action: { kind: "reject_stub", reason: "circuit_open" },
|
|
612
|
+
signals: []
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
state: { ...decayed, halfOpenAt: now },
|
|
617
|
+
action: event.activeCount >= MAX_ACTIVE_WATCHERS ? { kind: "evict_and_subscribe" } : { kind: "subscribe" },
|
|
618
|
+
signals: [{ kind: "circuit_probe" }]
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (decayed.backoffMs > 0 && now < decayed.lastFailureAt + decayed.backoffMs) {
|
|
622
|
+
const remaining = decayed.lastFailureAt + decayed.backoffMs - now;
|
|
623
|
+
return { state: decayed, action: { kind: "wait", ms: remaining }, signals: [] };
|
|
624
|
+
}
|
|
625
|
+
let nextState = decayed;
|
|
626
|
+
if (decayed.backoffMs === 0 && event.reason === "escalation") {
|
|
627
|
+
nextState = { ...decayed, backoffMs: STARTING_BACKOFF_MS_ESCALATION };
|
|
628
|
+
}
|
|
629
|
+
if (event.activeCount >= MAX_ACTIVE_WATCHERS) {
|
|
630
|
+
return { state: nextState, action: { kind: "evict_and_subscribe" }, signals: [] };
|
|
631
|
+
}
|
|
632
|
+
return { state: nextState, action: { kind: "subscribe" }, signals: [] };
|
|
633
|
+
}
|
|
634
|
+
function decideSuccess(state, _now) {
|
|
635
|
+
const wasHalfOpen = state.halfOpenAt !== null;
|
|
636
|
+
const next = {
|
|
637
|
+
failureCount: 0,
|
|
638
|
+
backoffMs: 0,
|
|
639
|
+
lastFailureAt: 0,
|
|
640
|
+
circuitOpenedAt: null,
|
|
641
|
+
circuitCooldownMs: CIRCUIT_OPEN_MS,
|
|
642
|
+
recentFailures: [],
|
|
643
|
+
halfOpenAt: null
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
state: next,
|
|
647
|
+
action: { kind: "noop" },
|
|
648
|
+
signals: wasHalfOpen ? [{ kind: "circuit_closed" }] : []
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function decideFailure(state, now) {
|
|
652
|
+
const recent = [...state.recentFailures.filter((t) => now - t < CIRCUIT_WINDOW_MS), now];
|
|
653
|
+
const baseBackoff = state.backoffMs === 0 ? STARTING_BACKOFF_MS_INITIAL : Math.min(state.backoffMs * 2, MAX_BACKOFF_MS);
|
|
654
|
+
if (state.halfOpenAt !== null) {
|
|
655
|
+
const nextCooldown = Math.min(state.circuitCooldownMs * 2, CIRCUIT_OPEN_MAX_MS);
|
|
656
|
+
return {
|
|
657
|
+
state: {
|
|
658
|
+
...state,
|
|
659
|
+
failureCount: state.failureCount + 1,
|
|
660
|
+
backoffMs: baseBackoff,
|
|
661
|
+
lastFailureAt: now,
|
|
662
|
+
recentFailures: recent,
|
|
663
|
+
circuitOpenedAt: now,
|
|
664
|
+
circuitCooldownMs: nextCooldown,
|
|
665
|
+
halfOpenAt: null
|
|
666
|
+
},
|
|
667
|
+
action: { kind: "noop" },
|
|
668
|
+
signals: [{ kind: "circuit_opened" }]
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (recent.length >= CIRCUIT_FAILURES && state.circuitOpenedAt === null) {
|
|
672
|
+
return {
|
|
673
|
+
state: {
|
|
674
|
+
...state,
|
|
675
|
+
failureCount: state.failureCount + 1,
|
|
676
|
+
backoffMs: baseBackoff,
|
|
677
|
+
lastFailureAt: now,
|
|
678
|
+
recentFailures: recent,
|
|
679
|
+
circuitOpenedAt: now,
|
|
680
|
+
circuitCooldownMs: CIRCUIT_OPEN_MS,
|
|
681
|
+
halfOpenAt: null
|
|
682
|
+
},
|
|
683
|
+
action: { kind: "noop" },
|
|
684
|
+
signals: [{ kind: "circuit_opened" }]
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
state: {
|
|
689
|
+
...state,
|
|
690
|
+
failureCount: state.failureCount + 1,
|
|
691
|
+
backoffMs: baseBackoff,
|
|
692
|
+
lastFailureAt: now,
|
|
693
|
+
recentFailures: recent
|
|
694
|
+
},
|
|
695
|
+
action: { kind: "noop" },
|
|
696
|
+
signals: []
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function decayBackoff(state, now) {
|
|
700
|
+
if (state.lastFailureAt === 0 || state.backoffMs === 0) return state;
|
|
701
|
+
const sinceLast = now - state.lastFailureAt;
|
|
702
|
+
if (sinceLast >= BACKOFF_RESET_AFTER_MS) {
|
|
703
|
+
return { ...state, backoffMs: 0, failureCount: 0, recentFailures: [] };
|
|
704
|
+
}
|
|
705
|
+
const halvings = Math.floor(sinceLast / 6e4);
|
|
706
|
+
if (halvings === 0) return state;
|
|
707
|
+
const next = Math.max(STARTING_BACKOFF_MS_INITIAL, state.backoffMs >> halvings);
|
|
708
|
+
return { ...state, backoffMs: next };
|
|
709
|
+
}
|
|
710
|
+
var moduleState = makeModuleState();
|
|
711
|
+
function makeModuleState() {
|
|
712
|
+
return {
|
|
713
|
+
active: /* @__PURE__ */ new Set(),
|
|
714
|
+
reservations: /* @__PURE__ */ new Set(),
|
|
715
|
+
perPath: /* @__PURE__ */ new Map(),
|
|
716
|
+
loggedOpenAt: /* @__PURE__ */ new Map(),
|
|
717
|
+
deps: defaultDeps(),
|
|
718
|
+
supervisor: null,
|
|
719
|
+
supervisorMetrics: null
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
var IN_VITEST = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
723
|
+
function defaultDeps() {
|
|
724
|
+
return {
|
|
725
|
+
/**
|
|
726
|
+
* Production path: route every subscribe through the watcher worker
|
|
727
|
+
* supervisor (which runs `@parcel/watcher` in a subprocess). The
|
|
728
|
+
* supervisor delivers `WatcherWorkerEventLike[]` whose 'evicted' variant
|
|
729
|
+
* is fired during worker respawns; we widen it to `GuardedEvent[]`
|
|
730
|
+
* (which already accommodates 'evicted' via the LRU eviction path).
|
|
731
|
+
*/
|
|
732
|
+
subscribe: (path, fn, opts) => {
|
|
733
|
+
if (IN_VITEST) return legacyDirectSubscribe(path, fn, opts);
|
|
734
|
+
return supervisorSubscribe(path, fn, opts);
|
|
735
|
+
},
|
|
736
|
+
log: () => {
|
|
737
|
+
},
|
|
738
|
+
now: () => Date.now(),
|
|
739
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
740
|
+
clearTimeout: (timer) => {
|
|
741
|
+
if (timer === null || timer === void 0) return;
|
|
742
|
+
clearTimeout(timer);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
var supervisorSubscribe = async (path, fn, opts) => {
|
|
747
|
+
const supervisor = ensureSupervisor();
|
|
748
|
+
const handle = await supervisor.subscribe(
|
|
749
|
+
path,
|
|
750
|
+
(err, events) => {
|
|
751
|
+
fn(err, events.map(toGuardedEvent));
|
|
752
|
+
},
|
|
753
|
+
toSupervisorOpts(opts)
|
|
754
|
+
);
|
|
755
|
+
return { unsubscribe: () => handle.unsubscribe() };
|
|
756
|
+
};
|
|
757
|
+
function toSupervisorOpts(opts) {
|
|
758
|
+
const out = {};
|
|
759
|
+
if (!opts) return out;
|
|
760
|
+
if (opts.ignore) out.ignore = opts.ignore;
|
|
761
|
+
if (opts.backend === "brute-force" || opts.backend === "watchman") {
|
|
762
|
+
out.backend = opts.backend;
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
function toGuardedEvent(e) {
|
|
767
|
+
switch (e.type) {
|
|
768
|
+
case "evicted":
|
|
769
|
+
return { type: "evicted", path: e.path };
|
|
770
|
+
case "create":
|
|
771
|
+
case "update":
|
|
772
|
+
case "delete":
|
|
773
|
+
return { type: e.type, path: e.path };
|
|
774
|
+
default:
|
|
775
|
+
return assertNever(e.type);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function ensureSupervisor() {
|
|
779
|
+
if (moduleState.supervisor) return moduleState.supervisor;
|
|
780
|
+
const workerPath = new URL("./worker.js", import.meta.url).pathname;
|
|
781
|
+
moduleState.supervisor = createWatcherWorkerSupervisor({
|
|
782
|
+
fork: nodeFork,
|
|
783
|
+
workerPath,
|
|
784
|
+
log: (entry) => moduleState.deps.log(entry),
|
|
785
|
+
metrics: moduleState.supervisorMetrics ?? void 0,
|
|
786
|
+
now: () => moduleState.deps.now(),
|
|
787
|
+
setTimeout: (fn, ms) => moduleState.deps.setTimeout(fn, ms),
|
|
788
|
+
clearTimeout: (timer) => moduleState.deps.clearTimeout(timer)
|
|
789
|
+
});
|
|
790
|
+
return moduleState.supervisor;
|
|
791
|
+
}
|
|
792
|
+
var nodeFork = (modulePath, args, options) => childProcessFork(modulePath, args, options);
|
|
793
|
+
function occupancy() {
|
|
794
|
+
return moduleState.active.size + moduleState.reservations.size;
|
|
795
|
+
}
|
|
796
|
+
function configureFileWatcherGuard(deps) {
|
|
797
|
+
moduleState.deps = { ...moduleState.deps, log: deps.log };
|
|
798
|
+
if (deps.metrics) {
|
|
799
|
+
moduleState.supervisorMetrics = deps.metrics;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
var legacyDirectSubscribe = async (path, fn, opts) => {
|
|
803
|
+
const mod = await import("@parcel/watcher");
|
|
804
|
+
return mod.subscribe(
|
|
805
|
+
path,
|
|
806
|
+
(err, events) => {
|
|
807
|
+
const widened = events.map((e) => ({ type: e.type, path: e.path }));
|
|
808
|
+
fn(err, widened);
|
|
809
|
+
},
|
|
810
|
+
opts
|
|
811
|
+
);
|
|
812
|
+
};
|
|
813
|
+
async function shutdownFileWatcherGuard() {
|
|
814
|
+
const supervisor = moduleState.supervisor;
|
|
815
|
+
if (!supervisor) return;
|
|
816
|
+
moduleState.supervisor = null;
|
|
817
|
+
await supervisor.shutdown();
|
|
818
|
+
}
|
|
819
|
+
async function guardedSubscribe(path, callback, opts, tier, reason) {
|
|
820
|
+
const { deps } = moduleState;
|
|
821
|
+
const now = deps.now();
|
|
822
|
+
const priorState = moduleState.perPath.get(path) ?? makeInitialAttemptState();
|
|
823
|
+
const decision = decideAction(
|
|
824
|
+
priorState,
|
|
825
|
+
{ kind: "request_subscribe", reason, activeCount: occupancy() },
|
|
826
|
+
now
|
|
827
|
+
);
|
|
828
|
+
moduleState.perPath.set(path, decision.state);
|
|
829
|
+
emitSignals(path, decision.signals);
|
|
830
|
+
switch (decision.action.kind) {
|
|
831
|
+
case "reject_stub":
|
|
832
|
+
return makeStubSubscription();
|
|
833
|
+
case "wait": {
|
|
834
|
+
const waitMs = decision.action.ms;
|
|
835
|
+
await new Promise((resolve) => deps.setTimeout(() => resolve(), waitMs));
|
|
836
|
+
return guardedSubscribe(path, callback, opts, tier, "escalation");
|
|
837
|
+
}
|
|
838
|
+
case "evict_and_subscribe":
|
|
839
|
+
evictOne(path);
|
|
840
|
+
return performSubscribe(path, callback, opts, tier);
|
|
841
|
+
case "subscribe":
|
|
842
|
+
return performSubscribe(path, callback, opts, tier);
|
|
843
|
+
case "noop":
|
|
844
|
+
throw new Error("guardedSubscribe: unexpected noop on request");
|
|
845
|
+
default:
|
|
846
|
+
return assertNever(decision.action);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function performSubscribe(path, consumerCallback, opts, tier) {
|
|
850
|
+
const { deps } = moduleState;
|
|
851
|
+
let entry = null;
|
|
852
|
+
const reservation = { path, tier, reservedAt: deps.now() };
|
|
853
|
+
moduleState.reservations.add(reservation);
|
|
854
|
+
const wrapped = (err, events) => {
|
|
855
|
+
if (entry) entry.lastUsedAt = deps.now();
|
|
856
|
+
consumerCallback(err, events);
|
|
857
|
+
};
|
|
858
|
+
try {
|
|
859
|
+
const sub = await deps.subscribe(path, wrapped, opts);
|
|
860
|
+
moduleState.reservations.delete(reservation);
|
|
861
|
+
const now = deps.now();
|
|
862
|
+
const priorState = moduleState.perPath.get(path) ?? makeInitialAttemptState();
|
|
863
|
+
const successDecision = decideAction(priorState, { kind: "subscribe_success" }, now);
|
|
864
|
+
if (isCleanAttemptState(successDecision.state)) {
|
|
865
|
+
moduleState.perPath.delete(path);
|
|
866
|
+
} else {
|
|
867
|
+
moduleState.perPath.set(path, successDecision.state);
|
|
868
|
+
}
|
|
869
|
+
emitSignals(path, successDecision.signals);
|
|
870
|
+
entry = {
|
|
871
|
+
path,
|
|
872
|
+
tier,
|
|
873
|
+
lastUsedAt: now,
|
|
874
|
+
unsubscribe: () => sub.unsubscribe(),
|
|
875
|
+
consumerCallback
|
|
876
|
+
};
|
|
877
|
+
moduleState.active.add(entry);
|
|
878
|
+
deps.log({ event: "file_watcher_active_count", count: moduleState.active.size });
|
|
879
|
+
return {
|
|
880
|
+
unsubscribe: async () => {
|
|
881
|
+
if (entry && moduleState.active.has(entry)) {
|
|
882
|
+
moduleState.active.delete(entry);
|
|
883
|
+
}
|
|
884
|
+
const current = moduleState.perPath.get(path);
|
|
885
|
+
if (current && isCleanAttemptState(current)) {
|
|
886
|
+
moduleState.perPath.delete(path);
|
|
887
|
+
}
|
|
888
|
+
await sub.unsubscribe();
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
} catch (err) {
|
|
892
|
+
moduleState.reservations.delete(reservation);
|
|
893
|
+
const now = deps.now();
|
|
894
|
+
const priorState = moduleState.perPath.get(path) ?? makeInitialAttemptState();
|
|
895
|
+
const failureDecision = decideAction(priorState, { kind: "subscribe_failure" }, now);
|
|
896
|
+
moduleState.perPath.set(path, failureDecision.state);
|
|
897
|
+
deps.log({
|
|
898
|
+
event: "file_watcher_subscribe_failed",
|
|
899
|
+
path,
|
|
900
|
+
err: err instanceof Error ? err.message : String(err)
|
|
901
|
+
});
|
|
902
|
+
emitSignals(path, failureDecision.signals);
|
|
903
|
+
if (failureDecision.state.circuitOpenedAt !== null) {
|
|
904
|
+
return makeStubSubscription();
|
|
905
|
+
}
|
|
906
|
+
throw err;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function pickOldest(entries, tier) {
|
|
910
|
+
let target = null;
|
|
911
|
+
for (const entry of entries) {
|
|
912
|
+
if (tier !== null && entry.tier !== tier) continue;
|
|
913
|
+
if (target === null || entry.lastUsedAt < target.lastUsedAt) target = entry;
|
|
914
|
+
}
|
|
915
|
+
return target;
|
|
916
|
+
}
|
|
917
|
+
function selectEvictionVictim(entries) {
|
|
918
|
+
const lazy = pickOldest(entries, "lazy");
|
|
919
|
+
if (lazy !== null) return { victim: lazy, evictingEssential: false };
|
|
920
|
+
const any = pickOldest(entries, null);
|
|
921
|
+
if (any !== null) return { victim: any, evictingEssential: true };
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
function evictOne(incomingPath) {
|
|
925
|
+
const { deps } = moduleState;
|
|
926
|
+
if (moduleState.active.size === 0) {
|
|
927
|
+
throw new Error("file-watcher-guard: cannot evict \u2014 active set is empty (counter is broken)");
|
|
928
|
+
}
|
|
929
|
+
const selection = selectEvictionVictim(moduleState.active);
|
|
930
|
+
if (selection === null) {
|
|
931
|
+
throw new Error("file-watcher-guard: eviction selection failed");
|
|
932
|
+
}
|
|
933
|
+
const { victim, evictingEssential } = selection;
|
|
934
|
+
moduleState.active.delete(victim);
|
|
935
|
+
deps.log({
|
|
936
|
+
event: evictingEssential ? "file_watcher_essential_evicted" : "file_watcher_evicted",
|
|
937
|
+
path: victim.path,
|
|
938
|
+
tier: victim.tier,
|
|
939
|
+
incomingPath
|
|
940
|
+
});
|
|
941
|
+
queueMicrotask(() => {
|
|
942
|
+
victim.consumerCallback(null, [{ type: "evicted", path: victim.path }]);
|
|
943
|
+
});
|
|
944
|
+
victim.unsubscribe().catch((err) => {
|
|
945
|
+
deps.log({
|
|
946
|
+
event: "file_watcher_unsubscribe_failed_during_eviction",
|
|
947
|
+
path: victim.path,
|
|
948
|
+
err: err instanceof Error ? err.message : String(err)
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
if (evictingEssential) {
|
|
952
|
+
scheduleEssentialResubscribe(victim);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function scheduleEssentialResubscribe(victim) {
|
|
956
|
+
const { deps } = moduleState;
|
|
957
|
+
deps.log({
|
|
958
|
+
event: "file_watcher_essential_re_subscribe_scheduled",
|
|
959
|
+
path: victim.path,
|
|
960
|
+
delayMs: ESSENTIAL_RESUBSCRIBE_DELAY_MS
|
|
961
|
+
});
|
|
962
|
+
deps.setTimeout(() => {
|
|
963
|
+
void retryEssentialSubscribe(victim);
|
|
964
|
+
}, ESSENTIAL_RESUBSCRIBE_DELAY_MS);
|
|
965
|
+
}
|
|
966
|
+
async function retryEssentialSubscribe(victim) {
|
|
967
|
+
const { deps } = moduleState;
|
|
968
|
+
try {
|
|
969
|
+
await guardedSubscribe(victim.path, victim.consumerCallback, {}, "essential", "rescan");
|
|
970
|
+
deps.log({ event: "file_watcher_essential_re_subscribed", path: victim.path });
|
|
971
|
+
} catch (err) {
|
|
972
|
+
deps.log({
|
|
973
|
+
event: "file_watcher_essential_re_subscribe_failed",
|
|
974
|
+
path: victim.path,
|
|
975
|
+
err: err instanceof Error ? err.message : String(err)
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function emitSignals(path, signals) {
|
|
980
|
+
const { deps } = moduleState;
|
|
981
|
+
for (const sig of signals) {
|
|
982
|
+
switch (sig.kind) {
|
|
983
|
+
case "circuit_opened":
|
|
984
|
+
deps.log({ event: "file_watcher_circuit_open", path });
|
|
985
|
+
moduleState.loggedOpenAt.set(path, deps.now());
|
|
986
|
+
break;
|
|
987
|
+
case "circuit_closed":
|
|
988
|
+
deps.log({ event: "file_watcher_circuit_closed", path });
|
|
989
|
+
moduleState.loggedOpenAt.delete(path);
|
|
990
|
+
break;
|
|
991
|
+
case "circuit_probe":
|
|
992
|
+
break;
|
|
993
|
+
default:
|
|
994
|
+
assertNever(sig);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function makeStubSubscription() {
|
|
999
|
+
return { unsubscribe: async () => {
|
|
1000
|
+
} };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export {
|
|
1004
|
+
createMetricsCollector,
|
|
1005
|
+
makeInitialAttemptState,
|
|
1006
|
+
decideAction,
|
|
1007
|
+
configureFileWatcherGuard,
|
|
1008
|
+
shutdownFileWatcherGuard,
|
|
1009
|
+
guardedSubscribe
|
|
1010
|
+
};
|
|
1011
|
+
//# sourceMappingURL=chunk-3MNPDCO5.js.map
|