tywrap 0.3.0 → 0.5.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 (101) hide show
  1. package/README.md +40 -5
  2. package/dist/cli.js +24 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +19 -13
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/core/analyzer.d.ts.map +1 -1
  8. package/dist/core/analyzer.js +0 -1
  9. package/dist/core/analyzer.js.map +1 -1
  10. package/dist/dev.d.ts +57 -0
  11. package/dist/dev.d.ts.map +1 -0
  12. package/dist/dev.js +743 -0
  13. package/dist/dev.js.map +1 -0
  14. package/dist/index.d.ts +5 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/runtime/base.d.ts +7 -7
  19. package/dist/runtime/base.js +8 -8
  20. package/dist/runtime/bounded-context.d.ts +15 -31
  21. package/dist/runtime/bounded-context.d.ts.map +1 -1
  22. package/dist/runtime/bounded-context.js +16 -15
  23. package/dist/runtime/bounded-context.js.map +1 -1
  24. package/dist/runtime/http.d.ts +24 -5
  25. package/dist/runtime/http.d.ts.map +1 -1
  26. package/dist/runtime/http.js +57 -12
  27. package/dist/runtime/http.js.map +1 -1
  28. package/dist/runtime/node.d.ts +33 -7
  29. package/dist/runtime/node.d.ts.map +1 -1
  30. package/dist/runtime/node.js +144 -120
  31. package/dist/runtime/node.js.map +1 -1
  32. package/dist/runtime/optimized-node.d.ts +5 -4
  33. package/dist/runtime/optimized-node.d.ts.map +1 -1
  34. package/dist/runtime/optimized-node.js +5 -4
  35. package/dist/runtime/optimized-node.js.map +1 -1
  36. package/dist/runtime/pooled-transport.d.ts +2 -18
  37. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  38. package/dist/runtime/pooled-transport.js +2 -29
  39. package/dist/runtime/pooled-transport.js.map +1 -1
  40. package/dist/runtime/process-io.d.ts +2 -18
  41. package/dist/runtime/process-io.d.ts.map +1 -1
  42. package/dist/runtime/process-io.js +3 -30
  43. package/dist/runtime/process-io.js.map +1 -1
  44. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts +12 -0
  45. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -0
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js +12 -0
  47. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -0
  48. package/dist/runtime/pyodide-io.d.ts +7 -37
  49. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  50. package/dist/runtime/pyodide-io.js +90 -214
  51. package/dist/runtime/pyodide-io.js.map +1 -1
  52. package/dist/runtime/pyodide.d.ts +25 -5
  53. package/dist/runtime/pyodide.d.ts.map +1 -1
  54. package/dist/runtime/pyodide.js +59 -10
  55. package/dist/runtime/pyodide.js.map +1 -1
  56. package/dist/runtime/{bridge-protocol.d.ts → rpc-client.d.ts} +59 -46
  57. package/dist/runtime/rpc-client.d.ts.map +1 -0
  58. package/dist/runtime/{bridge-protocol.js → rpc-client.js} +60 -42
  59. package/dist/runtime/rpc-client.js.map +1 -0
  60. package/dist/runtime/worker-pool.d.ts +8 -20
  61. package/dist/runtime/worker-pool.d.ts.map +1 -1
  62. package/dist/runtime/worker-pool.js +58 -59
  63. package/dist/runtime/worker-pool.js.map +1 -1
  64. package/dist/types/index.d.ts +8 -10
  65. package/dist/types/index.d.ts.map +1 -1
  66. package/dist/tywrap.d.ts +6 -0
  67. package/dist/tywrap.d.ts.map +1 -1
  68. package/dist/tywrap.js +10 -3
  69. package/dist/tywrap.js.map +1 -1
  70. package/package.json +22 -18
  71. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  72. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  73. package/runtime/python_bridge.py +85 -702
  74. package/runtime/safe_codec.py +10 -2
  75. package/runtime/tywrap_bridge_core.py +875 -0
  76. package/src/cli.ts +27 -6
  77. package/src/config/index.ts +28 -15
  78. package/src/core/analyzer.ts +1 -2
  79. package/src/dev.ts +983 -0
  80. package/src/index.ts +7 -7
  81. package/src/runtime/base.ts +8 -8
  82. package/src/runtime/bounded-context.ts +17 -56
  83. package/src/runtime/http.ts +85 -13
  84. package/src/runtime/node.ts +172 -178
  85. package/src/runtime/optimized-node.ts +5 -4
  86. package/src/runtime/pooled-transport.ts +2 -57
  87. package/src/runtime/process-io.ts +3 -55
  88. package/src/runtime/pyodide-bootstrap-core.generated.ts +12 -0
  89. package/src/runtime/pyodide-io.ts +92 -243
  90. package/src/runtime/pyodide.ts +87 -10
  91. package/src/runtime/{bridge-protocol.ts → rpc-client.ts} +76 -49
  92. package/src/runtime/worker-pool.ts +67 -88
  93. package/src/types/index.ts +20 -12
  94. package/src/tywrap.ts +17 -3
  95. package/dist/runtime/bridge-core.d.ts +0 -65
  96. package/dist/runtime/bridge-core.d.ts.map +0 -1
  97. package/dist/runtime/bridge-core.js +0 -379
  98. package/dist/runtime/bridge-core.js.map +0 -1
  99. package/dist/runtime/bridge-protocol.d.ts.map +0 -1
  100. package/dist/runtime/bridge-protocol.js.map +0 -1
  101. package/src/runtime/bridge-core.ts +0 -494
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Node.js runtime bridge for BridgeProtocol.
2
+ * Node.js runtime bridge.
3
3
  *
