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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +20 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +22 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/base.d.ts +17 -7
  7. package/dist/runtime/base.d.ts.map +1 -1
  8. package/dist/runtime/base.js +18 -1
  9. package/dist/runtime/base.js.map +1 -1
  10. package/dist/runtime/bounded-context.d.ts +252 -0
  11. package/dist/runtime/bounded-context.d.ts.map +1 -0
  12. package/dist/runtime/bounded-context.js +454 -0
  13. package/dist/runtime/bounded-context.js.map +1 -0
  14. package/dist/runtime/bridge-protocol.d.ts +167 -0
  15. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  16. package/dist/runtime/bridge-protocol.js +247 -0
  17. package/dist/runtime/bridge-protocol.js.map +1 -0
  18. package/dist/runtime/disposable.d.ts +40 -0
  19. package/dist/runtime/disposable.d.ts.map +1 -0
  20. package/dist/runtime/disposable.js +49 -0
  21. package/dist/runtime/disposable.js.map +1 -0
  22. package/dist/runtime/http-io.d.ts +91 -0
  23. package/dist/runtime/http-io.d.ts.map +1 -0
  24. package/dist/runtime/http-io.js +195 -0
  25. package/dist/runtime/http-io.js.map +1 -0
  26. package/dist/runtime/http.d.ts +47 -13
  27. package/dist/runtime/http.d.ts.map +1 -1
  28. package/dist/runtime/http.js +55 -74
  29. package/dist/runtime/http.js.map +1 -1
  30. package/dist/runtime/node.d.ts +97 -130
  31. package/dist/runtime/node.d.ts.map +1 -1
  32. package/dist/runtime/node.js +256 -523
  33. package/dist/runtime/node.js.map +1 -1
  34. package/dist/runtime/pooled-transport.d.ts +131 -0
  35. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  36. package/dist/runtime/pooled-transport.js +175 -0
  37. package/dist/runtime/pooled-transport.js.map +1 -0
  38. package/dist/runtime/process-io.d.ts +204 -0
  39. package/dist/runtime/process-io.d.ts.map +1 -0
  40. package/dist/runtime/process-io.js +695 -0
  41. package/dist/runtime/process-io.js.map +1 -0
  42. package/dist/runtime/pyodide-io.d.ts +155 -0
  43. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  44. package/dist/runtime/pyodide-io.js +397 -0
  45. package/dist/runtime/pyodide-io.js.map +1 -0
  46. package/dist/runtime/pyodide.d.ts +51 -19
  47. package/dist/runtime/pyodide.d.ts.map +1 -1
  48. package/dist/runtime/pyodide.js +57 -186
  49. package/dist/runtime/pyodide.js.map +1 -1
  50. package/dist/runtime/safe-codec.d.ts +81 -0
  51. package/dist/runtime/safe-codec.d.ts.map +1 -0
  52. package/dist/runtime/safe-codec.js +345 -0
  53. package/dist/runtime/safe-codec.js.map +1 -0
  54. package/dist/runtime/transport.d.ts +186 -0
  55. package/dist/runtime/transport.d.ts.map +1 -0
  56. package/dist/runtime/transport.js +86 -0
  57. package/dist/runtime/transport.js.map +1 -0
  58. package/dist/runtime/validators.d.ts +131 -0
  59. package/dist/runtime/validators.d.ts.map +1 -0
  60. package/dist/runtime/validators.js +219 -0
  61. package/dist/runtime/validators.js.map +1 -0
  62. package/dist/runtime/worker-pool.d.ts +196 -0
  63. package/dist/runtime/worker-pool.d.ts.map +1 -0
  64. package/dist/runtime/worker-pool.js +371 -0
  65. package/dist/runtime/worker-pool.js.map +1 -0
  66. package/dist/utils/codec.d.ts.map +1 -1
  67. package/dist/utils/codec.js +120 -1
  68. package/dist/utils/codec.js.map +1 -1
  69. package/package.json +2 -2
  70. package/runtime/python_bridge.py +30 -3
  71. package/runtime/safe_codec.py +344 -0
  72. package/src/index.ts +48 -5
  73. package/src/runtime/base.ts +18 -26
  74. package/src/runtime/bounded-context.ts +608 -0
  75. package/src/runtime/bridge-protocol.ts +319 -0
  76. package/src/runtime/disposable.ts +65 -0
  77. package/src/runtime/http-io.ts +244 -0
  78. package/src/runtime/http.ts +71 -117
  79. package/src/runtime/node.ts +358 -691
  80. package/src/runtime/pooled-transport.ts +252 -0
  81. package/src/runtime/process-io.ts +902 -0
  82. package/src/runtime/pyodide-io.ts +485 -0
  83. package/src/runtime/pyodide.ts +75 -215
  84. package/src/runtime/safe-codec.ts +443 -0
  85. package/src/runtime/transport.ts +273 -0
  86. package/src/runtime/validators.ts +241 -0
  87. package/src/runtime/worker-pool.ts +498 -0
  88. package/src/utils/codec.ts +126 -1
