tywrap 0.2.0 → 0.2.1
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 +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- 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-protocol.d.ts +167 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +247 -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/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 +47 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +55 -74
- 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 +256 -523
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +131 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +175 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +204 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +695 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +155 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +397 -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 +81 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +345 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +186 -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 +131 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +219 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +196 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +371 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +120 -1
- package/dist/utils/codec.js.map +1 -1
- package/package.json +2 -2
- package/runtime/python_bridge.py +30 -3
- package/runtime/safe_codec.py +344 -0
- package/src/index.ts +48 -5
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-protocol.ts +319 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/http-io.ts +244 -0
- package/src/runtime/http.ts +71 -117
- package/src/runtime/node.ts +358 -691
- package/src/runtime/pooled-transport.ts +252 -0
- package/src/runtime/process-io.ts +902 -0
- package/src/runtime/pyodide-io.ts +485 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +443 -0
- package/src/runtime/transport.ts +273 -0
- package/src/runtime/validators.ts +241 -0
- package/src/runtime/worker-pool.ts +498 -0
- package/src/utils/codec.ts +126 -1
package/dist/runtime/node.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
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 { BridgeProtocolError } from './errors.js';
|
|
19
|
+
import { ProcessIO } from './process-io.js';
|
|
20
|
+
import { PooledTransport } from './pooled-transport.js';
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// UTILITIES
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the default bridge script path.
|
|
26
|
+
*/
|
|
24
27
|
function resolveDefaultScriptPath() {
|
|
25
28
|
try {
|
|
26
29
|
return fileURLToPath(new URL('../../runtime/python_bridge.py', import.meta.url));
|
|
@@ -29,429 +32,226 @@ function resolveDefaultScriptPath() {
|
|
|
29
32
|
return 'runtime/python_bridge.py';
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve virtual environment paths.
|
|
37
|
+
*/
|
|
32
38
|
function resolveVirtualEnv(virtualEnv, cwd) {
|
|
33
39
|
const venvPath = resolve(cwd, virtualEnv);
|
|
34
40
|
const binDir = join(venvPath, getVenvBinDir());
|
|
35
41
|
const pythonPath = join(binDir, getVenvPythonExe());
|
|
36
42
|
return { venvPath, binDir, pythonPath };
|
|
37
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the environment variable key for PATH (case-insensitive on Windows).
|
|
46
|
+
*/
|
|
47
|
+
function getPathKey(env) {
|
|
48
|
+
for (const key of Object.keys(env)) {
|
|
49
|
+
if (key.toLowerCase() === 'path') {
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return 'PATH';
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// NODE BRIDGE
|
|
57
|
+
// =============================================================================
|
|
38
58
|
/**
|
|
39
59
|
* Node.js runtime bridge for executing Python code.
|
|
40
60
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
61
|
+
* NodeBridge provides subprocess-based Python execution with optional pooling
|
|
62
|
+
* for high-throughput workloads. By default, it runs in single-process mode.
|
|
63
|
+
*
|
|
64
|
+
* Features:
|
|
65
|
+
* - Single or multi-process execution via process pooling
|
|
66
|
+
* - Virtual environment support
|
|
67
|
+
* - Full SafeCodec validation (NaN/Infinity rejection, key validation)
|
|
68
|
+
* - Automatic Arrow decoding for DataFrames/ndarrays
|
|
69
|
+
* - Optional result caching for pure functions
|
|
70
|
+
* - Process warmup commands
|
|
43
71
|
*
|
|
44
72
|
* @example
|
|
45
73
|
* ```typescript
|
|
46
74
|
* // Single-process mode (default)
|
|
47
75
|
* const bridge = new NodeBridge();
|
|
76
|
+
* await bridge.init();
|
|
77
|
+
*
|
|
78
|
+
* const result = await bridge.call('math', 'sqrt', [16]);
|
|
79
|
+
* console.log(result); // 4.0
|
|
48
80
|
*
|
|
81
|
+
* await bridge.dispose();
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
49
86
|
* // Multi-process pooling for high throughput
|
|
50
87
|
* const pooledBridge = new NodeBridge({
|
|
51
|
-
*
|
|
52
|
-
*
|
|
88
|
+
* maxProcesses: 4,
|
|
89
|
+
* maxConcurrentPerProcess: 2,
|
|
53
90
|
* enableCache: true,
|
|
54
91
|
* });
|
|
92
|
+
* await pooledBridge.init();
|
|
55
93
|
* ```
|
|
56
94
|
*/
|
|
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
|
-
};
|
|
95
|
+
export class NodeBridge extends BridgeProtocol {
|
|
96
|
+
resolvedOptions;
|
|
97
|
+
pooledTransport;
|
|
98
|
+
/**
|
|
99
|
+
* Create a new NodeBridge instance.
|
|
100
|
+
*
|
|
101
|
+
* @param options - Configuration options for the bridge
|
|
102
|
+
*/
|
|
79
103
|
constructor(options = {}) {
|
|
80
|
-
super();
|
|
81
104
|
const cwd = options.cwd ?? process.cwd();
|
|
82
105
|
const virtualEnv = options.virtualEnv ? resolve(cwd, options.virtualEnv) : undefined;
|
|
83
106
|
const venv = virtualEnv ? resolveVirtualEnv(virtualEnv, cwd) : undefined;
|
|
84
107
|
const scriptPath = options.scriptPath ?? resolveDefaultScriptPath();
|
|
85
108
|
const resolvedScriptPath = isAbsolute(scriptPath) ? scriptPath : resolve(cwd, scriptPath);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
const maxProcesses = options.maxProcesses ?? 1;
|
|
110
|
+
const minProcesses = Math.min(options.minProcesses ?? 1, maxProcesses);
|
|
111
|
+
const resolvedOptions = {
|
|
112
|
+
minProcesses,
|
|
113
|
+
maxProcesses,
|
|
114
|
+
maxConcurrentPerProcess: options.maxConcurrentPerProcess ?? 10,
|
|
92
115
|
pythonPath: options.pythonPath ?? venv?.pythonPath ?? getDefaultPythonPath(),
|
|
93
116
|
scriptPath: resolvedScriptPath,
|
|
94
117
|
virtualEnv,
|
|
95
118
|
cwd,
|
|
96
119
|
timeoutMs: options.timeoutMs ?? 30000,
|
|
97
|
-
|
|
120
|
+
queueTimeoutMs: options.queueTimeoutMs ?? 30000,
|
|
98
121
|
inheritProcessEnv: options.inheritProcessEnv ?? false,
|
|
99
|
-
enableJsonFallback: options.enableJsonFallback ?? false,
|
|
100
122
|
enableCache: options.enableCache ?? false,
|
|
101
123
|
env: options.env ?? {},
|
|
124
|
+
codec: options.codec,
|
|
102
125
|
warmupCommands: options.warmupCommands ?? [],
|
|
103
126
|
};
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
// Build environment for ProcessIO
|
|
128
|
+
const processEnv = buildProcessEnv(resolvedOptions);
|
|
129
|
+
// Create warmup callback for per-worker initialization
|
|
130
|
+
const onWorkerReady = resolvedOptions.warmupCommands.length > 0
|
|
131
|
+
? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
|
|
132
|
+
: undefined;
|
|
133
|
+
// Create pooled transport with ProcessIO workers
|
|
134
|
+
const transport = new PooledTransport({
|
|
135
|
+
createTransport: () => new ProcessIO({
|
|
136
|
+
pythonPath: resolvedOptions.pythonPath,
|
|
137
|
+
bridgeScript: resolvedOptions.scriptPath,
|
|
138
|
+
env: processEnv,
|
|
139
|
+
cwd: resolvedOptions.cwd,
|
|
140
|
+
}),
|
|
141
|
+
maxWorkers: resolvedOptions.maxProcesses,
|
|
142
|
+
minWorkers: resolvedOptions.minProcesses,
|
|
143
|
+
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
144
|
+
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
145
|
+
onWorkerReady,
|
|
146
|
+
});
|
|
147
|
+
// Initialize BridgeProtocol with pooled transport
|
|
148
|
+
const protocolOptions = {
|
|
149
|
+
transport,
|
|
150
|
+
codec: resolvedOptions.codec,
|
|
151
|
+
defaultTimeoutMs: resolvedOptions.timeoutMs,
|
|
152
|
+
};
|
|
153
|
+
super(protocolOptions);
|
|
154
|
+
this.resolvedOptions = resolvedOptions;
|
|
155
|
+
this.pooledTransport = transport;
|
|
119
156
|
}
|
|
157
|
+
// ===========================================================================
|
|
158
|
+
// LIFECYCLE
|
|
159
|
+
// ===========================================================================
|
|
160
|
+
/**
|
|
161
|
+
* Initialize the bridge.
|
|
162
|
+
*
|
|
163
|
+
* Validates the bridge script exists, registers Arrow decoder,
|
|
164
|
+
* and initializes the transport pool (which runs warmup commands per-worker).
|
|
165
|
+
*/
|
|
120
166
|
async doInit() {
|
|
167
|
+
// Validate script exists
|
|
121
168
|
// 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.
|
|
169
|
+
if (!existsSync(this.resolvedOptions.scriptPath)) {
|
|
170
|
+
throw new BridgeProtocolError(`Python bridge script not found at ${this.resolvedOptions.scriptPath}`);
|
|
124
171
|
}
|
|
172
|
+
// Register Arrow decoder for DataFrames/ndarrays
|
|
125
173
|
const require = createRequire(import.meta.url);
|
|
126
174
|
await autoRegisterArrowDecoder({
|
|
127
175
|
loader: () => require('apache-arrow'),
|
|
128
176
|
});
|
|
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;
|
|
177
|
+
// Initialize parent (which initializes transport and runs warmup per-worker)
|
|
178
|
+
await super.doInit();
|
|
149
179
|
}
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// CACHING OVERRIDE
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Override call() to add optional caching.
|
|
185
|
+
*/
|
|
150
186
|
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;
|
|
187
|
+
// Check cache if enabled
|
|
188
|
+
if (this.resolvedOptions.enableCache) {
|
|
189
|
+
const cacheKey = this.safeCacheKey('runtime_call', module, functionName, args, kwargs);
|
|
190
|
+
if (cacheKey) {
|
|
191
|
+
const cached = await globalCache.get(cacheKey);
|
|
192
|
+
if (cached !== null) {
|
|
193
|
+
return cached;
|
|
194
|
+
}
|
|
162
195
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const result = await
|
|
166
|
-
method: 'call',
|
|
167
|
-
params: { module, functionName, args, kwargs },
|
|
168
|
-
});
|
|
196
|
+
// Execute and cache if pure function
|
|
197
|
+
const startTime = performance.now();
|
|
198
|
+
const result = await super.call(module, functionName, args, kwargs);
|
|
169
199
|
const duration = performance.now() - startTime;
|
|
170
|
-
// Cache result for pure functions (simple heuristic)
|
|
171
200
|
if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
|
|
172
201
|
await globalCache.set(cacheKey, result, {
|
|
173
202
|
computeTime: duration,
|
|
174
203
|
dependencies: [module],
|
|
175
204
|
});
|
|
176
205
|
}
|
|
177
|
-
this.updateStats(duration);
|
|
178
206
|
return result;
|
|
179
207
|
}
|
|
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
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Execute request with intelligent process selection
|
|
226
|
-
*/
|
|
227
|
-
async executeRequest(payload) {
|
|
228
|
-
let worker = this.selectOptimalWorker();
|
|
229
|
-
// Spawn new process if none available and under limit
|
|
230
|
-
if (!worker && this.processPool.length < this.options.maxProcesses) {
|
|
231
|
-
try {
|
|
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
|
-
});
|
|
208
|
+
// No caching - direct call
|
|
209
|
+
return super.call(module, functionName, args, kwargs);
|
|
336
210
|
}
|
|
211
|
+
// ===========================================================================
|
|
212
|
+
// POOL STATISTICS
|
|
213
|
+
// ===========================================================================
|
|
337
214
|
/**
|
|
338
|
-
*
|
|
215
|
+
* Get current pool statistics.
|
|
339
216
|
*/
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
},
|
|
217
|
+
getPoolStats() {
|
|
218
|
+
return {
|
|
219
|
+
workerCount: this.pooledTransport.workerCount,
|
|
220
|
+
queueLength: this.pooledTransport.queueLength,
|
|
221
|
+
totalInFlight: this.pooledTransport.totalInFlight,
|
|
366
222
|
};
|
|
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
223
|
}
|
|
387
224
|
/**
|
|
388
|
-
*
|
|
225
|
+
* Get bridge statistics.
|
|
226
|
+
*
|
|
227
|
+
* @deprecated Use getPoolStats() instead. This method is provided for
|
|
228
|
+
* backwards compatibility and returns a subset of the previous stats.
|
|
389
229
|
*/
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Handle worker process exit
|
|
417
|
-
*/
|
|
418
|
-
handleWorkerExit(worker, _code) {
|
|
419
|
-
if (!this.processPool.includes(worker)) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
worker.core.clear();
|
|
423
|
-
// Remove from pool
|
|
424
|
-
const index = this.processPool.indexOf(worker);
|
|
425
|
-
if (index >= 0) {
|
|
426
|
-
this.processPool.splice(index, 1);
|
|
427
|
-
this.stats.processDeaths++;
|
|
428
|
-
}
|
|
429
|
-
// Spawn replacement if needed and not disposing
|
|
430
|
-
if (!this.disposed && this.processPool.length < this.options.minProcesses) {
|
|
431
|
-
this.spawnProcess().catch(error => {
|
|
432
|
-
log.error('Failed to spawn replacement worker', { error: String(error) });
|
|
433
|
-
});
|
|
434
|
-
}
|
|
230
|
+
getStats() {
|
|
231
|
+
const poolStats = this.getPoolStats();
|
|
232
|
+
return {
|
|
233
|
+
// Legacy stats (no longer tracked, return 0)
|
|
234
|
+
totalRequests: 0,
|
|
235
|
+
totalTime: 0,
|
|
236
|
+
cacheHits: 0,
|
|
237
|
+
poolHits: 0,
|
|
238
|
+
poolMisses: 0,
|
|
239
|
+
processSpawns: 0,
|
|
240
|
+
processDeaths: 0,
|
|
241
|
+
memoryPeak: 0,
|
|
242
|
+
averageTime: 0,
|
|
243
|
+
cacheHitRate: 0,
|
|
244
|
+
// Current pool stats
|
|
245
|
+
poolSize: poolStats.workerCount,
|
|
246
|
+
busyWorkers: poolStats.totalInFlight,
|
|
247
|
+
};
|
|
435
248
|
}
|
|
249
|
+
// ===========================================================================
|
|
250
|
+
// PRIVATE HELPERS
|
|
251
|
+
// ===========================================================================
|
|
436
252
|
/**
|
|
437
|
-
*
|
|
253
|
+
* Generate a cache key, returning null if generation fails.
|
|
438
254
|
*/
|
|
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
255
|
safeCacheKey(prefix, ...inputs) {
|
|
456
256
|
try {
|
|
457
257
|
return globalCache.generateKey(prefix, ...inputs);
|
|
@@ -461,10 +261,9 @@ export class NodeBridge extends RuntimeBridge {
|
|
|
461
261
|
}
|
|
462
262
|
}
|
|
463
263
|
/**
|
|
464
|
-
* Heuristic to determine if function result should be cached
|
|
264
|
+
* Heuristic to determine if function result should be cached.
|
|
465
265
|
*/
|
|
466
266
|
isPureFunctionCandidate(functionName, args) {
|
|
467
|
-
// Simple heuristics - could be made more sophisticated
|
|
468
267
|
const pureFunctionPatterns = [
|
|
469
268
|
/^(get|fetch|read|load|find|search|query|select)_/i,
|
|
470
269
|
/^(compute|calculate|process|transform|convert)_/i,
|
|
@@ -475,176 +274,110 @@ export class NodeBridge extends RuntimeBridge {
|
|
|
475
274
|
/^(send|post|put|patch)_/i,
|
|
476
275
|
/random|uuid|timestamp|now|current/i,
|
|
477
276
|
];
|
|
478
|
-
// Don't cache if function name suggests mutation
|
|
479
277
|
if (impureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
480
278
|
return false;
|
|
481
279
|
}
|
|
482
|
-
// Cache if function name suggests pure computation
|
|
483
280
|
if (pureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
484
281
|
return true;
|
|
485
282
|
}
|
|
486
|
-
// Don't cache if args contain mutable objects (very basic check)
|
|
487
283
|
const hasComplexArgs = args.some(arg => arg !== null && typeof arg === 'object' && !(arg instanceof Date));
|
|
488
|
-
return !hasComplexArgs && args.length <= 3;
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Update performance statistics
|
|
492
|
-
*/
|
|
493
|
-
updateStats(duration, _error = false) {
|
|
494
|
-
this.stats.totalRequests++;
|
|
495
|
-
this.stats.totalTime += duration;
|
|
496
|
-
const currentMemory = process.memoryUsage().heapUsed;
|
|
497
|
-
if (currentMemory > this.stats.memoryPeak) {
|
|
498
|
-
this.stats.memoryPeak = currentMemory;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Get performance statistics
|
|
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
|
-
};
|
|
284
|
+
return !hasComplexArgs && args.length <= 3;
|
|
523
285
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Gracefully terminate a worker
|
|
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
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Start cleanup scheduler
|
|
576
|
-
*/
|
|
577
|
-
startCleanupScheduler() {
|
|
578
|
-
this.cleanupTimer = setInterval(async () => {
|
|
286
|
+
}
|
|
287
|
+
// =============================================================================
|
|
288
|
+
// WARMUP CALLBACK
|
|
289
|
+
// =============================================================================
|
|
290
|
+
/**
|
|
291
|
+
* Simple request ID generator for warmup commands.
|
|
292
|
+
*/
|
|
293
|
+
let warmupRequestId = 0;
|
|
294
|
+
function generateWarmupId() {
|
|
295
|
+
return `warmup_${++warmupRequestId}_${Date.now()}`;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Create a callback that runs warmup commands on each worker.
|
|
299
|
+
*
|
|
300
|
+
* The callback sends warmup commands directly to the worker's transport,
|
|
301
|
+
* bypassing the pool to ensure each worker gets warmed up individually.
|
|
302
|
+
*/
|
|
303
|
+
function createWarmupCallback(warmupCommands, timeoutMs) {
|
|
304
|
+
return async (worker) => {
|
|
305
|
+
for (const cmd of warmupCommands) {
|
|
579
306
|
try {
|
|
580
|
-
|
|
307
|
+
// Handle both new and legacy warmup command formats
|
|
308
|
+
if ('module' in cmd && 'functionName' in cmd) {
|
|
309
|
+
// Build the protocol message
|
|
310
|
+
const message = JSON.stringify({
|
|
311
|
+
id: generateWarmupId(),
|
|
312
|
+
type: 'call',
|
|
313
|
+
module: cmd.module,
|
|
314
|
+
functionName: cmd.functionName,
|
|
315
|
+
args: cmd.args ?? [],
|
|
316
|
+
kwargs: {},
|
|
317
|
+
});
|
|
318
|
+
// Send directly to this worker's transport
|
|
319
|
+
await worker.transport.send(message, timeoutMs);
|
|
320
|
+
}
|
|
321
|
+
// Legacy format { method, params } is ignored as it's not supported
|
|
581
322
|
}
|
|
582
|
-
catch
|
|
583
|
-
|
|
323
|
+
catch {
|
|
324
|
+
// Ignore warmup errors - they're not critical
|
|
584
325
|
}
|
|
585
|
-
}, 60000); // Cleanup every minute
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Dispose all resources
|
|
589
|
-
*/
|
|
590
|
-
async dispose() {
|
|
591
|
-
if (this.disposed) {
|
|
592
|
-
return;
|
|
593
326
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
// eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
|
|
612
|
-
baseEnv[key] = value;
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// =============================================================================
|
|
330
|
+
// ENVIRONMENT BUILDING
|
|
331
|
+
// =============================================================================
|
|
332
|
+
/**
|
|
333
|
+
* Build environment variables for ProcessIO.
|
|
334
|
+
*/
|
|
335
|
+
function buildProcessEnv(options) {
|
|
336
|
+
const allowedPrefixes = ['TYWRAP_'];
|
|
337
|
+
const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
|
|
338
|
+
const env = {};
|
|
339
|
+
// Copy allowed env vars from process.env
|
|
340
|
+
if (options.inheritProcessEnv) {
|
|
341
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
342
|
+
if (value !== undefined) {
|
|
343
|
+
env[key] = value;
|
|
613
344
|
}
|
|
614
345
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
349
|
+
if (value !== undefined &&
|
|
350
|
+
(allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))) {
|
|
351
|
+
env[key] = value;
|
|
622
352
|
}
|
|
623
353
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
// eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
|
|
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}`;
|
|
354
|
+
}
|
|
355
|
+
// Apply user overrides
|
|
356
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
357
|
+
if (value !== undefined) {
|
|
358
|
+
env[key] = value;
|
|
633
359
|
}
|
|
634
|
-
ensurePythonEncoding(env);
|
|
635
|
-
ensureJsonFallback(env, this.options.enableJsonFallback);
|
|
636
|
-
env = normalizeEnv(env, {});
|
|
637
|
-
return env;
|
|
638
360
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
361
|
+
// Configure virtual environment
|
|
362
|
+
if (options.virtualEnv) {
|
|
363
|
+
const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
|
|
364
|
+
env.VIRTUAL_ENV = venv.venvPath;
|
|
365
|
+
const pathKey = getPathKey(env);
|
|
366
|
+
const currentPath = env[pathKey] ?? '';
|
|
367
|
+
env[pathKey] = `${venv.binDir}${delimiter}${currentPath}`;
|
|
368
|
+
}
|
|
369
|
+
// Add cwd to PYTHONPATH so Python can find modules in the working directory
|
|
370
|
+
if (options.cwd) {
|
|
371
|
+
const currentPythonPath = env.PYTHONPATH ?? '';
|
|
372
|
+
env.PYTHONPATH = currentPythonPath
|
|
373
|
+
? `${options.cwd}${delimiter}${currentPythonPath}`
|
|
374
|
+
: options.cwd;
|
|
643
375
|
}
|
|
376
|
+
// Ensure Python uses UTF-8
|
|
377
|
+
env.PYTHONUTF8 = '1';
|
|
378
|
+
env.PYTHONIOENCODING = 'UTF-8';
|
|
379
|
+
env.PYTHONUNBUFFERED = '1';
|
|
380
|
+
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
381
|
+
return env;
|
|
644
382
|
}
|
|
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
383
|
//# sourceMappingURL=node.js.map
|