tywrap 0.2.1 → 0.4.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 (144) hide show
  1. package/README.md +100 -32
  2. package/dist/cli.js +70 -5
  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 +62 -20
  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 +46 -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/dev.d.ts +57 -0
  29. package/dist/dev.d.ts.map +1 -0
  30. package/dist/dev.js +743 -0
  31. package/dist/dev.js.map +1 -0
  32. package/dist/index.d.ts +8 -7
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/runtime/bridge-core.d.ts +1 -0
  37. package/dist/runtime/bridge-core.d.ts.map +1 -1
  38. package/dist/runtime/bridge-core.js +52 -17
  39. package/dist/runtime/bridge-core.js.map +1 -1
  40. package/dist/runtime/bridge-protocol.d.ts +17 -0
  41. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  42. package/dist/runtime/bridge-protocol.js +97 -0
  43. package/dist/runtime/bridge-protocol.js.map +1 -1
  44. package/dist/runtime/errors.d.ts +10 -0
  45. package/dist/runtime/errors.d.ts.map +1 -1
  46. package/dist/runtime/errors.js +9 -0
  47. package/dist/runtime/errors.js.map +1 -1
  48. package/dist/runtime/http-io.d.ts.map +1 -1
  49. package/dist/runtime/http-io.js.map +1 -1
  50. package/dist/runtime/http.d.ts +1 -0
  51. package/dist/runtime/http.d.ts.map +1 -1
  52. package/dist/runtime/http.js +6 -0
  53. package/dist/runtime/http.js.map +1 -1
  54. package/dist/runtime/node.d.ts.map +1 -1
  55. package/dist/runtime/node.js +206 -32
  56. package/dist/runtime/node.js.map +1 -1
  57. package/dist/runtime/optimized-node.d.ts +6 -5
  58. package/dist/runtime/optimized-node.d.ts.map +1 -1
  59. package/dist/runtime/optimized-node.js +6 -5
  60. package/dist/runtime/optimized-node.js.map +1 -1
  61. package/dist/runtime/pooled-transport.d.ts +4 -0
  62. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  63. package/dist/runtime/pooled-transport.js +2 -0
  64. package/dist/runtime/pooled-transport.js.map +1 -1
  65. package/dist/runtime/process-io.d.ts +5 -0
  66. package/dist/runtime/process-io.d.ts.map +1 -1
  67. package/dist/runtime/process-io.js +64 -30
  68. package/dist/runtime/process-io.js.map +1 -1
  69. package/dist/runtime/pyodide-io.d.ts +4 -5
  70. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  71. package/dist/runtime/pyodide-io.js +99 -37
  72. package/dist/runtime/pyodide-io.js.map +1 -1
  73. package/dist/runtime/safe-codec.d.ts +25 -3
  74. package/dist/runtime/safe-codec.d.ts.map +1 -1
  75. package/dist/runtime/safe-codec.js +231 -57
  76. package/dist/runtime/safe-codec.js.map +1 -1
  77. package/dist/runtime/transport.d.ts +6 -1
  78. package/dist/runtime/transport.d.ts.map +1 -1
  79. package/dist/runtime/transport.js.map +1 -1
  80. package/dist/runtime/validators.d.ts +0 -11
  81. package/dist/runtime/validators.d.ts.map +1 -1
  82. package/dist/runtime/validators.js +13 -3
  83. package/dist/runtime/validators.js.map +1 -1
  84. package/dist/runtime/worker-pool.d.ts +17 -0
  85. package/dist/runtime/worker-pool.d.ts.map +1 -1
  86. package/dist/runtime/worker-pool.js +97 -22
  87. package/dist/runtime/worker-pool.js.map +1 -1
  88. package/dist/types/index.d.ts +51 -8
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/tywrap.d.ts +6 -0
  91. package/dist/tywrap.d.ts.map +1 -1
  92. package/dist/tywrap.js +237 -331
  93. package/dist/tywrap.js.map +1 -1
  94. package/dist/utils/cache.d.ts.map +1 -1
  95. package/dist/utils/cache.js +53 -5
  96. package/dist/utils/cache.js.map +1 -1
  97. package/dist/utils/codec.d.ts.map +1 -1
  98. package/dist/utils/codec.js +10 -16
  99. package/dist/utils/codec.js.map +1 -1
  100. package/dist/utils/ir-cache.d.ts +28 -0
  101. package/dist/utils/ir-cache.d.ts.map +1 -0
  102. package/dist/utils/ir-cache.js +29 -0
  103. package/dist/utils/ir-cache.js.map +1 -0
  104. package/dist/utils/parallel-processor.d.ts +1 -1
  105. package/dist/utils/parallel-processor.d.ts.map +1 -1
  106. package/dist/utils/parallel-processor.js +111 -40
  107. package/dist/utils/parallel-processor.js.map +1 -1
  108. package/dist/utils/runtime.d.ts +5 -1
  109. package/dist/utils/runtime.d.ts.map +1 -1
  110. package/dist/utils/runtime.js +112 -18
  111. package/dist/utils/runtime.js.map +1 -1
  112. package/package.json +47 -21
  113. package/runtime/python_bridge.py +78 -12
  114. package/src/cli.ts +85 -5
  115. package/src/config/index.ts +91 -32
  116. package/src/core/analyzer.ts +51 -15
  117. package/src/core/annotation-parser.ts +500 -0
  118. package/src/core/discovery.ts +26 -12
  119. package/src/core/generator.ts +956 -116
  120. package/src/core/mapper.ts +158 -51
  121. package/src/dev.ts +983 -0
  122. package/src/index.ts +35 -6
  123. package/src/runtime/bridge-core.ts +58 -18
  124. package/src/runtime/bridge-protocol.ts +164 -0
  125. package/src/runtime/errors.ts +14 -0
  126. package/src/runtime/http-io.ts +2 -1
  127. package/src/runtime/http.ts +7 -0
  128. package/src/runtime/node.ts +312 -46
  129. package/src/runtime/optimized-node.ts +9 -5
  130. package/src/runtime/pooled-transport.ts +12 -1
  131. package/src/runtime/process-io.ts +83 -55
  132. package/src/runtime/pyodide-io.ts +110 -44
  133. package/src/runtime/safe-codec.ts +290 -82
  134. package/src/runtime/transport.ts +6 -1
  135. package/src/runtime/validators.ts +14 -3
  136. package/src/runtime/worker-pool.ts +120 -24
  137. package/src/types/global.d.ts +11 -1
  138. package/src/types/index.ts +63 -8
  139. package/src/tywrap.ts +296 -361
  140. package/src/utils/cache.ts +59 -5
  141. package/src/utils/codec.ts +11 -15
  142. package/src/utils/ir-cache.ts +51 -0
  143. package/src/utils/parallel-processor.ts +119 -43
  144. package/src/utils/runtime.ts +129 -18
