tywrap 0.1.2 → 0.2.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.
- package/README.md +11 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- 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-protocol.d.ts +167 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +247 -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/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 +47 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +55 -74
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +142 -28
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +321 -168
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +19 -125
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +19 -550
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +131 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +175 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +204 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +695 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +155 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +397 -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 +81 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +345 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +186 -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 +131 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +219 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +196 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +371 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +120 -1
- package/dist/utils/codec.js.map +1 -1
- package/package.json +2 -2
- package/runtime/python_bridge.py +30 -3
- package/runtime/safe_codec.py +344 -0
- package/src/index.ts +48 -5
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-protocol.ts +319 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/http-io.ts +244 -0
- package/src/runtime/http.ts +71 -117
- package/src/runtime/node.ts +460 -217
- package/src/runtime/optimized-node.ts +19 -761
- package/src/runtime/pooled-transport.ts +252 -0
- package/src/runtime/process-io.ts +902 -0
- package/src/runtime/pyodide-io.ts +485 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +443 -0
- package/src/runtime/transport.ts +273 -0
- package/src/runtime/validators.ts +241 -0
- package/src/runtime/worker-pool.ts +498 -0
- package/src/utils/codec.ts +126 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PyodideIO Transport - In-memory Pyodide communication for browser environments.
|
|
3
|
+
*
|
|
4
|
+
* This transport implements the Transport interface for direct in-memory
|
|
5
|
+
* communication with Pyodide (Python compiled to WebAssembly). It is designed
|
|
6
|
+
* for browser environments where Python code runs within the same process.
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Zero network overhead (in-memory calls)
|
|
10
|
+
* - Automatic Pyodide loading from CDN or module
|
|
11
|
+
* - Python package loading support
|
|
12
|
+
* - Proper proxy cleanup to prevent memory leaks
|
|
13
|
+
*
|
|
14
|
+
* @see https://github.com/bbopen/tywrap/issues/149
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { BoundedContext } from './bounded-context.js';
|
|
18
|
+
import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
19
|
+
import { PROTOCOL_ID, type Transport, type ProtocolMessage, type ProtocolResponse } from './transport.js';
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// TYPES
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for configuring the PyodideIO transport.
|
|
27
|
+
*/
|
|
28
|
+
export interface PyodideIOOptions {
|
|
29
|
+
/** URL for Pyodide CDN. Default: official CDN */
|
|
30
|
+
indexURL?: string;
|
|
31
|
+
/** Python packages to load during initialization */
|
|
32
|
+
packages?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pyodide loader function type.
|
|
37
|
+
* Avoids direct dependency on pyodide types.
|
|
38
|
+
*/
|
|
39
|
+
type LoadPyodide = (options: { indexURL: string }) => Promise<PyodideInstance>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimal Pyodide instance interface.
|
|
43
|
+
* Avoids direct dependency on pyodide types.
|
|
44
|
+
*/
|
|
45
|
+
interface PyodideInstance {
|
|
46
|
+
runPython: (code: string) => unknown;
|
|
47
|
+
runPythonAsync: (code: string) => Promise<unknown>;
|
|
48
|
+
globals: { get: (key: string) => unknown; set: (k: string, v: unknown) => void };
|
|
49
|
+
toPy: (obj: unknown) => unknown;
|
|
50
|
+
loadPackage: (name: string | string[]) => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// CONSTANTS
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/** Default Pyodide CDN URL */
|
|
58
|
+
const DEFAULT_INDEX_URL = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bootstrap Python code that sets up the dispatch function.
|
|
62
|
+
*
|
|
63
|
+
* This code is injected into Pyodide during initialization and provides
|
|
64
|
+
* a single entry point for all protocol messages. The dispatch function
|
|
65
|
+
* parses JSON messages, routes to appropriate handlers, and returns
|
|
66
|
+
* JSON responses.
|
|
67
|
+
*/
|
|
68
|
+
const BOOTSTRAP_PYTHON = `
|
|
69
|
+
import json
|
|
70
|
+
import importlib
|
|
71
|
+
|
|
72
|
+
__tywrap_instances = {}
|
|
73
|
+
|
|
74
|
+
def __tywrap_dispatch(message_json):
|
|
75
|
+
"""
|
|
76
|
+
Dispatch a protocol message and return a JSON response.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
message_json: JSON-encoded ProtocolMessage string
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
JSON-encoded ProtocolResponse string
|
|
83
|
+
"""
|
|
84
|
+
msg = None
|
|
85
|
+
try:
|
|
86
|
+
msg = json.loads(message_json)
|
|
87
|
+
msg_id = msg['id']
|
|
88
|
+
msg_type = msg['type']
|
|
89
|
+
|
|
90
|
+
if msg_type == 'call':
|
|
91
|
+
mod = importlib.import_module(msg['module'])
|
|
92
|
+
fn = getattr(mod, msg['functionName'])
|
|
93
|
+
result = fn(*msg.get('args', []), **msg.get('kwargs', {}))
|
|
94
|
+
return json.dumps({'id': msg_id, 'result': result})
|
|
95
|
+
|
|
96
|
+
elif msg_type == 'instantiate':
|
|
97
|
+
mod = importlib.import_module(msg['module'])
|
|
98
|
+
cls = getattr(mod, msg['className'])
|
|
99
|
+
obj = cls(*msg.get('args', []), **msg.get('kwargs', {}))
|
|
100
|
+
handle = str(id(obj))
|
|
101
|
+
__tywrap_instances[handle] = obj
|
|
102
|
+
return json.dumps({'id': msg_id, 'result': handle})
|
|
103
|
+
|
|
104
|
+
elif msg_type == 'call_method':
|
|
105
|
+
obj = __tywrap_instances[msg['handle']]
|
|
106
|
+
method = getattr(obj, msg['methodName'])
|
|
107
|
+
result = method(*msg.get('args', []), **msg.get('kwargs', {}))
|
|
108
|
+
return json.dumps({'id': msg_id, 'result': result})
|
|
109
|
+
|
|
110
|
+
elif msg_type == 'dispose_instance':
|
|
111
|
+
__tywrap_instances.pop(msg['handle'], None)
|
|
112
|
+
return json.dumps({'id': msg_id, 'result': None})
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
return json.dumps({
|
|
116
|
+
'id': msg_id,
|
|
117
|
+
'error': {
|
|
118
|
+
'type': 'ValueError',
|
|
119
|
+
'message': f'Unknown message type: {msg_type}'
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
import traceback
|
|
125
|
+
return json.dumps({
|
|
126
|
+
'id': msg.get('id', 'unknown') if msg else 'unknown',
|
|
127
|
+
'error': {
|
|
128
|
+
'type': type(e).__name__,
|
|
129
|
+
'message': str(e),
|
|
130
|
+
'traceback': traceback.format_exc()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// PYODIDE IO TRANSPORT
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Transport implementation for in-memory Pyodide communication.
|
|
141
|
+
*
|
|
142
|
+
* This transport extends BoundedContext for lifecycle management and
|
|
143
|
+
* implements the Transport interface for message-based communication.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const transport = new PyodideIO({ packages: ['numpy'] });
|
|
148
|
+
* await transport.init();
|
|
149
|
+
*
|
|
150
|
+
* const response = await transport.send(
|
|
151
|
+
* JSON.stringify({
|
|
152
|
+
* id: '1',
|
|
153
|
+
* type: 'call',
|
|
154
|
+
* module: 'math',
|
|
155
|
+
* functionName: 'sqrt',
|
|
156
|
+
* args: [16]
|
|
157
|
+
* }),
|
|
158
|
+
* 5000
|
|
159
|
+
* );
|
|
160
|
+
*
|
|
161
|
+
* await transport.dispose();
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export class PyodideIO extends BoundedContext implements Transport {
|
|
165
|
+
private readonly indexURL: string;
|
|
166
|
+
private readonly packages: readonly string[];
|
|
167
|
+
private requestId = 0;
|
|
168
|
+
private py?: PyodideInstance;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a new PyodideIO transport.
|
|
172
|
+
*
|
|
173
|
+
* @param options - Configuration options
|
|
174
|
+
*/
|
|
175
|
+
constructor(options: PyodideIOOptions = {}) {
|
|
176
|
+
super();
|
|
177
|
+
this.indexURL = options.indexURL ?? DEFAULT_INDEX_URL;
|
|
178
|
+
this.packages = Object.freeze([...(options.packages ?? [])]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ===========================================================================
|
|
182
|
+
// LIFECYCLE (BoundedContext implementation)
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Initialize the Pyodide runtime.
|
|
187
|
+
*
|
|
188
|
+
* This method:
|
|
189
|
+
* 1. Resolves the loadPyodide function (global or dynamic import)
|
|
190
|
+
* 2. Loads Pyodide with the configured indexURL
|
|
191
|
+
* 3. Loads any requested Python packages
|
|
192
|
+
* 4. Bootstraps the dispatch helper function
|
|
193
|
+
*
|
|
194
|
+
* @throws BridgeProtocolError if Pyodide is not available
|
|
195
|
+
*/
|
|
196
|
+
protected async doInit(): Promise<void> {
|
|
197
|
+
const loadPyodideFn = await this.resolveLoadPyodide();
|
|
198
|
+
if (!loadPyodideFn) {
|
|
199
|
+
throw new BridgeProtocolError(
|
|
200
|
+
'Pyodide is not available in this environment. ' +
|
|
201
|
+
'Include the Pyodide script tag or install the pyodide package.'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.py = await loadPyodideFn({ indexURL: this.indexURL });
|
|
206
|
+
|
|
207
|
+
if (this.packages.length > 0) {
|
|
208
|
+
await this.py.loadPackage([...this.packages]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await this.bootstrapDispatcher();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Clean up Pyodide resources.
|
|
216
|
+
*
|
|
217
|
+
* Note: Pyodide does not have an explicit dispose mechanism.
|
|
218
|
+
* We clear our reference and rely on garbage collection.
|
|
219
|
+
*/
|
|
220
|
+
protected async doDispose(): Promise<void> {
|
|
221
|
+
this.py = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
// TRANSPORT INTERFACE
|
|
226
|
+
// ===========================================================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Send a protocol message to Pyodide and wait for the response.
|
|
230
|
+
*
|
|
231
|
+
* This method:
|
|
232
|
+
* 1. Validates the transport is ready
|
|
233
|
+
* 2. Calls the Python dispatch function with the JSON message
|
|
234
|
+
* 3. Returns the JSON response string
|
|
235
|
+
*
|
|
236
|
+
* The timeout and abort signal are respected, though in-memory calls
|
|
237
|
+
* are typically fast enough that timeouts rarely trigger.
|
|
238
|
+
*
|
|
239
|
+
* @param message - JSON-encoded ProtocolMessage
|
|
240
|
+
* @param timeoutMs - Timeout in milliseconds (0 = no timeout)
|
|
241
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
242
|
+
* @returns JSON-encoded ProtocolResponse
|
|
243
|
+
*
|
|
244
|
+
* @throws BridgeDisposedError if transport is disposed
|
|
245
|
+
* @throws BridgeProtocolError if message is invalid or Pyodide not ready
|
|
246
|
+
* @throws BridgeTimeoutError if operation times out or is aborted
|
|
247
|
+
* @throws BridgeExecutionError for Python execution errors
|
|
248
|
+
*/
|
|
249
|
+
async send(message: string, timeoutMs: number, signal?: AbortSignal): Promise<string> {
|
|
250
|
+
return this.execute(
|
|
251
|
+
async () => {
|
|
252
|
+
if (!this.py) {
|
|
253
|
+
throw new BridgeProtocolError('Pyodide not initialized');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate message is parseable JSON
|
|
257
|
+
let parsed: ProtocolMessage;
|
|
258
|
+
try {
|
|
259
|
+
parsed = JSON.parse(message) as ProtocolMessage;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
throw new BridgeProtocolError(`Invalid JSON message: ${(e as Error).message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Validate required fields
|
|
265
|
+
if (!parsed.id || !parsed.method) {
|
|
266
|
+
throw new BridgeProtocolError('Message missing required fields: id, method');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get the dispatch function
|
|
270
|
+
const dispatchFn = this.py.globals.get('__tywrap_dispatch');
|
|
271
|
+
if (!dispatchFn) {
|
|
272
|
+
throw new BridgeProtocolError('Pyodide dispatch function not initialized');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Call the dispatch function
|
|
277
|
+
const invoke = dispatchFn as (messageJson: string) => string;
|
|
278
|
+
const responseJson = invoke(message);
|
|
279
|
+
|
|
280
|
+
// Validate response is valid JSON
|
|
281
|
+
try {
|
|
282
|
+
const response = JSON.parse(responseJson) as ProtocolResponse;
|
|
283
|
+
if (response.error) {
|
|
284
|
+
// Return the response as-is; let the caller handle the error
|
|
285
|
+
return responseJson;
|
|
286
|
+
}
|
|
287
|
+
return responseJson;
|
|
288
|
+
} catch {
|
|
289
|
+
throw new BridgeProtocolError(`Invalid JSON response from Python: ${responseJson}`);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
// Clean up the proxy
|
|
293
|
+
this.destroyPyProxy(dispatchFn);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{ timeoutMs, signal }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ===========================================================================
|
|
301
|
+
// RUNTIME EXECUTION (BoundedContext abstract methods)
|
|
302
|
+
// ===========================================================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Call a Python function.
|
|
306
|
+
*
|
|
307
|
+
* Convenience method that constructs a 'call' message and sends it.
|
|
308
|
+
*/
|
|
309
|
+
async call<T = unknown>(
|
|
310
|
+
module: string,
|
|
311
|
+
functionName: string,
|
|
312
|
+
args: unknown[],
|
|
313
|
+
kwargs?: Record<string, unknown>
|
|
314
|
+
): Promise<T> {
|
|
315
|
+
const message: ProtocolMessage = {
|
|
316
|
+
id: this.generateId(),
|
|
317
|
+
protocol: PROTOCOL_ID,
|
|
318
|
+
method: 'call',
|
|
319
|
+
params: {
|
|
320
|
+
module,
|
|
321
|
+
functionName,
|
|
322
|
+
args: args ?? [],
|
|
323
|
+
kwargs,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const responseJson = await this.send(JSON.stringify(message), 30000);
|
|
328
|
+
return this.parseResponse<T>(responseJson);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Instantiate a Python class.
|
|
333
|
+
*
|
|
334
|
+
* Convenience method that constructs an 'instantiate' message and sends it.
|
|
335
|
+
*/
|
|
336
|
+
async instantiate<T = unknown>(
|
|
337
|
+
module: string,
|
|
338
|
+
className: string,
|
|
339
|
+
args: unknown[],
|
|
340
|
+
kwargs?: Record<string, unknown>
|
|
341
|
+
): Promise<T> {
|
|
342
|
+
const message: ProtocolMessage = {
|
|
343
|
+
id: this.generateId(),
|
|
344
|
+
protocol: PROTOCOL_ID,
|
|
345
|
+
method: 'instantiate',
|
|
346
|
+
params: {
|
|
347
|
+
module,
|
|
348
|
+
className,
|
|
349
|
+
args: args ?? [],
|
|
350
|
+
kwargs,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const responseJson = await this.send(JSON.stringify(message), 30000);
|
|
355
|
+
return this.parseResponse<T>(responseJson);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Call a method on a Python instance.
|
|
360
|
+
*
|
|
361
|
+
* Convenience method that constructs a 'call_method' message and sends it.
|
|
362
|
+
*/
|
|
363
|
+
async callMethod<T = unknown>(
|
|
364
|
+
handle: string,
|
|
365
|
+
methodName: string,
|
|
366
|
+
args: unknown[],
|
|
367
|
+
kwargs?: Record<string, unknown>
|
|
368
|
+
): Promise<T> {
|
|
369
|
+
const message: ProtocolMessage = {
|
|
370
|
+
id: this.generateId(),
|
|
371
|
+
protocol: PROTOCOL_ID,
|
|
372
|
+
method: 'call_method',
|
|
373
|
+
params: {
|
|
374
|
+
handle,
|
|
375
|
+
methodName,
|
|
376
|
+
args: args ?? [],
|
|
377
|
+
kwargs,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const responseJson = await this.send(JSON.stringify(message), 30000);
|
|
382
|
+
return this.parseResponse<T>(responseJson);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Dispose a Python instance.
|
|
387
|
+
*
|
|
388
|
+
* Convenience method that constructs a 'dispose_instance' message and sends it.
|
|
389
|
+
*/
|
|
390
|
+
async disposeInstance(handle: string): Promise<void> {
|
|
391
|
+
const message: ProtocolMessage = {
|
|
392
|
+
id: this.generateId(),
|
|
393
|
+
protocol: PROTOCOL_ID,
|
|
394
|
+
method: 'dispose_instance',
|
|
395
|
+
params: {
|
|
396
|
+
handle,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const responseJson = await this.send(JSON.stringify(message), 30000);
|
|
401
|
+
this.parseResponse<void>(responseJson);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ===========================================================================
|
|
405
|
+
// PRIVATE HELPERS
|
|
406
|
+
// ===========================================================================
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Resolve the loadPyodide function.
|
|
410
|
+
*
|
|
411
|
+
* First checks for a global loadPyodide (browser script tag include),
|
|
412
|
+
* then falls back to dynamic import of the 'pyodide' module.
|
|
413
|
+
*/
|
|
414
|
+
private async resolveLoadPyodide(): Promise<LoadPyodide | undefined> {
|
|
415
|
+
// Check for global loadPyodide (browser script tag)
|
|
416
|
+
const g = globalThis as unknown as { loadPyodide?: LoadPyodide };
|
|
417
|
+
if (typeof g.loadPyodide === 'function') {
|
|
418
|
+
return g.loadPyodide;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Try dynamic import
|
|
422
|
+
try {
|
|
423
|
+
const mod = (await import('pyodide')) as unknown as { loadPyodide?: LoadPyodide };
|
|
424
|
+
if (typeof mod.loadPyodide === 'function') {
|
|
425
|
+
return mod.loadPyodide;
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// Pyodide module not available
|
|
429
|
+
// This is expected in most environments
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Bootstrap the Python dispatch function.
|
|
437
|
+
*/
|
|
438
|
+
private async bootstrapDispatcher(): Promise<void> {
|
|
439
|
+
if (!this.py) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await this.py.runPythonAsync(BOOTSTRAP_PYTHON);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Parse a JSON response and extract the result or throw an error.
|
|
447
|
+
*/
|
|
448
|
+
private parseResponse<T>(responseJson: string): T {
|
|
449
|
+
const response = JSON.parse(responseJson) as ProtocolResponse;
|
|
450
|
+
|
|
451
|
+
if (response.error) {
|
|
452
|
+
const err = new BridgeExecutionError(
|
|
453
|
+
`${response.error.type}: ${response.error.message}`,
|
|
454
|
+
{ code: response.error.type }
|
|
455
|
+
);
|
|
456
|
+
err.traceback = response.error.traceback;
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return response.result as T;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Generate a unique message ID.
|
|
465
|
+
*/
|
|
466
|
+
private generateId(): number {
|
|
467
|
+
return ++this.requestId;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Safely destroy a Pyodide proxy object.
|
|
472
|
+
*
|
|
473
|
+
* Pyodide proxy objects must be explicitly destroyed to prevent memory leaks.
|
|
474
|
+
* This method checks if the value has a destroy method and calls it.
|
|
475
|
+
*/
|
|
476
|
+
private destroyPyProxy(value: unknown): void {
|
|
477
|
+
if (value && typeof (value as { destroy?: () => void }).destroy === 'function') {
|
|
478
|
+
try {
|
|
479
|
+
(value as { destroy: () => void }).destroy();
|
|
480
|
+
} catch {
|
|
481
|
+
// Ignore cleanup failures
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|