tywrap 0.2.0 → 0.3.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/README.md +65 -31
- package/dist/cli.js +49 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -7
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +47 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +114 -1
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +40 -18
- package/runtime/python_bridge.py +108 -15
- package/runtime/safe_codec.py +344 -0
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/index.ts +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BridgeProtocol - Unified abstraction for JS<->Python communication.
|
|
3
|
+
*
|
|
4
|
+
* Combines BoundedContext + SafeCodec + Transport into a single base class
|
|
5
|
+
* that handles all cross-boundary concerns:
|
|
6
|
+
* - Lifecycle management (init/dispose state machine)
|
|
7
|
+
* - Request/response encoding with validation
|
|
8
|
+
* - Transport-agnostic message passing
|
|
9
|
+
* - Bounded execution (timeout, retry, abort)
|
|
10
|
+
*
|
|
11
|
+
* Subclasses (NodeBridge, HttpBridge, PyodideBridge) only need to:
|
|
12
|
+
* 1. Create their transport in their constructor
|
|
13
|
+
* 2. Pass it to super() via BridgeProtocolOptions
|
|
14
|
+
* 3. Optionally override doInit() and doDispose() for additional setup/teardown
|
|
15
|
+
*
|
|
16
|
+
* @see https://github.com/bbopen/tywrap/issues/149
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { BridgeInfo } from '../types/index.js';
|
|
20
|
+
|
|
21
|
+
import { BoundedContext, type ExecuteOptions } from './bounded-context.js';
|
|
22
|
+
import { BridgeProtocolError } from './errors.js';
|
|
23
|
+
import { SafeCodec, type CodecOptions } from './safe-codec.js';
|
|
24
|
+
import { TYWRAP_PROTOCOL_VERSION } from './protocol.js';
|
|
25
|
+
import { PROTOCOL_ID, type Transport, type ProtocolMessage } from './transport.js';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// TYPES
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export interface GetBridgeInfoOptions {
|
|
32
|
+
/**
|
|
33
|
+
* If true, bypasses the cached info and queries the bridge again.
|
|
34
|
+
* This is useful when you want up-to-date instance counts or diagnostics.
|
|
35
|
+
*/
|
|
36
|
+
refresh?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Configuration options for BridgeProtocol.
|
|
41
|
+
*/
|
|
42
|
+
export interface BridgeProtocolOptions {
|
|
43
|
+
/** The transport to use for communication */
|
|
44
|
+
transport: Transport;
|
|
45
|
+
|
|
46
|
+
/** Codec options for validation/serialization */
|
|
47
|
+
codec?: CodecOptions;
|
|
48
|
+
|
|
49
|
+
/** Default timeout for operations in ms. Default: 30000 (30s) */
|
|
50
|
+
defaultTimeoutMs?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateBridgeInfoPayload(value: unknown): BridgeInfo {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
55
|
+
const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
56
|
+
throw new BridgeProtocolError(`Invalid bridge info payload: expected object, got ${kind}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface BridgeInfoWire {
|
|
60
|
+
protocol?: unknown;
|
|
61
|
+
protocolVersion?: unknown;
|
|
62
|
+
bridge?: unknown;
|
|
63
|
+
pythonVersion?: unknown;
|
|
64
|
+
pid?: unknown;
|
|
65
|
+
codecFallback?: unknown;
|
|
66
|
+
arrowAvailable?: unknown;
|
|
67
|
+
scipyAvailable?: unknown;
|
|
68
|
+
torchAvailable?: unknown;
|
|
69
|
+
sklearnAvailable?: unknown;
|
|
70
|
+
instances?: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const formatValue = (val: unknown): string => {
|
|
74
|
+
try {
|
|
75
|
+
const serialized = JSON.stringify(val);
|
|
76
|
+
return serialized ?? String(val);
|
|
77
|
+
} catch {
|
|
78
|
+
return String(val);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const obj = value as BridgeInfoWire;
|
|
83
|
+
|
|
84
|
+
const protocol = obj.protocol;
|
|
85
|
+
if (protocol !== PROTOCOL_ID) {
|
|
86
|
+
throw new BridgeProtocolError(
|
|
87
|
+
`Invalid bridge info payload: protocol expected "${PROTOCOL_ID}", got ${formatValue(protocol)}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const protocolVersion = obj.protocolVersion;
|
|
92
|
+
if (protocolVersion !== TYWRAP_PROTOCOL_VERSION) {
|
|
93
|
+
throw new BridgeProtocolError(
|
|
94
|
+
`Invalid bridge info payload: protocolVersion expected ${TYWRAP_PROTOCOL_VERSION}, got ${formatValue(protocolVersion)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const bridge = obj.bridge;
|
|
99
|
+
if (bridge !== 'python-subprocess') {
|
|
100
|
+
throw new BridgeProtocolError(
|
|
101
|
+
`Invalid bridge info payload: bridge expected "python-subprocess", got ${formatValue(bridge)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pythonVersion = obj.pythonVersion;
|
|
106
|
+
if (typeof pythonVersion !== 'string' || pythonVersion.length === 0) {
|
|
107
|
+
throw new BridgeProtocolError(
|
|
108
|
+
`Invalid bridge info payload: pythonVersion expected non-empty string, got ${formatValue(pythonVersion)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pid = obj.pid;
|
|
113
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {
|
|
114
|
+
throw new BridgeProtocolError(
|
|
115
|
+
`Invalid bridge info payload: pid expected positive integer, got ${formatValue(pid)}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const codecFallback = obj.codecFallback;
|
|
120
|
+
if (codecFallback !== 'json' && codecFallback !== 'none') {
|
|
121
|
+
throw new BridgeProtocolError(
|
|
122
|
+
`Invalid bridge info payload: codecFallback expected "json" or "none", got ${formatValue(codecFallback)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const arrowAvailable = obj.arrowAvailable;
|
|
127
|
+
if (typeof arrowAvailable !== 'boolean') {
|
|
128
|
+
throw new BridgeProtocolError(
|
|
129
|
+
`Invalid bridge info payload: arrowAvailable expected boolean, got ${formatValue(arrowAvailable)}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const scipyAvailable = obj.scipyAvailable;
|
|
134
|
+
if (typeof scipyAvailable !== 'boolean') {
|
|
135
|
+
throw new BridgeProtocolError(
|
|
136
|
+
`Invalid bridge info payload: scipyAvailable expected boolean, got ${formatValue(scipyAvailable)}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const torchAvailable = obj.torchAvailable;
|
|
141
|
+
if (typeof torchAvailable !== 'boolean') {
|
|
142
|
+
throw new BridgeProtocolError(
|
|
143
|
+
`Invalid bridge info payload: torchAvailable expected boolean, got ${formatValue(torchAvailable)}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sklearnAvailable = obj.sklearnAvailable;
|
|
148
|
+
if (typeof sklearnAvailable !== 'boolean') {
|
|
149
|
+
throw new BridgeProtocolError(
|
|
150
|
+
`Invalid bridge info payload: sklearnAvailable expected boolean, got ${formatValue(sklearnAvailable)}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const instances = obj.instances;
|
|
155
|
+
if (typeof instances !== 'number' || !Number.isInteger(instances) || instances < 0) {
|
|
156
|
+
throw new BridgeProtocolError(
|
|
157
|
+
`Invalid bridge info payload: instances expected non-negative integer, got ${formatValue(instances)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
protocol: PROTOCOL_ID,
|
|
163
|
+
protocolVersion: TYWRAP_PROTOCOL_VERSION,
|
|
164
|
+
bridge: 'python-subprocess',
|
|
165
|
+
pythonVersion,
|
|
166
|
+
pid,
|
|
167
|
+
codecFallback,
|
|
168
|
+
arrowAvailable,
|
|
169
|
+
scipyAvailable,
|
|
170
|
+
torchAvailable,
|
|
171
|
+
sklearnAvailable,
|
|
172
|
+
instances,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// BRIDGE PROTOCOL BASE CLASS
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* BridgeProtocol combines BoundedContext + SafeCodec + Transport
|
|
182
|
+
* into a unified abstraction for all JS<->Python communication.
|
|
183
|
+
*
|
|
184
|
+
* This class provides:
|
|
185
|
+
* - Automatic transport lifecycle management
|
|
186
|
+
* - Request encoding with guardrails (special float rejection, key validation)
|
|
187
|
+
* - Response decoding with Arrow support
|
|
188
|
+
* - Full RuntimeExecution interface implementation
|
|
189
|
+
*
|
|
190
|
+
* Subclasses should:
|
|
191
|
+
* 1. Create their transport in their constructor
|
|
192
|
+
* 2. Pass it to super() via BridgeProtocolOptions
|
|
193
|
+
* 3. Optionally override doInit() and doDispose() for additional setup/teardown
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* class NodeBridge extends BridgeProtocol {
|
|
198
|
+
* constructor(options: NodeBridgeOptions) {
|
|
199
|
+
* const transport = new ProcessIO(options);
|
|
200
|
+
* super({ transport, defaultTimeoutMs: options.timeout });
|
|
201
|
+
* }
|
|
202
|
+
* }
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export class BridgeProtocol extends BoundedContext {
|
|
206
|
+
/** Codec instance for validation and serialization */
|
|
207
|
+
protected readonly codec: SafeCodec;
|
|
208
|
+
|
|
209
|
+
/** Transport instance for message passing */
|
|
210
|
+
protected readonly transport: Transport;
|
|
211
|
+
|
|
212
|
+
/** Default timeout for operations in milliseconds */
|
|
213
|
+
protected readonly defaultTimeoutMs: number;
|
|
214
|
+
|
|
215
|
+
/** Counter for generating unique request IDs */
|
|
216
|
+
private requestId = 0;
|
|
217
|
+
|
|
218
|
+
/** Cached bridge diagnostics info (populated by getBridgeInfo). */
|
|
219
|
+
private bridgeInfoCache?: BridgeInfo;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a new BridgeProtocol instance.
|
|
223
|
+
*
|
|
224
|
+
* @param options - Configuration options including transport and codec settings
|
|
225
|
+
*/
|
|
226
|
+
constructor(options: BridgeProtocolOptions) {
|
|
227
|
+
super();
|
|
228
|
+
this.codec = new SafeCodec(options.codec);
|
|
229
|
+
this.transport = options.transport;
|
|
230
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 30000;
|
|
231
|
+
|
|
232
|
+
// Track the transport for automatic cleanup during dispose
|
|
233
|
+
this.trackResource(this.transport);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ===========================================================================
|
|
237
|
+
// LIFECYCLE
|
|
238
|
+
// ===========================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Initialize the protocol.
|
|
242
|
+
*
|
|
243
|
+
* Initializes the underlying transport. Subclasses can override this
|
|
244
|
+
* to add additional initialization logic, but must call super.doInit().
|
|
245
|
+
*/
|
|
246
|
+
protected async doInit(): Promise<void> {
|
|
247
|
+
await this.transport.init();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Dispose the protocol.
|
|
252
|
+
*
|
|
253
|
+
* The transport is tracked as a resource and will be disposed automatically
|
|
254
|
+
* by BoundedContext. Subclasses can override this to add additional cleanup,
|
|
255
|
+
* but should not need to dispose the transport manually.
|
|
256
|
+
*/
|
|
257
|
+
protected async doDispose(): Promise<void> {
|
|
258
|
+
this.bridgeInfoCache = undefined;
|
|
259
|
+
// Transport is tracked and will be disposed by BoundedContext
|
|
260
|
+
// Subclasses can override to add additional cleanup
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ===========================================================================
|
|
264
|
+
// MESSAGE SENDING
|
|
265
|
+
// ===========================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Send a protocol message and receive a typed response.
|
|
269
|
+
*
|
|
270
|
+
* This method:
|
|
271
|
+
* 1. Generates a unique request ID
|
|
272
|
+
* 2. Encodes the request with validation (special float rejection, etc.)
|
|
273
|
+
* 3. Sends via transport with timeout/abort support
|
|
274
|
+
* 4. Decodes and validates the response
|
|
275
|
+
*
|
|
276
|
+
* @param message - The protocol message (without id field)
|
|
277
|
+
* @param options - Execution options (timeout, retries, validation)
|
|
278
|
+
* @returns The typed response from Python
|
|
279
|
+
*
|
|
280
|
+
* @throws BridgeProtocolError if encoding/decoding fails
|
|
281
|
+
* @throws BridgeExecutionError if Python returns an error
|
|
282
|
+
* @throws BridgeTimeoutError if the operation times out
|
|
283
|
+
*/
|
|
284
|
+
protected async sendMessage<T>(
|
|
285
|
+
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
286
|
+
options?: ExecuteOptions<T>
|
|
287
|
+
): Promise<T> {
|
|
288
|
+
const fullMessage: ProtocolMessage = {
|
|
289
|
+
...message,
|
|
290
|
+
id: this.generateId(),
|
|
291
|
+
protocol: PROTOCOL_ID,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return this.execute(async () => {
|
|
295
|
+
// 1. Encode request (validates args)
|
|
296
|
+
const encoded = this.codec.encodeRequest(fullMessage);
|
|
297
|
+
|
|
298
|
+
// 2. Send via transport
|
|
299
|
+
const responseStr = await this.transport.send(
|
|
300
|
+
encoded,
|
|
301
|
+
options?.timeoutMs ?? this.defaultTimeoutMs,
|
|
302
|
+
options?.signal
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// 3. Decode response (validates result)
|
|
306
|
+
return this.codec.decodeResponse<T>(responseStr);
|
|
307
|
+
}, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Async version that uses decodeResponseAsync for Arrow support.
|
|
312
|
+
*
|
|
313
|
+
* Use this method when the response may contain encoded DataFrames,
|
|
314
|
+
* ndarrays, or other Arrow-encoded data structures.
|
|
315
|
+
*
|
|
316
|
+
* @param message - The protocol message (without id field)
|
|
317
|
+
* @param options - Execution options (timeout, retries, validation)
|
|
318
|
+
* @returns The typed response from Python with Arrow decoding applied
|
|
319
|
+
*
|
|
320
|
+
* @throws BridgeProtocolError if encoding/decoding fails
|
|
321
|
+
* @throws BridgeExecutionError if Python returns an error
|
|
322
|
+
* @throws BridgeTimeoutError if the operation times out
|
|
323
|
+
*/
|
|
324
|
+
protected async sendMessageAsync<T>(
|
|
325
|
+
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
326
|
+
options?: ExecuteOptions<T>
|
|
327
|
+
): Promise<T> {
|
|
328
|
+
const fullMessage: ProtocolMessage = {
|
|
329
|
+
...message,
|
|
330
|
+
id: this.generateId(),
|
|
331
|
+
protocol: PROTOCOL_ID,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return this.execute(async () => {
|
|
335
|
+
// 1. Encode request (validates args)
|
|
336
|
+
const encoded = this.codec.encodeRequest(fullMessage);
|
|
337
|
+
|
|
338
|
+
// 2. Send via transport
|
|
339
|
+
const responseStr = await this.transport.send(
|
|
340
|
+
encoded,
|
|
341
|
+
options?.timeoutMs ?? this.defaultTimeoutMs,
|
|
342
|
+
options?.signal
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// 3. Decode response with Arrow support
|
|
346
|
+
return this.codec.decodeResponseAsync<T>(responseStr);
|
|
347
|
+
}, options);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Generate a unique request ID.
|
|
352
|
+
*
|
|
353
|
+
* Returns a monotonically increasing integer that ensures uniqueness
|
|
354
|
+
* within a process lifetime.
|
|
355
|
+
*/
|
|
356
|
+
private generateId(): number {
|
|
357
|
+
return ++this.requestId;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ===========================================================================
|
|
361
|
+
// RUNTIME EXECUTION INTERFACE
|
|
362
|
+
// ===========================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Call a Python function.
|
|
366
|
+
*
|
|
367
|
+
* @param module - Python module path (e.g., 'numpy', 'mypackage.submodule')
|
|
368
|
+
* @param functionName - Function name to call
|
|
369
|
+
* @param args - Positional arguments
|
|
370
|
+
* @param kwargs - Keyword arguments
|
|
371
|
+
* @returns The function result
|
|
372
|
+
*/
|
|
373
|
+
async call<T = unknown>(
|
|
374
|
+
module: string,
|
|
375
|
+
functionName: string,
|
|
376
|
+
args: unknown[],
|
|
377
|
+
kwargs?: Record<string, unknown>
|
|
378
|
+
): Promise<T> {
|
|
379
|
+
return this.sendMessageAsync<T>({
|
|
380
|
+
method: 'call',
|
|
381
|
+
params: {
|
|
382
|
+
module,
|
|
383
|
+
functionName,
|
|
384
|
+
args,
|
|
385
|
+
kwargs,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Instantiate a Python class.
|
|
392
|
+
*
|
|
393
|
+
* @param module - Python module path containing the class
|
|
394
|
+
* @param className - Class name to instantiate
|
|
395
|
+
* @param args - Positional constructor arguments
|
|
396
|
+
* @param kwargs - Keyword constructor arguments
|
|
397
|
+
* @returns A handle to the created instance
|
|
398
|
+
*/
|
|
399
|
+
async instantiate<T = unknown>(
|
|
400
|
+
module: string,
|
|
401
|
+
className: string,
|
|
402
|
+
args: unknown[],
|
|
403
|
+
kwargs?: Record<string, unknown>
|
|
404
|
+
): Promise<T> {
|
|
405
|
+
return this.sendMessageAsync<T>({
|
|
406
|
+
method: 'instantiate',
|
|
407
|
+
params: {
|
|
408
|
+
module,
|
|
409
|
+
className,
|
|
410
|
+
args,
|
|
411
|
+
kwargs,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Call a method on a Python instance.
|
|
418
|
+
*
|
|
419
|
+
* @param handle - Instance handle returned from instantiate()
|
|
420
|
+
* @param methodName - Method name to call
|
|
421
|
+
* @param args - Positional arguments
|
|
422
|
+
* @param kwargs - Keyword arguments
|
|
423
|
+
* @returns The method result
|
|
424
|
+
*/
|
|
425
|
+
async callMethod<T = unknown>(
|
|
426
|
+
handle: string,
|
|
427
|
+
methodName: string,
|
|
428
|
+
args: unknown[],
|
|
429
|
+
kwargs?: Record<string, unknown>
|
|
430
|
+
): Promise<T> {
|
|
431
|
+
return this.sendMessageAsync<T>({
|
|
432
|
+
method: 'call_method',
|
|
433
|
+
params: {
|
|
434
|
+
handle,
|
|
435
|
+
methodName,
|
|
436
|
+
args,
|
|
437
|
+
kwargs,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Dispose a Python instance.
|
|
444
|
+
*
|
|
445
|
+
* Releases the instance handle on the Python side, allowing
|
|
446
|
+
* the object to be garbage collected.
|
|
447
|
+
*
|
|
448
|
+
* @param handle - Instance handle to dispose
|
|
449
|
+
*/
|
|
450
|
+
async disposeInstance(handle: string): Promise<void> {
|
|
451
|
+
await this.sendMessageAsync<void>({
|
|
452
|
+
method: 'dispose_instance',
|
|
453
|
+
params: {
|
|
454
|
+
handle,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Fetch bridge diagnostics and feature availability.
|
|
461
|
+
*
|
|
462
|
+
* The Python bridge supports a `meta` method that returns protocol and environment info
|
|
463
|
+
* (including optional codec availability and current instance count).
|
|
464
|
+
*/
|
|
465
|
+
async getBridgeInfo(options: GetBridgeInfoOptions = {}): Promise<BridgeInfo> {
|
|
466
|
+
if (!options.refresh && this.bridgeInfoCache) {
|
|
467
|
+
return this.bridgeInfoCache;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const info = await this.sendMessage<BridgeInfo>(
|
|
471
|
+
{
|
|
472
|
+
method: 'meta',
|
|
473
|
+
params: {},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
validate: validateBridgeInfoPayload,
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
this.bridgeInfoCache = info;
|
|
481
|
+
return info;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disposable interface for resources that need cleanup.
|
|
3
|
+
*
|
|
4
|
+
* This interface enables the BoundedContext resource tracking system
|
|
5
|
+
* to automatically dispose of owned resources when the context is disposed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for resources that require explicit cleanup.
|
|
10
|
+
*/
|
|
11
|
+
export interface Disposable {
|
|
12
|
+
/**
|
|
13
|
+
* Release resources held by this object.
|
|
14
|
+
* This method should be idempotent (safe to call multiple times).
|
|
15
|
+
*/
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type guard to check if a value implements the Disposable interface.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The value to check
|
|
23
|
+
* @returns True if the value has a dispose method
|
|
24
|
+
*/
|
|
25
|
+
export function isDisposable(value: unknown): value is Disposable {
|
|
26
|
+
return (
|
|
27
|
+
value !== null &&
|
|
28
|
+
typeof value === 'object' &&
|
|
29
|
+
typeof (value as Disposable).dispose === 'function'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Safely dispose a value if it implements Disposable.
|
|
35
|
+
* Does nothing if the value is not disposable.
|
|
36
|
+
*
|
|
37
|
+
* @param value - The value to dispose
|
|
38
|
+
* @returns Promise that resolves when disposal is complete
|
|
39
|
+
*/
|
|
40
|
+
export async function safeDispose(value: unknown): Promise<void> {
|
|
41
|
+
if (isDisposable(value)) {
|
|
42
|
+
await value.dispose();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Dispose multiple resources, collecting any errors.
|
|
48
|
+
* All resources will be attempted even if some fail.
|
|
49
|
+
*
|
|
50
|
+
* @param resources - Iterable of Disposable resources
|
|
51
|
+
* @returns Array of errors that occurred during disposal (empty if all succeeded)
|
|
52
|
+
*/
|
|
53
|
+
export async function disposeAll(resources: Iterable<Disposable>): Promise<Error[]> {
|
|
54
|
+
const errors: Error[] = [];
|
|
55
|
+
|
|
56
|
+
for (const resource of resources) {
|
|
57
|
+
try {
|
|
58
|
+
await resource.dispose();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
errors.push(e instanceof Error ? e : new Error(String(e)));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
package/src/runtime/errors.ts
CHANGED
|
@@ -14,6 +14,20 @@ export class BridgeProtocolError extends BridgeError {}
|
|
|
14
14
|
export class BridgeTimeoutError extends BridgeError {}
|
|
15
15
|
export class BridgeDisposedError extends BridgeError {}
|
|
16
16
|
|
|
17
|
+
export class BridgeCodecError extends BridgeError {
|
|
18
|
+
codecPhase?: string;
|
|
19
|
+
valueType?: string;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
options?: { code?: string; cause?: unknown; codecPhase?: string; valueType?: string }
|
|
24
|
+
) {
|
|
25
|
+
super(message, { code: options?.code, cause: options?.cause });
|
|
26
|
+
this.codecPhase = options?.codecPhase;
|
|
27
|
+
this.valueType = options?.valueType;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export class BridgeExecutionError extends BridgeError {
|
|
18
32
|
traceback?: string;
|
|
19
33
|
}
|