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.
Files changed (163) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +23 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +23 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/base.d.ts +17 -7
  33. package/dist/runtime/base.d.ts.map +1 -1
  34. package/dist/runtime/base.js +18 -1
  35. package/dist/runtime/base.js.map +1 -1
  36. package/dist/runtime/bounded-context.d.ts +252 -0
  37. package/dist/runtime/bounded-context.d.ts.map +1 -0
  38. package/dist/runtime/bounded-context.js +454 -0
  39. package/dist/runtime/bounded-context.js.map +1 -0
  40. package/dist/runtime/bridge-core.d.ts +1 -0
  41. package/dist/runtime/bridge-core.d.ts.map +1 -1
  42. package/dist/runtime/bridge-core.js +52 -17
  43. package/dist/runtime/bridge-core.js.map +1 -1
  44. package/dist/runtime/bridge-protocol.d.ts +184 -0
  45. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  46. package/dist/runtime/bridge-protocol.js +344 -0
  47. package/dist/runtime/bridge-protocol.js.map +1 -0
  48. package/dist/runtime/disposable.d.ts +40 -0
  49. package/dist/runtime/disposable.d.ts.map +1 -0
  50. package/dist/runtime/disposable.js +49 -0
  51. package/dist/runtime/disposable.js.map +1 -0
  52. package/dist/runtime/errors.d.ts +10 -0
  53. package/dist/runtime/errors.d.ts.map +1 -1
  54. package/dist/runtime/errors.js +9 -0
  55. package/dist/runtime/errors.js.map +1 -1
  56. package/dist/runtime/http-io.d.ts +91 -0
  57. package/dist/runtime/http-io.d.ts.map +1 -0
  58. package/dist/runtime/http-io.js +195 -0
  59. package/dist/runtime/http-io.js.map +1 -0
  60. package/dist/runtime/http.d.ts +48 -13
  61. package/dist/runtime/http.d.ts.map +1 -1
  62. package/dist/runtime/http.js +60 -73
  63. package/dist/runtime/http.js.map +1 -1
  64. package/dist/runtime/node.d.ts +97 -130
  65. package/dist/runtime/node.d.ts.map +1 -1
  66. package/dist/runtime/node.js +392 -521
  67. package/dist/runtime/node.js.map +1 -1
  68. package/dist/runtime/optimized-node.d.ts +1 -1
  69. package/dist/runtime/optimized-node.d.ts.map +1 -1
  70. package/dist/runtime/optimized-node.js +1 -1
  71. package/dist/runtime/optimized-node.js.map +1 -1
  72. package/dist/runtime/pooled-transport.d.ts +135 -0
  73. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  74. package/dist/runtime/pooled-transport.js +177 -0
  75. package/dist/runtime/pooled-transport.js.map +1 -0
  76. package/dist/runtime/process-io.d.ts +209 -0
  77. package/dist/runtime/process-io.d.ts.map +1 -0
  78. package/dist/runtime/process-io.js +729 -0
  79. package/dist/runtime/process-io.js.map +1 -0
  80. package/dist/runtime/pyodide-io.d.ts +154 -0
  81. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  82. package/dist/runtime/pyodide-io.js +459 -0
  83. package/dist/runtime/pyodide-io.js.map +1 -0
  84. package/dist/runtime/pyodide.d.ts +51 -19
  85. package/dist/runtime/pyodide.d.ts.map +1 -1
  86. package/dist/runtime/pyodide.js +57 -186
  87. package/dist/runtime/pyodide.js.map +1 -1
  88. package/dist/runtime/safe-codec.d.ts +103 -0
  89. package/dist/runtime/safe-codec.d.ts.map +1 -0
  90. package/dist/runtime/safe-codec.js +519 -0
  91. package/dist/runtime/safe-codec.js.map +1 -0
  92. package/dist/runtime/transport.d.ts +191 -0
  93. package/dist/runtime/transport.d.ts.map +1 -0
  94. package/dist/runtime/transport.js +86 -0
  95. package/dist/runtime/transport.js.map +1 -0
  96. package/dist/runtime/validators.d.ts +120 -0
  97. package/dist/runtime/validators.d.ts.map +1 -0
  98. package/dist/runtime/validators.js +229 -0
  99. package/dist/runtime/validators.js.map +1 -0
  100. package/dist/runtime/worker-pool.d.ts +208 -0
  101. package/dist/runtime/worker-pool.d.ts.map +1 -0
  102. package/dist/runtime/worker-pool.js +419 -0
  103. package/dist/runtime/worker-pool.js.map +1 -0
  104. package/dist/types/index.d.ts +51 -1
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/tywrap.d.ts.map +1 -1
  107. package/dist/tywrap.js +227 -328
  108. package/dist/tywrap.js.map +1 -1
  109. package/dist/utils/cache.d.ts.map +1 -1
  110. package/dist/utils/cache.js +53 -5
  111. package/dist/utils/cache.js.map +1 -1
  112. package/dist/utils/codec.d.ts.map +1 -1
  113. package/dist/utils/codec.js +114 -1
  114. package/dist/utils/codec.js.map +1 -1
  115. package/dist/utils/ir-cache.d.ts +28 -0
  116. package/dist/utils/ir-cache.d.ts.map +1 -0
  117. package/dist/utils/ir-cache.js +29 -0
  118. package/dist/utils/ir-cache.js.map +1 -0
  119. package/dist/utils/parallel-processor.d.ts +1 -1
  120. package/dist/utils/parallel-processor.d.ts.map +1 -1
  121. package/dist/utils/parallel-processor.js +111 -40
  122. package/dist/utils/parallel-processor.js.map +1 -1
  123. package/dist/utils/runtime.d.ts +5 -1
  124. package/dist/utils/runtime.d.ts.map +1 -1
  125. package/dist/utils/runtime.js +112 -18
  126. package/dist/utils/runtime.js.map +1 -1
  127. package/package.json +40 -18
  128. package/runtime/python_bridge.py +108 -15
  129. package/runtime/safe_codec.py +344 -0
  130. package/src/cli.ts +61 -2
  131. package/src/config/index.ts +63 -17
  132. package/src/core/analyzer.ts +52 -15
  133. package/src/core/annotation-parser.ts +500 -0
  134. package/src/core/discovery.ts +26 -12
  135. package/src/core/generator.ts +956 -116
  136. package/src/core/mapper.ts +158 -51
  137. package/src/index.ts +78 -6
  138. package/src/runtime/base.ts +18 -26
  139. package/src/runtime/bounded-context.ts +608 -0
  140. package/src/runtime/bridge-core.ts +58 -18
  141. package/src/runtime/bridge-protocol.ts +483 -0
  142. package/src/runtime/disposable.ts +65 -0
  143. package/src/runtime/errors.ts +14 -0
  144. package/src/runtime/http-io.ts +245 -0
  145. package/src/runtime/http.ts +76 -115
  146. package/src/runtime/node.ts +563 -676
  147. package/src/runtime/optimized-node.ts +4 -1
  148. package/src/runtime/pooled-transport.ts +263 -0
  149. package/src/runtime/process-io.ts +930 -0
  150. package/src/runtime/pyodide-io.ts +551 -0
  151. package/src/runtime/pyodide.ts +75 -215
  152. package/src/runtime/safe-codec.ts +651 -0
  153. package/src/runtime/transport.ts +278 -0
  154. package/src/runtime/validators.ts +252 -0
  155. package/src/runtime/worker-pool.ts +559 -0
  156. package/src/types/global.d.ts +11 -1
  157. package/src/types/index.ts +63 -0
  158. package/src/tywrap.ts +279 -358
  159. package/src/utils/cache.ts +59 -5
  160. package/src/utils/codec.ts +122 -1
  161. package/src/utils/ir-cache.ts +51 -0
  162. package/src/utils/parallel-processor.ts +119 -43
  163. package/src/utils/runtime.ts +129 -18
