tywrap 0.2.0 → 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 +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- 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 +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- 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 +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- 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 +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- 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 +114 -1
- 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 +108 -15
- package/runtime/safe_codec.py +344 -0
- 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 +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- 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 +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/dist/runtime/node.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node.js
|
|
2
|
+
* Node.js runtime bridge for BridgeProtocol.
|
|
3
3
|
*
|
|
4
|
-
* NodeBridge
|
|
5
|
-
*
|
|
6
|
-
* in single-process mode for maximum compatibility with the original NodeBridge behavior.
|
|
4
|
+
* NodeBridge extends BridgeProtocol and uses ProcessIO transports with
|
|
5
|
+
* optional pooling for concurrent Python execution.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* @see https://github.com/bbopen/tywrap/issues/149
|
|
9
8
|
*/
|
|
10
9
|
import { existsSync } from 'node:fs';
|
|
11
10
|
import { delimiter, isAbsolute, join, resolve } from 'node:path';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
13
12
|
import { createRequire } from 'node:module';
|
|
14
|
-
import {
|
|
15
|
-
import { globalCache } from '../utils/cache.js';
|
|
16
|
-
import { autoRegisterArrowDecoder, decodeValueAsync } from '../utils/codec.js';
|
|
13
|
+
import { autoRegisterArrowDecoder } from '../utils/codec.js';
|
|
17
14
|
import { getDefaultPythonPath } from '../utils/python.js';
|
|
18
15
|
import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
16
|
+
import { globalCache } from '../utils/cache.js';
|
|
17
|
+
import { BridgeProtocol } from './bridge-protocol.js';
|
|
18
|
+
import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
19
|
+
import { ProcessIO } from './process-io.js';
|
|
20
|
+
import { PooledTransport } from './pooled-transport.js';
|
|
21
|
+
import { PROTOCOL_ID } from './transport.js';
|
|
22
|
+
const WORKER_READY_TIMEOUT_MS = 5000;
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// UTILITIES
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the default bridge script path.
|
|
28
|
+
*/
|
|
24
29
|
function resolveDefaultScriptPath() {
|
|
25
30
|
try {
|
|
26
31
|
return fileURLToPath(new URL('../../runtime/python_bridge.py', import.meta.url));
|
|
@@ -29,429 +34,296 @@ function resolveDefaultScriptPath() {
|
|
|
29
34
|
return 'runtime/python_bridge.py';
|
|
30
35
|
}
|
|
31
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve virtual environment paths.
|
|
39
|
+
*/
|
|
32
40
|
function resolveVirtualEnv(virtualEnv, cwd) {
|
|
33
41
|
const venvPath = resolve(cwd, virtualEnv);
|
|
34
42
|
const binDir = join(venvPath, getVenvBinDir());
|
|
35
43
|
const pythonPath = join(binDir, getVenvPythonExe());
|
|
36
44
|
return { venvPath, binDir, pythonPath };
|
|
37
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the environment variable key for PATH (case-insensitive on Windows).
|
|
48
|
+
*/
|
|
49
|
+
function getPathKey(env) {
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(env, 'PATH')) {
|
|
51
|
+
return 'PATH';
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(env)) {
|
|
54
|
+
if (key.toLowerCase() === 'path') {
|
|
55
|
+
return key;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return 'PATH';
|
|
59
|
+
}
|
|
60
|
+
function setPathValue(env, value) {
|
|
61
|
+
setEnvValue(env, 'PATH', value);
|
|
62
|
+
if (process.platform !== 'win32') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const key of Object.keys(env)) {
|
|
66
|
+
if (key !== 'PATH' && key.toLowerCase() === 'path') {
|
|
67
|
+
setEnvValue(env, key, value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const DANGEROUS_ENV_OVERRIDE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
72
|
+
function createNullPrototypeEnv() {
|
|
73
|
+
return Object.create(null);
|
|
74
|
+
}
|
|
75
|
+
function setEnvValue(env, key, value) {
|
|
76
|
+
Object.defineProperty(env, key, {
|
|
77
|
+
value,
|
|
78
|
+
writable: true,
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function getEnvValue(env, key) {
|
|
84
|
+
const value = Reflect.get(env, key);
|
|
85
|
+
return typeof value === 'string' ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
function assertSafeEnvOverrideKey(key) {
|
|
88
|
+
if (DANGEROUS_ENV_OVERRIDE_KEYS.has(key)) {
|
|
89
|
+
throw new BridgeProtocolError(`Invalid environment override key "${key}" in options.env`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function normalizeWarmupCommands(commands) {
|
|
93
|
+
if (commands === undefined) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(commands)) {
|
|
97
|
+
throw new BridgeProtocolError('warmupCommands must be an array when provided');
|
|
98
|
+
}
|
|
99
|
+
const warmups = commands;
|
|
100
|
+
return warmups.map((command, index) => {
|
|
101
|
+
if (!command || typeof command !== 'object' || Array.isArray(command)) {
|
|
102
|
+
throw new BridgeProtocolError(`Invalid warmup command at index ${index + 1}: expected { module, functionName, args? }`);
|
|
103
|
+
}
|
|
104
|
+
const candidate = command;
|
|
105
|
+
if ('method' in candidate || 'params' in candidate) {
|
|
106
|
+
throw new BridgeProtocolError(`Invalid warmup command at index ${index + 1}: legacy { method, params } format is no longer supported. Use { module, functionName, args? }.`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof candidate.module !== 'string' || candidate.module.trim().length === 0) {
|
|
109
|
+
throw new BridgeProtocolError(`Invalid warmup command at index ${index + 1}: "module" must be a non-empty string`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof candidate.functionName !== 'string' || candidate.functionName.trim().length === 0) {
|
|
112
|
+
throw new BridgeProtocolError(`Invalid warmup command at index ${index + 1}: "functionName" must be a non-empty string`);
|
|
113
|
+
}
|
|
114
|
+
if (candidate.args !== undefined && !Array.isArray(candidate.args)) {
|
|
115
|
+
throw new BridgeProtocolError(`Invalid warmup command at index ${index + 1}: "args" must be an array when provided`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
module: candidate.module,
|
|
119
|
+
functionName: candidate.functionName,
|
|
120
|
+
args: candidate.args,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// NODE BRIDGE
|
|
126
|
+
// =============================================================================
|
|
38
127
|
/**
|
|
39
128
|
* Node.js runtime bridge for executing Python code.
|
|
40
129
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
130
|
+
* NodeBridge provides subprocess-based Python execution with optional pooling
|
|
131
|
+
* for high-throughput workloads. By default, it runs in single-process mode.
|
|
132
|
+
*
|
|
133
|
+
* Features:
|
|
134
|
+
* - Single or multi-process execution via process pooling
|
|
135
|
+
* - Virtual environment support
|
|
136
|
+
* - Full SafeCodec validation (NaN/Infinity rejection, key validation)
|
|
137
|
+
* - Automatic Arrow decoding for DataFrames/ndarrays
|
|
138
|
+
* - Optional result caching for pure functions
|
|
139
|
+
* - Process warmup commands
|
|
43
140
|
*
|
|
44
141
|
* @example
|
|
45
142
|
* ```typescript
|
|
46
143
|
* // Single-process mode (default)
|
|
47
144
|
* const bridge = new NodeBridge();
|
|
145
|
+
* await bridge.init();
|
|
146
|
+
*
|
|
147
|
+
* const result = await bridge.call('math', 'sqrt', [16]);
|
|
148
|
+
* console.log(result); // 4.0
|
|
149
|
+
*
|
|
150
|
+
* await bridge.dispose();
|
|
151
|
+
* ```
|
|
48
152
|
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
49
155
|
* // Multi-process pooling for high throughput
|
|
50
156
|
* const pooledBridge = new NodeBridge({
|
|
51
|
-
*
|
|
52
|
-
*
|
|
157
|
+
* maxProcesses: 4,
|
|
158
|
+
* maxConcurrentPerProcess: 2,
|
|
53
159
|
* enableCache: true,
|
|
54
160
|
* });
|
|
161
|
+
* await pooledBridge.init();
|
|
55
162
|
* ```
|
|
56
163
|
*/
|
|
57
|
-
export class NodeBridge extends
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
initPromise;
|
|
66
|
-
// Performance monitoring
|
|
67
|
-
stats = {
|
|
68
|
-
totalRequests: 0,
|
|
69
|
-
totalTime: 0,
|
|
70
|
-
cacheHits: 0,
|
|
71
|
-
poolHits: 0,
|
|
72
|
-
poolMisses: 0,
|
|
73
|
-
processSpawns: 0,
|
|
74
|
-
processDeaths: 0,
|
|
75
|
-
memoryPeak: 0,
|
|
76
|
-
averageTime: 0,
|
|
77
|
-
cacheHitRate: 0,
|
|
78
|
-
};
|
|
164
|
+
export class NodeBridge extends BridgeProtocol {
|
|
165
|
+
resolvedOptions;
|
|
166
|
+
pooledTransport;
|
|
167
|
+
/**
|
|
168
|
+
* Create a new NodeBridge instance.
|
|
169
|
+
*
|
|
170
|
+
* @param options - Configuration options for the bridge
|
|
171
|
+
*/
|
|
79
172
|
constructor(options = {}) {
|
|
80
|
-
super();
|
|
81
173
|
const cwd = options.cwd ?? process.cwd();
|
|
82
174
|
const virtualEnv = options.virtualEnv ? resolve(cwd, options.virtualEnv) : undefined;
|
|
83
175
|
const venv = virtualEnv ? resolveVirtualEnv(virtualEnv, cwd) : undefined;
|
|
84
176
|
const scriptPath = options.scriptPath ?? resolveDefaultScriptPath();
|
|
85
177
|
const resolvedScriptPath = isAbsolute(scriptPath) ? scriptPath : resolve(cwd, scriptPath);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
178
|
+
const maxProcesses = options.maxProcesses ?? 1;
|
|
179
|
+
const minProcesses = Math.min(options.minProcesses ?? 1, maxProcesses);
|
|
180
|
+
const warmupCommands = normalizeWarmupCommands(options.warmupCommands);
|
|
181
|
+
const resolvedOptions = {
|
|
182
|
+
minProcesses,
|
|
183
|
+
maxProcesses,
|
|
184
|
+
maxConcurrentPerProcess: options.maxConcurrentPerProcess ?? 10,
|
|
92
185
|
pythonPath: options.pythonPath ?? venv?.pythonPath ?? getDefaultPythonPath(),
|
|
93
186
|
scriptPath: resolvedScriptPath,
|
|
94
187
|
virtualEnv,
|
|
95
188
|
cwd,
|
|
96
189
|
timeoutMs: options.timeoutMs ?? 30000,
|
|
97
|
-
|
|
190
|
+
queueTimeoutMs: options.queueTimeoutMs ?? 30000,
|
|
98
191
|
inheritProcessEnv: options.inheritProcessEnv ?? false,
|
|
99
|
-
enableJsonFallback: options.enableJsonFallback ?? false,
|
|
100
192
|
enableCache: options.enableCache ?? false,
|
|
101
193
|
env: options.env ?? {},
|
|
102
|
-
|
|
194
|
+
codec: options.codec,
|
|
195
|
+
warmupCommands,
|
|
103
196
|
};
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
197
|
+
// Build environment for ProcessIO
|
|
198
|
+
const processEnv = buildProcessEnv(resolvedOptions);
|
|
199
|
+
// Create warmup callback for per-worker initialization
|
|
200
|
+
const userWarmup = resolvedOptions.warmupCommands.length > 0
|
|
201
|
+
? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
|
|
202
|
+
: undefined;
|
|
203
|
+
const replacementWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
|
|
204
|
+
// Create pooled transport with ProcessIO workers
|
|
205
|
+
const transport = new PooledTransport({
|
|
206
|
+
createTransport: () => new ProcessIO({
|
|
207
|
+
pythonPath: resolvedOptions.pythonPath,
|
|
208
|
+
bridgeScript: resolvedOptions.scriptPath,
|
|
209
|
+
env: processEnv,
|
|
210
|
+
cwd: resolvedOptions.cwd,
|
|
211
|
+
}),
|
|
212
|
+
maxWorkers: resolvedOptions.maxProcesses,
|
|
213
|
+
minWorkers: resolvedOptions.minProcesses,
|
|
214
|
+
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
215
|
+
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
216
|
+
onWorkerReady: userWarmup,
|
|
217
|
+
onReplacementWorkerReady: replacementWorkerReady,
|
|
218
|
+
});
|
|
219
|
+
// Initialize BridgeProtocol with pooled transport
|
|
220
|
+
const protocolOptions = {
|
|
221
|
+
transport,
|
|
222
|
+
codec: resolvedOptions.codec,
|
|
223
|
+
defaultTimeoutMs: resolvedOptions.timeoutMs,
|
|
224
|
+
};
|
|
225
|
+
super(protocolOptions);
|
|
226
|
+
this.resolvedOptions = resolvedOptions;
|
|
227
|
+
this.pooledTransport = transport;
|
|
119
228
|
}
|
|
229
|
+
// ===========================================================================
|
|
230
|
+
// LIFECYCLE
|
|
231
|
+
// ===========================================================================
|
|
232
|
+
/**
|
|
233
|
+
* Initialize the bridge.
|
|
234
|
+
*
|
|
235
|
+
* Validates the bridge script exists, registers Arrow decoder,
|
|
236
|
+
* and initializes the transport pool (which runs warmup commands per-worker).
|
|
237
|
+
*/
|
|
120
238
|
async doInit() {
|
|
239
|
+
// Validate script exists
|
|
121
240
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- script path is user-configured
|
|
122
|
-
if (!existsSync(this.
|
|
123
|
-
throw new BridgeProtocolError(`Python bridge script not found at ${this.
|
|
241
|
+
if (!existsSync(this.resolvedOptions.scriptPath)) {
|
|
242
|
+
throw new BridgeProtocolError(`Python bridge script not found at ${this.resolvedOptions.scriptPath}`);
|
|
124
243
|
}
|
|
244
|
+
// Register Arrow decoder for DataFrames/ndarrays
|
|
125
245
|
const require = createRequire(import.meta.url);
|
|
126
246
|
await autoRegisterArrowDecoder({
|
|
127
247
|
loader: () => require('apache-arrow'),
|
|
128
248
|
});
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
await this.spawnProcess();
|
|
132
|
-
}
|
|
133
|
-
// Validate protocol version
|
|
134
|
-
await this.refreshBridgeInfo();
|
|
135
|
-
// Warm up processes if configured
|
|
136
|
-
if (this.options.warmupCommands.length > 0) {
|
|
137
|
-
await this.warmupProcesses();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async getBridgeInfo(options = {}) {
|
|
141
|
-
await this.init();
|
|
142
|
-
if (!this.bridgeInfo || options.refresh) {
|
|
143
|
-
await this.refreshBridgeInfo();
|
|
144
|
-
}
|
|
145
|
-
if (!this.bridgeInfo) {
|
|
146
|
-
throw new BridgeProtocolError('Bridge info unavailable');
|
|
147
|
-
}
|
|
148
|
-
return this.bridgeInfo;
|
|
249
|
+
// Initialize parent (which initializes transport and runs warmup per-worker)
|
|
250
|
+
await super.doInit();
|
|
149
251
|
}
|
|
252
|
+
// ===========================================================================
|
|
253
|
+
// CACHING OVERRIDE
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
/**
|
|
256
|
+
* Override call() to add optional caching.
|
|
257
|
+
*/
|
|
150
258
|
async call(module, functionName, args, kwargs) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.stats.cacheHits++;
|
|
160
|
-
this.updateStats(performance.now() - startTime);
|
|
161
|
-
return cached;
|
|
259
|
+
// Check cache if enabled
|
|
260
|
+
if (this.resolvedOptions.enableCache) {
|
|
261
|
+
const cacheKey = this.safeCacheKey('runtime_call', module, functionName, args, kwargs);
|
|
262
|
+
if (cacheKey) {
|
|
263
|
+
const cached = await globalCache.get(cacheKey);
|
|
264
|
+
if (cached !== null) {
|
|
265
|
+
return cached;
|
|
266
|
+
}
|
|
162
267
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const result = await
|
|
166
|
-
method: 'call',
|
|
167
|
-
params: { module, functionName, args, kwargs },
|
|
168
|
-
});
|
|
268
|
+
// Execute and cache if pure function
|
|
269
|
+
const startTime = performance.now();
|
|
270
|
+
const result = await super.call(module, functionName, args, kwargs);
|
|
169
271
|
const duration = performance.now() - startTime;
|
|
170
|
-
// Cache result for pure functions (simple heuristic)
|
|
171
272
|
if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
|
|
172
273
|
await globalCache.set(cacheKey, result, {
|
|
173
274
|
computeTime: duration,
|
|
174
275
|
dependencies: [module],
|
|
175
276
|
});
|
|
176
277
|
}
|
|
177
|
-
this.updateStats(duration);
|
|
178
278
|
return result;
|
|
179
279
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async instantiate(module, className, args, kwargs) {
|
|
186
|
-
await this.init();
|
|
187
|
-
const startTime = performance.now();
|
|
188
|
-
try {
|
|
189
|
-
const result = await this.executeRequest({
|
|
190
|
-
method: 'instantiate',
|
|
191
|
-
params: { module, className, args, kwargs },
|
|
192
|
-
});
|
|
193
|
-
this.updateStats(performance.now() - startTime);
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
this.updateStats(performance.now() - startTime, true);
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async callMethod(handle, methodName, args, kwargs) {
|
|
202
|
-
await this.init();
|
|
203
|
-
const startTime = performance.now();
|
|
204
|
-
try {
|
|
205
|
-
const result = await this.executeRequest({
|
|
206
|
-
method: 'call_method',
|
|
207
|
-
params: { handle, methodName, args, kwargs },
|
|
208
|
-
});
|
|
209
|
-
this.updateStats(performance.now() - startTime);
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
this.updateStats(performance.now() - startTime, true);
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async disposeInstance(handle) {
|
|
218
|
-
await this.init();
|
|
219
|
-
await this.executeRequest({
|
|
220
|
-
method: 'dispose_instance',
|
|
221
|
-
params: { handle },
|
|
222
|
-
});
|
|
280
|
+
// No caching - direct call
|
|
281
|
+
return super.call(module, functionName, args, kwargs);
|
|
223
282
|
}
|
|
283
|
+
// ===========================================================================
|
|
284
|
+
// POOL STATISTICS
|
|
285
|
+
// ===========================================================================
|
|
224
286
|
/**
|
|
225
|
-
*
|
|
287
|
+
* Get current pool statistics.
|
|
226
288
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
worker = await this.spawnProcess();
|
|
233
|
-
this.stats.poolMisses++;
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
throw new Error(`Failed to spawn worker process: ${error}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// Wait for worker if all are busy
|
|
240
|
-
worker ??= await this.waitForAvailableWorker();
|
|
241
|
-
this.stats.poolHits++;
|
|
242
|
-
return this.sendToWorker(worker, payload);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Select optimal worker based on load and performance
|
|
246
|
-
*/
|
|
247
|
-
selectOptimalWorker() {
|
|
248
|
-
// For single-process mode, allow concurrent requests to the same worker.
|
|
249
|
-
// BridgeCore handles request ID multiplexing, so multiple in-flight requests are safe.
|
|
250
|
-
// This preserves original NodeBridge behavior where concurrent calls were allowed.
|
|
251
|
-
if (this.options.maxProcesses === 1 && this.processPool.length === 1) {
|
|
252
|
-
const worker = this.processPool[0];
|
|
253
|
-
if (worker && !worker.quarantined && worker.process.exitCode === null) {
|
|
254
|
-
return worker;
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
// For multi-worker pools, use busy status for load balancing
|
|
259
|
-
const availableWorkers = this.processPool.filter(w => !w.busy && !w.quarantined && w.process.exitCode === null);
|
|
260
|
-
if (availableWorkers.length === 0) {
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
// Simple round-robin for now, could be enhanced with load-based selection
|
|
264
|
-
const worker = availableWorkers[this.roundRobinIndex % availableWorkers.length];
|
|
265
|
-
this.roundRobinIndex = (this.roundRobinIndex + 1) % availableWorkers.length;
|
|
266
|
-
return worker ?? null;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Wait for any worker to become available
|
|
270
|
-
*/
|
|
271
|
-
async waitForAvailableWorker(timeoutMs = 5000) {
|
|
272
|
-
return new Promise((resolvePromise, reject) => {
|
|
273
|
-
const timeout = setTimeout(() => {
|
|
274
|
-
reject(new Error('Timeout waiting for available worker'));
|
|
275
|
-
}, timeoutMs);
|
|
276
|
-
const checkWorker = () => {
|
|
277
|
-
const worker = this.selectOptimalWorker();
|
|
278
|
-
if (worker) {
|
|
279
|
-
clearTimeout(timeout);
|
|
280
|
-
resolvePromise(worker);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
// Check again in 10ms
|
|
284
|
-
setTimeout(checkWorker, 10);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
checkWorker();
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Send request to specific worker
|
|
292
|
-
*/
|
|
293
|
-
async sendToWorker(worker, payload) {
|
|
294
|
-
worker.busy = true;
|
|
295
|
-
worker.requestCount++;
|
|
296
|
-
worker.lastUsed = Date.now();
|
|
297
|
-
const startTime = performance.now();
|
|
298
|
-
try {
|
|
299
|
-
const result = await worker.core.send(payload);
|
|
300
|
-
const duration = performance.now() - startTime;
|
|
301
|
-
worker.stats.totalTime += duration;
|
|
302
|
-
worker.stats.totalRequests++;
|
|
303
|
-
worker.stats.averageTime = worker.stats.totalTime / worker.stats.totalRequests;
|
|
304
|
-
return result;
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
worker.stats.errorCount++;
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
worker.busy = false;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
quarantineWorker(worker, error) {
|
|
315
|
-
if (worker.quarantined) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
worker.quarantined = true;
|
|
319
|
-
log.warn('Quarantining worker', { workerId: worker.id, error: String(error) });
|
|
320
|
-
this.terminateWorker(worker, { force: true })
|
|
321
|
-
.then(() => {
|
|
322
|
-
if (!this.disposed && this.processPool.length < this.options.minProcesses) {
|
|
323
|
-
this.spawnProcess().catch(spawnError => {
|
|
324
|
-
log.error('Failed to spawn replacement worker after quarantine', {
|
|
325
|
-
error: String(spawnError),
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
})
|
|
330
|
-
.catch(terminateError => {
|
|
331
|
-
log.warn('Failed to terminate quarantined worker', {
|
|
332
|
-
workerId: worker.id,
|
|
333
|
-
error: String(terminateError),
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Spawn new worker process with optimizations
|
|
339
|
-
*/
|
|
340
|
-
async spawnProcess() {
|
|
341
|
-
const { spawn } = await import('child_process');
|
|
342
|
-
const workerId = `worker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
343
|
-
const env = this.buildEnv();
|
|
344
|
-
env.PYTHONUNBUFFERED = '1'; // Ensure immediate output
|
|
345
|
-
env.PYTHONDONTWRITEBYTECODE = '1'; // Skip .pyc files for faster startup
|
|
346
|
-
const maxLineLength = this.options.maxLineLength ?? getMaxLineLengthFromEnv(env);
|
|
347
|
-
const childProcess = spawn(this.options.pythonPath, [this.options.scriptPath], {
|
|
348
|
-
cwd: this.options.cwd,
|
|
349
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
350
|
-
env,
|
|
351
|
-
});
|
|
352
|
-
const worker = {
|
|
353
|
-
process: childProcess,
|
|
354
|
-
id: workerId,
|
|
355
|
-
requestCount: 0,
|
|
356
|
-
lastUsed: Date.now(),
|
|
357
|
-
busy: false,
|
|
358
|
-
quarantined: false,
|
|
359
|
-
core: null,
|
|
360
|
-
stats: {
|
|
361
|
-
totalRequests: 0,
|
|
362
|
-
totalTime: 0,
|
|
363
|
-
averageTime: 0,
|
|
364
|
-
errorCount: 0,
|
|
365
|
-
},
|
|
289
|
+
getPoolStats() {
|
|
290
|
+
return {
|
|
291
|
+
workerCount: this.pooledTransport.workerCount,
|
|
292
|
+
queueLength: this.pooledTransport.queueLength,
|
|
293
|
+
totalInFlight: this.pooledTransport.totalInFlight,
|
|
366
294
|
};
|
|
367
|
-
worker.core = new BridgeCore({
|
|
368
|
-
write: (data) => {
|
|
369
|
-
if (!worker.process.stdin?.writable) {
|
|
370
|
-
throw new BridgeProtocolError('Worker process stdin not writable');
|
|
371
|
-
}
|
|
372
|
-
worker.process.stdin.write(data);
|
|
373
|
-
},
|
|
374
|
-
}, {
|
|
375
|
-
timeoutMs: this.options.timeoutMs,
|
|
376
|
-
maxLineLength,
|
|
377
|
-
decodeValue: decodeValueAsync,
|
|
378
|
-
onFatalError: (error) => this.quarantineWorker(worker, error),
|
|
379
|
-
onTimeout: (error) => this.quarantineWorker(worker, error),
|
|
380
|
-
});
|
|
381
|
-
// Setup process event handlers
|
|
382
|
-
this.setupProcessHandlers(worker);
|
|
383
|
-
this.processPool.push(worker);
|
|
384
|
-
this.stats.processSpawns++;
|
|
385
|
-
return worker;
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Setup event handlers for worker process
|
|
389
|
-
*/
|
|
390
|
-
setupProcessHandlers(worker) {
|
|
391
|
-
const childProcess = worker.process;
|
|
392
|
-
childProcess.stdout?.on('data', (chunk) => {
|
|
393
|
-
worker.core.handleStdoutData(chunk);
|
|
394
|
-
});
|
|
395
|
-
childProcess.stderr?.on('data', (chunk) => {
|
|
396
|
-
worker.core.handleStderrData(chunk);
|
|
397
|
-
const errorText = chunk.toString().trim();
|
|
398
|
-
if (errorText) {
|
|
399
|
-
log.warn('Worker stderr', { workerId: worker.id, output: errorText });
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
// Handle process exit
|
|
403
|
-
childProcess.on('exit', code => {
|
|
404
|
-
log.warn('Worker exited', { workerId: worker.id, code });
|
|
405
|
-
worker.core.handleProcessExit();
|
|
406
|
-
this.handleWorkerExit(worker, code);
|
|
407
|
-
});
|
|
408
|
-
// Handle process errors
|
|
409
|
-
childProcess.on('error', error => {
|
|
410
|
-
log.error('Worker error', { workerId: worker.id, error: String(error) });
|
|
411
|
-
worker.core.handleProcessError(error);
|
|
412
|
-
this.handleWorkerExit(worker, -1);
|
|
413
|
-
});
|
|
414
295
|
}
|
|
415
296
|
/**
|
|
416
|
-
*
|
|
297
|
+
* Get bridge statistics.
|
|
298
|
+
*
|
|
299
|
+
* @deprecated Use getPoolStats() instead. This method is provided for
|
|
300
|
+
* backwards compatibility and returns a subset of the previous stats.
|
|
417
301
|
*/
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
302
|
+
getStats() {
|
|
303
|
+
const poolStats = this.getPoolStats();
|
|
304
|
+
return {
|
|
305
|
+
// Legacy stats (no longer tracked, return 0)
|
|
306
|
+
totalRequests: 0,
|
|
307
|
+
totalTime: 0,
|
|
308
|
+
cacheHits: 0,
|
|
309
|
+
poolHits: 0,
|
|
310
|
+
poolMisses: 0,
|
|
311
|
+
processSpawns: 0,
|
|
312
|
+
processDeaths: 0,
|
|
313
|
+
memoryPeak: 0,
|
|
314
|
+
averageTime: 0,
|
|
315
|
+
cacheHitRate: 0,
|
|
316
|
+
// Current pool stats
|
|
317
|
+
poolSize: poolStats.workerCount,
|
|
318
|
+
busyWorkers: poolStats.totalInFlight,
|
|
319
|
+
};
|
|
435
320
|
}
|
|
321
|
+
// ===========================================================================
|
|
322
|
+
// PRIVATE HELPERS
|
|
323
|
+
// ===========================================================================
|
|
436
324
|
/**
|
|
437
|
-
*
|
|
325
|
+
* Generate a cache key, returning null if generation fails.
|
|
438
326
|
*/
|
|
439
|
-
async warmupProcesses() {
|
|
440
|
-
const warmupPromises = this.processPool.map(async (worker) => {
|
|
441
|
-
for (const cmd of this.options.warmupCommands) {
|
|
442
|
-
try {
|
|
443
|
-
await this.sendToWorker(worker, {
|
|
444
|
-
method: cmd.method,
|
|
445
|
-
params: cmd.params,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
catch (error) {
|
|
449
|
-
log.warn('Warmup command failed', { workerId: worker.id, error: String(error) });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
await Promise.all(warmupPromises);
|
|
454
|
-
}
|
|
455
327
|
safeCacheKey(prefix, ...inputs) {
|
|
456
328
|
try {
|
|
457
329
|
return globalCache.generateKey(prefix, ...inputs);
|
|
@@ -461,10 +333,9 @@ export class NodeBridge extends RuntimeBridge {
|
|
|
461
333
|
}
|
|
462
334
|
}
|
|
463
335
|
/**
|
|
464
|
-
* Heuristic to determine if function result should be cached
|
|
336
|
+
* Heuristic to determine if function result should be cached.
|
|
465
337
|
*/
|
|
466
338
|
isPureFunctionCandidate(functionName, args) {
|
|
467
|
-
// Simple heuristics - could be made more sophisticated
|
|
468
339
|
const pureFunctionPatterns = [
|
|
469
340
|
/^(get|fetch|read|load|find|search|query|select)_/i,
|
|
470
341
|
/^(compute|calculate|process|transform|convert)_/i,
|
|
@@ -475,176 +346,176 @@ export class NodeBridge extends RuntimeBridge {
|
|
|
475
346
|
/^(send|post|put|patch)_/i,
|
|
476
347
|
/random|uuid|timestamp|now|current/i,
|
|
477
348
|
];
|
|
478
|
-
// Don't cache if function name suggests mutation
|
|
479
349
|
if (impureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
480
350
|
return false;
|
|
481
351
|
}
|
|
482
|
-
// Cache if function name suggests pure computation
|
|
483
352
|
if (pureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
484
353
|
return true;
|
|
485
354
|
}
|
|
486
|
-
// Don't cache if args contain mutable objects (very basic check)
|
|
487
355
|
const hasComplexArgs = args.some(arg => arg !== null && typeof arg === 'object' && !(arg instanceof Date));
|
|
488
|
-
return !hasComplexArgs && args.length <= 3;
|
|
356
|
+
return !hasComplexArgs && args.length <= 3;
|
|
489
357
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
358
|
+
}
|
|
359
|
+
// =============================================================================
|
|
360
|
+
// WARMUP CALLBACK
|
|
361
|
+
// =============================================================================
|
|
362
|
+
/**
|
|
363
|
+
* Simple request ID generator for warmup commands.
|
|
364
|
+
*/
|
|
365
|
+
let warmupRequestId = 0;
|
|
366
|
+
function generateWarmupId() {
|
|
367
|
+
return ++warmupRequestId;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Create a callback that runs warmup commands on each worker.
|
|
371
|
+
*
|
|
372
|
+
* The callback sends warmup commands directly to the worker's transport,
|
|
373
|
+
* bypassing the pool to ensure each worker gets warmed up individually.
|
|
374
|
+
*/
|
|
375
|
+
function createWarmupCallback(warmupCommands, timeoutMs) {
|
|
376
|
+
return async (worker) => {
|
|
377
|
+
for (const [index, cmd] of warmupCommands.entries()) {
|
|
378
|
+
const commandLabel = `${cmd.module}.${cmd.functionName}`;
|
|
379
|
+
await executeWorkerCall(worker, {
|
|
380
|
+
module: cmd.module,
|
|
381
|
+
functionName: cmd.functionName,
|
|
382
|
+
args: cmd.args ?? [],
|
|
383
|
+
}, timeoutMs, {
|
|
384
|
+
label: `Warmup command #${index + 1} (${commandLabel})`,
|
|
385
|
+
invalidJsonMessage: 'returned invalid JSON response',
|
|
386
|
+
malformedEnvelopeMessage: requestId => `returned malformed response envelope for request ${requestId}`,
|
|
387
|
+
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
388
|
+
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
389
|
+
});
|
|
499
390
|
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function createWorkerReadyCallback(timeoutMs, extraWarmup) {
|
|
394
|
+
return async (worker) => {
|
|
395
|
+
const result = await executeWorkerCall(worker, {
|
|
396
|
+
module: 'builtins',
|
|
397
|
+
functionName: 'len',
|
|
398
|
+
args: [[]],
|
|
399
|
+
}, Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS), {
|
|
400
|
+
label: 'Worker readiness probe (builtins.len)',
|
|
401
|
+
invalidJsonMessage: 'returned invalid JSON response',
|
|
402
|
+
malformedEnvelopeMessage: requestId => `returned malformed response envelope for request ${requestId}`,
|
|
403
|
+
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
404
|
+
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
405
|
+
});
|
|
406
|
+
if (result !== 0) {
|
|
407
|
+
throw new BridgeExecutionError(`Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`);
|
|
408
|
+
}
|
|
409
|
+
await extraWarmup?.(worker);
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async function executeWorkerCall(worker, request, timeoutMs, messages) {
|
|
413
|
+
const requestId = generateWarmupId();
|
|
414
|
+
let message;
|
|
415
|
+
try {
|
|
416
|
+
message = JSON.stringify({
|
|
417
|
+
id: requestId,
|
|
418
|
+
protocol: PROTOCOL_ID,
|
|
419
|
+
method: 'call',
|
|
420
|
+
params: {
|
|
421
|
+
module: request.module,
|
|
422
|
+
functionName: request.functionName,
|
|
423
|
+
args: request.args,
|
|
424
|
+
kwargs: {},
|
|
425
|
+
},
|
|
426
|
+
});
|
|
500
427
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
*/
|
|
504
|
-
getStats() {
|
|
505
|
-
const avgTime = this.stats.totalRequests > 0 ? this.stats.totalTime / this.stats.totalRequests : 0;
|
|
506
|
-
const hitRate = this.stats.totalRequests > 0 ? this.stats.cacheHits / this.stats.totalRequests : 0;
|
|
507
|
-
return {
|
|
508
|
-
...this.stats,
|
|
509
|
-
averageTime: avgTime,
|
|
510
|
-
cacheHitRate: hitRate,
|
|
511
|
-
poolSize: this.processPool.length,
|
|
512
|
-
busyWorkers: this.processPool.filter(w => w.busy).length,
|
|
513
|
-
memoryUsage: process.memoryUsage(),
|
|
514
|
-
workerStats: this.processPool.map(w => ({
|
|
515
|
-
id: w.id,
|
|
516
|
-
requestCount: w.requestCount,
|
|
517
|
-
averageTime: w.stats.averageTime,
|
|
518
|
-
errorCount: w.stats.errorCount,
|
|
519
|
-
busy: w.busy,
|
|
520
|
-
pendingRequests: w.core.getPendingCount(),
|
|
521
|
-
})),
|
|
522
|
-
};
|
|
428
|
+
catch (error) {
|
|
429
|
+
throw new BridgeExecutionError(`${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
523
430
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
async cleanup() {
|
|
528
|
-
const now = Date.now();
|
|
529
|
-
const idleWorkers = this.processPool.filter(w => !w.busy &&
|
|
530
|
-
now - w.lastUsed > this.options.maxIdleTime &&
|
|
531
|
-
this.processPool.length > this.options.minProcesses);
|
|
532
|
-
for (const worker of idleWorkers) {
|
|
533
|
-
await this.terminateWorker(worker);
|
|
534
|
-
}
|
|
535
|
-
// Restart workers that have handled too many requests
|
|
536
|
-
const overusedWorkers = this.processPool.filter(w => !w.busy && w.requestCount >= this.options.maxRequestsPerProcess);
|
|
537
|
-
for (const worker of overusedWorkers) {
|
|
538
|
-
await this.terminateWorker(worker);
|
|
539
|
-
if (this.processPool.length < this.options.minProcesses) {
|
|
540
|
-
await this.spawnProcess();
|
|
541
|
-
}
|
|
542
|
-
}
|
|
431
|
+
let response;
|
|
432
|
+
try {
|
|
433
|
+
response = await worker.transport.send(message, timeoutMs);
|
|
543
434
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
*/
|
|
547
|
-
async terminateWorker(worker, options = {}) {
|
|
548
|
-
if (worker.busy && !options.force) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
const index = this.processPool.indexOf(worker);
|
|
552
|
-
if (index >= 0) {
|
|
553
|
-
this.processPool.splice(index, 1);
|
|
554
|
-
this.stats.processDeaths++;
|
|
555
|
-
}
|
|
556
|
-
worker.core.handleProcessExit();
|
|
557
|
-
worker.core.clear();
|
|
558
|
-
// Graceful termination
|
|
559
|
-
try {
|
|
560
|
-
if (worker.process.exitCode === null) {
|
|
561
|
-
worker.process.kill('SIGTERM');
|
|
562
|
-
// Force kill if not terminated in 5 seconds
|
|
563
|
-
setTimeout(() => {
|
|
564
|
-
if (worker.process.exitCode === null) {
|
|
565
|
-
worker.process.kill('SIGKILL');
|
|
566
|
-
}
|
|
567
|
-
}, 5000);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
catch (error) {
|
|
571
|
-
log.warn('Error terminating worker', { workerId: worker.id, error: String(error) });
|
|
572
|
-
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
throw new BridgeExecutionError(`${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
|
|
573
437
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
startCleanupScheduler() {
|
|
578
|
-
this.cleanupTimer = setInterval(async () => {
|
|
579
|
-
try {
|
|
580
|
-
await this.cleanup();
|
|
581
|
-
}
|
|
582
|
-
catch (error) {
|
|
583
|
-
log.error('Cleanup error', { error: String(error) });
|
|
584
|
-
}
|
|
585
|
-
}, 60000); // Cleanup every minute
|
|
438
|
+
let parsed;
|
|
439
|
+
try {
|
|
440
|
+
parsed = JSON.parse(response);
|
|
586
441
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (this.disposed) {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
this.disposed = true;
|
|
595
|
-
if (this.cleanupTimer) {
|
|
596
|
-
clearInterval(this.cleanupTimer);
|
|
597
|
-
this.cleanupTimer = undefined;
|
|
598
|
-
}
|
|
599
|
-
// Terminate all workers
|
|
600
|
-
const terminationPromises = this.processPool.map(worker => this.terminateWorker(worker, { force: true }));
|
|
601
|
-
await Promise.all(terminationPromises);
|
|
602
|
-
this.processPool.length = 0;
|
|
603
|
-
this.emitter.removeAllListeners();
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
|
|
444
|
+
cause: error instanceof Error ? error : undefined,
|
|
445
|
+
});
|
|
604
446
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
447
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
448
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`);
|
|
449
|
+
}
|
|
450
|
+
const envelope = parsed;
|
|
451
|
+
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
452
|
+
const errorPayload = envelope.error;
|
|
453
|
+
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
454
|
+
const err = errorPayload;
|
|
455
|
+
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
456
|
+
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
|
|
457
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`);
|
|
458
|
+
}
|
|
459
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
|
|
460
|
+
}
|
|
461
|
+
if (!('result' in envelope)) {
|
|
462
|
+
throw new BridgeExecutionError(`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`);
|
|
463
|
+
}
|
|
464
|
+
return envelope.result;
|
|
465
|
+
}
|
|
466
|
+
// =============================================================================
|
|
467
|
+
// ENVIRONMENT BUILDING
|
|
468
|
+
// =============================================================================
|
|
469
|
+
/**
|
|
470
|
+
* Build environment variables for ProcessIO.
|
|
471
|
+
*/
|
|
472
|
+
function buildProcessEnv(options) {
|
|
473
|
+
const allowedPrefixes = ['TYWRAP_'];
|
|
474
|
+
const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
|
|
475
|
+
const env = createNullPrototypeEnv();
|
|
476
|
+
// Copy allowed env vars from process.env
|
|
477
|
+
if (options.inheritProcessEnv) {
|
|
478
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
479
|
+
if (value !== undefined) {
|
|
480
|
+
setEnvValue(env, key, value);
|
|
613
481
|
}
|
|
614
482
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
486
|
+
if (value !== undefined &&
|
|
487
|
+
(allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))) {
|
|
488
|
+
setEnvValue(env, key, value);
|
|
622
489
|
}
|
|
623
490
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const currentPath = env[pathKey] ?? '';
|
|
631
|
-
// eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
|
|
632
|
-
env[pathKey] = `${venv.binDir}${delimiter}${currentPath}`;
|
|
491
|
+
}
|
|
492
|
+
// Apply user overrides
|
|
493
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
494
|
+
assertSafeEnvOverrideKey(key);
|
|
495
|
+
if (value !== undefined) {
|
|
496
|
+
setEnvValue(env, key, value);
|
|
633
497
|
}
|
|
634
|
-
ensurePythonEncoding(env);
|
|
635
|
-
ensureJsonFallback(env, this.options.enableJsonFallback);
|
|
636
|
-
env = normalizeEnv(env, {});
|
|
637
|
-
return env;
|
|
638
498
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
499
|
+
// Configure virtual environment
|
|
500
|
+
if (options.virtualEnv) {
|
|
501
|
+
const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
|
|
502
|
+
env.VIRTUAL_ENV = venv.venvPath;
|
|
503
|
+
const pathKey = getPathKey(env);
|
|
504
|
+
const currentPath = getEnvValue(env, pathKey);
|
|
505
|
+
setPathValue(env, currentPath ? `${venv.binDir}${delimiter}${currentPath}` : venv.binDir);
|
|
643
506
|
}
|
|
507
|
+
// Add cwd to PYTHONPATH so Python can find modules in the working directory
|
|
508
|
+
if (options.cwd) {
|
|
509
|
+
const currentPythonPath = env.PYTHONPATH ?? '';
|
|
510
|
+
env.PYTHONPATH = currentPythonPath
|
|
511
|
+
? `${options.cwd}${delimiter}${currentPythonPath}`
|
|
512
|
+
: options.cwd;
|
|
513
|
+
}
|
|
514
|
+
// Ensure Python uses UTF-8
|
|
515
|
+
env.PYTHONUTF8 = '1';
|
|
516
|
+
env.PYTHONIOENCODING = 'UTF-8';
|
|
517
|
+
env.PYTHONUNBUFFERED = '1';
|
|
518
|
+
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
519
|
+
return env;
|
|
644
520
|
}
|
|
645
|
-
/**
|
|
646
|
-
* @deprecated Use NodeBridge with minProcesses/maxProcesses options instead.
|
|
647
|
-
* This alias is provided for backward compatibility.
|
|
648
|
-
*/
|
|
649
|
-
export { NodeBridge as OptimizedNodeBridge };
|
|
650
521
|
//# sourceMappingURL=node.js.map
|