@juspay/neurolink 9.93.2 → 9.94.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/browser/neurolink.min.js +263 -263
  3. package/dist/cli/commands/proxy.js +672 -182
  4. package/dist/cli/utils/audioPlayer.d.ts +44 -9
  5. package/dist/cli/utils/audioPlayer.js +162 -65
  6. package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
  7. package/dist/lib/proxy/rollingProxyServer.js +149 -0
  8. package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
  9. package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
  10. package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
  11. package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
  12. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
  14. package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
  15. package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
  16. package/dist/lib/skills/skillMatcher.d.ts +7 -1
  17. package/dist/lib/skills/skillMatcher.js +23 -8
  18. package/dist/lib/types/cli.d.ts +47 -0
  19. package/dist/lib/types/proxy.d.ts +156 -0
  20. package/dist/lib/utils/errorHandling.d.ts +8 -0
  21. package/dist/lib/utils/errorHandling.js +20 -0
  22. package/dist/proxy/rollingProxyServer.d.ts +3 -0
  23. package/dist/proxy/rollingProxyServer.js +148 -0
  24. package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
  25. package/dist/proxy/rollingWorkerProcess.js +193 -0
  26. package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
  27. package/dist/proxy/rollingWorkerProtocol.js +42 -0
  28. package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
  29. package/dist/proxy/rollingWorkerSupervisor.js +401 -0
  30. package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
  31. package/dist/proxy/socketWorkerRuntime.js +293 -0
  32. package/dist/skills/skillMatcher.d.ts +7 -1
  33. package/dist/skills/skillMatcher.js +23 -8
  34. package/dist/types/cli.d.ts +47 -0
  35. package/dist/types/proxy.d.ts +156 -0
  36. package/dist/utils/errorHandling.d.ts +8 -0
  37. package/dist/utils/errorHandling.js +20 -0
  38. package/package.json +3 -2