@@ -1,26 +1,29 @@
1
1
  /**
2
- * Node.js Runtime Bridge with Optional Connection Pooling
2
+ * Node.js runtime bridge for BridgeProtocol.
3
3
  *
4
- * NodeBridge is the unified Node.js runtime bridge that supports both single-process
5
- * (correctness-first) and multi-process (pooled) configurations. By default, it runs
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
- * For high-throughput workloads, configure pooling via minProcesses/maxProcesses options.
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 { EventEmitter } from 'events';
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 { RuntimeBridge } from './base.js';
20
- import { BridgeDisposedError, BridgeProtocolError } from './errors.js';
21
- import { BridgeCore, ensureJsonFallback, ensurePythonEncoding, getMaxLineLengthFromEnv, getPathKey, normalizeEnv, validateBridgeInfo, } from './bridge-core.js';
22
- import { getComponentLogger } from '../utils/logger.js';
23
- const log = getComponentLogger('NodeBridge');
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
- * By default, runs in single-process mode for correctness-first behavior.
42
- * Configure minProcesses/maxProcesses for process pooling in high-throughput scenarios.
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
- * minProcesses: 2,
52
- * maxProcesses: 8,
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 RuntimeBridge {
58
- processPool = [];
59
- roundRobinIndex = 0;
60
- cleanupTimer;
61
- options;
62
- emitter = new EventEmitter();
63
- disposed = false;
64
- bridgeInfo;
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
- this.options = {
87
- // Default to single-process mode for backward compatibility
88
- minProcesses: options.minProcesses ?? 1,
89
- maxProcesses: options.maxProcesses ?? 1,
90
- maxIdleTime: options.maxIdleTime ?? 300000, // 5 minutes
91
- maxRequestsPerProcess: options.maxRequestsPerProcess ?? 1000,
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
- maxLineLength: options.maxLineLength,
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
- // Start cleanup scheduler for pooled mode
105
- this.startCleanupScheduler();
106
- }
107
- async init() {
108
- if (this.disposed) {
109
- throw new BridgeDisposedError('Bridge has been disposed');
110
- }
111
- if (this.processPool.length >= this.options.minProcesses) {
112
- return; // Already initialized
113
- }
114
- if (this.initPromise) {
115
- return this.initPromise;
116
- }
117
- this.initPromise = this.doInit();
118
- return this.initPromise;
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.options.scriptPath)) {
123
- throw new BridgeProtocolError(`Python bridge script not found at ${this.options.scriptPath}`);
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
- // Ensure minimum processes are available
130
- while (this.processPool.length < this.options.minProcesses) {
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
- await this.init();
152
- const startTime = performance.now();
153
- const cacheKey = this.options.enableCache
154
- ? this.safeCacheKey('runtime_call', module, functionName, args, kwargs)
155
- : null;
156
- if (cacheKey) {
157
- const cached = await globalCache.get(cacheKey);
158
- if (cached !== null) {
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
- try {
165
- const result = await this.executeRequest({
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
- catch (error) {
181
- this.updateStats(performance.now() - startTime, true);
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
- * Spawn new worker process with optimizations
215
+ * Get current pool statistics.
339
216
  */
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
- },
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
- * Setup event handlers for worker process
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
- 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
- }
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
- * Warm up processes with configured commands
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; // Cache simple calls with few args
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
- * Cleanup idle processes
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
- }
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
- await this.cleanup();
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 (error) {
583
- log.error('Cleanup error', { error: String(error) });
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
- 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();
604
- }
605
- buildEnv() {
606
- const allowedPrefixes = ['TYWRAP_'];
607
- const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
608
- const baseEnv = {};
609
- if (this.options.inheritProcessEnv) {
610
- for (const [key, value] of Object.entries(process.env)) {
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
- else {
616
- for (const [key, value] of Object.entries(process.env)) {
617
- if (allowedKeys.has(key.toLowerCase()) ||
618
- allowedPrefixes.some(prefix => key.startsWith(prefix))) {
619
- // eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
620
- baseEnv[key] = value;
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
- let env = normalizeEnv(baseEnv, this.options.env);
625
- if (this.options.virtualEnv) {
626
- const venv = resolveVirtualEnv(this.options.virtualEnv, this.options.cwd);
627
- env.VIRTUAL_ENV = venv.venvPath;
628
- const pathKey = getPathKey(env);
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
- async refreshBridgeInfo() {
640
- const info = await this.executeRequest({ method: 'meta', params: {} });
641
- validateBridgeInfo(info);
642
- this.bridgeInfo = info;
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