tywrap 0.6.0 → 0.7.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 (118) hide show
  1. package/dist/core/annotation-parser.d.ts.map +1 -1
  2. package/dist/core/annotation-parser.js +19 -14
  3. package/dist/core/annotation-parser.js.map +1 -1
  4. package/dist/core/discovery.d.ts +17 -0
  5. package/dist/core/discovery.d.ts.map +1 -1
  6. package/dist/core/discovery.js +59 -49
  7. package/dist/core/discovery.js.map +1 -1
  8. package/dist/core/generator.d.ts.map +1 -1
  9. package/dist/core/generator.js +40 -9
  10. package/dist/core/generator.js.map +1 -1
  11. package/dist/core/validation.d.ts +23 -0
  12. package/dist/core/validation.d.ts.map +1 -1
  13. package/dist/core/validation.js +52 -48
  14. package/dist/core/validation.js.map +1 -1
  15. package/dist/dev.d.ts.map +1 -1
  16. package/dist/dev.js +175 -133
  17. package/dist/dev.js.map +1 -1
  18. package/dist/runtime/base-bridge.d.ts +57 -0
  19. package/dist/runtime/base-bridge.d.ts.map +1 -0
  20. package/dist/runtime/base-bridge.js +72 -0
  21. package/dist/runtime/base-bridge.js.map +1 -0
  22. package/dist/runtime/bridge-codec.d.ts +15 -0
  23. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  24. package/dist/runtime/bridge-codec.js +45 -48
  25. package/dist/runtime/bridge-codec.js.map +1 -1
  26. package/dist/runtime/http-transport.d.ts +11 -1
  27. package/dist/runtime/http-transport.d.ts.map +1 -1
  28. package/dist/runtime/http-transport.js +19 -0
  29. package/dist/runtime/http-transport.js.map +1 -1
  30. package/dist/runtime/http.d.ts +5 -12
  31. package/dist/runtime/http.d.ts.map +1 -1
  32. package/dist/runtime/http.js +6 -29
  33. package/dist/runtime/http.js.map +1 -1
  34. package/dist/runtime/index.d.ts +1 -1
  35. package/dist/runtime/index.d.ts.map +1 -1
  36. package/dist/runtime/index.js.map +1 -1
  37. package/dist/runtime/node.d.ts +12 -19
  38. package/dist/runtime/node.d.ts.map +1 -1
  39. package/dist/runtime/node.js +14 -34
  40. package/dist/runtime/node.js.map +1 -1
  41. package/dist/runtime/pooled-transport.d.ts +21 -2
  42. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  43. package/dist/runtime/pooled-transport.js +16 -0
  44. package/dist/runtime/pooled-transport.js.map +1 -1
  45. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  47. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  48. package/dist/runtime/pyodide-transport.d.ts +12 -1
  49. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  50. package/dist/runtime/pyodide-transport.js +20 -0
  51. package/dist/runtime/pyodide-transport.js.map +1 -1
  52. package/dist/runtime/pyodide.d.ts +5 -12
  53. package/dist/runtime/pyodide.d.ts.map +1 -1
  54. package/dist/runtime/pyodide.js +6 -29
  55. package/dist/runtime/pyodide.js.map +1 -1
  56. package/dist/runtime/rpc-client.d.ts +32 -1
  57. package/dist/runtime/rpc-client.d.ts.map +1 -1
  58. package/dist/runtime/rpc-client.js +45 -24
  59. package/dist/runtime/rpc-client.js.map +1 -1
  60. package/dist/runtime/subprocess-transport.d.ts +26 -1
  61. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  62. package/dist/runtime/subprocess-transport.js +72 -35
  63. package/dist/runtime/subprocess-transport.js.map +1 -1
  64. package/dist/runtime/transport.d.ts +59 -0
  65. package/dist/runtime/transport.d.ts.map +1 -1
  66. package/dist/runtime/transport.js +1 -0
  67. package/dist/runtime/transport.js.map +1 -1
  68. package/dist/types/index.d.ts +35 -0
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/tywrap.d.ts +0 -5
  71. package/dist/tywrap.d.ts.map +1 -1
  72. package/dist/tywrap.js +212 -169
  73. package/dist/tywrap.js.map +1 -1
  74. package/dist/utils/cache.d.ts +11 -0
  75. package/dist/utils/cache.d.ts.map +1 -1
  76. package/dist/utils/cache.js +50 -58
  77. package/dist/utils/cache.js.map +1 -1
  78. package/dist/utils/codec.d.ts +2 -0
  79. package/dist/utils/codec.d.ts.map +1 -1
  80. package/dist/utils/codec.js +53 -2
  81. package/dist/utils/codec.js.map +1 -1
  82. package/dist/utils/python.d.ts.map +1 -1
  83. package/dist/utils/python.js +28 -17
  84. package/dist/utils/python.js.map +1 -1
  85. package/dist/utils/runtime.d.ts.map +1 -1
  86. package/dist/utils/runtime.js +22 -16
  87. package/dist/utils/runtime.js.map +1 -1
  88. package/dist/version.js +1 -1
  89. package/package.json +7 -1
  90. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  91. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  92. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  93. package/runtime/tywrap_bridge_core.py +55 -3
  94. package/src/core/annotation-parser.ts +23 -18
  95. package/src/core/discovery.ts +70 -54
  96. package/src/core/generator.ts +50 -11
  97. package/src/core/validation.ts +84 -48
  98. package/src/dev.ts +237 -153
  99. package/src/runtime/base-bridge.ts +106 -0
  100. package/src/runtime/bridge-codec.ts +58 -70
  101. package/src/runtime/http-transport.ts +21 -1
  102. package/src/runtime/http.ts +7 -51
  103. package/src/runtime/index.ts +1 -0
  104. package/src/runtime/node.ts +17 -52
  105. package/src/runtime/pooled-transport.ts +25 -2
  106. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  107. package/src/runtime/pyodide-transport.ts +22 -0
  108. package/src/runtime/pyodide.ts +7 -52
  109. package/src/runtime/rpc-client.ts +56 -32
  110. package/src/runtime/subprocess-transport.ts +89 -40
  111. package/src/runtime/transport.ts +71 -0
  112. package/src/types/index.ts +37 -0
  113. package/src/tywrap.ts +274 -187
  114. package/src/utils/cache.ts +59 -61
  115. package/src/utils/codec.ts +61 -4
  116. package/src/utils/python.ts +33 -20
  117. package/src/utils/runtime.ts +24 -13
  118. package/src/version.ts +1 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * BasePythonBridge - shared RPC delegation for the bridge facades.
