tywrap 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -6
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/base.d.ts +7 -7
  7. package/dist/runtime/base.js +8 -8
  8. package/dist/runtime/bounded-context.d.ts +15 -31
  9. package/dist/runtime/bounded-context.d.ts.map +1 -1
  10. package/dist/runtime/bounded-context.js +16 -15
  11. package/dist/runtime/bounded-context.js.map +1 -1
  12. package/dist/runtime/http.d.ts +24 -5
  13. package/dist/runtime/http.d.ts.map +1 -1
  14. package/dist/runtime/http.js +57 -12
  15. package/dist/runtime/http.js.map +1 -1
  16. package/dist/runtime/node.d.ts +33 -7
  17. package/dist/runtime/node.d.ts.map +1 -1
  18. package/dist/runtime/node.js +139 -151
  19. package/dist/runtime/node.js.map +1 -1
  20. package/dist/runtime/pooled-transport.d.ts +2 -18
  21. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  22. package/dist/runtime/pooled-transport.js +2 -29
  23. package/dist/runtime/pooled-transport.js.map +1 -1
  24. package/dist/runtime/process-io.d.ts +2 -18
  25. package/dist/runtime/process-io.d.ts.map +1 -1
  26. package/dist/runtime/process-io.js +3 -30
  27. package/dist/runtime/process-io.js.map +1 -1
  28. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts +12 -0
  29. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -0
  30. package/dist/runtime/pyodide-bootstrap-core.generated.js +12 -0
  31. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -0
  32. package/dist/runtime/pyodide-io.d.ts +7 -37
  33. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  34. package/dist/runtime/pyodide-io.js +90 -214
  35. package/dist/runtime/pyodide-io.js.map +1 -1
  36. package/dist/runtime/pyodide.d.ts +25 -5
  37. package/dist/runtime/pyodide.d.ts.map +1 -1
  38. package/dist/runtime/pyodide.js +59 -10
  39. package/dist/runtime/pyodide.js.map +1 -1
  40. package/dist/runtime/{bridge-protocol.d.ts → rpc-client.d.ts} +59 -46
  41. package/dist/runtime/rpc-client.d.ts.map +1 -0
  42. package/dist/runtime/{bridge-protocol.js → rpc-client.js} +60 -42
  43. package/dist/runtime/rpc-client.js.map +1 -0
  44. package/dist/runtime/worker-pool.d.ts +2 -19
  45. package/dist/runtime/worker-pool.d.ts.map +1 -1
  46. package/dist/runtime/worker-pool.js +2 -30
  47. package/dist/runtime/worker-pool.js.map +1 -1
  48. package/dist/types/index.d.ts +8 -3
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/tywrap.d.ts.map +1 -1
  51. package/dist/tywrap.js +1 -3
  52. package/dist/tywrap.js.map +1 -1
  53. package/package.json +9 -12
  54. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  55. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  56. package/runtime/python_bridge.py +85 -702
  57. package/runtime/safe_codec.py +10 -2
  58. package/runtime/tywrap_bridge_core.py +875 -0
  59. package/src/index.ts +6 -6
  60. package/src/runtime/base.ts +8 -8
  61. package/src/runtime/bounded-context.ts +17 -56
  62. package/src/runtime/http.ts +85 -13
  63. package/src/runtime/node.ts +165 -217
  64. package/src/runtime/pooled-transport.ts +2 -57
  65. package/src/runtime/process-io.ts +3 -55
  66. package/src/runtime/pyodide-bootstrap-core.generated.ts +12 -0
  67. package/src/runtime/pyodide-io.ts +92 -243
  68. package/src/runtime/pyodide.ts +87 -10
  69. package/src/runtime/{bridge-protocol.ts → rpc-client.ts} +76 -49
  70. package/src/runtime/worker-pool.ts +2 -58
  71. package/src/types/index.ts +20 -4
  72. package/src/tywrap.ts +1 -3
  73. package/dist/core/analyzer.d.ts +0 -64
  74. package/dist/core/analyzer.d.ts.map +0 -1
  75. package/dist/core/analyzer.js +0 -710
  76. package/dist/core/analyzer.js.map +0 -1
  77. package/dist/runtime/bridge-core.d.ts +0 -65
  78. package/dist/runtime/bridge-core.d.ts.map +0 -1
  79. package/dist/runtime/bridge-core.js +0 -379
  80. package/dist/runtime/bridge-core.js.map +0 -1
  81. package/dist/runtime/bridge-protocol.d.ts.map +0 -1
  82. package/dist/runtime/bridge-protocol.js.map +0 -1
  83. package/dist/utils/parallel-processor.d.ts +0 -146
  84. package/dist/utils/parallel-processor.d.ts.map +0 -1
  85. package/dist/utils/parallel-processor.js +0 -707
  86. package/dist/utils/parallel-processor.js.map +0 -1
  87. package/src/core/analyzer.ts +0 -824
  88. package/src/runtime/bridge-core.ts +0 -494
  89. package/src/utils/parallel-processor.ts +0 -955
@@ -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.
@@ -343,11 +346,19 @@ export class NodeBridge extends BridgeProtocol {
343
346
  // Build environment for ProcessIO
344
347
  const processEnv = buildProcessEnv(resolvedOptions);
345
348
 
346
- const userWarmup =
347
- resolvedOptions.warmupCommands.length > 0
348
- ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
349
- : undefined;
350
- const onWorkerReady = 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
+ );
351
362
 
