tywrap 0.2.1 → 0.4.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/README.md +100 -32
- package/dist/cli.js +70 -5
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +62 -20
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +46 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +10 -16
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/src/runtime/node.ts
CHANGED
|
@@ -18,10 +18,11 @@ import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
|
|
|
18
18
|
import { globalCache } from '../utils/cache.js';
|
|
19
19
|
|
|
20
20
|
import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
|
|
21
|
-
import { BridgeProtocolError } from './errors.js';
|
|
21
|
+
import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
22
22
|
import { ProcessIO } from './process-io.js';
|
|
23
23
|
import { PooledTransport } from './pooled-transport.js';
|
|
24
24
|
import type { CodecOptions } from './safe-codec.js';
|
|
25
|
+
import { PROTOCOL_ID } from './transport.js';
|
|
25
26
|
import type { PooledWorker } from './worker-pool.js';
|
|
26
27
|
|
|
27
28
|
// =============================================================================
|
|
@@ -73,8 +74,7 @@ export interface NodeBridgeOptions {
|
|
|
73
74
|
|
|
74
75
|
/** Commands to run on each process at startup for warming up. */
|
|
75
76
|
warmupCommands?: Array<
|
|
76
|
-
|
|
77
|
-
| { method: string; params: unknown } // Legacy format for backwards compatibility
|
|
77
|
+
{ module: string; functionName: string; args?: unknown[] } | { method: string; params: unknown } // Legacy shape preserved so runtime can surface a migration error
|
|
78
78
|
>;
|
|
79
79
|
|
|
80
80
|
// ===========================================================================
|
|
@@ -120,12 +120,17 @@ interface ResolvedOptions {
|
|
|
120
120
|
enableCache: boolean;
|
|
121
121
|
env: Record<string, string | undefined>;
|
|
122
122
|
codec?: CodecOptions;
|
|
123
|
-
warmupCommands:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
warmupCommands: WarmupCommand[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface WarmupCommand {
|
|
127
|
+
module: string;
|
|
128
|
+
functionName: string;
|
|
129
|
+
args?: unknown[];
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
const WORKER_READY_TIMEOUT_MS = 5000;
|
|
133
|
+
|
|
129
134
|
// =============================================================================
|
|
130
135
|
// UTILITIES
|
|
131
136
|
// =============================================================================
|
|
@@ -158,6 +163,10 @@ function resolveVirtualEnv(
|
|
|
158
163
|
* Get the environment variable key for PATH (case-insensitive on Windows).
|
|
159
164
|
*/
|
|
160
165
|
function getPathKey(env: Record<string, string | undefined>): string {
|
|
166
|
+
if (Object.prototype.hasOwnProperty.call(env, 'PATH')) {
|
|
167
|
+
return 'PATH';
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
for (const key of Object.keys(env)) {
|
|
162
171
|
if (key.toLowerCase() === 'path') {
|
|
163
172
|
return key;
|
|
@@ -166,6 +175,93 @@ function getPathKey(env: Record<string, string | undefined>): string {
|
|
|
166
175
|
return 'PATH';
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
function setPathValue(env: Record<string, string>, value: string): void {
|
|
179
|
+
setEnvValue(env, 'PATH', value);
|
|
180
|
+
|
|
181
|
+
if (process.platform !== 'win32') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const key of Object.keys(env)) {
|
|
186
|
+
if (key !== 'PATH' && key.toLowerCase() === 'path') {
|
|
187
|
+
setEnvValue(env, key, value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const DANGEROUS_ENV_OVERRIDE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
193
|
+
|
|
194
|
+
function createNullPrototypeEnv(): Record<string, string> {
|
|
195
|
+
return Object.create(null) as Record<string, string>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function setEnvValue(env: Record<string, string>, key: string, value: string): void {
|
|
199
|
+
Object.defineProperty(env, key, {
|
|
200
|
+
value,
|
|
201
|
+
writable: true,
|
|
202
|
+
enumerable: true,
|
|
203
|
+
configurable: true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getEnvValue(env: Record<string, string>, key: string): string | undefined {
|
|
208
|
+
const value = Reflect.get(env, key);
|
|
209
|
+
return typeof value === 'string' ? value : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function assertSafeEnvOverrideKey(key: string): void {
|
|
213
|
+
if (DANGEROUS_ENV_OVERRIDE_KEYS.has(key)) {
|
|
214
|
+
throw new BridgeProtocolError(`Invalid environment override key "${key}" in options.env`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']): WarmupCommand[] {
|
|
219
|
+
if (commands === undefined) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(commands)) {
|
|
223
|
+
throw new BridgeProtocolError('warmupCommands must be an array when provided');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const warmups = commands;
|
|
227
|
+
return warmups.map((command, index) => {
|
|
228
|
+
if (!command || typeof command !== 'object' || Array.isArray(command)) {
|
|
229
|
+
throw new BridgeProtocolError(
|
|
230
|
+
`Invalid warmup command at index ${index + 1}: expected { module, functionName, args? }`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const candidate = command as Record<string, unknown>;
|
|
235
|
+
if ('method' in candidate || 'params' in candidate) {
|
|
236
|
+
throw new BridgeProtocolError(
|
|
237
|
+
`Invalid warmup command at index ${index + 1}: legacy { method, params } format is no longer supported. Use { module, functionName, args? }.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof candidate.module !== 'string' || candidate.module.trim().length === 0) {
|
|
242
|
+
throw new BridgeProtocolError(
|
|
243
|
+
`Invalid warmup command at index ${index + 1}: "module" must be a non-empty string`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (typeof candidate.functionName !== 'string' || candidate.functionName.trim().length === 0) {
|
|
247
|
+
throw new BridgeProtocolError(
|
|
248
|
+
`Invalid warmup command at index ${index + 1}: "functionName" must be a non-empty string`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (candidate.args !== undefined && !Array.isArray(candidate.args)) {
|
|
252
|
+
throw new BridgeProtocolError(
|
|
253
|
+
`Invalid warmup command at index ${index + 1}: "args" must be an array when provided`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
module: candidate.module,
|
|
259
|
+
functionName: candidate.functionName,
|
|
260
|
+
args: candidate.args as unknown[] | undefined,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
169
265
|
// =============================================================================
|
|
170
266
|
// NODE BRIDGE
|
|
171
267
|
// =============================================================================
|
|
@@ -226,6 +322,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
226
322
|
const maxProcesses = options.maxProcesses ?? 1;
|
|
227
323
|
const minProcesses = Math.min(options.minProcesses ?? 1, maxProcesses);
|
|
228
324
|
|
|
325
|
+
const warmupCommands = normalizeWarmupCommands(options.warmupCommands);
|
|
326
|
+
|
|
229
327
|
const resolvedOptions: ResolvedOptions = {
|
|
230
328
|
minProcesses,
|
|
231
329
|
maxProcesses,
|
|
@@ -240,20 +338,20 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
240
338
|
enableCache: options.enableCache ?? false,
|
|
241
339
|
env: options.env ?? {},
|
|
242
340
|
codec: options.codec,
|
|
243
|
-
warmupCommands
|
|
341
|
+
warmupCommands,
|
|
244
342
|
};
|
|
245
|
-
|
|
246
343
|
// Build environment for ProcessIO
|
|
247
344
|
const processEnv = buildProcessEnv(resolvedOptions);
|
|
248
345
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
346
|
+
const userWarmup =
|
|
347
|
+
resolvedOptions.warmupCommands.length > 0
|
|
348
|
+
? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
|
|
349
|
+
: undefined;
|
|
350
|
+
const onWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
|
|
253
351
|
|
|
254
352
|
// Create pooled transport with ProcessIO workers
|
|
255
353
|
const transport = new PooledTransport({
|
|
256
|
-
createTransport: () =>
|
|
354
|
+
createTransport: (): ProcessIO =>
|
|
257
355
|
new ProcessIO({
|
|
258
356
|
pythonPath: resolvedOptions.pythonPath,
|
|
259
357
|
bridgeScript: resolvedOptions.scriptPath,
|
|
@@ -265,6 +363,7 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
265
363
|
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
266
364
|
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
267
365
|
onWorkerReady,
|
|
366
|
+
onReplacementWorkerReady: onWorkerReady,
|
|
268
367
|
});
|
|
269
368
|
|
|
270
369
|
// Initialize BridgeProtocol with pooled transport
|
|
@@ -460,8 +559,74 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
460
559
|
* Simple request ID generator for warmup commands.
|
|
461
560
|
*/
|
|
462
561
|
let warmupRequestId = 0;
|
|
463
|
-
function generateWarmupId():
|
|
464
|
-
return
|
|
562
|
+
function generateWarmupId(): number {
|
|
563
|
+
return ++warmupRequestId;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function sendWarmupRequest(
|
|
567
|
+
worker: PooledWorker,
|
|
568
|
+
timeoutMs: number,
|
|
569
|
+
requestId: number,
|
|
570
|
+
label: string,
|
|
571
|
+
messagePayload: Record<string, unknown>
|
|
572
|
+
): Promise<void> {
|
|
573
|
+
let message: string;
|
|
574
|
+
try {
|
|
575
|
+
message = JSON.stringify({
|
|
576
|
+
id: requestId,
|
|
577
|
+
protocol: PROTOCOL_ID,
|
|
578
|
+
...messagePayload,
|
|
579
|
+
});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw new BridgeExecutionError(
|
|
582
|
+
`${label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let response: string;
|
|
588
|
+
try {
|
|
589
|
+
response = await worker.transport.send(message, timeoutMs);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw new BridgeExecutionError(
|
|
592
|
+
`${label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
593
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let parsed: unknown;
|
|
598
|
+
try {
|
|
599
|
+
parsed = JSON.parse(response);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new BridgeExecutionError(`${label} returned invalid JSON response`, {
|
|
602
|
+
cause: error instanceof Error ? error : undefined,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
607
|
+
throw new BridgeExecutionError(
|
|
608
|
+
`${label} returned malformed response envelope for request ${requestId}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
613
|
+
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
614
|
+
const errorPayload = envelope.error;
|
|
615
|
+
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
616
|
+
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
617
|
+
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
618
|
+
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown warmup error';
|
|
619
|
+
throw new BridgeExecutionError(`${label} failed: ${errType}: ${errMessage}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
throw new BridgeExecutionError(`${label} failed with malformed error payload`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!('result' in envelope)) {
|
|
626
|
+
throw new BridgeExecutionError(
|
|
627
|
+
`${label} returned malformed response envelope for request ${requestId}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
465
630
|
}
|
|
466
631
|
|
|
467
632
|
/**
|
|
@@ -471,38 +636,138 @@ function generateWarmupId(): string {
|
|
|
471
636
|
* bypassing the pool to ensure each worker gets warmed up individually.
|
|
472
637
|
*/
|
|
473
638
|
function createWarmupCallback(
|
|
474
|
-
warmupCommands:
|
|
475
|
-
| { module: string; functionName: string; args?: unknown[] }
|
|
476
|
-
| { method: string; params: unknown }
|
|
477
|
-
>,
|
|
639
|
+
warmupCommands: WarmupCommand[],
|
|
478
640
|
timeoutMs: number
|
|
479
641
|
): (worker: PooledWorker) => Promise<void> {
|
|
480
642
|
return async (worker: PooledWorker) => {
|
|
481
|
-
for (const cmd of warmupCommands) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
643
|
+
for (const [index, cmd] of warmupCommands.entries()) {
|
|
644
|
+
const commandLabel = `${cmd.module}.${cmd.functionName}`;
|
|
645
|
+
await executeWorkerCall(
|
|
646
|
+
worker,
|
|
647
|
+
{
|
|
648
|
+
module: cmd.module,
|
|
649
|
+
functionName: cmd.functionName,
|
|
650
|
+
args: cmd.args ?? [],
|
|
651
|
+
},
|
|
652
|
+
timeoutMs,
|
|
653
|
+
{
|
|
654
|
+
label: `Warmup command #${index + 1} (${commandLabel})`,
|
|
655
|
+
invalidJsonMessage: 'returned invalid JSON response',
|
|
656
|
+
malformedEnvelopeMessage: requestId =>
|
|
657
|
+
`returned malformed response envelope for request ${requestId}`,
|
|
658
|
+
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
659
|
+
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
497
660
|
}
|
|
498
|
-
|
|
499
|
-
} catch {
|
|
500
|
-
// Ignore warmup errors - they're not critical
|
|
501
|
-
}
|
|
661
|
+
);
|
|
502
662
|
}
|
|
503
663
|
};
|
|
504
664
|
}
|
|
505
665
|
|
|
666
|
+
function createWorkerReadyCallback(
|
|
667
|
+
timeoutMs: number,
|
|
668
|
+
extraWarmup?: (worker: PooledWorker) => Promise<void>
|
|
669
|
+
): (worker: PooledWorker) => Promise<void> {
|
|
670
|
+
return async (worker: PooledWorker) => {
|
|
671
|
+
const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
|
|
672
|
+
await sendWarmupRequest(worker, readyTimeoutMs, generateWarmupId(), 'Worker warmup check', {
|
|
673
|
+
method: 'meta',
|
|
674
|
+
params: {},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
await extraWarmup?.(worker);
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
interface WorkerCallSpec {
|
|
682
|
+
module: string;
|
|
683
|
+
functionName: string;
|
|
684
|
+
args: unknown[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
interface WorkerCallErrorMessages {
|
|
688
|
+
label: string;
|
|
689
|
+
invalidJsonMessage: string;
|
|
690
|
+
malformedEnvelopeMessage: (requestId: number) => string;
|
|
691
|
+
pythonErrorMessage: (errorType: string, errorMessage: string) => string;
|
|
692
|
+
malformedErrorPayloadMessage: string;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function executeWorkerCall(
|
|
696
|
+
worker: PooledWorker,
|
|
697
|
+
request: WorkerCallSpec,
|
|
698
|
+
timeoutMs: number,
|
|
699
|
+
messages: WorkerCallErrorMessages
|
|
700
|
+
): Promise<unknown> {
|
|
701
|
+
const requestId = generateWarmupId();
|
|
702
|
+
let message: string;
|
|
703
|
+
try {
|
|
704
|
+
message = JSON.stringify({
|
|
705
|
+
id: requestId,
|
|
706
|
+
protocol: PROTOCOL_ID,
|
|
707
|
+
method: 'call',
|
|
708
|
+
params: {
|
|
709
|
+
module: request.module,
|
|
710
|
+
functionName: request.functionName,
|
|
711
|
+
args: request.args,
|
|
712
|
+
kwargs: {},
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
throw new BridgeExecutionError(
|
|
717
|
+
`${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
718
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let response: string;
|
|
723
|
+
try {
|
|
724
|
+
response = await worker.transport.send(message, timeoutMs);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
throw new BridgeExecutionError(
|
|
727
|
+
`${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
728
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let parsed: unknown;
|
|
733
|
+
try {
|
|
734
|
+
parsed = JSON.parse(response);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
|
|
737
|
+
cause: error instanceof Error ? error : undefined,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
742
|
+
throw new BridgeExecutionError(
|
|
743
|
+
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
748
|
+
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
749
|
+
const errorPayload = envelope.error;
|
|
750
|
+
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
751
|
+
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
752
|
+
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
753
|
+
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
|
|
754
|
+
throw new BridgeExecutionError(
|
|
755
|
+
`${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!('result' in envelope)) {
|
|
763
|
+
throw new BridgeExecutionError(
|
|
764
|
+
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return envelope.result;
|
|
769
|
+
}
|
|
770
|
+
|
|
506
771
|
// =============================================================================
|
|
507
772
|
// ENVIRONMENT BUILDING
|
|
508
773
|
// =============================================================================
|
|
@@ -513,13 +778,13 @@ function createWarmupCallback(
|
|
|
513
778
|
function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
514
779
|
const allowedPrefixes = ['TYWRAP_'];
|
|
515
780
|
const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
|
|
516
|
-
const env
|
|
781
|
+
const env = createNullPrototypeEnv();
|
|
517
782
|
|
|
518
783
|
// Copy allowed env vars from process.env
|
|
519
784
|
if (options.inheritProcessEnv) {
|
|
520
785
|
for (const [key, value] of Object.entries(process.env)) {
|
|
521
786
|
if (value !== undefined) {
|
|
522
|
-
env
|
|
787
|
+
setEnvValue(env, key, value);
|
|
523
788
|
}
|
|
524
789
|
}
|
|
525
790
|
} else {
|
|
@@ -528,15 +793,16 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
|
528
793
|
value !== undefined &&
|
|
529
794
|
(allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))
|
|
530
795
|
) {
|
|
531
|
-
env
|
|
796
|
+
setEnvValue(env, key, value);
|
|
532
797
|
}
|
|
533
798
|
}
|
|
534
799
|
}
|
|
535
800
|
|
|
536
801
|
// Apply user overrides
|
|
537
802
|
for (const [key, value] of Object.entries(options.env)) {
|
|
803
|
+
assertSafeEnvOverrideKey(key);
|
|
538
804
|
if (value !== undefined) {
|
|
539
|
-
env
|
|
805
|
+
setEnvValue(env, key, value);
|
|
540
806
|
}
|
|
541
807
|
}
|
|
542
808
|
|
|
@@ -545,8 +811,8 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
|
545
811
|
const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
|
|
546
812
|
env.VIRTUAL_ENV = venv.venvPath;
|
|
547
813
|
const pathKey = getPathKey(env);
|
|
548
|
-
const currentPath = env
|
|
549
|
-
env
|
|
814
|
+
const currentPath = getEnvValue(env, pathKey);
|
|
815
|
+
setPathValue(env, currentPath ? `${venv.binDir}${delimiter}${currentPath}` : venv.binDir);
|
|
550
816
|
}
|
|
551
817
|
|
|
552
818
|
// Add cwd to PYTHONPATH so Python can find modules in the working directory
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @deprecated Import from './node.js' instead.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* now supports both single-process mode (default) and multi-process
|
|
6
|
-
* via the minProcesses/maxProcesses options.
|
|
4
|
+
* Backward-compat shim for older deep imports. NodeBridge is the public API,
|
|
5
|
+
* and it now supports both single-process mode (default) and multi-process
|
|
6
|
+
* pooling via the minProcesses/maxProcesses options.
|
|
7
7
|
*
|
|
8
8
|
* Migration:
|
|
9
9
|
* ```typescript
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* const bridge = new NodeBridge({ minProcesses: 2, maxProcesses: 4 });
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
-
* This file is
|
|
19
|
+
* This file is not exposed through the package exports map and is maintained
|
|
20
|
+
* for backward compatibility only.
|
|
20
21
|
*/
|
|
21
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
NodeBridge as OptimizedNodeBridge,
|
|
24
|
+
type NodeBridgeOptions as ProcessPoolOptions,
|
|
25
|
+
} from './node.js';
|
|
@@ -41,6 +41,11 @@ export interface PooledTransportOptions {
|
|
|
41
41
|
* Use this for per-worker warmup (e.g., importing modules, running setup).
|
|
42
42
|
*/
|
|
43
43
|
onWorkerReady?: (worker: PooledWorker) => Promise<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Optional callback used only for background replacement workers.
|
|
47
|
+
*/
|
|
48
|
+
onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
// =============================================================================
|
|
@@ -78,8 +83,12 @@ export interface PooledTransportOptions {
|
|
|
78
83
|
* ```
|
|
79
84
|
*/
|
|
80
85
|
export class PooledTransport extends BoundedContext implements Transport {
|
|
81
|
-
private readonly poolOptions: Omit<
|
|
86
|
+
private readonly poolOptions: Omit<
|
|
87
|
+
Required<PooledTransportOptions>,
|
|
88
|
+
'onWorkerReady' | 'onReplacementWorkerReady'
|
|
89
|
+
> & {
|
|
82
90
|
onWorkerReady?: (worker: PooledWorker) => Promise<void>;
|
|
91
|
+
onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
|
|
83
92
|
};
|
|
84
93
|
private pool?: WorkerPool;
|
|
85
94
|
|
|
@@ -102,6 +111,7 @@ export class PooledTransport extends BoundedContext implements Transport {
|
|
|
102
111
|
queueTimeoutMs: options.queueTimeoutMs ?? 30000,
|
|
103
112
|
maxConcurrentPerWorker: options.maxConcurrentPerWorker ?? 10,
|
|
104
113
|
onWorkerReady: options.onWorkerReady,
|
|
114
|
+
onReplacementWorkerReady: options.onReplacementWorkerReady,
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -123,6 +133,7 @@ export class PooledTransport extends BoundedContext implements Transport {
|
|
|
123
133
|
queueTimeoutMs: this.poolOptions.queueTimeoutMs,
|
|
124
134
|
maxConcurrentPerWorker: this.poolOptions.maxConcurrentPerWorker,
|
|
125
135
|
onWorkerReady: this.poolOptions.onWorkerReady,
|
|
136
|
+
onReplacementWorkerReady: this.poolOptions.onReplacementWorkerReady,
|
|
126
137
|
});
|
|
127
138
|
|
|
128
139
|
await this.pool.init();
|