4
- * NodeBridge extends BridgeProtocol and uses ProcessIO transports with
5
- * optional pooling for concurrent Python execution.
4
+ * NodeBridge is a thin facade: it extends DisposableBase (lifecycle/resources)
5
+ * and implements PythonRuntime by HOLDING an RpcClient. It uses ProcessIO
6
+ * transports with optional pooling for concurrent Python execution.
6
7
  *
7
8
  * @see https://github.com/bbopen/tywrap/issues/149
8
9
  */
@@ -12,17 +13,18 @@ import { delimiter, isAbsolute, join, resolve } from 'node:path';
12
13
  import { fileURLToPath } from 'node:url';
13
14
  import { createRequire } from 'node:module';
14
15
 
16
+ import type { PythonRuntime, BridgeInfo } from '../types/index.js';
15
17
  import { autoRegisterArrowDecoder } from '../utils/codec.js';
16
18
  import { getDefaultPythonPath } from '../utils/python.js';
17
19
  import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
18
20
  import { globalCache } from '../utils/cache.js';
19
21
 
20
- import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
21
- import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
22
+ import { DisposableBase } from './bounded-context.js';
23
+ import { RpcClient, type GetBridgeInfoOptions } from './rpc-client.js';
24
+ import { BridgeCodecError, BridgeExecutionError, BridgeProtocolError } from './errors.js';
22
25
  import { ProcessIO } from './process-io.js';
23
26
  import { PooledTransport } from './pooled-transport.js';
24
27
  import type { CodecOptions } from './safe-codec.js';
25
- import { PROTOCOL_ID } from './transport.js';
26
28
  import type { PooledWorker } from './worker-pool.js';
27
29
 
28
30
  // =============================================================================
@@ -303,9 +305,10 @@ function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']):
303
305
  * await pooledBridge.init();