@@ -18,10 +18,11 @@ import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
18
18
  import { globalCache } from '../utils/cache.js';
19
19
 
20
20
  import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
21
- import { BridgeProtocolError } from './errors.js';
21
+ import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
22
22
  import { ProcessIO } from './process-io.js';
23
23
  import { PooledTransport } from './pooled-transport.js';
24
24
  import type { CodecOptions } from './safe-codec.js';
25
+ import { PROTOCOL_ID } from './transport.js';
25
26
  import type { PooledWorker } from './worker-pool.js';
26
27
 
27
28
  // =============================================================================
@@ -73,8 +74,7 @@ export interface NodeBridgeOptions {
73
74
 
74
75
  /** Commands to run on each process at startup for warming up. */
75
76
  warmupCommands?: Array<
76
- | { module: string; functionName: string; args?: unknown[] }
77
- | { method: string; params: unknown } // Legacy format for backwards compatibility
77
+ { module: string; functionName: string; args?: unknown[] } | { method: string; params: unknown } // Legacy shape preserved so runtime can surface a migration error
78
78
  >;
79
79
 
80
80
  // ===========================================================================
@@ -120,12 +120,17 @@ interface ResolvedOptions {
120
120
  enableCache: boolean;
121
121
  env: Record<string, string | undefined>;
122
122
  codec?: CodecOptions;
123
- warmupCommands: Array<
124
- | { module: string; functionName: string; args?: unknown[] }
125
- | { method: string; params: unknown }
126
- >;
123
+ warmupCommands: WarmupCommand[];
124
+ }
125
+
126
+ interface WarmupCommand {
127
+ module: string;
128
+ functionName: string;
129
+ args?: unknown[];
127
130
  }
128
131
 
132
+ const WORKER_READY_TIMEOUT_MS = 5000;
133
+
129
134
  // =============================================================================
130
135
  // UTILITIES
131
136
  // =============================================================================
@@ -158,6 +163,10 @@ function resolveVirtualEnv(
158
163
  * Get the environment variable key for PATH (case-insensitive on Windows).
159
164
  */
160
165
  function getPathKey(env: Record<string, string | undefined>): string {
166
+ if (Object.prototype.hasOwnProperty.call(env, 'PATH')) {
167
+ return 'PATH';
168
+ }
169
+
161
170
  for (const key of Object.keys(env)) {
162
171
  if (key.toLowerCase() === 'path') {
163
172
  return key;
@@ -166,6 +175,93 @@ function getPathKey(env: Record<string, string | undefined>): string {
166
175
  return 'PATH';
167
176
  }
168
177
 
178
+ function setPathValue(env: Record<string, string>, value: string): void {
179
+ setEnvValue(env, 'PATH', value);
180
+
181
+ if (process.platform !== 'win32') {
182
+ return;
183
+ }
184
+
185
+ for (const key of Object.keys(env)) {
186
+ if (key !== 'PATH' && key.toLowerCase() === 'path') {
187
+ setEnvValue(env, key, value);
188
+ }
189
+ }
190
+ }
191
+
192
+ const DANGEROUS_ENV_OVERRIDE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
193
+
194
+ function createNullPrototypeEnv(): Record<string, string> {
195
+ return Object.create(null) as Record<string, string>;
196
+ }
197
+
198
+ function setEnvValue(env: Record<string, string>, key: string, value: string): void {
199
+ Object.defineProperty(env, key, {
200
+ value,
201
+ writable: true,
202
+ enumerable: true,
203
+ configurable: true,
204
+ });
205
+ }
206
+
207
+ function getEnvValue(env: Record<string, string>, key: string): string | undefined {
208
+ const value = Reflect.get(env, key);
209
+ return typeof value === 'string' ? value : undefined;
210
+ }
211
+
212
+ function assertSafeEnvOverrideKey(key: string): void {
213
+ if (DANGEROUS_ENV_OVERRIDE_KEYS.has(key)) {
214
+ throw new BridgeProtocolError(`Invalid environment override key "${key}" in options.env`);
215
+ }
216
+ }
217
+
218
+ function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']): WarmupCommand[] {
219
+ if (commands === undefined) {
220
+ return [];
221
+ }
222
+ if (!Array.isArray(commands)) {
223
+ throw new BridgeProtocolError('warmupCommands must be an array when provided');
224
+ }
225
+
226
+ const warmups = commands;
227
+ return warmups.map((command, index) => {
228
+ if (!command || typeof command !== 'object' || Array.isArray(command)) {
229
+ throw new BridgeProtocolError(
230
+ `Invalid warmup command at index ${index + 1}: expected { module, functionName, args? }`
231
+ );
232
+ }
233
+
234
+ const candidate = command as Record<string, unknown>;
235
+ if ('method' in candidate || 'params' in candidate) {
236
+ throw new BridgeProtocolError(
237
+ `Invalid warmup command at index ${index + 1}: legacy { method, params } format is no longer supported. Use { module, functionName, args? }.`
238
+ );
239
+ }
240
+
241
+ if (typeof candidate.module !== 'string' || candidate.module.trim().length === 0) {
242
+ throw new BridgeProtocolError(
243
+ `Invalid warmup command at index ${index + 1}: "module" must be a non-empty string`
244
+ );
245
+ }
246
+ if (typeof candidate.functionName !== 'string' || candidate.functionName.trim().length === 0) {
247
+ throw new BridgeProtocolError(
248
+ `Invalid warmup command at index ${index + 1}: "functionName" must be a non-empty string`
249
+ );
250
+ }
251
+ if (candidate.args !== undefined && !Array.isArray(candidate.args)) {
252
+ throw new BridgeProtocolError(
253
+ `Invalid warmup command at index ${index + 1}: "args" must be an array when provided`
254
+ );
255
+ }
256
+
257
+ return {
258
+ module: candidate.module,
259
+ functionName: candidate.functionName,
260
+ args: candidate.args as unknown[] | undefined,
261
+ };
262
+ });
263
+ }
264
+
169
265
  // =============================================================================
170
266
  // NODE BRIDGE
171
267
  // =============================================================================
@@ -226,6 +322,8 @@ export class NodeBridge extends BridgeProtocol {
226
322
  const maxProcesses = options.maxProcesses ?? 1;
227
323
  const minProcesses = Math.min(options.minProcesses ?? 1, maxProcesses);
228
324
 
325
+ const warmupCommands = normalizeWarmupCommands(options.warmupCommands);
326
+
229
327
  const resolvedOptions: ResolvedOptions = {
230
328
  minProcesses,
231
329
  maxProcesses,
@@ -240,20 +338,20 @@ export class NodeBridge extends BridgeProtocol {
240
338
  enableCache: options.enableCache ?? false,
241
339
  env: options.env ?? {},
242
340
  codec: options.codec,
243
- warmupCommands: options.warmupCommands ?? [],
341
+ warmupCommands,
244
342
  };
245
-
246
343
  // Build environment for ProcessIO
247
344
  const processEnv = buildProcessEnv(resolvedOptions);
248
345
 
249
- // Create warmup callback for per-worker initialization
250
- const onWorkerReady = resolvedOptions.warmupCommands.length > 0
251
- ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
252
- : undefined;
346
+ const userWarmup =
347
+ resolvedOptions.warmupCommands.length > 0
348
+ ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
349
+ : undefined;
350
+ const onWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
253
351
 
254
352
  // Create pooled transport with ProcessIO workers
255
353
  const transport = new PooledTransport({
256
- createTransport: () =>
354
+ createTransport: (): ProcessIO =>
257
355
  new ProcessIO({
258
356
  pythonPath: resolvedOptions.pythonPath,
259
357
  bridgeScript: resolvedOptions.scriptPath,
@@ -265,6 +363,7 @@ export class NodeBridge extends BridgeProtocol {
265
363
  queueTimeoutMs: resolvedOptions.queueTimeoutMs,
266
364
  maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
267
365
  onWorkerReady,
366
+ onReplacementWorkerReady: onWorkerReady,
268
367
  });
269
368
 
270
369
  // Initialize BridgeProtocol with pooled transport
@@ -460,8 +559,74 @@ export class NodeBridge extends BridgeProtocol {
460
559
  * Simple request ID generator for warmup commands.
461
560
  */
462
561
  let warmupRequestId = 0;
463
- function generateWarmupId(): string {
464
- return `warmup_${++warmupRequestId}_${Date.now()}`;
562
+ function generateWarmupId(): number {
563
+ return ++warmupRequestId;
564
+ }
565
+
566
+ async function sendWarmupRequest(
567
+ worker: PooledWorker,
568
+ timeoutMs: number,
569
+ requestId: number,
570
+ label: string,
571
+ messagePayload: Record<string, unknown>
572
+ ): Promise<void> {
573
+ let message: string;
574
+ try {
575
+ message = JSON.stringify({
576
+ id: requestId,
577
+ protocol: PROTOCOL_ID,
578
+ ...messagePayload,
579
+ });
580
+ } catch (error) {
581
+ throw new BridgeExecutionError(
582
+ `${label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
583
+ { cause: error instanceof Error ? error : undefined }
584
+ );
585
+ }
586
+
587
+ let response: string;
588
+ try {
589
+ response = await worker.transport.send(message, timeoutMs);
590
+ } catch (error) {
591
+ throw new BridgeExecutionError(
592
+ `${label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
593
+ { cause: error instanceof Error ? error : undefined }
594
+ );
595
+ }
596
+
597
+ let parsed: unknown;
598
+ try {
599
+ parsed = JSON.parse(response);
600
+ } catch (error) {
601
+ throw new BridgeExecutionError(`${label} returned invalid JSON response`, {
602
+ cause: error instanceof Error ? error : undefined,
603
+ });
604
+ }
605
+
606
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
607
+ throw new BridgeExecutionError(
608
+ `${label} returned malformed response envelope for request ${requestId}`
609
+ );
610
+ }
611
+
612
+ const envelope = parsed as { result?: unknown; error?: unknown };
613
+ if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
614
+ const errorPayload = envelope.error;
615
+ if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
616
+ const err = errorPayload as { type?: unknown; message?: unknown };
617
+ const errType = typeof err.type === 'string' ? err.type : 'Error';
618
+ const errMessage = typeof err.message === 'string' ? err.message : 'Unknown warmup error';
619
+ throw new BridgeExecutionError(`${label} failed: ${errType}: ${errMessage}`);
620
+ }
621
+
622
+ throw new BridgeExecutionError(`${label} failed with malformed error payload`);
623
+ }
624
+
625
+ if (!('result' in envelope)) {
626
+ throw new BridgeExecutionError(
627
+ `${label} returned malformed response envelope for request ${requestId}`
628
+ );
629
+ }
465
630
  }
466
631
 
467
632
  /**
@@ -471,38 +636,138 @@ function generateWarmupId(): string {
471
636
  * bypassing the pool to ensure each worker gets warmed up individually.
472
637
  */
473
638
  function createWarmupCallback(
474
- warmupCommands: Array<
475
- | { module: string; functionName: string; args?: unknown[] }
476
- | { method: string; params: unknown }
477
- >,
639
+ warmupCommands: WarmupCommand[],
478
640
  timeoutMs: number
479
641
  ): (worker: PooledWorker) => Promise<void> {
480
642
  return async (worker: PooledWorker) => {
481
- for (const cmd of warmupCommands) {
482
- try {
483
- // Handle both new and legacy warmup command formats
484
- if ('module' in cmd && 'functionName' in cmd) {
485
- // Build the protocol message
486
- const message = JSON.stringify({
487
- id: generateWarmupId(),
488
- type: 'call',
489
- module: cmd.module,
490
- functionName: cmd.functionName,
491
- args: cmd.args ?? [],
492
- kwargs: {},
493
- });
494
-
495
- // Send directly to this worker's transport
496
- await worker.transport.send(message, timeoutMs);
643
+ for (const [index, cmd] of warmupCommands.entries()) {
644
+ const commandLabel = `${cmd.module}.${cmd.functionName}`;
645
+ await executeWorkerCall(
646
+ worker,
647
+ {
648
+ module: cmd.module,
649
+ functionName: cmd.functionName,
650
+ args: cmd.args ?? [],
651
+ },
652
+ timeoutMs,
653
+ {
654
+ label: `Warmup command #${index + 1} (${commandLabel})`,
655
+ invalidJsonMessage: 'returned invalid JSON response',
656
+ malformedEnvelopeMessage: requestId =>
657
+ `returned malformed response envelope for request ${requestId}`,
658
+ pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
659
+ malformedErrorPayloadMessage: 'failed with malformed error payload',
497
660
  }
498
- // Legacy format { method, params } is ignored as it's not supported
499
- } catch {
500
- // Ignore warmup errors - they're not critical
501
- }
661
+ );
502
662
  }
503
663
  };
504
664
  }
505
665
 
666
+ function createWorkerReadyCallback(
667
+ timeoutMs: number,
668
+ extraWarmup?: (worker: PooledWorker) => Promise<void>
669
+ ): (worker: PooledWorker) => Promise<void> {
670
+ return async (worker: PooledWorker) => {
671
+ const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
672
+ await sendWarmupRequest(worker, readyTimeoutMs, generateWarmupId(), 'Worker warmup check', {
673
+ method: 'meta',
674
+ params: {},
675
+ });
676
+
677
+ await extraWarmup?.(worker);
678
+ };
679
+ }
680
+
681
+ interface WorkerCallSpec {
682
+ module: string;
683
+ functionName: string;
684
+ args: unknown[];
685
+ }
686
+
687
+ interface WorkerCallErrorMessages {
688
+ label: string;
689
+ invalidJsonMessage: string;
690
+ malformedEnvelopeMessage: (requestId: number) => string;
691
+ pythonErrorMessage: (errorType: string, errorMessage: string) => string;
692
+ malformedErrorPayloadMessage: string;
693
+ }
694
+
695
+ async function executeWorkerCall(
696
+ worker: PooledWorker,
697
+ request: WorkerCallSpec,
698
+ timeoutMs: number,
699
+ messages: WorkerCallErrorMessages
700
+ ): Promise<unknown> {
701
+ const requestId = generateWarmupId();
702
+ let message: string;
703
+ try {
704
+ message = JSON.stringify({
705
+ id: requestId,
706
+ protocol: PROTOCOL_ID,
707
+ method: 'call',
708
+ params: {
709
+ module: request.module,
710
+ functionName: request.functionName,
711
+ args: request.args,
712
+ kwargs: {},
713
+ },
714
+ });
715
+ } catch (error) {
716
+ throw new BridgeExecutionError(
717
+ `${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
718
+ { cause: error instanceof Error ? error : undefined }
719
+ );
720
+ }
721
+
722
+ let response: string;
723
+ try {
724
+ response = await worker.transport.send(message, timeoutMs);
725
+ } catch (error) {
726
+ throw new BridgeExecutionError(
727
+ `${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
728
+ { cause: error instanceof Error ? error : undefined }
729
+ );
730
+ }
731
+
732
+ let parsed: unknown;
733
+ try {
734
+ parsed = JSON.parse(response);
735
+ } catch (error) {
736
+ throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
737
+ cause: error instanceof Error ? error : undefined,
738
+ });
739
+ }
740
+
741
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
742
+ throw new BridgeExecutionError(
743
+ `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
744
+ );
745
+ }
746
+
747
+ const envelope = parsed as { result?: unknown; error?: unknown };
748
+ if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
749
+ const errorPayload = envelope.error;
750
+ if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
751
+ const err = errorPayload as { type?: unknown; message?: unknown };
752
+ const errType = typeof err.type === 'string' ? err.type : 'Error';
753
+ const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
754
+ throw new BridgeExecutionError(
755
+ `${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
756
+ );
757
+ }
758
+
759
+ throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
760
+ }
761
+
762
+ if (!('result' in envelope)) {
763
+ throw new BridgeExecutionError(
764
+ `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
765
+ );
766
+ }
767
+
768
+ return envelope.result;
769
+ }
770
+
506
771
  // =============================================================================
507
772
  // ENVIRONMENT BUILDING
508
773
  // =============================================================================
@@ -513,13 +778,13 @@ function createWarmupCallback(
513
778
  function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
514
779
  const allowedPrefixes = ['TYWRAP_'];
515
780
  const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
516
- const env: Record<string, string> = {};
781
+ const env = createNullPrototypeEnv();
517
782
 
518
783
  // Copy allowed env vars from process.env
519
784
  if (options.inheritProcessEnv) {
520
785
  for (const [key, value] of Object.entries(process.env)) {
521
786
  if (value !== undefined) {
522
- env[key] = value;
787
+ setEnvValue(env, key, value);
523
788
  }
524
789
  }
525
790
  } else {
@@ -528,15 +793,16 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
528
793
  value !== undefined &&
529
794
  (allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))
530
795
  ) {
531
- env[key] = value;
796
+ setEnvValue(env, key, value);
532
797
  }
533
798
  }
534
799
  }
535
800
 
536
801
  // Apply user overrides
537
802
  for (const [key, value] of Object.entries(options.env)) {
803
+ assertSafeEnvOverrideKey(key);
538
804
  if (value !== undefined) {
539
- env[key] = value;
805
+ setEnvValue(env, key, value);
540
806
  }
541
807
  }
542
808
 
@@ -545,8 +811,8 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
545
811
  const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
546
812
  env.VIRTUAL_ENV = venv.venvPath;
547
813
  const pathKey = getPathKey(env);
548
- const currentPath = env[pathKey] ?? '';
549
- env[pathKey] = `${venv.binDir}${delimiter}${currentPath}`;
814
+ const currentPath = getEnvValue(env, pathKey);
815
+ setPathValue(env, currentPath ? `${venv.binDir}${delimiter}${currentPath}` : venv.binDir);
550
816
  }
551
817
 
552
818
  // Add cwd to PYTHONPATH so Python can find modules in the working directory
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @deprecated Import from './node.js' instead.
3
3
  *
4
- * OptimizedNodeBridge has been unified with NodeBridge. The NodeBridge class
5
- * now supports both single-process mode (default) and multi-process pooling
6
- * via the minProcesses/maxProcesses options.
4
+ * Backward-compat shim for older deep imports. NodeBridge is the public API,
5
+ * and it now supports both single-process mode (default) and multi-process
6
+ * pooling via the minProcesses/maxProcesses options.
7
7
  *
8
8
  * Migration:
9
9
  * ```typescript
@@ -16,6 +16,10 @@
16
16
  * const bridge = new NodeBridge({ minProcesses: 2, maxProcesses: 4 });
17
17
  * ```
18
18
  *
19
- * This file is maintained for backward compatibility only.
19
+ * This file is not exposed through the package exports map and is maintained
20
+ * for backward compatibility only.
20
21
  */
21
- export { NodeBridge as OptimizedNodeBridge, type NodeBridgeOptions as ProcessPoolOptions } from './node.js';
22
+ export {
23
+ NodeBridge as OptimizedNodeBridge,
24
+ type NodeBridgeOptions as ProcessPoolOptions,
25
+ } from './node.js';
@@ -41,6 +41,11 @@ export interface PooledTransportOptions {
41
41
  * Use this for per-worker warmup (e.g., importing modules, running setup).
42
42
  */
43
43
  onWorkerReady?: (worker: PooledWorker) => Promise<void>;
44
+
45
+ /**
46
+ * Optional callback used only for background replacement workers.
47
+ */
48
+ onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
44
49
  }
45
50
 
46
51
  // =============================================================================
@@ -78,8 +83,12 @@ export interface PooledTransportOptions {
78
83
  * ```
79
84
  */
80
85
  export class PooledTransport extends BoundedContext implements Transport {
81
- private readonly poolOptions: Omit<Required<PooledTransportOptions>, 'onWorkerReady'> & {
86
+ private readonly poolOptions: Omit<
87
+ Required<PooledTransportOptions>,
88
+ 'onWorkerReady' | 'onReplacementWorkerReady'
89
+ > & {
82
90
  onWorkerReady?: (worker: PooledWorker) => Promise<void>;
91
+ onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
83
92
  };
84
93
  private pool?: WorkerPool;
85
94
 
@@ -102,6 +111,7 @@ export class PooledTransport extends BoundedContext implements Transport {
102
111
  queueTimeoutMs: options.queueTimeoutMs ?? 30000,
103
112
  maxConcurrentPerWorker: options.maxConcurrentPerWorker ?? 10,
104
113
  onWorkerReady: options.onWorkerReady,
114
+ onReplacementWorkerReady: options.onReplacementWorkerReady,
105
115
  };
106
116
  }
107
117
 
@@ -123,6 +133,7 @@ export class PooledTransport extends BoundedContext implements Transport {
123
133
  queueTimeoutMs: this.poolOptions.queueTimeoutMs,
124
134
  maxConcurrentPerWorker: this.poolOptions.maxConcurrentPerWorker,
125
135
  onWorkerReady: this.poolOptions.onWorkerReady,
136
+ onReplacementWorkerReady: this.poolOptions.onReplacementWorkerReady,
126
137
  });
127
138
 
128
139
  await this.pool.init();