tywrap 0.2.1 → 0.3.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 +65 -31
- package/dist/cli.js +49 -2
- 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 +43 -7
- 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 +47 -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/index.d.ts +7 -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 +170 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- 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 +12 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +53 -5
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- 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 +40 -18
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -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/index.ts +34 -5
- 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 +264 -44
- package/src/runtime/optimized-node.ts +4 -1
- 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 +67 -6
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- 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/http-io.ts
CHANGED
|
@@ -135,7 +135,8 @@ export class HttpIO implements Transport {
|
|
|
135
135
|
|
|
136
136
|
// Determine effective timeout
|
|
137
137
|
// 0 = no timeout (per interface contract), negative = use default
|
|
138
|
-
const effectiveTimeout =
|
|
138
|
+
const effectiveTimeout =
|
|
139
|
+
timeoutMs === 0 ? 0 : timeoutMs > 0 ? timeoutMs : this.defaultTimeoutMs;
|
|
139
140
|
|
|
140
141
|
// Create abort controller for timeout
|
|
141
142
|
const controller = new AbortController();
|
package/src/runtime/http.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
|
|
11
11
|
import { HttpIO } from './http-io.js';
|
|
12
12
|
import type { CodecOptions } from './safe-codec.js';
|
|
13
|
+
import { autoRegisterArrowDecoder } from '../utils/codec.js';
|
|
13
14
|
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// OPTIONS
|
|
@@ -83,4 +84,10 @@ export class HttpBridge extends BridgeProtocol {
|
|
|
83
84
|
|
|
84
85
|
super(protocolOptions);
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
protected async doInit(): Promise<void> {
|
|
89
|
+
// Best-effort: keep apache-arrow optional and avoid breaking non-Node runtimes.
|
|
90
|
+
await autoRegisterArrowDecoder();
|
|
91
|
+
await super.doInit();
|
|
92
|
+
}
|
|
86
93
|
}
|
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,22 @@ 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
343
|
|
|
246
344
|
// Build environment for ProcessIO
|
|
247
345
|
const processEnv = buildProcessEnv(resolvedOptions);
|
|
248
346
|
|
|
249
347
|
// Create warmup callback for per-worker initialization
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
348
|
+
const userWarmup =
|
|
349
|
+
resolvedOptions.warmupCommands.length > 0
|
|
350
|
+
? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
|
|
351
|
+
: undefined;
|
|
352
|
+
const replacementWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
|
|
253
353
|
|
|
254
354
|
// Create pooled transport with ProcessIO workers
|
|
255
355
|
const transport = new PooledTransport({
|
|
256
|
-
createTransport: () =>
|
|
356
|
+
createTransport: (): ProcessIO =>
|
|
257
357
|
new ProcessIO({
|
|
258
358
|
pythonPath: resolvedOptions.pythonPath,
|
|
259
359
|
bridgeScript: resolvedOptions.scriptPath,
|
|
@@ -264,7 +364,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
264
364
|
minWorkers: resolvedOptions.minProcesses,
|
|
265
365
|
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
266
366
|
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
267
|
-
onWorkerReady,
|
|
367
|
+
onWorkerReady: userWarmup,
|
|
368
|
+
onReplacementWorkerReady: replacementWorkerReady,
|
|
268
369
|
});
|
|
269
370
|
|
|
270
371
|
// Initialize BridgeProtocol with pooled transport
|
|
@@ -460,8 +561,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
460
561
|
* Simple request ID generator for warmup commands.
|
|
461
562
|
*/
|
|
462
563
|
let warmupRequestId = 0;
|
|
463
|
-
function generateWarmupId():
|
|
464
|
-
return
|
|
564
|
+
function generateWarmupId(): number {
|
|
565
|
+
return ++warmupRequestId;
|
|
465
566
|
}
|
|
466
567
|
|
|
467
568
|
/**
|
|
@@ -471,38 +572,156 @@ function generateWarmupId(): string {
|
|
|
471
572
|
* bypassing the pool to ensure each worker gets warmed up individually.
|
|
472
573
|
*/
|
|
473
574
|
function createWarmupCallback(
|
|
474
|
-
warmupCommands:
|
|
475
|
-
| { module: string; functionName: string; args?: unknown[] }
|
|
476
|
-
| { method: string; params: unknown }
|
|
477
|
-
>,
|
|
575
|
+
warmupCommands: WarmupCommand[],
|
|
478
576
|
timeoutMs: number
|
|
479
577
|
): (worker: PooledWorker) => Promise<void> {
|
|
480
578
|
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
|
-
|
|
579
|
+
for (const [index, cmd] of warmupCommands.entries()) {
|
|
580
|
+
const commandLabel = `${cmd.module}.${cmd.functionName}`;
|
|
581
|
+
await executeWorkerCall(
|
|
582
|
+
worker,
|
|
583
|
+
{
|
|
584
|
+
module: cmd.module,
|
|
585
|
+
functionName: cmd.functionName,
|
|
586
|
+
args: cmd.args ?? [],
|
|
587
|
+
},
|
|
588
|
+
timeoutMs,
|
|
589
|
+
{
|
|
590
|
+
label: `Warmup command #${index + 1} (${commandLabel})`,
|
|
591
|
+
invalidJsonMessage: 'returned invalid JSON response',
|
|
592
|
+
malformedEnvelopeMessage: requestId =>
|
|
593
|
+
`returned malformed response envelope for request ${requestId}`,
|
|
594
|
+
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
595
|
+
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
497
596
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function createWorkerReadyCallback(
|
|
603
|
+
timeoutMs: number,
|
|
604
|
+
extraWarmup?: (worker: PooledWorker) => Promise<void>
|
|
605
|
+
): (worker: PooledWorker) => Promise<void> {
|
|
606
|
+
return async (worker: PooledWorker) => {
|
|
607
|
+
const result = await executeWorkerCall(
|
|
608
|
+
worker,
|
|
609
|
+
{
|
|
610
|
+
module: 'builtins',
|
|
611
|
+
functionName: 'len',
|
|
612
|
+
args: [[]],
|
|
613
|
+
},
|
|
614
|
+
Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS),
|
|
615
|
+
{
|
|
616
|
+
label: 'Worker readiness probe (builtins.len)',
|
|
617
|
+
invalidJsonMessage: 'returned invalid JSON response',
|
|
618
|
+
malformedEnvelopeMessage: requestId =>
|
|
619
|
+
`returned malformed response envelope for request ${requestId}`,
|
|
620
|
+
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
621
|
+
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
501
622
|
}
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
if (result !== 0) {
|
|
626
|
+
throw new BridgeExecutionError(
|
|
627
|
+
`Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`
|
|
628
|
+
);
|
|
502
629
|
}
|
|
630
|
+
|
|
631
|
+
await extraWarmup?.(worker);
|
|
503
632
|
};
|
|
504
633
|
}
|
|
505
634
|
|
|
635
|
+
interface WorkerCallSpec {
|
|
636
|
+
module: string;
|
|
637
|
+
functionName: string;
|
|
638
|
+
args: unknown[];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
interface WorkerCallErrorMessages {
|
|
642
|
+
label: string;
|
|
643
|
+
invalidJsonMessage: string;
|
|
644
|
+
malformedEnvelopeMessage: (requestId: number) => string;
|
|
645
|
+
pythonErrorMessage: (errorType: string, errorMessage: string) => string;
|
|
646
|
+
malformedErrorPayloadMessage: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function executeWorkerCall(
|
|
650
|
+
worker: PooledWorker,
|
|
651
|
+
request: WorkerCallSpec,
|
|
652
|
+
timeoutMs: number,
|
|
653
|
+
messages: WorkerCallErrorMessages
|
|
654
|
+
): Promise<unknown> {
|
|
655
|
+
const requestId = generateWarmupId();
|
|
656
|
+
let message: string;
|
|
657
|
+
try {
|
|
658
|
+
message = JSON.stringify({
|
|
659
|
+
id: requestId,
|
|
660
|
+
protocol: PROTOCOL_ID,
|
|
661
|
+
method: 'call',
|
|
662
|
+
params: {
|
|
663
|
+
module: request.module,
|
|
664
|
+
functionName: request.functionName,
|
|
665
|
+
args: request.args,
|
|
666
|
+
kwargs: {},
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
} catch (error) {
|
|
670
|
+
throw new BridgeExecutionError(
|
|
671
|
+
`${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
672
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
let response: string;
|
|
677
|
+
try {
|
|
678
|
+
response = await worker.transport.send(message, timeoutMs);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
throw new BridgeExecutionError(
|
|
681
|
+
`${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
682
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let parsed: unknown;
|
|
687
|
+
try {
|
|
688
|
+
parsed = JSON.parse(response);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
|
|
691
|
+
cause: error instanceof Error ? error : undefined,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
696
|
+
throw new BridgeExecutionError(
|
|
697
|
+
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
702
|
+
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
703
|
+
const errorPayload = envelope.error;
|
|
704
|
+
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
705
|
+
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
706
|
+
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
707
|
+
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
|
|
708
|
+
throw new BridgeExecutionError(
|
|
709
|
+
`${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!('result' in envelope)) {
|
|
717
|
+
throw new BridgeExecutionError(
|
|
718
|
+
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return envelope.result;
|
|
723
|
+
}
|
|
724
|
+
|
|
506
725
|
// =============================================================================
|
|
507
726
|
// ENVIRONMENT BUILDING
|
|
508
727
|
// =============================================================================
|
|
@@ -513,13 +732,13 @@ function createWarmupCallback(
|
|
|
513
732
|
function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
514
733
|
const allowedPrefixes = ['TYWRAP_'];
|
|
515
734
|
const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
|
|
516
|
-
const env
|
|
735
|
+
const env = createNullPrototypeEnv();
|
|
517
736
|
|
|
518
737
|
// Copy allowed env vars from process.env
|
|
519
738
|
if (options.inheritProcessEnv) {
|
|
520
739
|
for (const [key, value] of Object.entries(process.env)) {
|
|
521
740
|
if (value !== undefined) {
|
|
522
|
-
env
|
|
741
|
+
setEnvValue(env, key, value);
|
|
523
742
|
}
|
|
524
743
|
}
|
|
525
744
|
} else {
|
|
@@ -528,15 +747,16 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
|
528
747
|
value !== undefined &&
|
|
529
748
|
(allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))
|
|
530
749
|
) {
|
|
531
|
-
env
|
|
750
|
+
setEnvValue(env, key, value);
|
|
532
751
|
}
|
|
533
752
|
}
|
|
534
753
|
}
|
|
535
754
|
|
|
536
755
|
// Apply user overrides
|
|
537
756
|
for (const [key, value] of Object.entries(options.env)) {
|
|
757
|
+
assertSafeEnvOverrideKey(key);
|
|
538
758
|
if (value !== undefined) {
|
|
539
|
-
env
|
|
759
|
+
setEnvValue(env, key, value);
|
|
540
760
|
}
|
|
541
761
|
}
|
|
542
762
|
|
|
@@ -545,8 +765,8 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
|
|
|
545
765
|
const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
|
|
546
766
|
env.VIRTUAL_ENV = venv.venvPath;
|
|
547
767
|
const pathKey = getPathKey(env);
|
|
548
|
-
const currentPath = env
|
|
549
|
-
env
|
|
768
|
+
const currentPath = getEnvValue(env, pathKey);
|
|
769
|
+
setPathValue(env, currentPath ? `${venv.binDir}${delimiter}${currentPath}` : venv.binDir);
|
|
550
770
|
}
|
|
551
771
|
|
|
552
772
|
// Add cwd to PYTHONPATH so Python can find modules in the working directory
|
|
@@ -18,4 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* This file is maintained for backward compatibility only.
|
|
20
20
|
*/
|
|
21
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
NodeBridge as OptimizedNodeBridge,
|
|
23
|
+
type NodeBridgeOptions as ProcessPoolOptions,
|
|
24
|
+
} 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();
|