304
306
  * ```
305
307
  */
306
- export class NodeBridge extends BridgeProtocol {
308
+ export class NodeBridge extends DisposableBase implements PythonRuntime {
307
309
  private readonly resolvedOptions: ResolvedOptions;
308
310
  private readonly pooledTransport: PooledTransport;
311
+ private readonly rpc: RpcClient;
309
312
 
310
313
  /**
311
314
  * Create a new NodeBridge instance.
@@ -340,16 +343,22 @@ export class NodeBridge extends BridgeProtocol {
340
343
  codec: options.codec,
341
344
  warmupCommands,
342
345
  };
343
-
344
346
  // Build environment for ProcessIO
345
347
  const processEnv = buildProcessEnv(resolvedOptions);
346
348
 
347
- // Create warmup callback for per-worker initialization
348
- const userWarmup =
349
- resolvedOptions.warmupCommands.length > 0
350
- ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
351
- : undefined;
352
- const replacementWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
349
+ // Why a late-bound holder: PooledTransport copies onWorkerReady eagerly at
350
+ // construction (before super()), but the callback needs the RpcClient, and
351
+ // a derived constructor cannot touch `this` before super(). So the closure
352
+ // captures a mutable holder whose .rpc is assigned right after super(). The
353
+ // closure body only runs at worker-spawn time during init(), by which point
354
+ // rpc is set. Warmup goes through rpc.sendOn (NOT rpc.execute): it runs
355
+ // inside transport.init() during rpc.init(), so it must not auto-init.
356
+ const rpcHolder: { rpc?: RpcClient } = {};
357
+ const onWorkerReady = createWorkerReadyCallback(
358
+ rpcHolder,
359
+ resolvedOptions.warmupCommands,
360
+ resolvedOptions.timeoutMs
361
+ );
353
362
 
354
363
  // Create pooled transport with ProcessIO workers
355
364
  const transport = new PooledTransport({
@@ -364,21 +373,25 @@ export class NodeBridge extends BridgeProtocol {
364
373
  minWorkers: resolvedOptions.minProcesses,
365
374
  queueTimeoutMs: resolvedOptions.queueTimeoutMs,
366
375
  maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
367
- onWorkerReady: userWarmup,
368
- onReplacementWorkerReady: replacementWorkerReady,
376
+ onWorkerReady,
377
+ onReplacementWorkerReady: onWorkerReady,
369
378
  });
370
379
 
371
- // Initialize BridgeProtocol with pooled transport
372
- const protocolOptions: BridgeProtocolOptions = {
380
+ super();
381
+
382
+ this.resolvedOptions = resolvedOptions;
383
+ this.pooledTransport = transport;
384
+ this.rpc = new RpcClient({
373
385
  transport,
374
386
  codec: resolvedOptions.codec,
375
387
  defaultTimeoutMs: resolvedOptions.timeoutMs,
376
- };
377
-
378
- super(protocolOptions);
388
+ });
389
+ // Track the RpcClient (which itself tracks the transport): one disposal
390
+ // chain facade -> rpc -> transport, and rpc.doDispose clears its info cache.
391
+ this.trackResource(this.rpc);
379
392
 
380
- this.resolvedOptions = resolvedOptions;
381
- this.pooledTransport = transport;
393
+ // Publish the rpc into the holder so the warmup closure can reach it.
394
+ rpcHolder.rpc = this.rpc;
382
395
  }
383
396
 
384
397
  // ===========================================================================
@@ -389,9 +402,10 @@ export class NodeBridge extends BridgeProtocol {
389
402
  * Initialize the bridge.
390
403
  *
391
404
  * Validates the bridge script exists, registers Arrow decoder,
392
- * and initializes the transport pool (which runs warmup commands per-worker).
405
+ * and initializes the held RpcClient (which initializes the transport pool,
406
+ * running warmup commands per-worker).
393
407
  */
394
- protected override async doInit(): Promise<void> {
408
+ protected async doInit(): Promise<void> {
395
409
  // Validate script exists
396
410
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- script path is user-configured
397
411
  if (!existsSync(this.resolvedOptions.scriptPath)) {
@@ -406,18 +420,29 @@ export class NodeBridge extends BridgeProtocol {
406
420
  loader: () => require('apache-arrow'),
407
421
  });
408
422
 
409
- // Initialize parent (which initializes transport and runs warmup per-worker)
410
- await super.doInit();
423
+ // Initialize the RpcClient (which initializes transport and runs warmup).
424
+ await this.rpc.init();
425
+ }
426
+
427
+ /**
428
+ * No facade-specific teardown: the RpcClient (and through it the transport
429
+ * pool) is tracked as a resource and disposed automatically by DisposableBase.
430
+ */
431
+ protected async doDispose(): Promise<void> {
432
+ // Intentionally empty; tracked resources handle disposal.
411
433
  }
412
434
 
413
435
  // ===========================================================================
414
- // CACHING OVERRIDE
436
+ // RPC METHODS (delegate to the held RpcClient)
415
437
  // ===========================================================================
416
438
 
417
439
  /**
418
- * Override call() to add optional caching.
440
+ * Call a Python function, with optional result caching.
441
+ *
442
+ * Cache lookup stays FIRST so cache hits return without forcing init,
443
+ * preserving the pre-composition behavior.
419
444
  */
420
- override async call<T = unknown>(
445
+ async call<T = unknown>(
421
446
  module: string,
422
447
  functionName: string,
423
448
  args: unknown[],
@@ -435,7 +460,8 @@ export class NodeBridge extends BridgeProtocol {
435
460
 
436
461
  // Execute and cache if pure function
437
462
  const startTime = performance.now();
438
- const result = await super.call<T>(module, functionName, args, kwargs);
463
+ await this.ensureReady();
464
+ const result = await this.rpc.call<T>(module, functionName, args, kwargs);
439
465
  const duration = performance.now() - startTime;
440
466
 
441
467
  if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
@@ -449,7 +475,52 @@ export class NodeBridge extends BridgeProtocol {
449
475
  }
450
476
 
451
477
  // No caching - direct call
452
- return super.call<T>(module, functionName, args, kwargs);
478
+ await this.ensureReady();
479
+ return this.rpc.call<T>(module, functionName, args, kwargs);
480
+ }
481
+
482
+ async instantiate<T = unknown>(
483
+ module: string,
484
+ className: string,
485
+ args: unknown[],
486
+ kwargs?: Record<string, unknown>
487
+ ): Promise<T> {
488
+ await this.ensureReady();
489
+ return this.rpc.instantiate<T>(module, className, args, kwargs);
490
+ }
491
+
492
+ async callMethod<T = unknown>(
493
+ handle: string,
494
+ methodName: string,
495
+ args: unknown[],
496
+ kwargs?: Record<string, unknown>
497
+ ): Promise<T> {
498
+ await this.ensureReady();
499
+ return this.rpc.callMethod<T>(handle, methodName, args, kwargs);
500
+ }
501
+
502
+ async disposeInstance(handle: string): Promise<void> {
503
+ await this.ensureReady();
504
+ return this.rpc.disposeInstance(handle);
505
+ }
506
+
507
+ /**
508
+ * Fetch bridge diagnostics and feature availability.
509
+ */
510
+ async getBridgeInfo(options?: GetBridgeInfoOptions): Promise<BridgeInfo> {
511
+ await this.ensureReady();
512
+ return this.rpc.getBridgeInfo(options);
513
+ }
514
+
515
+ /**
516
+ * Ensure the facade is initialized before delegating an RPC. Replicates the
517
+ * auto-init that BoundedContext.execute() gave for free, so the facade's
518
+ * own doInit pre-work (script check, Arrow decoder) runs before any RPC.
519
+ */
520
+ private async ensureReady(): Promise<void> {
521
+ if (!this.isReady) {
522
+ await this.init();
523
+ }
453
524
  }
454
525
 
455
526
  // ===========================================================================
@@ -558,168 +629,91 @@ export class NodeBridge extends BridgeProtocol {
558
629
  // =============================================================================
559
630
 
560
631
  /**
561
- * Simple request ID generator for warmup commands.
632
+ * Wrap an error from rpc.sendOn() with the warmup command's context label,
633
+ * preserving the exact phrasing the warmup tests assert on. The single
634
+ * RpcClient codec+id-counter is reused (via sendOn); only this label wrapping
635
+ * is bespoke, and only because warmup surfaces command-specific diagnostics.
636
+ *
637
+ * Maps codec/protocol error shapes to the historical messages:
638
+ * - encode failure (BridgeCodecError, e.g. BigInt) -> "failed to encode request"
639
+ * - missing result/error envelope (BridgeProtocolError) -> "malformed response envelope"
640
+ * - Python error (BridgeExecutionError "Type: message") -> "failed: Type: message"
641
+ * - anything else (transport/send) -> "failed to send"
562
642
  */
563
- let warmupRequestId = 0;
564
- function generateWarmupId(): number {
565
- return ++warmupRequestId;
643
+ function wrapWarmupError(label: string, error: unknown): BridgeExecutionError {
644
+ const cause = error instanceof Error ? error : undefined;
645
+ const message = error instanceof Error ? error.message : String(error);
646
+
647
+ if (error instanceof BridgeCodecError && error.codecPhase === 'encode') {
648
+ return new BridgeExecutionError(`${label} failed to encode request: ${message}`, { cause });
649
+ }
650
+ if (error instanceof BridgeProtocolError) {
651
+ // The codec rejects an envelope that has neither "result" nor "error".
652
+ // Distinguish that specific malformed-envelope case from transport-level
653
+ // protocol errors (e.g. garbled stdout), whose original message must be
654
+ // preserved so callers still see the underlying "Protocol error" text.
655
+ if (/missing "result" or "error"/.test(message)) {
656
+ return new BridgeExecutionError(`${label} returned malformed response envelope`, { cause });
657
+ }
658
+ return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
659
+ }
660
+ if (error instanceof BridgeExecutionError) {
661
+ // Python-side error already formatted as "Type: message" by the codec.
662
+ return new BridgeExecutionError(`${label} failed: ${message}`, { cause });
663
+ }
664
+ return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
566
665
  }
567
666
 
568
667
  /**
569
- * Create a callback that runs warmup commands on each worker.
668
+ * Build the per-worker onWorkerReady callback. It runs at worker-spawn time
669
+ * during init(); by then rpcHolder.rpc is set (assigned right after super()).
570
670
  *
571
- * The callback sends warmup commands directly to the worker's transport,
572
- * bypassing the pool to ensure each worker gets warmed up individually.
671
+ * The callback first does a readiness `meta` probe, then runs each warmup
672
+ * command. Both go through rpc.sendOn (raw encode/send/decode, NO auto-init)
673
+ * so they cannot re-await the in-flight rpc.init().
573
674
  */
574
- function createWarmupCallback(
675
+ function createWorkerReadyCallback(
676
+ rpcHolder: { rpc?: RpcClient },
575
677
  warmupCommands: WarmupCommand[],
576
678
  timeoutMs: number
577
679
  ): (worker: PooledWorker) => Promise<void> {
578
680
  return async (worker: PooledWorker) => {
579
- for (const [index, cmd] of warmupCommands.entries()) {
580
- const commandLabel = `${cmd.module}.${cmd.functionName}`;
581
- await executeWorkerCall(
582
- worker,
583
- {
584
- module: cmd.module,
585
- functionName: cmd.functionName,
586
- args: cmd.args ?? [],
587
- },
588
- timeoutMs,
589
- {
590
- label: `Warmup command #${index + 1} (${commandLabel})`,
591
- invalidJsonMessage: 'returned invalid JSON response',
592
- malformedEnvelopeMessage: requestId =>
593
- `returned malformed response envelope for request ${requestId}`,
594
- pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
595
- malformedErrorPayloadMessage: 'failed with malformed error payload',
596
- }
597
- );
681
+ const rpc = rpcHolder.rpc;
682
+ if (!rpc) {
683
+ throw new BridgeExecutionError('Worker warmup attempted before RpcClient was wired');
598
684
  }
