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.
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +19 -14
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts +17 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +59 -49
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +40 -9
- package/dist/core/generator.js.map +1 -1
- package/dist/core/validation.d.ts +23 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +52 -48
- package/dist/core/validation.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +175 -133
- package/dist/dev.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +57 -0
- package/dist/runtime/base-bridge.d.ts.map +1 -0
- package/dist/runtime/base-bridge.js +72 -0
- package/dist/runtime/base-bridge.js.map +1 -0
- package/dist/runtime/bridge-codec.d.ts +15 -0
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +45 -48
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/http-transport.d.ts +11 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +19 -0
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/http.d.ts +5 -12
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -29
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +12 -19
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +14 -34
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +21 -2
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +16 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +12 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +20 -0
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/pyodide.d.ts +5 -12
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +6 -29
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +32 -1
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +45 -24
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +26 -1
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +72 -35
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/transport.d.ts +59 -0
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -0
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +0 -5
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +212 -169
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +50 -58
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts +2 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +53 -2
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js +28 -17
- package/dist/utils/python.js.map +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +22 -16
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/tywrap_bridge_core.py +55 -3
- package/src/core/annotation-parser.ts +23 -18
- package/src/core/discovery.ts +70 -54
- package/src/core/generator.ts +50 -11
- package/src/core/validation.ts +84 -48
- package/src/dev.ts +237 -153
- package/src/runtime/base-bridge.ts +106 -0
- package/src/runtime/bridge-codec.ts +58 -70
- package/src/runtime/http-transport.ts +21 -1
- package/src/runtime/http.ts +7 -51
- package/src/runtime/index.ts +1 -0
- package/src/runtime/node.ts +17 -52
- package/src/runtime/pooled-transport.ts +25 -2
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +22 -0
- package/src/runtime/pyodide.ts +7 -52
- package/src/runtime/rpc-client.ts +56 -32
- package/src/runtime/subprocess-transport.ts +89 -40
- package/src/runtime/transport.ts +71 -0
- package/src/types/index.ts +37 -0
- package/src/tywrap.ts +274 -187
- package/src/utils/cache.ts +59 -61
- package/src/utils/codec.ts +61 -4
- package/src/utils/python.ts +33 -20
- package/src/utils/runtime.ts +24 -13
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ===========================================================================
|
package/src/runtime/http.ts
CHANGED
|
@@ -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 {
|
|
15
|
-
import { RpcClient
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
await this.init();
|
|
163
|
-
}
|
|
118
|
+
protected getRpcClient(): RpcClient {
|
|
119
|
+
return this.rpc;
|
|
164
120
|
}
|
|
165
121
|
}
|
package/src/runtime/index.ts
CHANGED
package/src/runtime/node.ts
CHANGED
|
@@ -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 {
|
|
23
|
-
import { RpcClient
|
|
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
|
|
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
|
|
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
|
-
*
|
|
443
|
-
*
|
|
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
|
-
/**
|
|
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
|
// ===========================================================================
|