@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +306 -306
  3. package/dist/cli/commands/proxy.js +687 -200
  4. package/dist/cli/utils/serverUtils.js +4 -1
  5. package/dist/lib/proxy/globalInstaller.js +1 -1
  6. package/dist/lib/proxy/openaiFormat.js +1 -0
  7. package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
  8. package/dist/lib/proxy/rollingProxyServer.js +149 -0
  9. package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
  10. package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
  11. package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
  12. package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
  14. package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
  15. package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
  16. package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
  17. package/dist/lib/proxy/sseInterceptor.js +1 -1
  18. package/dist/lib/proxy/workerLog.d.ts +6 -0
  19. package/dist/lib/proxy/workerLog.js +42 -0
  20. package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
  21. package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
  22. package/dist/lib/types/cli.d.ts +38 -0
  23. package/dist/lib/types/proxy.d.ts +156 -0
  24. package/dist/lib/utils/errorHandling.d.ts +8 -0
  25. package/dist/lib/utils/errorHandling.js +20 -0
  26. package/dist/proxy/globalInstaller.js +1 -1
  27. package/dist/proxy/openaiFormat.js +1 -0
  28. package/dist/proxy/rollingProxyServer.d.ts +3 -0
  29. package/dist/proxy/rollingProxyServer.js +148 -0
  30. package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
  31. package/dist/proxy/rollingWorkerProcess.js +193 -0
  32. package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
  33. package/dist/proxy/rollingWorkerProtocol.js +42 -0
  34. package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
  35. package/dist/proxy/rollingWorkerSupervisor.js +401 -0
  36. package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
  37. package/dist/proxy/socketWorkerRuntime.js +293 -0
  38. package/dist/proxy/sseInterceptor.js +1 -1
  39. package/dist/proxy/workerLog.d.ts +6 -0
  40. package/dist/proxy/workerLog.js +41 -0
  41. package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
  42. package/dist/server/routes/openaiProxyRoutes.js +16 -1
  43. package/dist/types/cli.d.ts +38 -0
  44. package/dist/types/proxy.d.ts +156 -0
  45. package/dist/utils/errorHandling.d.ts +8 -0
  46. package/dist/utils/errorHandling.js +20 -0
  47. package/package.json +3 -2
@@ -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
@@ -0,0 +1,14 @@
1
+ import type { Server } from "node:http";
2
+ import type { SocketWorkerRuntime, SocketWorkerRuntimeOptions } from "../types/index.js";
3
+ /**
4
+ * Adapts transferred TCP sockets to an HTTP server without opening another
5
+ * listener. Active responses finish normally; idle keep-alive sockets close as
6
+ * soon as draining begins.
7
+ */
8
+ export declare function createSocketWorkerRuntime(server: Server, options?: SocketWorkerRuntimeOptions): SocketWorkerRuntime;
9
+ export declare function attachSocketWorkerProcess(server: Server, input: {
10
+ generation: number;
11
+ version: string;
12
+ onActivated?: () => void;
13
+ onDrained?: () => void;
14
+ }): SocketWorkerRuntime;