@juspay/neurolink 9.93.1 → 9.94.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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +306 -306
- package/dist/cli/commands/proxy.js +687 -200
- package/dist/cli/utils/serverUtils.js +4 -1
- package/dist/lib/proxy/globalInstaller.js +1 -1
- package/dist/lib/proxy/openaiFormat.js +1 -0
- package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
- package/dist/lib/proxy/rollingProxyServer.js +149 -0
- package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
- package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
- package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
- package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
- package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
- package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
- package/dist/lib/proxy/sseInterceptor.js +1 -1
- package/dist/lib/proxy/workerLog.d.ts +6 -0
- package/dist/lib/proxy/workerLog.js +42 -0
- package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/lib/types/cli.d.ts +38 -0
- package/dist/lib/types/proxy.d.ts +156 -0
- package/dist/lib/utils/errorHandling.d.ts +8 -0
- package/dist/lib/utils/errorHandling.js +20 -0
- package/dist/proxy/globalInstaller.js +1 -1
- package/dist/proxy/openaiFormat.js +1 -0
- package/dist/proxy/rollingProxyServer.d.ts +3 -0
- package/dist/proxy/rollingProxyServer.js +148 -0
- package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
- package/dist/proxy/rollingWorkerProcess.js +193 -0
- package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
- package/dist/proxy/rollingWorkerProtocol.js +42 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
- package/dist/proxy/rollingWorkerSupervisor.js +401 -0
- package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
- package/dist/proxy/socketWorkerRuntime.js +293 -0
- package/dist/proxy/sseInterceptor.js +1 -1
- package/dist/proxy/workerLog.d.ts +6 -0
- package/dist/proxy/workerLog.js +41 -0
- package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/types/cli.d.ts +38 -0
- package/dist/types/proxy.d.ts +156 -0
- package/dist/utils/errorHandling.d.ts +8 -0
- package/dist/utils/errorHandling.js +20 -0
- package/package.json +3 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { isProxyWorkerControlMessage } from "./rollingWorkerProtocol.js";
|
|
2
|
+
function isTransferableProxySocket(handle) {
|
|
3
|
+
if (!handle || typeof handle !== "object") {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const candidate = handle;
|
|
7
|
+
return (typeof candidate.pause === "function" &&
|
|
8
|
+
typeof candidate.resume === "function" &&
|
|
9
|
+
typeof candidate.destroy === "function" &&
|
|
10
|
+
typeof candidate.end === "function" &&
|
|
11
|
+
typeof candidate.once === "function");
|
|
12
|
+
}
|
|
13
|
+
function destroyTransferredHandle(handle) {
|
|
14
|
+
if (handle &&
|
|
15
|
+
typeof handle === "object" &&
|
|
16
|
+
typeof handle.destroy === "function") {
|
|
17
|
+
try {
|
|
18
|
+
handle.destroy();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Invalid IPC handles must not crash the serving worker.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adapts transferred TCP sockets to an HTTP server without opening another
|
|
27
|
+
* listener. Active responses finish normally; idle keep-alive sockets close as
|
|
28
|
+
* soon as draining begins.
|
|
29
|
+
*/
|
|
30
|
+
export function createSocketWorkerRuntime(server, options) {
|
|
31
|
+
const sockets = new Set();
|
|
32
|
+
const activeBySocket = new Map();
|
|
33
|
+
const activeResponses = new Set();
|
|
34
|
+
let draining = false;
|
|
35
|
+
let drained = false;
|
|
36
|
+
const maybeFinishDrain = () => {
|
|
37
|
+
if (draining && !drained && sockets.size === 0) {
|
|
38
|
+
drained = true;
|
|
39
|
+
options?.onDrained?.();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const requestStarted = (request, response) => {
|
|
43
|
+
const socket = request.socket;
|
|
44
|
+
activeBySocket.set(socket, (activeBySocket.get(socket) ?? 0) + 1);
|
|
45
|
+
activeResponses.add(response);
|
|
46
|
+
if (draining) {
|
|
47
|
+
response.shouldKeepAlive = false;
|
|
48
|
+
}
|
|
49
|
+
let settled = false;
|
|
50
|
+
const settle = () => {
|
|
51
|
+
if (settled) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
settled = true;
|
|
55
|
+
activeResponses.delete(response);
|
|
56
|
+
const remaining = Math.max(0, (activeBySocket.get(socket) ?? 1) - 1);
|
|
57
|
+
if (remaining === 0) {
|
|
58
|
+
activeBySocket.delete(socket);
|
|
59
|
+
if (draining) {
|
|
60
|
+
socket.end();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
activeBySocket.set(socket, remaining);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
response.once("finish", settle);
|
|
68
|
+
response.once("close", settle);
|
|
69
|
+
};
|
|
70
|
+
server.prependListener("request", requestStarted);
|
|
71
|
+
const acceptSocket = (socket) => {
|
|
72
|
+
if (draining) {
|
|
73
|
+
socket.destroy();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sockets.add(socket);
|
|
77
|
+
socket.once("close", () => {
|
|
78
|
+
sockets.delete(socket);
|
|
79
|
+
activeBySocket.delete(socket);
|
|
80
|
+
maybeFinishDrain();
|
|
81
|
+
});
|
|
82
|
+
server.emit("connection", socket);
|
|
83
|
+
socket.resume();
|
|
84
|
+
};
|
|
85
|
+
const drain = () => {
|
|
86
|
+
if (draining) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
draining = true;
|
|
90
|
+
for (const response of activeResponses) {
|
|
91
|
+
response.shouldKeepAlive = false;
|
|
92
|
+
}
|
|
93
|
+
for (const socket of sockets) {
|
|
94
|
+
if (!activeBySocket.has(socket)) {
|
|
95
|
+
socket.end();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
maybeFinishDrain();
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
acceptSocket,
|
|
102
|
+
drain,
|
|
103
|
+
close: () => {
|
|
104
|
+
server.off("request", requestStarted);
|
|
105
|
+
for (const socket of sockets) {
|
|
106
|
+
socket.destroy();
|
|
107
|
+
}
|
|
108
|
+
sockets.clear();
|
|
109
|
+
activeBySocket.clear();
|
|
110
|
+
activeResponses.clear();
|
|
111
|
+
draining = true;
|
|
112
|
+
maybeFinishDrain();
|
|
113
|
+
},
|
|
114
|
+
snapshot: () => ({
|
|
115
|
+
draining,
|
|
116
|
+
sockets: sockets.size,
|
|
117
|
+
activeRequests: [...activeBySocket.values()].reduce((total, count) => total + count, 0),
|
|
118
|
+
drained,
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function attachSocketWorkerProcess(server, input) {
|
|
123
|
+
let activated = false;
|
|
124
|
+
let gracefulDrain = false;
|
|
125
|
+
const pendingSockets = new Map();
|
|
126
|
+
const send = (message) => {
|
|
127
|
+
if (!process.connected || !process.send) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
process.send(message);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Parent supervision handles the disconnected worker.
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const runtime = createSocketWorkerRuntime(server, {
|
|
138
|
+
onDrained: () => {
|
|
139
|
+
send({
|
|
140
|
+
type: "proxy-worker:drained",
|
|
141
|
+
generation: input.generation,
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
});
|
|
144
|
+
input.onDrained?.();
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const drainWhenPendingSettled = () => {
|
|
148
|
+
if (!gracefulDrain || pendingSockets.size > 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Defer to a macrotask so a request buffered on a just-committed socket can
|
|
152
|
+
// reach the HTTP server before drain() ends idle sockets; draining
|
|
153
|
+
// synchronously here would truncate that in-flight request.
|
|
154
|
+
setImmediate(() => runtime.drain());
|
|
155
|
+
};
|
|
156
|
+
const onMessage = (message, handle) => {
|
|
157
|
+
if (message &&
|
|
158
|
+
typeof message === "object" &&
|
|
159
|
+
message.type === "proxy-worker:socket") {
|
|
160
|
+
const socketMessage = message;
|
|
161
|
+
if (socketMessage.generation === input.generation &&
|
|
162
|
+
typeof message.socketId === "string" &&
|
|
163
|
+
message.socketId.length > 0 &&
|
|
164
|
+
isTransferableProxySocket(handle)) {
|
|
165
|
+
const socketId = message.socketId;
|
|
166
|
+
const socket = handle;
|
|
167
|
+
socket.pause();
|
|
168
|
+
if (!activated ||
|
|
169
|
+
!process.connected ||
|
|
170
|
+
!process.send ||
|
|
171
|
+
gracefulDrain) {
|
|
172
|
+
socket.destroy();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (pendingSockets.has(socketId)) {
|
|
176
|
+
socket.destroy();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
pendingSockets.set(socketId, socket);
|
|
180
|
+
try {
|
|
181
|
+
process.send({
|
|
182
|
+
type: "proxy-worker:socket-accepted",
|
|
183
|
+
generation: input.generation,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
socketId,
|
|
186
|
+
}, (error) => {
|
|
187
|
+
if (error) {
|
|
188
|
+
pendingSockets.delete(socketId);
|
|
189
|
+
socket.destroy();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
pendingSockets.delete(socketId);
|
|
195
|
+
socket.destroy();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
destroyTransferredHandle(handle);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (handle !== undefined) {
|
|
204
|
+
destroyTransferredHandle(handle);
|
|
205
|
+
}
|
|
206
|
+
if (isProxyWorkerControlMessage(message) &&
|
|
207
|
+
message.generation === input.generation) {
|
|
208
|
+
if (message.type === "proxy-worker:activate") {
|
|
209
|
+
if (!activated) {
|
|
210
|
+
try {
|
|
211
|
+
input.onActivated?.();
|
|
212
|
+
activated = true;
|
|
213
|
+
send({
|
|
214
|
+
type: "proxy-worker:activated",
|
|
215
|
+
generation: input.generation,
|
|
216
|
+
pid: process.pid,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
send({
|
|
221
|
+
type: "proxy-worker:fatal",
|
|
222
|
+
generation: input.generation,
|
|
223
|
+
pid: process.pid,
|
|
224
|
+
message: error instanceof Error ? error.message : String(error),
|
|
225
|
+
});
|
|
226
|
+
runtime.drain();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (message.type === "proxy-worker:socket-commit" ||
|
|
231
|
+
message.type === "proxy-worker:socket-cancel") {
|
|
232
|
+
const socket = pendingSockets.get(message.socketId);
|
|
233
|
+
if (!socket) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
pendingSockets.delete(message.socketId);
|
|
237
|
+
if (message.type === "proxy-worker:socket-commit") {
|
|
238
|
+
runtime.acceptSocket(socket);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
socket.destroy();
|
|
242
|
+
}
|
|
243
|
+
drainWhenPendingSettled();
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Graceful drain: a socket-commit for an already-accepted socket may
|
|
247
|
+
// still be in flight. Stop admitting new offers and defer the drain
|
|
248
|
+
// until every pending socket is committed or canceled, so an
|
|
249
|
+
// acknowledged connection is served instead of silently dropped.
|
|
250
|
+
gracefulDrain = true;
|
|
251
|
+
drainWhenPendingSettled();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
// Terminal shutdown path: the parent IPC channel is gone (disconnect) or the
|
|
256
|
+
// process is being killed (SIGTERM/SIGINT). Unlike the graceful rolling drain
|
|
257
|
+
// above, pending offers can no longer be committed, so they are destroyed and
|
|
258
|
+
// the drain runs immediately — a best-effort close is correct when the process
|
|
259
|
+
// is exiting. The zero-downtime guarantee applies to the rolling handoff
|
|
260
|
+
// (control-message) path, not to process termination.
|
|
261
|
+
const drain = () => {
|
|
262
|
+
for (const socket of pendingSockets.values()) {
|
|
263
|
+
socket.destroy();
|
|
264
|
+
}
|
|
265
|
+
pendingSockets.clear();
|
|
266
|
+
runtime.drain();
|
|
267
|
+
};
|
|
268
|
+
const onTerminationSignal = () => drain();
|
|
269
|
+
process.on("message", onMessage);
|
|
270
|
+
process.once("disconnect", drain);
|
|
271
|
+
process.once("SIGTERM", onTerminationSignal);
|
|
272
|
+
process.once("SIGINT", onTerminationSignal);
|
|
273
|
+
send({
|
|
274
|
+
type: "proxy-worker:ready",
|
|
275
|
+
generation: input.generation,
|
|
276
|
+
pid: process.pid,
|
|
277
|
+
version: input.version,
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
...runtime,
|
|
281
|
+
close: () => {
|
|
282
|
+
process.off("message", onMessage);
|
|
283
|
+
process.off("disconnect", drain);
|
|
284
|
+
process.off("SIGTERM", onTerminationSignal);
|
|
285
|
+
process.off("SIGINT", onTerminationSignal);
|
|
286
|
+
for (const socket of pendingSockets.values()) {
|
|
287
|
+
socket.destroy();
|
|
288
|
+
}
|
|
289
|
+
pendingSockets.clear();
|
|
290
|
+
runtime.close();
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=socketWorkerRuntime.js.map
|
|
@@ -337,7 +337,7 @@ function processEvent(acc, event) {
|
|
|
337
337
|
const message = nestedError?.message ?? payload.message;
|
|
338
338
|
acc.streamErrorMessage =
|
|
339
339
|
typeof message === "string" && message.trim()
|
|
340
|
-
? message.trim()
|
|
340
|
+
? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
|
|
341
341
|
: truncateString(event.data, MAX_EVENT_DATA_BYTES);
|
|
342
342
|
}
|
|
343
343
|
switch (event.event) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, statSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
/** Open a restrictive worker log without making proxy startup depend on it. */
|
|
5
|
+
export function openProxyWorkerLog(filename, logDir = join(homedir(), ".neurolink", "logs")) {
|
|
6
|
+
let fd;
|
|
7
|
+
try {
|
|
8
|
+
if (basename(filename) !== filename) {
|
|
9
|
+
throw new Error("worker log filename must not contain a path");
|
|
10
|
+
}
|
|
11
|
+
if (!existsSync(logDir)) {
|
|
12
|
+
mkdirSync(logDir, { recursive: true, mode: 0o700 });
|
|
13
|
+
}
|
|
14
|
+
else if (!statSync(logDir).isDirectory()) {
|
|
15
|
+
throw new Error("worker log path exists but is not a directory");
|
|
16
|
+
}
|
|
17
|
+
chmodSync(logDir, 0o700);
|
|
18
|
+
const path = join(logDir, filename);
|
|
19
|
+
fd = openSync(path, "a", 0o600);
|
|
20
|
+
chmodSync(path, 0o600);
|
|
21
|
+
return {
|
|
22
|
+
stdio: fd,
|
|
23
|
+
close: () => closeSync(fd),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (fd !== undefined) {
|
|
28
|
+
try {
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// The log is optional and startup must remain fail-open.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
stdio: "ignore",
|
|
37
|
+
close: () => undefined,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=workerLog.js.map
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
|
|
13
13
|
*/
|
|
14
14
|
import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } from "../../types/index.js";
|
|
15
|
+
declare function resolveStreamCancellationLifecycle(terminalStreamError: string | undefined): {
|
|
16
|
+
status: number;
|
|
17
|
+
errorType: string;
|
|
18
|
+
errorMessage: string;
|
|
19
|
+
};
|
|
15
20
|
/**
|
|
16
21
|
* Create OpenAI-compatible proxy routes.
|
|
17
22
|
*
|
|
@@ -27,3 +32,7 @@ import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } fro
|
|
|
27
32
|
* @returns RouteGroup with OpenAI-compatible endpoints.
|
|
28
33
|
*/
|
|
29
34
|
export declare function createOpenAIProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, loopbackPort?: number, runtimeConfigProvider?: ProxyRuntimeConfigProvider): RouteGroup;
|
|
35
|
+
export declare const __testHooks: {
|
|
36
|
+
resolveStreamCancellationLifecycle: typeof resolveStreamCancellationLifecycle;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
@@ -27,6 +27,19 @@ const LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — long enough for slow
|
|
|
27
27
|
// `createOpenAIProxyRoutes`'s third argument when the actual listener port is
|
|
28
28
|
// known (e.g. when started from the CLI handler).
|
|
29
29
|
const DEFAULT_LOOPBACK_PORT = 55669;
|
|
30
|
+
function resolveStreamCancellationLifecycle(terminalStreamError) {
|
|
31
|
+
return terminalStreamError
|
|
32
|
+
? {
|
|
33
|
+
status: 502,
|
|
34
|
+
errorType: "loopback_stream_error",
|
|
35
|
+
errorMessage: terminalStreamError,
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
status: 499,
|
|
39
|
+
errorType: "client_cancelled",
|
|
40
|
+
errorMessage: "Client cancelled the OpenAI-compatible stream",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Build an OpenAI-shaped error as a typed Response with the intended status.
|
|
32
45
|
*
|
|
@@ -188,7 +201,8 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
188
201
|
await reader.cancel(reason);
|
|
189
202
|
}
|
|
190
203
|
finally {
|
|
191
|
-
|
|
204
|
+
const cancellation = resolveStreamCancellationLifecycle(terminalStreamError);
|
|
205
|
+
await finishLifecycle(cancellation.status, cancellation.errorType, cancellation.errorMessage);
|
|
192
206
|
}
|
|
193
207
|
},
|
|
194
208
|
});
|
|
@@ -381,4 +395,5 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
|
|
|
381
395
|
],
|
|
382
396
|
};
|
|
383
397
|
}
|
|
398
|
+
export const __testHooks = { resolveStreamCancellationLifecycle };
|
|
384
399
|
//# sourceMappingURL=openaiProxyRoutes.js.map
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -820,6 +820,42 @@ export type FallbackInfo = {
|
|
|
820
820
|
provider: string;
|
|
821
821
|
model: string;
|
|
822
822
|
};
|
|
823
|
+
export type ProxyRollingState = {
|
|
824
|
+
generation: number;
|
|
825
|
+
active: {
|
|
826
|
+
pid: number;
|
|
827
|
+
version: string;
|
|
828
|
+
generation: number;
|
|
829
|
+
} | null;
|
|
830
|
+
candidate: {
|
|
831
|
+
pid: number;
|
|
832
|
+
expectedVersion: string;
|
|
833
|
+
generation: number;
|
|
834
|
+
} | null;
|
|
835
|
+
draining: Array<{
|
|
836
|
+
pid: number;
|
|
837
|
+
version: string;
|
|
838
|
+
generation: number;
|
|
839
|
+
}>;
|
|
840
|
+
queuedSockets: number;
|
|
841
|
+
rejectedSockets: number;
|
|
842
|
+
failedTransfers: number;
|
|
843
|
+
lastFailure: {
|
|
844
|
+
at: string;
|
|
845
|
+
generation: number;
|
|
846
|
+
version: string;
|
|
847
|
+
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
848
|
+
message: string;
|
|
849
|
+
} | null;
|
|
850
|
+
};
|
|
851
|
+
export type ProxySupervisorState = {
|
|
852
|
+
pid: number;
|
|
853
|
+
host: string;
|
|
854
|
+
port: number;
|
|
855
|
+
startTime: string;
|
|
856
|
+
updaterPid?: number;
|
|
857
|
+
rolling: ProxyRollingState;
|
|
858
|
+
};
|
|
823
859
|
/** Persisted state for a running proxy instance */
|
|
824
860
|
export type ProxyState = {
|
|
825
861
|
pid: number;
|
|
@@ -840,6 +876,8 @@ export type ProxyState = {
|
|
|
840
876
|
guardPid?: number;
|
|
841
877
|
/** Dedicated updater PID for launchd-managed proxy installations. */
|
|
842
878
|
updaterPid?: number;
|
|
879
|
+
/** Stable listener supervisor PID when requests are served by socket workers. */
|
|
880
|
+
supervisorPid?: number;
|
|
843
881
|
/** How the proxy was launched — "launchd" if installed as service, "manual" otherwise */
|
|
844
882
|
managedBy?: "launchd" | "manual";
|
|
845
883
|
/** Whether the proxy is running in transparent passthrough mode */
|
|
@@ -1455,6 +1455,162 @@ export type UpdaterWorkerSupervisor = {
|
|
|
1455
1455
|
checkNow: () => number | undefined;
|
|
1456
1456
|
stop: () => void;
|
|
1457
1457
|
};
|
|
1458
|
+
export type ProxyWorkerControlMessage = {
|
|
1459
|
+
type: "proxy-worker:activate";
|
|
1460
|
+
generation: number;
|
|
1461
|
+
} | {
|
|
1462
|
+
type: "proxy-worker:drain";
|
|
1463
|
+
generation: number;
|
|
1464
|
+
} | {
|
|
1465
|
+
type: "proxy-worker:shutdown";
|
|
1466
|
+
generation: number;
|
|
1467
|
+
} | {
|
|
1468
|
+
type: "proxy-worker:socket-commit";
|
|
1469
|
+
generation: number;
|
|
1470
|
+
socketId: string;
|
|
1471
|
+
} | {
|
|
1472
|
+
type: "proxy-worker:socket-cancel";
|
|
1473
|
+
generation: number;
|
|
1474
|
+
socketId: string;
|
|
1475
|
+
};
|
|
1476
|
+
export type ProxyWorkerStatusMessage = {
|
|
1477
|
+
type: "proxy-worker:ready";
|
|
1478
|
+
generation: number;
|
|
1479
|
+
pid: number;
|
|
1480
|
+
version: string;
|
|
1481
|
+
} | {
|
|
1482
|
+
type: "proxy-worker:drained";
|
|
1483
|
+
generation: number;
|
|
1484
|
+
pid: number;
|
|
1485
|
+
} | {
|
|
1486
|
+
type: "proxy-worker:activated";
|
|
1487
|
+
generation: number;
|
|
1488
|
+
pid: number;
|
|
1489
|
+
} | {
|
|
1490
|
+
type: "proxy-worker:fatal";
|
|
1491
|
+
generation: number;
|
|
1492
|
+
pid: number;
|
|
1493
|
+
message: string;
|
|
1494
|
+
} | {
|
|
1495
|
+
type: "proxy-worker:socket-accepted";
|
|
1496
|
+
generation: number;
|
|
1497
|
+
pid: number;
|
|
1498
|
+
socketId: string;
|
|
1499
|
+
};
|
|
1500
|
+
export type ProxyWorkerSocketMessage = {
|
|
1501
|
+
type: "proxy-worker:socket";
|
|
1502
|
+
generation: number;
|
|
1503
|
+
socketId: string;
|
|
1504
|
+
};
|
|
1505
|
+
export type ProxyWorkerIpcMessage = ProxyWorkerControlMessage | ProxyWorkerStatusMessage | ProxyWorkerSocketMessage;
|
|
1506
|
+
export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy" | "end" | "pause" | "resume" | "once">;
|
|
1507
|
+
export type RollingWorkerHandle = {
|
|
1508
|
+
pid: number;
|
|
1509
|
+
sendControl: (message: ProxyWorkerControlMessage) => void;
|
|
1510
|
+
sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void) => void;
|
|
1511
|
+
terminate: (signal?: NodeJS.Signals) => void;
|
|
1512
|
+
onMessage: (listener: (message: ProxyWorkerStatusMessage) => void) => () => void;
|
|
1513
|
+
onExit: (listener: (code: number | null, signal: string | null) => void) => () => void;
|
|
1514
|
+
};
|
|
1515
|
+
export type SpawnProxySocketWorkerOptions = {
|
|
1516
|
+
generation: number;
|
|
1517
|
+
expectedVersion: string;
|
|
1518
|
+
command: string;
|
|
1519
|
+
args: string[];
|
|
1520
|
+
env?: NodeJS.ProcessEnv;
|
|
1521
|
+
stdout?: "inherit" | "ignore";
|
|
1522
|
+
stderr?: "inherit" | "ignore";
|
|
1523
|
+
socketAckTimeoutMs?: number;
|
|
1524
|
+
};
|
|
1525
|
+
export type RollingWorkerSupervisorSnapshot = {
|
|
1526
|
+
generation: number;
|
|
1527
|
+
active: {
|
|
1528
|
+
pid: number;
|
|
1529
|
+
version: string;
|
|
1530
|
+
generation: number;
|
|
1531
|
+
} | null;
|
|
1532
|
+
candidate: {
|
|
1533
|
+
pid: number;
|
|
1534
|
+
expectedVersion: string;
|
|
1535
|
+
generation: number;
|
|
1536
|
+
} | null;
|
|
1537
|
+
draining: Array<{
|
|
1538
|
+
pid: number;
|
|
1539
|
+
version: string;
|
|
1540
|
+
generation: number;
|
|
1541
|
+
}>;
|
|
1542
|
+
queuedSockets: number;
|
|
1543
|
+
rejectedSockets: number;
|
|
1544
|
+
failedTransfers: number;
|
|
1545
|
+
lastFailure: {
|
|
1546
|
+
at: string;
|
|
1547
|
+
generation: number;
|
|
1548
|
+
version: string;
|
|
1549
|
+
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
1550
|
+
message: string;
|
|
1551
|
+
} | null;
|
|
1552
|
+
};
|
|
1553
|
+
export type RollingWorkerSupervisorOptions = {
|
|
1554
|
+
spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
|
|
1555
|
+
readyTimeoutMs?: number;
|
|
1556
|
+
socketQueueLimit?: number;
|
|
1557
|
+
socketQueueTimeoutMs?: number;
|
|
1558
|
+
shutdownTimeoutMs?: number;
|
|
1559
|
+
onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
|
|
1560
|
+
log?: (message: string) => void;
|
|
1561
|
+
};
|
|
1562
|
+
export type RollingProxyServerOptions = {
|
|
1563
|
+
host: string;
|
|
1564
|
+
port: number;
|
|
1565
|
+
initialVersion: string;
|
|
1566
|
+
spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
|
|
1567
|
+
readyTimeoutMs?: number;
|
|
1568
|
+
socketQueueLimit?: number;
|
|
1569
|
+
socketQueueTimeoutMs?: number;
|
|
1570
|
+
shutdownTimeoutMs?: number;
|
|
1571
|
+
recoveryDelayMs?: number;
|
|
1572
|
+
maxRecoveryDelayMs?: number;
|
|
1573
|
+
onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
|
|
1574
|
+
log?: (message: string) => void;
|
|
1575
|
+
};
|
|
1576
|
+
export type RollingProxyServer = {
|
|
1577
|
+
address: {
|
|
1578
|
+
host: string;
|
|
1579
|
+
port: number;
|
|
1580
|
+
};
|
|
1581
|
+
replace: (expectedVersion: string) => Promise<RollingWorkerSupervisorSnapshot>;
|
|
1582
|
+
snapshot: () => RollingWorkerSupervisorSnapshot;
|
|
1583
|
+
close: () => Promise<void>;
|
|
1584
|
+
};
|
|
1585
|
+
export type RollingManagedWorker = {
|
|
1586
|
+
handle: RollingWorkerHandle;
|
|
1587
|
+
generation: number;
|
|
1588
|
+
version: string;
|
|
1589
|
+
dispose: () => void;
|
|
1590
|
+
};
|
|
1591
|
+
export type RollingCandidateWorker = RollingManagedWorker & {
|
|
1592
|
+
expectedVersion: string;
|
|
1593
|
+
activationRequested: boolean;
|
|
1594
|
+
settle: (error?: Error) => void;
|
|
1595
|
+
};
|
|
1596
|
+
export type RollingQueuedSocket = {
|
|
1597
|
+
socket: TransferableProxySocket;
|
|
1598
|
+
timeout: NodeJS.Timeout;
|
|
1599
|
+
};
|
|
1600
|
+
export type SocketWorkerRuntime = {
|
|
1601
|
+
acceptSocket: (socket: TransferableProxySocket) => void;
|
|
1602
|
+
drain: () => void;
|
|
1603
|
+
close: () => void;
|
|
1604
|
+
snapshot: () => {
|
|
1605
|
+
draining: boolean;
|
|
1606
|
+
sockets: number;
|
|
1607
|
+
activeRequests: number;
|
|
1608
|
+
drained: boolean;
|
|
1609
|
+
};
|
|
1610
|
+
};
|
|
1611
|
+
export type SocketWorkerRuntimeOptions = {
|
|
1612
|
+
onDrained?: () => void;
|
|
1613
|
+
};
|
|
1458
1614
|
/** Shape of the dynamically-imported js-yaml module. `dump` is optional —
|
|
1459
1615
|
* read-only consumers (proxy config loader) only need `load`; writers
|
|
1460
1616
|
* (CLI primary-account commands) check `dump` before calling. */
|
|
@@ -14,6 +14,7 @@ export declare const ERROR_CODES: {
|
|
|
14
14
|
readonly MEMORY_EXHAUSTED: "MEMORY_EXHAUSTED";
|
|
15
15
|
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
16
16
|
readonly PERMISSION_DENIED: "PERMISSION_DENIED";
|
|
17
|
+
readonly PROXY_WORKER_LIFECYCLE_FAILED: "PROXY_WORKER_LIFECYCLE_FAILED";
|
|
17
18
|
readonly PROVIDER_NOT_AVAILABLE: "PROVIDER_NOT_AVAILABLE";
|
|
18
19
|
readonly PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED";
|
|
19
20
|
readonly PROVIDER_QUOTA_EXCEEDED: "PROVIDER_QUOTA_EXCEEDED";
|
|
@@ -247,6 +248,13 @@ export declare class ErrorFactory {
|
|
|
247
248
|
* Create an evaluation execution failed error
|
|
248
249
|
*/
|
|
249
250
|
static evaluationExecutionFailed(operation: string, originalError: Error): NeuroLinkError;
|
|
251
|
+
/**
|
|
252
|
+
* Create a proxy rolling-worker lifecycle error. Preserves the caller's exact
|
|
253
|
+
* message so existing substring-based assertions keep working, while giving
|
|
254
|
+
* callers a typed `code`/`category` to branch on instead of string-matching
|
|
255
|
+
* generic `Error` instances.
|
|
256
|
+
*/
|
|
257
|
+
static proxyWorkerLifecycle(message: string, context?: Record<string, unknown>): NeuroLinkError;
|
|
250
258
|
}
|
|
251
259
|
/**
|
|
252
260
|
* Timeout wrapper for async operations
|
|
@@ -20,6 +20,7 @@ export const ERROR_CODES = {
|
|
|
20
20
|
MEMORY_EXHAUSTED: "MEMORY_EXHAUSTED",
|
|
21
21
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
22
22
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
23
|
+
PROXY_WORKER_LIFECYCLE_FAILED: "PROXY_WORKER_LIFECYCLE_FAILED",
|
|
23
24
|
// Provider errors
|
|
24
25
|
PROVIDER_NOT_AVAILABLE: "PROVIDER_NOT_AVAILABLE",
|
|
25
26
|
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
@@ -818,6 +819,25 @@ export class ErrorFactory {
|
|
|
818
819
|
originalError,
|
|
819
820
|
});
|
|
820
821
|
}
|
|
822
|
+
// ============================================================================
|
|
823
|
+
// PROXY ROLLING-WORKER ERRORS
|
|
824
|
+
// ============================================================================
|
|
825
|
+
/**
|
|
826
|
+
* Create a proxy rolling-worker lifecycle error. Preserves the caller's exact
|
|
827
|
+
* message so existing substring-based assertions keep working, while giving
|
|
828
|
+
* callers a typed `code`/`category` to branch on instead of string-matching
|
|
829
|
+
* generic `Error` instances.
|
|
830
|
+
*/
|
|
831
|
+
static proxyWorkerLifecycle(message, context) {
|
|
832
|
+
return new NeuroLinkError({
|
|
833
|
+
code: ERROR_CODES.PROXY_WORKER_LIFECYCLE_FAILED,
|
|
834
|
+
message,
|
|
835
|
+
category: ErrorCategory.SYSTEM,
|
|
836
|
+
severity: ErrorSeverity.HIGH,
|
|
837
|
+
retriable: false,
|
|
838
|
+
context: context || {},
|
|
839
|
+
});
|
|
840
|
+
}
|
|
821
841
|
}
|
|
822
842
|
/**
|
|
823
843
|
* Timeout wrapper for async operations
|