@@ -1,26 +1,31 @@
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 { 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
- * By default, runs in single-process mode for correctness-first behavior.
42
- * Configure minProcesses/maxProcesses for process pooling in high-throughput scenarios.
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
- * minProcesses: 2,
52
- * maxProcesses: 8,
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 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
- };
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
- 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,
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
- maxLineLength: options.maxLineLength,
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
- warmupCommands: options.warmupCommands ?? [],
194
+ codec: options.codec,
195
+ warmupCommands,
103
196
  };
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;
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.options.scriptPath)) {
123
- throw new BridgeProtocolError(`Python bridge script not found at ${this.options.scriptPath}`);
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
- // 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;
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
- 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;
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
- try {
165
- const result = await this.executeRequest({
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
- 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
- });
280
+ // No caching - direct call
281
+ return super.call(module, functionName, args, kwargs);
223
282
  }
283
+ // ===========================================================================
284
+ // POOL STATISTICS
285
+ // ===========================================================================
224
286
  /**
225
- * Execute request with intelligent process selection
287
+ * Get current pool statistics.
226
288
  */
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
- });
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
- * Handle worker process exit
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
- 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
- }
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
- * Warm up processes with configured commands
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; // Cache simple calls with few args
356
+ return !hasComplexArgs && args.length <= 3;
489
357
  }
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;
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
- * 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
- };
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
- * 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
- }
431
+ let response;
432
+ try {
433
+ response = await worker.transport.send(message, timeoutMs);
543
434
  }
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
- }
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
- * Start cleanup scheduler
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
- * Dispose all resources
589
- */
590
- async dispose() {
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
- 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;
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
- 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
- }
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
- 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}`;
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
- async refreshBridgeInfo() {
640
- const info = await this.executeRequest({ method: 'meta', params: {} });
641
- validateBridgeInfo(info);
642
- this.bridgeInfo = info;
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