3
+ *
4
+ * The three bridge facades (NodeBridge/HttpBridge/PyodideBridge) all extend
5
+ * DisposableBase (lifecycle/resources) and implement PythonRuntime by HOLDING
6
+ * an RpcClient. The PythonRuntime delegation was byte-identical across all
7
+ * three: each method does `await this.ensureReady()` then forwards to the held
8
+ * RpcClient. This base collapses that duplication onto a single
9
+ * `getRpcClient()` accessor while leaving each facade free to own its own
10
+ * RpcClient field (for constructor wiring, resource tracking, and — in
11
+ * NodeBridge's case — a caching override of call()).
12
+ *
13
+ * It carries no transport/codec/lifecycle specifics: doInit/doDispose remain
14
+ * abstract on DisposableBase and stay per-facade.
15
+ *
16
+ * @see https://github.com/bbopen/tywrap/issues/149
17
+ */
18
+
19
+ import type { PythonRuntime, BridgeInfo } from '../types/index.js';
20
+
21
+ import { DisposableBase } from './bounded-context.js';
22
+ import type { RpcClient, GetBridgeInfoOptions } from './rpc-client.js';
23
+ import type { TransportCapabilities } from './transport.js';
24
+
25
+ /**
26
+ * Shared base for the bridge facades. Implements the PythonRuntime RPC
27
+ * delegation (plus getBridgeInfo) over a single held RpcClient, exposed by the
28
+ * abstract {@link getRpcClient} accessor. Lifecycle (doInit/doDispose) and the
29
+ * RpcClient's construction/ownership remain the subclass's responsibility.
30
+ */
31
+ export abstract class BasePythonBridge extends DisposableBase implements PythonRuntime {
32
+ /**
33
+ * Return the held RpcClient that the shared delegating methods forward to.
34
+ * Subclasses construct, track, and own the RpcClient; this accessor lets the
35
+ * base reach it without dictating how it is stored.
36
+ */
37
+ protected abstract getRpcClient(): RpcClient;
38
+
39
+ // ===========================================================================
40
+ // RPC METHODS (delegate to the held RpcClient)
41
+ // ===========================================================================
42
+
43
+ async call<T = unknown>(
44
+ module: string,
45
+ functionName: string,
46
+ args: unknown[],
47
+ kwargs?: Record<string, unknown>
48
+ ): Promise<T> {
49
+ await this.ensureReady();
50
+ return this.getRpcClient().call<T>(module, functionName, args, kwargs);
51
+ }
52
+
53
+ async instantiate<T = unknown>(
54
+ module: string,
55
+ className: string,
56
+ args: unknown[],
57
+ kwargs?: Record<string, unknown>
58
+ ): Promise<T> {
59
+ await this.ensureReady();
60
+ return this.getRpcClient().instantiate<T>(module, className, args, kwargs);
61
+ }
62
+
63
+ async callMethod<T = unknown>(
64
+ handle: string,
65
+ methodName: string,
66
+ args: unknown[],
67
+ kwargs?: Record<string, unknown>
68
+ ): Promise<T> {
69
+ await this.ensureReady();
70
+ return this.getRpcClient().callMethod<T>(handle, methodName, args, kwargs);
71
+ }
72
+
73
+ async disposeInstance(handle: string): Promise<void> {
74
+ await this.ensureReady();
75
+ return this.getRpcClient().disposeInstance(handle);
76
+ }
77
+
78
+ /**
79
+ * Fetch bridge diagnostics and feature availability.
80
+ */
81
+ async getBridgeInfo(options?: GetBridgeInfoOptions): Promise<BridgeInfo> {
82
+ await this.ensureReady();
83
+ return this.getRpcClient().getBridgeInfo(options);
84
+ }
85
+
86
+ /**
87
+ * Describe this backend's transport-level capabilities (Arrow/binary/chunking/
88
+ * streaming support, max frame size). The descriptor is static and does NOT
89
+ * depend on lifecycle state, so this intentionally does not `ensureReady()` —
90
+ * callers can query it before connecting to decide how to use the bridge.
91
+ */
92
+ capabilities(): TransportCapabilities {
93
+ return this.getRpcClient().capabilities();
94
+ }
95
+
96
+ /**
97
+ * Ensure the facade is initialized before delegating an RPC. Replicates the
98
+ * auto-init that the bounded execute path provided pre-composition, so the
99
+ * facade's own doInit pre-work runs before any RPC.
100
+ */
101
+ protected async ensureReady(): Promise<void> {
102
+ if (!this.isReady) {
103
+ await this.init();
104
+ }
105
+ }
106
+ }
@@ -410,6 +410,60 @@ export class BridgeCodec {
410
410
  return bridgeError;
411
411
  }
412
412
 
413
+ /**
414
+ * Shared prelude for both decode paths: size guard, JSON parse (with bytes
415
+ * revival), protocol-version validation, and RPC-envelope unwrapping.
416
+ *
417
+ * Behavior-preserving extraction of the common head of decodeResponse and
418
+ * decodeResponseAsync; the only divergence between the two is what they do
419
+ * with the returned result (sync returns it as-is, async applies Arrow
420
+ * decoding) and so that divergence stays in the callers.
421
+ */
422
+ private parseResponseResult(payload: string): unknown {
423
+ // Check payload size first
424
+ const payloadBytes = new TextEncoder().encode(payload).length;
425
+ if (payloadBytes > this.maxPayloadBytes) {
426
+ throw new BridgeCodecError(
427
+ `Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
428
+ { codecPhase: 'decode', valueType: 'payload' }
429
+ );
430
+ }
431
+
432
+ // Parse JSON
433
+ let parsed: unknown;
434
+ try {
435
+ parsed = JSON.parse(payload, this.reviveValueBound);
436
+ } catch (err) {
437
+ if (err instanceof BridgeCodecError || err instanceof BridgeProtocolError) {
438
+ throw err;
439
+ }
440
+ const errorMessage = err instanceof Error ? err.message : String(err);
441
+ throw new BridgeCodecError(
442
+ `JSON parse failed: ${errorMessage}. Payload snippet: ${summarizePayloadForError(payload)}`,
443
+ { codecPhase: 'decode', valueType: 'json' }
444
+ );
445
+ }
446
+
447
+ // Validate protocol version (if present)
448
+ validateProtocolVersion(parsed);
449
+
450
+ return this.extractResultFromRpcResponse(parsed);
451
+ }
452
+
453
+ /**
454
+ * Post-decode guard: reject non-finite numbers (NaN/Infinity) when enabled.
455
+ * Shared by both decode paths.
456
+ */
457
+ private assertNoSpecialFloats(value: unknown): void {
458
+ if (this.rejectSpecialFloats && containsSpecialFloat(value)) {
459
+ const floatPath = findSpecialFloatPath(value);
460
+ throw new BridgeCodecError(
461
+ `Response contains non-finite number (NaN or Infinity) at ${floatPath}`,
462
+ { codecPhase: 'decode', valueType: 'number' }
463
+ );
464
+ }
465
+ }
466
+
413
467
  private extractResultFromRpcResponse(parsed: unknown): unknown {
414
468
  if (isRpcResponse(parsed)) {
415
469
  const response = parsed;
@@ -526,43 +580,10 @@ export class BridgeCodec {
526
580
  * @throws BridgeExecutionError if response contains a Python error
527
581
  */
528
582
  decodeResponse<T>(payload: string): T {
529
- // Check payload size first
530
- const payloadBytes = new TextEncoder().encode(payload).length;
531
- if (payloadBytes > this.maxPayloadBytes) {
532
- throw new BridgeCodecError(
533
- `Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
534
- { codecPhase: 'decode', valueType: 'payload' }
535
- );
536
- }
537
-
538
- // Parse JSON
539
- let parsed: unknown;
540
- try {
541
- parsed = JSON.parse(payload, this.reviveValueBound);
542
- } catch (err) {
543
- if (err instanceof BridgeCodecError || err instanceof BridgeProtocolError) {
544
- throw err;
545
- }
546
- const errorMessage = err instanceof Error ? err.message : String(err);
547
- throw new BridgeCodecError(
548
- `JSON parse failed: ${errorMessage}. Payload snippet: ${summarizePayloadForError(payload)}`,
549
- { codecPhase: 'decode', valueType: 'json' }
550
- );
551
- }
552
-
553
- // Validate protocol version (if present)
554
- validateProtocolVersion(parsed);
555
-
556
- const result = this.extractResultFromRpcResponse(parsed);
583
+ const result = this.parseResponseResult(payload);
557
584
 
558
585
  // Post-decode validation for special floats if enabled
559
- if (this.rejectSpecialFloats && containsSpecialFloat(result)) {
560
- const floatPath = findSpecialFloatPath(result);
561
- throw new BridgeCodecError(
562
- `Response contains non-finite number (NaN or Infinity) at ${floatPath}`,
563
- { codecPhase: 'decode', valueType: 'number' }
564
- );
565
- }
586
+ this.assertNoSpecialFloats(result);
566
587
 
567
588
  return result as T;
568
589
  }
@@ -578,34 +599,7 @@ export class BridgeCodec {
578
599
  * @throws BridgeExecutionError if response contains a Python error
579
600
  */
580
601
  async decodeResponseAsync<T>(payload: string): Promise<T> {
581
- // Check payload size first
582
- const payloadBytes = new TextEncoder().encode(payload).length;
583
- if (payloadBytes > this.maxPayloadBytes) {
584
- throw new BridgeCodecError(
585
- `Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
586
- { codecPhase: 'decode', valueType: 'payload' }
587
- );
588
- }
589
-
590
- // Parse JSON
591
- let parsed: unknown;
592
- try {
593
- parsed = JSON.parse(payload, this.reviveValueBound);
594
- } catch (err) {
595
- if (err instanceof BridgeCodecError || err instanceof BridgeProtocolError) {
596
- throw err;
597
- }
598
- const errorMessage = err instanceof Error ? err.message : String(err);
599
- throw new BridgeCodecError(
600
- `JSON parse failed: ${errorMessage}. Payload snippet: ${summarizePayloadForError(payload)}`,
601
- { codecPhase: 'decode', valueType: 'json' }
602
- );
603
- }
604
-
605
- // Validate protocol version (if present)
606
- validateProtocolVersion(parsed);
607
-
608
- const result = this.extractResultFromRpcResponse(parsed);
602
+ const result = this.parseResponseResult(payload);
609
603
 
610
604
  // Apply Arrow decoding to the result
611
605
  let decoded: unknown;
@@ -621,13 +615,7 @@ export class BridgeCodec {
621
615
 
622
616
  // Post-decode validation for special floats if enabled
623
617
  // Note: Arrow decoders can introduce NaN/Infinity from binary representations.
624
- if (this.rejectSpecialFloats && containsSpecialFloat(decoded)) {
625
- const floatPath = findSpecialFloatPath(decoded);
626
- throw new BridgeCodecError(
627
- `Response contains non-finite number (NaN or Infinity) at ${floatPath}`,
628
- { codecPhase: 'decode', valueType: 'number' }
629
- );
630
- }
618
+ this.assertNoSpecialFloats(decoded);
631
619
 
632
620
  return decoded as T;
633
621
  }
@@ -13,7 +13,7 @@ import {
13
13
  BridgeProtocolError,
14
14
  BridgeTimeoutError,
15
15
  } from './errors.js';
16
- import type { Transport } from './transport.js';
16
+ import type { Transport, TransportCapabilities } from './transport.js';
17
17
 
18
18
  // =============================================================================
19
19
  // OPTIONS
@@ -111,6 +111,26 @@ export class HttpTransport implements Transport {
111
111
  return !this._isDisposed;
112
112
  }
113
113
 
114
+ /**
115
+ * Static capability descriptor for the HTTP backend.
116
+ *
117
+ * The Python server can return Arrow IPC and arbitrary binary (bytes
118
+ * envelopes) in the HTTP response body. Chunking/streaming are not implemented
119
+ * (0.8.0). HTTP imposes no transport-level frame ceiling — the whole body is
120
+ * read in one shot — so `maxFrameBytes` is `Number.POSITIVE_INFINITY` (a higher
121
+ * layer, e.g. the codec payload limit, may still cap the size).
122
+ */
123
+ capabilities(): TransportCapabilities {
124
+ return {
125
+ backend: 'http',
126
+ supportsArrow: true,
127
+ supportsBinary: true,
128
+ supportsChunking: false,
129
+ supportsStreaming: false,
130
+ maxFrameBytes: Number.POSITIVE_INFINITY,
131
+ };
132
+ }
133
+
114
134
  // ===========================================================================
115
135
  // SEND
116
136
  // ===========================================================================
@@ -8,11 +8,10 @@
8
8
  * @see https://github.com/bbopen/tywrap/issues/149
9
9
  */
10
10
 
11
- import type { PythonRuntime, BridgeInfo } from '../types/index.js';
12
11
  import { autoRegisterArrowDecoder } from '../utils/codec.js';
13
12
 
14
- import { DisposableBase } from './bounded-context.js';
15
- import { RpcClient, type GetBridgeInfoOptions } from './rpc-client.js';
13
+ import { BasePythonBridge } from './base-bridge.js';
14
+ import { RpcClient } from './rpc-client.js';
16
15
  import { HttpTransport } from './http-transport.js';
17
16
  import type { CodecOptions } from './bridge-codec.js';
18
17
 
@@ -65,7 +64,7 @@ export interface HttpBridgeOptions {
65
64
  * await bridge.dispose();
66
65
  * ```
67
66
  */
68
- export class HttpBridge extends DisposableBase implements PythonRuntime {
67
+ export class HttpBridge extends BasePythonBridge {
69
68
  private readonly rpc: RpcClient;
70
69
 
71
70
  /**
@@ -110,56 +109,13 @@ export class HttpBridge extends DisposableBase implements PythonRuntime {
110
109
  }
111
110
 
112
111
  // ===========================================================================
113
- // RPC METHODS (delegate to the held RpcClient)
112
+ // RPC DELEGATION (the held RpcClient)
114
113
  // ===========================================================================
115
114
 
116
- async call<T = unknown>(
117
- module: string,
118
- functionName: string,
119
- args: unknown[],
120
- kwargs?: Record<string, unknown>
121
- ): Promise<T> {
122
- await this.ensureReady();
123
- return this.rpc.call<T>(module, functionName, args, kwargs);
124
- }
125
-
126
- async instantiate<T = unknown>(
127
- module: string,
128
- className: string,
129
- args: unknown[],
130
- kwargs?: Record<string, unknown>
131
- ): Promise<T> {
132
- await this.ensureReady();
133
- return this.rpc.instantiate<T>(module, className, args, kwargs);
134
- }
135
-
136
- async callMethod<T = unknown>(
137
- handle: string,
138
- methodName: string,
139
- args: unknown[],
140
- kwargs?: Record<string, unknown>
141
- ): Promise<T> {
142
- await this.ensureReady();
143
- return this.rpc.callMethod<T>(handle, methodName, args, kwargs);
144
- }
145
-
146
- async disposeInstance(handle: string): Promise<void> {
147
- await this.ensureReady();
148
- return this.rpc.disposeInstance(handle);
149
- }
150
-
151
- async getBridgeInfo(options?: GetBridgeInfoOptions): Promise<BridgeInfo> {
152
- await this.ensureReady();
153
- return this.rpc.getBridgeInfo(options);
154
- }
155
-
156
115
  /**
157
- * Ensure the facade is initialized before delegating an RPC, replicating the
158
- * auto-init that the bounded execute path provided pre-composition.
116
+ * Expose the held RpcClient to BasePythonBridge's shared delegating methods.
159
117
  */
160
- private async ensureReady(): Promise<void> {
161
- if (!this.isReady) {
162
- await this.init();
163
- }
118
+ protected getRpcClient(): RpcClient {
119
+ return this.rpc;
164
120
  }
165
121
  }
@@ -15,6 +15,7 @@ export { BridgeCodec, type CodecOptions } from './bridge-codec.js';
15
15
  // Transport contract — abstract I/O channel interface and guards
16
16
  export type {
17
17
  Transport,
18
+ TransportCapabilities,
18
19
  TransportOptions,
19
20
  ProtocolMessage,
20
21
  ProtocolResponse,
@@ -13,14 +13,13 @@ import { delimiter, isAbsolute, join, resolve } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { createRequire } from 'node:module';
15
15
 
16
- import type { PythonRuntime, BridgeInfo } from '../types/index.js';
17
16
  import { autoRegisterArrowDecoder } from '../utils/codec.js';
18
17
  import { getDefaultPythonPath } from '../utils/python.js';
19
18
  import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
20
19
  import { globalCache } from '../utils/cache.js';
21
20
 
22
- import { DisposableBase } from './bounded-context.js';
23
- import { RpcClient, type GetBridgeInfoOptions } from './rpc-client.js';
21
+ import { BasePythonBridge } from './base-bridge.js';
22
+ import { RpcClient } from './rpc-client.js';
24
23
  import { BridgeCodecError, BridgeExecutionError, BridgeProtocolError } from './errors.js';
25
24
  import { SubprocessTransport } from './subprocess-transport.js';
26
25
  import { PooledTransport } from './pooled-transport.js';
@@ -305,7 +304,7 @@ function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']):
305
304
  * await pooledBridge.init();
306
305
  * ```
307
306
  */
308
- export class NodeBridge extends DisposableBase implements PythonRuntime {
307
+ export class NodeBridge extends BasePythonBridge {
309
308
  private readonly resolvedOptions: ResolvedOptions;
310
309
  private readonly pooledTransport: PooledTransport;
311
310
  private readonly rpc: RpcClient;
@@ -433,16 +432,26 @@ export class NodeBridge extends DisposableBase implements PythonRuntime {
433
432
  }
434
433
 
435
434
  // ===========================================================================
436
- // RPC METHODS (delegate to the held RpcClient)
435
+ // RPC DELEGATION (the held RpcClient)
437
436
  // ===========================================================================
438
437
 
438
+ /**
439
+ * Expose the held RpcClient to BasePythonBridge's shared delegating methods
440
+ * (instantiate/callMethod/disposeInstance/getBridgeInfo). call() is
441
+ * overridden below to layer caching on top.
442
+ */
443
+ protected getRpcClient(): RpcClient {
444
+ return this.rpc;
445
+ }
446
+
439
447
  /**
440
448
  * Call a Python function, with optional result caching.
441
449
  *
442
- * Cache lookup stays FIRST so cache hits return without forcing init,
443
- * preserving the pre-composition behavior.
450
+ * Overrides BasePythonBridge.call() to layer the cache lookup/writeback on
451
+ * top of the shared delegation. Cache lookup stays FIRST so cache hits return
452
+ * without forcing init, preserving the pre-composition behavior.
444
453
  */
445
- async call<T = unknown>(
454
+ override async call<T = unknown>(
446
455
  module: string,
447
456
  functionName: string,
448
457
  args: unknown[],
@@ -479,50 +488,6 @@ export class NodeBridge extends DisposableBase implements PythonRuntime {
479
488
  return this.rpc.call<T>(module, functionName, args, kwargs);
480
489
  }
481
490
 
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
- }
524
- }
525
-
526
491
  // ===========================================================================
527
492
  // POOL STATISTICS
528
493
  // ===========================================================================
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { DisposableBase } from './bounded-context.js';
12
12
  import { BridgeDisposedError, BridgeExecutionError } from './errors.js';
13
- import type { Transport } from './transport.js';
13
+ import type { Transport, TransportCapabilities } from './transport.js';
14
14
  import { TransportPool, type TransportLease } from './transport-pool.js';
15
15
 
16
16
  // =============================================================================
@@ -21,7 +21,13 @@ import { TransportPool, type TransportLease } from './transport-pool.js';
21
21
  * Options for creating a PooledTransport.
22
22
  */
23
23
  export interface PooledTransportOptions {
24
- /** Factory function to create transports for each worker */
24
+ /**
25
+ * Factory function to create transports for each worker.
26
+ *
27
+ * Construction MUST be side-effect-free — spawn processes/open connections in
28
+ * `init()`/`send()`, never in the constructor. The pool may build a probe
29
+ * instance solely to read its {@link Transport.capabilities} descriptor.
30
+ */
25
31
  createTransport: () => Transport;
26
32
 
27
33
  /** Maximum number of workers in the pool. Default: 1 */
@@ -91,6 +97,8 @@ export class PooledTransport extends DisposableBase implements Transport {
91
97
  onReplacementWorkerReady?: (worker: TransportLease) => Promise<void>;
92
98
  };
93
99
  private pool?: TransportPool;
100
+ /** Memoized capability descriptor — built at most once (see {@link capabilities}). */
101
+ private cachedCapabilities?: TransportCapabilities;
94
102
 
95
103
  /**
96
104
  * Create a new PooledTransport.
@@ -181,6 +189,21 @@ export class PooledTransport extends DisposableBase implements Transport {
181
189
  });
182
190
  }
183
191
 
192
+ /**
193
+ * Static capability descriptor for the pool.
194
+ *
195
+ * A pool's wire behavior is exactly that of the workers it distributes across,
196
+ * so this reads the descriptor from one probe transport built by the same
197
+ * factory. `createTransport` MUST be construction-side-effect-free — the
198
+ * built-in transports spawn nothing until `init()`/`send()`. The result is
199
+ * memoized so at most one probe is ever built regardless of call count, and
200
+ * this stays safe to call at any lifecycle point.
201
+ */
202
+ capabilities(): TransportCapabilities {
203
+ this.cachedCapabilities ??= this.poolOptions.createTransport().capabilities();
204
+ return this.cachedCapabilities;
205
+ }
206
+
184
207
  // ===========================================================================
185
208
  // POOL STATISTICS
186
209
  // ===========================================================================