@@ -0,0 +1,194 @@
1
+ import { spawn } from "node:child_process";
2
+ import { ErrorFactory } from "../utils/errorHandling.js";
3
+ import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, } from "./rollingWorkerProtocol.js";
4
+ export function spawnProxySocketWorker(options) {
5
+ const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 5_000);
6
+ let nextSocketId = 0;
7
+ const pendingSockets = new Map();
8
+ const statusListeners = new Set();
9
+ const pendingStatusMessages = [];
10
+ let spawnError;
11
+ const child = spawn(options.command, options.args, {
12
+ env: {
13
+ ...process.env,
14
+ ...options.env,
15
+ [PROXY_SOCKET_WORKER_ENV]: "1",
16
+ NEUROLINK_PROXY_WORKER_GENERATION: String(options.generation),
17
+ NEUROLINK_PROXY_WORKER_EXPECTED_VERSION: options.expectedVersion,
18
+ },
19
+ stdio: [
20
+ "ignore",
21
+ options.stdout ?? "inherit",
22
+ options.stderr ?? "inherit",
23
+ "ipc",
24
+ ],
25
+ });
26
+ const childPid = child.pid;
27
+ const publishStatus = (message) => {
28
+ if (statusListeners.size === 0) {
29
+ if (pendingStatusMessages.length >= 32) {
30
+ pendingStatusMessages.shift();
31
+ }
32
+ pendingStatusMessages.push(message);
33
+ return;
34
+ }
35
+ for (const listener of statusListeners) {
36
+ listener(message);
37
+ }
38
+ };
39
+ child.once("error", (error) => {
40
+ spawnError = error;
41
+ if (!childPid) {
42
+ return;
43
+ }
44
+ publishStatus({
45
+ type: "proxy-worker:fatal",
46
+ generation: options.generation,
47
+ pid: childPid,
48
+ message: error.message,
49
+ });
50
+ });
51
+ if (!childPid) {
52
+ child.kill("SIGTERM");
53
+ throw ErrorFactory.proxyWorkerLifecycle("proxy worker spawn did not return a pid");
54
+ }
55
+ const settleSocket = (socketId, error) => {
56
+ const pending = pendingSockets.get(socketId);
57
+ if (!pending) {
58
+ return;
59
+ }
60
+ pendingSockets.delete(socketId);
61
+ clearTimeout(pending.timeout);
62
+ if (!error) {
63
+ pending.socket.destroy();
64
+ }
65
+ pending.callback(error);
66
+ };
67
+ const onInternalMessage = (message) => {
68
+ if (isProxyWorkerStatusMessage(message) &&
69
+ message.type === "proxy-worker:socket-accepted" &&
70
+ message.generation === options.generation &&
71
+ message.pid === childPid) {
72
+ const pending = pendingSockets.get(message.socketId);
73
+ if (!pending || pending.accepted) {
74
+ return;
75
+ }
76
+ pending.accepted = true;
77
+ try {
78
+ child.send({
79
+ type: "proxy-worker:socket-commit",
80
+ generation: options.generation,
81
+ socketId: message.socketId,
82
+ }, (error) => settleSocket(message.socketId, error ?? undefined));
83
+ }
84
+ catch (error) {
85
+ settleSocket(message.socketId, error instanceof Error ? error : new Error(String(error)));
86
+ }
87
+ }
88
+ };
89
+ child.on("message", (message) => {
90
+ onInternalMessage(message);
91
+ if (isProxyWorkerStatusMessage(message)) {
92
+ publishStatus(message);
93
+ }
94
+ });
95
+ child.once("exit", () => {
96
+ for (const socketId of [...pendingSockets.keys()]) {
97
+ settleSocket(socketId, new Error(`proxy worker ${childPid} exited before accepting socket`));
98
+ }
99
+ });
100
+ const sendControl = (message) => {
101
+ if (!child.connected) {
102
+ throw new Error(`proxy worker ${childPid} IPC channel is closed`);
103
+ }
104
+ child.send(message, (error) => {
105
+ if (error && child.exitCode === null && child.signalCode === null) {
106
+ child.kill("SIGTERM");
107
+ }
108
+ });
109
+ };
110
+ return {
111
+ pid: childPid,
112
+ sendControl,
113
+ sendSocket: (generation, socket, callback) => {
114
+ if (!child.connected) {
115
+ callback(new Error(`proxy worker ${childPid} IPC channel is closed`));
116
+ return;
117
+ }
118
+ const socketId = `${generation}:${++nextSocketId}`;
119
+ const timeout = setTimeout(() => {
120
+ if (child.connected) {
121
+ try {
122
+ child.send({
123
+ type: "proxy-worker:socket-cancel",
124
+ generation,
125
+ socketId,
126
+ });
127
+ }
128
+ catch {
129
+ // The worker is terminated after the failed transfer is reported.
130
+ }
131
+ }
132
+ settleSocket(socketId, new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`));
133
+ }, socketAckTimeoutMs);
134
+ timeout.unref?.();
135
+ pendingSockets.set(socketId, {
136
+ socket,
137
+ callback,
138
+ timeout,
139
+ accepted: false,
140
+ });
141
+ try {
142
+ child.send({ type: "proxy-worker:socket", generation, socketId }, socket, { keepOpen: true }, (error) => {
143
+ if (error) {
144
+ settleSocket(socketId, error);
145
+ }
146
+ });
147
+ }
148
+ catch (error) {
149
+ settleSocket(socketId, error instanceof Error ? error : new Error(String(error)));
150
+ }
151
+ },
152
+ terminate: (signal = "SIGTERM") => {
153
+ if (child.exitCode === null && child.signalCode === null) {
154
+ child.kill(signal);
155
+ }
156
+ },
157
+ onMessage: (listener) => {
158
+ statusListeners.add(listener);
159
+ if (pendingStatusMessages.length > 0) {
160
+ const buffered = pendingStatusMessages.splice(0);
161
+ queueMicrotask(() => {
162
+ if (statusListeners.has(listener)) {
163
+ for (const message of buffered) {
164
+ listener(message);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ else if (spawnError) {
170
+ const error = spawnError;
171
+ queueMicrotask(() => {
172
+ if (statusListeners.has(listener)) {
173
+ listener({
174
+ type: "proxy-worker:fatal",
175
+ generation: options.generation,
176
+ pid: childPid,
177
+ message: error.message,
178
+ });
179
+ }
180
+ });
181
+ }
182
+ return () => statusListeners.delete(listener);
183
+ },
184
+ onExit: (listener) => {
185
+ if (child.exitCode !== null || child.signalCode !== null) {
186
+ queueMicrotask(() => listener(child.exitCode, child.signalCode));
187
+ return () => undefined;
188
+ }
189
+ child.on("exit", listener);
190
+ return () => child.off("exit", listener);
191
+ },
192
+ };
193
+ }
194
+ //# sourceMappingURL=rollingWorkerProcess.js.map
@@ -0,0 +1,5 @@
1
+ import type { ProxyWorkerControlMessage, ProxyWorkerStatusMessage } from "../types/index.js";
2
+ export declare const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
3
+ export declare const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
4
+ export declare function isProxyWorkerControlMessage(value: unknown): value is ProxyWorkerControlMessage;
5
+ export declare function isProxyWorkerStatusMessage(value: unknown): value is ProxyWorkerStatusMessage;
@@ -0,0 +1,43 @@
1
+ export const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
2
+ export const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
3
+ export function isProxyWorkerControlMessage(value) {
4
+ if (!value || typeof value !== "object") {
5
+ return false;
6
+ }
7
+ const message = value;
8
+ if (!Number.isSafeInteger(message.generation) ||
9
+ Number(message.generation) <= 0) {
10
+ return false;
11
+ }
12
+ if (message.type === "proxy-worker:socket-commit" ||
13
+ message.type === "proxy-worker:socket-cancel") {
14
+ return typeof message.socketId === "string" && message.socketId.length > 0;
15
+ }
16
+ return (message.type === "proxy-worker:drain" ||
17
+ message.type === "proxy-worker:activate" ||
18
+ message.type === "proxy-worker:shutdown");
19
+ }
20
+ export function isProxyWorkerStatusMessage(value) {
21
+ if (!value || typeof value !== "object") {
22
+ return false;
23
+ }
24
+ const message = value;
25
+ if (!Number.isSafeInteger(message.generation) ||
26
+ Number(message.generation) <= 0 ||
27
+ !Number.isSafeInteger(message.pid) ||
28
+ Number(message.pid) <= 0) {
29
+ return false;
30
+ }
31
+ if (message.type === "proxy-worker:ready") {
32
+ return typeof message.version === "string" && message.version.length > 0;
33
+ }
34
+ if (message.type === "proxy-worker:activated" ||
35
+ message.type === "proxy-worker:drained") {
36
+ return true;
37
+ }
38
+ if (message.type === "proxy-worker:socket-accepted") {
39
+ return typeof message.socketId === "string" && message.socketId.length > 0;
40
+ }
41
+ return (message.type === "proxy-worker:fatal" && typeof message.message === "string");
42
+ }
43
+ //# sourceMappingURL=rollingWorkerProtocol.js.map
@@ -0,0 +1,39 @@
1
+ import type { RollingWorkerSupervisorOptions, RollingWorkerSupervisorSnapshot, TransferableProxySocket } from "../types/index.js";
2
+ /**
3
+ * Owns worker generations while the caller owns the public listening socket.
4
+ * Sockets are transferred once to the active worker, so response bytes never
5
+ * traverse a supervisor-side proxy hop.
6
+ */
7
+ export declare class RollingWorkerSupervisor {
8
+ private readonly options;
9
+ private generation;
10
+ private active;
11
+ private candidate;
12
+ private readonly draining;
13
+ private readonly queuedSockets;
14
+ private replacement;
15
+ private rejectedSockets;
16
+ private failedTransfers;
17
+ private lastFailure;
18
+ private closed;
19
+ private shutdownPromise;
20
+ private readonly shutdownWaiters;
21
+ constructor(options: RollingWorkerSupervisorOptions);
22
+ snapshot(): RollingWorkerSupervisorSnapshot;
23
+ start(expectedVersion: string): Promise<RollingWorkerSupervisorSnapshot>;
24
+ replace(expectedVersion: string): Promise<RollingWorkerSupervisorSnapshot>;
25
+ acceptSocket(socket: TransferableProxySocket): void;
26
+ private queueSocket;
27
+ close(): Promise<void>;
28
+ private closeWorkers;
29
+ private requestWorkerShutdown;
30
+ private forceTerminateWorkers;
31
+ private notifyShutdownWaiters;
32
+ private spawnCandidate;
33
+ private flushQueuedSockets;
34
+ private transferSocket;
35
+ private handleTransferFailure;
36
+ private rejectSocket;
37
+ private recordFailure;
38
+ private publishState;
39
+ }
@@ -0,0 +1,402 @@
1
+ import { ErrorFactory } from "../utils/errorHandling.js";
2
+ const DEFAULT_READY_TIMEOUT_MS = 30_000;
3
+ const DEFAULT_SOCKET_QUEUE_LIMIT = 1_024;
4
+ const DEFAULT_SOCKET_QUEUE_TIMEOUT_MS = 30_000;
5
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
6
+ /**
7
+ * Owns worker generations while the caller owns the public listening socket.
8
+ * Sockets are transferred once to the active worker, so response bytes never
9
+ * traverse a supervisor-side proxy hop.
10
+ */
11
+ export class RollingWorkerSupervisor {
12
+ options;
13
+ generation = 0;
14
+ active = null;
15
+ candidate = null;
16
+ draining = new Map();
17
+ queuedSockets = [];
18
+ replacement = null;
19
+ rejectedSockets = 0;
20
+ failedTransfers = 0;
21
+ lastFailure = null;
22
+ closed = false;
23
+ shutdownPromise = null;
24
+ shutdownWaiters = new Set();
25
+ constructor(options) {
26
+ this.options = {
27
+ ...options,
28
+ readyTimeoutMs: options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
29
+ socketQueueLimit: options.socketQueueLimit ?? DEFAULT_SOCKET_QUEUE_LIMIT,
30
+ socketQueueTimeoutMs: options.socketQueueTimeoutMs ?? DEFAULT_SOCKET_QUEUE_TIMEOUT_MS,
31
+ shutdownTimeoutMs: options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
32
+ };
33
+ }
34
+ snapshot() {
35
+ return {
36
+ generation: this.generation,
37
+ active: this.active
38
+ ? {
39
+ pid: this.active.handle.pid,
40
+ version: this.active.version,
41
+ generation: this.active.generation,
42
+ }
43
+ : null,
44
+ candidate: this.candidate
45
+ ? {
46
+ pid: this.candidate.handle.pid,
47
+ expectedVersion: this.candidate.expectedVersion,
48
+ generation: this.candidate.generation,
49
+ }
50
+ : null,
51
+ draining: [...this.draining.values()].map((worker) => ({
52
+ pid: worker.handle.pid,
53
+ version: worker.version,
54
+ generation: worker.generation,
55
+ })),
56
+ queuedSockets: this.queuedSockets.length,
57
+ rejectedSockets: this.rejectedSockets,
58
+ failedTransfers: this.failedTransfers,
59
+ lastFailure: this.lastFailure,
60
+ };
61
+ }
62
+ start(expectedVersion) {
63
+ if (this.active) {
64
+ return this.active.version === expectedVersion
65
+ ? Promise.resolve(this.snapshot())
66
+ : this.replace(expectedVersion);
67
+ }
68
+ return this.replace(expectedVersion);
69
+ }
70
+ replace(expectedVersion) {
71
+ if (this.closed) {
72
+ return Promise.reject(ErrorFactory.proxyWorkerLifecycle("rolling worker supervisor is closed"));
73
+ }
74
+ if (!/^\d+\.\d+\.\d+$/.test(expectedVersion)) {
75
+ return Promise.reject(ErrorFactory.proxyWorkerLifecycle(`invalid expected worker version: ${expectedVersion}`, { expectedVersion }));
76
+ }
77
+ if (this.replacement) {
78
+ return this.candidate?.expectedVersion === expectedVersion
79
+ ? this.replacement
80
+ : Promise.reject(ErrorFactory.proxyWorkerLifecycle(`worker replacement for v${this.candidate?.expectedVersion ?? "unknown"} is already in progress`, { requestedVersion: expectedVersion }));
81
+ }
82
+ this.lastFailure = null;
83
+ this.replacement = this.spawnCandidate(expectedVersion).finally(() => {
84
+ this.replacement = null;
85
+ });
86
+ return this.replacement;
87
+ }
88
+ acceptSocket(socket) {
89
+ socket.pause();
90
+ if (this.closed) {
91
+ this.rejectSocket(socket);
92
+ return;
93
+ }
94
+ if (this.active) {
95
+ this.transferSocket(this.active, socket);
96
+ return;
97
+ }
98
+ this.queueSocket(socket);
99
+ }
100
+ queueSocket(socket) {
101
+ if (this.queuedSockets.length >= this.options.socketQueueLimit) {
102
+ this.rejectSocket(socket);
103
+ return;
104
+ }
105
+ const queued = {
106
+ socket,
107
+ timeout: setTimeout(() => {
108
+ const index = this.queuedSockets.indexOf(queued);
109
+ if (index >= 0) {
110
+ this.queuedSockets.splice(index, 1);
111
+ this.rejectSocket(socket);
112
+ }
113
+ }, this.options.socketQueueTimeoutMs),
114
+ };
115
+ queued.timeout.unref?.();
116
+ this.queuedSockets.push(queued);
117
+ this.publishState();
118
+ }
119
+ close() {
120
+ if (this.shutdownPromise) {
121
+ return this.shutdownPromise;
122
+ }
123
+ this.closed = true;
124
+ this.shutdownPromise = this.closeWorkers();
125
+ return this.shutdownPromise;
126
+ }
127
+ async closeWorkers() {
128
+ for (const queued of this.queuedSockets.splice(0)) {
129
+ clearTimeout(queued.timeout);
130
+ this.rejectSocket(queued.socket);
131
+ }
132
+ if (this.candidate) {
133
+ this.candidate.settle(new Error("rolling worker supervisor closed during replacement"));
134
+ }
135
+ if (this.active) {
136
+ this.requestWorkerShutdown(this.active);
137
+ }
138
+ for (const worker of this.draining.values()) {
139
+ this.requestWorkerShutdown(worker);
140
+ }
141
+ this.publishState();
142
+ if (!this.active && this.draining.size === 0) {
143
+ return;
144
+ }
145
+ await new Promise((resolve) => {
146
+ const finish = () => {
147
+ clearTimeout(timeout);
148
+ this.shutdownWaiters.delete(finish);
149
+ resolve();
150
+ };
151
+ const timeout = setTimeout(() => {
152
+ this.forceTerminateWorkers();
153
+ finish();
154
+ }, this.options.shutdownTimeoutMs);
155
+ timeout.unref?.();
156
+ this.shutdownWaiters.add(finish);
157
+ });
158
+ }
159
+ requestWorkerShutdown(worker) {
160
+ try {
161
+ worker.handle.sendControl({
162
+ type: "proxy-worker:shutdown",
163
+ generation: worker.generation,
164
+ });
165
+ }
166
+ catch {
167
+ worker.handle.terminate("SIGTERM");
168
+ }
169
+ }
170
+ forceTerminateWorkers() {
171
+ if (this.active) {
172
+ this.active.handle.terminate("SIGTERM");
173
+ this.active.dispose();
174
+ this.active = null;
175
+ }
176
+ for (const worker of this.draining.values()) {
177
+ worker.handle.terminate("SIGTERM");
178
+ worker.dispose();
179
+ }
180
+ this.draining.clear();
181
+ this.publishState();
182
+ }
183
+ notifyShutdownWaiters() {
184
+ if (!this.closed || this.active || this.draining.size > 0) {
185
+ return;
186
+ }
187
+ for (const finish of [...this.shutdownWaiters]) {
188
+ finish();
189
+ }
190
+ }
191
+ spawnCandidate(expectedVersion) {
192
+ const generation = ++this.generation;
193
+ let handle;
194
+ try {
195
+ handle = this.options.spawnWorker(generation, expectedVersion);
196
+ }
197
+ catch (error) {
198
+ this.recordFailure(generation, expectedVersion, "startup", error instanceof Error ? error.message : String(error));
199
+ this.publishState();
200
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
201
+ }
202
+ return new Promise((resolve, reject) => {
203
+ let settled = false;
204
+ const finish = (error, phase = "startup") => {
205
+ if (settled) {
206
+ return;
207
+ }
208
+ settled = true;
209
+ clearTimeout(readyTimeout);
210
+ if (error) {
211
+ if (!this.closed) {
212
+ this.recordFailure(generation, expectedVersion, phase, error.message);
213
+ }
214
+ if (this.candidate?.generation === generation) {
215
+ this.candidate = null;
216
+ }
217
+ handle.terminate("SIGTERM");
218
+ dispose();
219
+ this.publishState();
220
+ reject(error);
221
+ return;
222
+ }
223
+ resolve(this.snapshot());
224
+ };
225
+ const offMessage = handle.onMessage((message) => {
226
+ if (message.generation !== generation || message.pid !== handle.pid) {
227
+ return;
228
+ }
229
+ if (message.type === "proxy-worker:fatal") {
230
+ const error = new Error(`worker ${handle.pid} failed: ${message.message}`);
231
+ if (this.candidate?.generation === generation) {
232
+ finish(error);
233
+ }
234
+ else if (this.active?.generation === generation) {
235
+ this.active.dispose();
236
+ this.active = null;
237
+ handle.terminate("SIGTERM");
238
+ this.recordFailure(generation, expectedVersion, "runtime", message.message);
239
+ this.options.log?.(`[proxy-supervisor] active worker reported fatal generation=${generation} pid=${handle.pid}: ${message.message}`);
240
+ this.publishState();
241
+ this.notifyShutdownWaiters();
242
+ }
243
+ return;
244
+ }
245
+ if (message.type === "proxy-worker:ready") {
246
+ if (message.version !== expectedVersion) {
247
+ finish(new Error(`worker ${handle.pid} reported v${message.version}; expected v${expectedVersion}`));
248
+ return;
249
+ }
250
+ const candidate = this.candidate;
251
+ if (!candidate) {
252
+ return;
253
+ }
254
+ if (!candidate.activationRequested) {
255
+ try {
256
+ handle.sendControl({
257
+ type: "proxy-worker:activate",
258
+ generation,
259
+ });
260
+ candidate.activationRequested = true;
261
+ }
262
+ catch (error) {
263
+ finish(error instanceof Error ? error : new Error(String(error)), "activation");
264
+ }
265
+ }
266
+ return;
267
+ }
268
+ if (message.type !== "proxy-worker:activated") {
269
+ return;
270
+ }
271
+ if (!this.candidate?.activationRequested) {
272
+ finish(new Error(`worker ${handle.pid} acknowledged activation before readiness`), "activation");
273
+ return;
274
+ }
275
+ const previous = this.active;
276
+ const activated = {
277
+ handle,
278
+ generation,
279
+ version: expectedVersion,
280
+ dispose,
281
+ };
282
+ this.active = activated;
283
+ this.candidate = null;
284
+ this.flushQueuedSockets();
285
+ if (previous) {
286
+ this.draining.set(previous.generation, previous);
287
+ try {
288
+ previous.handle.sendControl({
289
+ type: "proxy-worker:drain",
290
+ generation: previous.generation,
291
+ });
292
+ }
293
+ catch {
294
+ previous.handle.terminate("SIGTERM");
295
+ }
296
+ }
297
+ this.options.log?.(`[proxy-supervisor] activated generation=${generation} pid=${handle.pid} version=${expectedVersion}`);
298
+ this.publishState();
299
+ finish();
300
+ });
301
+ const offExit = handle.onExit((code, signal) => {
302
+ if (this.candidate?.generation === generation) {
303
+ finish(new Error(`worker ${handle.pid} exited before readiness (code=${code ?? "none"}, signal=${signal ?? "none"})`));
304
+ return;
305
+ }
306
+ if (this.active?.generation === generation) {
307
+ this.active.dispose();
308
+ this.active = null;
309
+ if (!this.closed) {
310
+ this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})`);
311
+ }
312
+ this.options.log?.(`[proxy-supervisor] active worker exited generation=${generation} pid=${handle.pid}`);
313
+ }
314
+ const drained = this.draining.get(generation);
315
+ if (drained) {
316
+ drained.dispose();
317
+ this.draining.delete(generation);
318
+ }
319
+ this.publishState();
320
+ this.notifyShutdownWaiters();
321
+ });
322
+ const dispose = () => {
323
+ offMessage();
324
+ offExit();
325
+ };
326
+ const readyTimeout = setTimeout(() => {
327
+ finish(new Error(`worker ${handle.pid} did not become ready within ${this.options.readyTimeoutMs}ms`));
328
+ }, this.options.readyTimeoutMs);
329
+ readyTimeout.unref?.();
330
+ this.candidate = {
331
+ handle,
332
+ generation,
333
+ version: expectedVersion,
334
+ expectedVersion,
335
+ activationRequested: false,
336
+ dispose,
337
+ settle: finish,
338
+ };
339
+ this.publishState();
340
+ });
341
+ }
342
+ flushQueuedSockets() {
343
+ // Capture the active worker once: a synchronous sendSocket failure inside
344
+ // transferSocket can clear this.active mid-loop, and re-reading it would
345
+ // pass null into transferSocket and strand the queued socket.
346
+ const worker = this.active;
347
+ if (!worker) {
348
+ return;
349
+ }
350
+ for (const queued of this.queuedSockets.splice(0)) {
351
+ clearTimeout(queued.timeout);
352
+ this.transferSocket(worker, queued.socket);
353
+ }
354
+ }
355
+ transferSocket(worker, socket) {
356
+ try {
357
+ worker.handle.sendSocket(worker.generation, socket, (error) => {
358
+ if (error) {
359
+ this.handleTransferFailure(worker, socket);
360
+ }
361
+ });
362
+ }
363
+ catch {
364
+ this.handleTransferFailure(worker, socket);
365
+ }
366
+ }
367
+ handleTransferFailure(worker, socket) {
368
+ this.failedTransfers += 1;
369
+ this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket`);
370
+ if (this.active?.generation === worker.generation && !this.closed) {
371
+ // Do not let worker-side graceful shutdown call shutdown(2) on an
372
+ // offered-but-uncommitted duplicate descriptor.
373
+ worker.dispose();
374
+ worker.handle.terminate("SIGKILL");
375
+ this.active = null;
376
+ }
377
+ this.rejectSocket(socket);
378
+ }
379
+ rejectSocket(socket) {
380
+ this.rejectedSockets += 1;
381
+ socket.destroy();
382
+ this.publishState();
383
+ }
384
+ recordFailure(generation, version, phase, message) {
385
+ this.lastFailure = {
386
+ at: new Date().toISOString(),
387
+ generation,
388
+ version,
389
+ phase,
390
+ message: message.slice(0, 1_000),
391
+ };
392
+ }
393
+ publishState() {
394
+ try {
395
+ this.options.onStateChange?.(this.snapshot());
396
+ }
397
+ catch (error) {
398
+ this.options.log?.(`[proxy-supervisor] failed to persist supervisor state: ${error instanceof Error ? error.message : String(error)}`);
399
+ }
400
+ }
401
+ }
402
+ //# sourceMappingURL=rollingWorkerSupervisor.js.map