352
363
  // Create pooled transport with ProcessIO workers
353
364
  const transport = new PooledTransport({
@@ -366,17 +377,21 @@ export class NodeBridge extends BridgeProtocol {
366
377
  onReplacementWorkerReady: onWorkerReady,
367
378
  });
368
379
 
369
- // Initialize BridgeProtocol with pooled transport
370
- const protocolOptions: BridgeProtocolOptions = {
380
+ super();
381
+
382
+ this.resolvedOptions = resolvedOptions;
383
+ this.pooledTransport = transport;
384
+ this.rpc = new RpcClient({
371
385
  transport,
372
386
  codec: resolvedOptions.codec,
373
387
  defaultTimeoutMs: resolvedOptions.timeoutMs,
374
- };
375
-
376
- 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);
377
392
 
378
- this.resolvedOptions = resolvedOptions;
379
- this.pooledTransport = transport;
393
+ // Publish the rpc into the holder so the warmup closure can reach it.
394
+ rpcHolder.rpc = this.rpc;
380
395
  }
381
396
 
382
397
  // ===========================================================================
@@ -387,9 +402,10 @@ export class NodeBridge extends BridgeProtocol {
387
402
  * Initialize the bridge.
388
403
  *
389
404
  * Validates the bridge script exists, registers Arrow decoder,
390
- * 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).
391
407
  */
392
- protected override async doInit(): Promise<void> {
408
+ protected async doInit(): Promise<void> {
393
409
  // Validate script exists
394
410
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- script path is user-configured
395
411
  if (!existsSync(this.resolvedOptions.scriptPath)) {
@@ -404,18 +420,29 @@ export class NodeBridge extends BridgeProtocol {
404
420
  loader: () => require('apache-arrow'),
405
421
  });
406
422
 
407
- // Initialize parent (which initializes transport and runs warmup per-worker)
408
- 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.
409
433
  }
410
434
 
411
435
  // ===========================================================================
412
- // CACHING OVERRIDE
436
+ // RPC METHODS (delegate to the held RpcClient)
413
437
  // ===========================================================================
414
438
 
415
439
  /**
416
- * 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.
417
444
  */
418
- override async call<T = unknown>(
445
+ async call<T = unknown>(
419
446
  module: string,
420
447
  functionName: string,
421
448
  args: unknown[],
@@ -433,7 +460,8 @@ export class NodeBridge extends BridgeProtocol {
433
460
 
434
461
  // Execute and cache if pure function
435
462
  const startTime = performance.now();
436
- 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);
437
465
  const duration = performance.now() - startTime;
438
466
 
439
467
  if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
@@ -447,7 +475,52 @@ export class NodeBridge extends BridgeProtocol {
447
475
  }
448
476
 
449
477
  // No caching - direct call
450
- 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
+ }
451
524
  }
452
525
 
453
526
  // ===========================================================================
@@ -556,216 +629,91 @@ export class NodeBridge extends BridgeProtocol {
556
629
  // =============================================================================
557
630
 
558
631
  /**
559
- * 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"
560
642
  */
561
- let warmupRequestId = 0;
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
- }
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);
605
646
 
606
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
607
- throw new BridgeExecutionError(
608
- `${label} returned malformed response envelope for request ${requestId}`
609
- );
647
+ if (error instanceof BridgeCodecError && error.codecPhase === 'encode') {
648
+ return new BridgeExecutionError(`${label} failed to encode request: ${message}`, { cause });
610
649
  }
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}`);
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 });
620
657
  }
621
-
622
- throw new BridgeExecutionError(`${label} failed with malformed error payload`);
658
+ return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
623
659
  }
624
-
625
- if (!('result' in envelope)) {
626
- throw new BridgeExecutionError(
627
- `${label} returned malformed response envelope for request ${requestId}`
628
- );
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 });
629
663
  }
664
+ return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
630
665
  }
631
666
 
632
667
  /**
633
- * 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()).
634
670
  *
635
- * The callback sends warmup commands directly to the worker's transport,
636
- * 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().
637
674
  */
638
- function createWarmupCallback(
675
+ function createWorkerReadyCallback(
676
+ rpcHolder: { rpc?: RpcClient },
639
677
  warmupCommands: WarmupCommand[],
640
678
  timeoutMs: number
641
679
  ): (worker: PooledWorker) => Promise<void> {
642
680
  return async (worker: PooledWorker) => {
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',
660
- }
661
- );
681
+ const rpc = rpcHolder.rpc;
682
+ if (!rpc) {
683
+ throw new BridgeExecutionError('Worker warmup attempted before RpcClient was wired');
662
684
  }
663
- };
664
- }
665
685
 
666
- function createWorkerReadyCallback(
667
- timeoutMs: number,
668
- extraWarmup?: (worker: PooledWorker) => Promise<void>
669
- ): (worker: PooledWorker) => Promise<void> {
670
- return async (worker: PooledWorker) => {
671
686
  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
687
 
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
- );
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);
757
693
  }
758
694
 
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;
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
+ }
715
+ }
716
+ };
769
717
  }
770
718
 
771
719
  // =============================================================================
@@ -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
  // ===========================================================================