599
- };
600
- }
601
685
 
602
- function createWorkerReadyCallback(
603
- timeoutMs: number,
604
- extraWarmup?: (worker: PooledWorker) => Promise<void>
605
- ): (worker: PooledWorker) => Promise<void> {
606
- return async (worker: PooledWorker) => {
607
- const result = await executeWorkerCall(
608
- worker,
609
- {
610
- module: 'builtins',
611
- functionName: 'len',
612
- args: [[]],
613
- },
614
- Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS),
615
- {
616
- label: 'Worker readiness probe (builtins.len)',
617
- invalidJsonMessage: 'returned invalid JSON response',
618
- malformedEnvelopeMessage: requestId =>
619
- `returned malformed response envelope for request ${requestId}`,
620
- pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
621
- malformedErrorPayloadMessage: 'failed with malformed error payload',
622
- }
623
- );
686
+ const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
624
687
 
625
- if (result !== 0) {
626
- throw new BridgeExecutionError(
627
- `Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`
628
- );
688
+ // Readiness probe (mirrors getBridgeInfo's meta request, per-worker).
689
+ try {
690
+ await rpc.sendOn(worker.transport, { method: 'meta', params: {} }, { timeoutMs: readyTimeoutMs });
691
+ } catch (error) {
692
+ throw wrapWarmupError('Worker warmup check', error);
629
693
  }
630
694
 
631
- await extraWarmup?.(worker);
632
- };
633
- }
634
-
635
- interface WorkerCallSpec {
636
- module: string;
637
- functionName: string;
638
- args: unknown[];
639
- }
640
-
641
- interface WorkerCallErrorMessages {
642
- label: string;
643
- invalidJsonMessage: string;
644
- malformedEnvelopeMessage: (requestId: number) => string;
645
- pythonErrorMessage: (errorType: string, errorMessage: string) => string;
646
- malformedErrorPayloadMessage: string;
647
- }
648
-
649
- async function executeWorkerCall(
650
- worker: PooledWorker,
651
- request: WorkerCallSpec,
652
- timeoutMs: number,
653
- messages: WorkerCallErrorMessages
654
- ): Promise<unknown> {
655
- const requestId = generateWarmupId();
656
- let message: string;
657
- try {
658
- message = JSON.stringify({
659
- id: requestId,
660
- protocol: PROTOCOL_ID,
661
- method: 'call',
662
- params: {
663
- module: request.module,
664
- functionName: request.functionName,
665
- args: request.args,
666
- kwargs: {},
667
- },
668
- });
669
- } catch (error) {
670
- throw new BridgeExecutionError(
671
- `${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
672
- { cause: error instanceof Error ? error : undefined }
673
- );
674
- }
675
-
676
- let response: string;
677
- try {
678
- response = await worker.transport.send(message, timeoutMs);
679
- } catch (error) {
680
- throw new BridgeExecutionError(
681
- `${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
682
- { cause: error instanceof Error ? error : undefined }
683
- );
684
- }
685
-
686
- let parsed: unknown;
687
- try {
688
- parsed = JSON.parse(response);
689
- } catch (error) {
690
- throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
691
- cause: error instanceof Error ? error : undefined,
692
- });
693
- }
694
-
695
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
696
- throw new BridgeExecutionError(
697
- `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
698
- );
699
- }
700
-
701
- const envelope = parsed as { result?: unknown; error?: unknown };
702
- if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
703
- const errorPayload = envelope.error;
704
- if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
705
- const err = errorPayload as { type?: unknown; message?: unknown };
706
- const errType = typeof err.type === 'string' ? err.type : 'Error';
707
- const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
708
- throw new BridgeExecutionError(
709
- `${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
710
- );
695
+ // User-provided warmup commands.
696
+ for (const [index, cmd] of warmupCommands.entries()) {
697
+ const label = `Warmup command #${index + 1} (${cmd.module}.${cmd.functionName})`;
698
+ try {
699
+ await rpc.sendOn(
700
+ worker.transport,
701
+ {
702
+ method: 'call',
703
+ params: {
704
+ module: cmd.module,
705
+ functionName: cmd.functionName,
706
+ args: cmd.args ?? [],
707
+ kwargs: {},
708
+ },
709
+ },
710
+ { timeoutMs }
711
+ );
712
+ } catch (error) {
713
+ throw wrapWarmupError(label, error);
714
+ }
711
715
  }
712
-
713
- throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
714
- }
715
-
716
- if (!('result' in envelope)) {
717
- throw new BridgeExecutionError(
718
- `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
719
- );
720
- }
721
-
722
- return envelope.result;
716
+ };
723
717
  }
724
718
 
725
719
  // =============================================================================
@@ -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,7 +16,8 @@
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
22
  export {
22
23
  NodeBridge as OptimizedNodeBridge,
@@ -8,7 +8,7 @@
8
8
  * @see https://github.com/bbopen/tywrap/issues/149
9
9
  */
10
10
 
11
- import { BoundedContext } from './bounded-context.js';
11
+ import { DisposableBase } from './bounded-context.js';
12
12
  import { BridgeDisposedError, BridgeExecutionError } from './errors.js';
13
13
  import type { Transport } from './transport.js';
14
14
  import { WorkerPool, type PooledWorker } from './worker-pool.js';
@@ -82,7 +82,7 @@ export interface PooledTransportOptions {
82
82
  * await transport.dispose();
83
83
  * ```
84
84
  */
85
- export class PooledTransport extends BoundedContext implements Transport {
85
+ export class PooledTransport extends DisposableBase implements Transport {
86
86
  private readonly poolOptions: Omit<
87
87
  Required<PooledTransportOptions>,
88
88
  'onWorkerReady' | 'onReplacementWorkerReady'
@@ -205,59 +205,4 @@ export class PooledTransport extends BoundedContext implements Transport {
205
205
  get totalInFlight(): number {
206
206
  return this.pool?.totalInFlight ?? 0;
207
207
  }
208
-
209
- // ===========================================================================
210
- // RUNTIME EXECUTION (Not implemented - PooledTransport is just a transport)
211
- // ===========================================================================
212
-
213
- /**
214
- * Not implemented - PooledTransport is a transport, use BridgeProtocol.
215
- */
216
- async call<T = unknown>(
217
- _module: string,
218
- _functionName: string,
219
- _args: unknown[],
220
- _kwargs?: Record<string, unknown>
221
- ): Promise<T> {
222
- throw new BridgeExecutionError(
223
- 'PooledTransport is a transport, use BridgeProtocol for operations'
224
- );
225
- }
226
-
227
- /**
228
- * Not implemented - PooledTransport is a transport, use BridgeProtocol.
229
- */
230
- async instantiate<T = unknown>(
231
- _module: string,
232
- _className: string,
233
- _args: unknown[],
234
- _kwargs?: Record<string, unknown>
235
- ): Promise<T> {
236
- throw new BridgeExecutionError(
237
- 'PooledTransport is a transport, use BridgeProtocol for operations'
238
- );
239
- }
240
-
241
- /**
242
- * Not implemented - PooledTransport is a transport, use BridgeProtocol.
243
- */
244
- async callMethod<T = unknown>(
245
- _handle: string,
246
- _methodName: string,
247
- _args: unknown[],
248
- _kwargs?: Record<string, unknown>
249
- ): Promise<T> {
250
- throw new BridgeExecutionError(
251
- 'PooledTransport is a transport, use BridgeProtocol for operations'
252
- );
253
- }
254
-
255
- /**
256
- * Not implemented - PooledTransport is a transport, use BridgeProtocol.
257
- */
258
- async disposeInstance(_handle: string): Promise<void> {
259
- throw new BridgeExecutionError(
260
- 'PooledTransport is a transport, use BridgeProtocol for operations'
261
- );
262
- }
263
208
  }
@@ -14,13 +14,8 @@
14
14
  */
15
15
 
16
16
  import { spawn, type ChildProcess } from 'child_process';
17
- import { BoundedContext } from './bounded-context.js';
18
- import {
19
- BridgeDisposedError,
20
- BridgeProtocolError,
21
- BridgeTimeoutError,
22
- BridgeExecutionError,
23
- } from './errors.js';
17
+ import { DisposableBase } from './bounded-context.js';
18
+ import { BridgeDisposedError, BridgeProtocolError, BridgeTimeoutError } from './errors.js';
24
19
  import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
25
20
  import type { Transport } from './transport.js';
26
21
 
@@ -162,7 +157,7 @@ function extractMessageId(json: string): number | null {
162
157
  * await transport.dispose();
163
158
  * ```
164
159
  */
165
- export class ProcessIO extends BoundedContext implements Transport {
160
+ export class ProcessIO extends DisposableBase implements Transport {
166
161
  // Configuration
167
162
  private readonly pythonPath: string;
168
163
  private readonly bridgeScript: string;
@@ -372,53 +367,6 @@ export class ProcessIO extends BoundedContext implements Transport {
372
367
  this.requestCount = 0;
373
368
  }
374
369
 
375
- // ===========================================================================
376
- // ABSTRACT METHOD STUBS (from BoundedContext)
377
- // ===========================================================================
378
-
379
- /**
380
- * Not implemented - ProcessIO is a transport, not a full bridge.
381
- */
382
- call<T = unknown>(
383
- _module: string,
384
- _functionName: string,
385
- _args: unknown[],
386
- _kwargs?: Record<string, unknown>
387
- ): Promise<T> {
388
- throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
389
- }
390
-
391
- /**
392
- * Not implemented - ProcessIO is a transport, not a full bridge.
393
- */
394
- instantiate<T = unknown>(
395
- _module: string,
396
- _className: string,
397
- _args: unknown[],
398
- _kwargs?: Record<string, unknown>
399
- ): Promise<T> {
400
- throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
401
- }
402
-
403
- /**
404
- * Not implemented - ProcessIO is a transport, not a full bridge.
405
- */
406
- callMethod<T = unknown>(
407
- _handle: string,
408
- _methodName: string,
409
- _args: unknown[],
410
- _kwargs?: Record<string, unknown>
411
- ): Promise<T> {
412
- throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
413
- }
414
-
415
- /**
416
- * Not implemented - ProcessIO is a transport, not a full bridge.
417
- */
418
- disposeInstance(_handle: string): Promise<void> {
419
- throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
420
- }
421
-
422
370
  // ===========================================================================
423
371
  // PROCESS MANAGEMENT
424
372
  // ===========================================================================