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,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoundedContext - Unified abstraction for cross-boundary concerns.
|
|
3
|
+
*
|
|
4
|
+
* This base class provides consistent handling of:
|
|
5
|
+
* - Lifecycle management (init/dispose state machine)
|
|
6
|
+
* - Validation helpers
|
|
7
|
+
* - Error classification
|
|
8
|
+
* - Bounded execution (timeout, retry)
|
|
9
|
+
* - Resource ownership tracking
|
|
10
|
+
*
|
|
11
|
+
* All runtime bridges (NodeBridge, PyodideBridge, HttpBridge) extend this class.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/bbopen/tywrap/issues/149
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RuntimeExecution } from '../types/index.js';
|
|
17
|
+
import {
|
|
18
|
+
BridgeDisposedError,
|
|
19
|
+
BridgeError,
|
|
20
|
+
BridgeExecutionError,
|
|
21
|
+
BridgeProtocolError,
|
|
22
|
+
BridgeTimeoutError,
|
|
23
|
+
} from './errors.js';
|
|
24
|
+
import type { Disposable } from './disposable.js';
|
|
25
|
+
import { disposeAll } from './disposable.js';
|
|
26
|
+
import {
|
|
27
|
+
assertFiniteNumber,
|
|
28
|
+
assertNonEmptyString,
|
|
29
|
+
assertPositive,
|
|
30
|
+
assertString,
|
|
31
|
+
ValidationError,
|
|
32
|
+
} from './validators.js';
|
|
33
|
+
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// TYPES
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lifecycle states for a BoundedContext.
|
|
40
|
+
*
|
|
41
|
+
* State transitions:
|
|
42
|
+
* - idle → initializing → ready (on successful init)
|
|
43
|
+
* - idle → initializing → idle (on failed init, allows retry)
|
|
44
|
+
* - ready → disposing → disposed (on dispose)
|
|
45
|
+
* - disposed → (terminal, no further transitions)
|
|
46
|
+
*/
|
|
47
|
+
export type ContextState = 'idle' | 'initializing' | 'ready' | 'disposing' | 'disposed';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for bounded execution.
|
|
51
|
+
*/
|
|
52
|
+
export interface ExecuteOptions<T = unknown> {
|
|
53
|
+
/** Timeout in milliseconds. Default: 30000 (30s). Set to 0 to disable. */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Number of retry attempts on retryable errors. Default: 0. */
|
|
56
|
+
retries?: number;
|
|
57
|
+
/** Base delay between retries in ms. Multiplied by attempt number. Default: 100. */
|
|
58
|
+
retryDelayMs?: number;
|
|
59
|
+
/** Optional validation function applied to the result. */
|
|
60
|
+
validate?: (result: T) => T;
|
|
61
|
+
/** Optional abort signal for external cancellation. */
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// BOUNDED CONTEXT BASE CLASS
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Abstract base class for runtime bridges with unified boundary management.
|
|
71
|
+
*
|
|
72
|
+
* Provides lifecycle management, validation, error classification,
|
|
73
|
+
* bounded execution, and resource tracking.
|
|
74
|
+
*/
|
|
75
|
+
export abstract class BoundedContext implements RuntimeExecution {
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
// STATE
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
private _state: ContextState = 'idle';
|
|
81
|
+
private _initPromise?: Promise<void>;
|
|
82
|
+
private readonly _resources = new Set<Disposable>();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Current lifecycle state of the context.
|
|
86
|
+
*/
|
|
87
|
+
get state(): ContextState {
|
|
88
|
+
return this._state;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the context is ready for operations.
|
|
93
|
+
*/
|
|
94
|
+
get isReady(): boolean {
|
|
95
|
+
return this._state === 'ready';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether the context has been disposed.
|
|
100
|
+
*/
|
|
101
|
+
get isDisposed(): boolean {
|
|
102
|
+
return this._state === 'disposed';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
|
+
// LIFECYCLE (addresses #142, #148)
|
|
107
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Initialize the context.
|
|
111
|
+
*
|
|
112
|
+
* This method:
|
|
113
|
+
* - Is idempotent (safe to call multiple times)
|
|
114
|
+
* - Deduplicates concurrent calls (returns the same promise)
|
|
115
|
+
* - Allows retry after failure (resets to idle state)
|
|
116
|
+
* - Throws BridgeDisposedError if already disposed
|
|
117
|
+
*
|
|
118
|
+
* @throws BridgeDisposedError if the context has been disposed
|
|
119
|
+
* @throws BridgeError subclass if initialization fails
|
|
120
|
+
*/
|
|
121
|
+
async init(): Promise<void> {
|
|
122
|
+
// Guard against disposed or disposing states
|
|
123
|
+
if (this._state === 'disposed' || this._state === 'disposing') {
|
|
124
|
+
throw new BridgeDisposedError('Context has been disposed');
|
|
125
|
+
}
|
|
126
|
+
if (this._state === 'ready') {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this._initPromise) {
|
|
130
|
+
return this._initPromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this._state = 'initializing';
|
|
134
|
+
this._initPromise = this.doInit()
|
|
135
|
+
.then(() => {
|
|
136
|
+
// Guard against dispose() being called during init
|
|
137
|
+
if (this._state === 'initializing') {
|
|
138
|
+
this._state = 'ready';
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.catch(err => {
|
|
142
|
+
// Only allow retry by resetting to idle if still initializing
|
|
143
|
+
// (don't revive a disposed/disposing context)
|
|
144
|
+
if (this._state === 'initializing') {
|
|
145
|
+
this._state = 'idle';
|
|
146
|
+
this._initPromise = undefined;
|
|
147
|
+
}
|
|
148
|
+
throw this.classifyError(err);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return this._initPromise;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Dispose the context and all tracked resources.
|
|
156
|
+
*
|
|
157
|
+
* This method:
|
|
158
|
+
* - Is idempotent (safe to call multiple times)
|
|
159
|
+
* - Disposes all tracked resources before calling doDispose()
|
|
160
|
+
* - Collects errors from resource disposal and reports them
|
|
161
|
+
* - Transitions to disposed state even if disposal errors occur
|
|
162
|
+
*
|
|
163
|
+
* @throws AggregateError if multiple disposal errors occur
|
|
164
|
+
* @throws Error if a single disposal error occurs
|
|
165
|
+
*/
|
|
166
|
+
async dispose(): Promise<void> {
|
|
167
|
+
if (this._state === 'disposed') {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (this._state === 'disposing') {
|
|
171
|
+
// Another dispose is in progress; don't overlap
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this._state = 'disposing';
|
|
176
|
+
|
|
177
|
+
// Dispose all tracked resources
|
|
178
|
+
const resourceErrors = await disposeAll(this._resources);
|
|
179
|
+
this._resources.clear();
|
|
180
|
+
|
|
181
|
+
// Call subclass dispose logic
|
|
182
|
+
let doDisposeError: Error | undefined;
|
|
183
|
+
try {
|
|
184
|
+
await this.doDispose();
|
|
185
|
+
} catch (e) {
|
|
186
|
+
doDisposeError = e instanceof Error ? e : new Error(String(e));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Always transition to disposed
|
|
190
|
+
this._state = 'disposed';
|
|
191
|
+
this._initPromise = undefined;
|
|
192
|
+
|
|
193
|
+
// Report any errors
|
|
194
|
+
const allErrors = doDisposeError ? [...resourceErrors, doDisposeError] : resourceErrors;
|
|
195
|
+
|
|
196
|
+
if (allErrors.length === 1) {
|
|
197
|
+
throw allErrors[0];
|
|
198
|
+
}
|
|
199
|
+
if (allErrors.length > 1) {
|
|
200
|
+
throw new AggregateError(allErrors, 'Multiple errors during dispose');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Subclass initialization logic.
|
|
206
|
+
* Called during init() after state transitions to 'initializing'.
|
|
207
|
+
*/
|
|
208
|
+
protected abstract doInit(): Promise<void>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Subclass disposal logic.
|
|
212
|
+
* Called during dispose() after tracked resources are disposed.
|
|
213
|
+
*/
|
|
214
|
+
protected abstract doDispose(): Promise<void>;
|
|
215
|
+
|
|
216
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
217
|
+
// VALIDATION (addresses #141, #145, #146, #147)
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate that a value is a finite number.
|
|
222
|
+
* Wraps validation errors in appropriate BridgeError.
|
|
223
|
+
*
|
|
224
|
+
* @param value - The value to validate
|
|
225
|
+
* @param name - Parameter name for error messages
|
|
226
|
+
* @returns The validated number
|
|
227
|
+
* @throws BridgeError if validation fails
|
|
228
|
+
*/
|
|
229
|
+
protected validateNumeric(value: unknown, name: string): number {
|
|
230
|
+
try {
|
|
231
|
+
return assertFiniteNumber(value, name);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw this.classifyError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate that a value is a positive number.
|
|
239
|
+
* Wraps validation errors in appropriate BridgeError.
|
|
240
|
+
*
|
|
241
|
+
* @param value - The value to validate
|
|
242
|
+
* @param name - Parameter name for error messages
|
|
243
|
+
* @returns The validated number
|
|
244
|
+
* @throws BridgeError if validation fails
|
|
245
|
+
*/
|
|
246
|
+
protected validatePositive(value: unknown, name: string): number {
|
|
247
|
+
try {
|
|
248
|
+
return assertPositive(value, name);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw this.classifyError(error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Validate that a value is a string.
|
|
256
|
+
* Wraps validation errors in appropriate BridgeError.
|
|
257
|
+
*
|
|
258
|
+
* @param value - The value to validate
|
|
259
|
+
* @param name - Parameter name for error messages
|
|
260
|
+
* @returns The validated string
|
|
261
|
+
* @throws BridgeError if validation fails
|
|
262
|
+
*/
|
|
263
|
+
protected validateString(value: unknown, name: string): string {
|
|
264
|
+
try {
|
|
265
|
+
return assertString(value, name);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw this.classifyError(error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate that a value is a non-empty string.
|
|
273
|
+
* Wraps validation errors in appropriate BridgeError.
|
|
274
|
+
*
|
|
275
|
+
* @param value - The value to validate
|
|
276
|
+
* @param name - Parameter name for error messages
|
|
277
|
+
* @returns The validated string
|
|
278
|
+
* @throws BridgeError if validation fails
|
|
279
|
+
*/
|
|
280
|
+
protected validateNonEmptyString(value: unknown, name: string): string {
|
|
281
|
+
try {
|
|
282
|
+
return assertNonEmptyString(value, name);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw this.classifyError(error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate input before processing.
|
|
290
|
+
* Override in subclasses for domain-specific input validation.
|
|
291
|
+
*
|
|
292
|
+
* @param input - The input to validate
|
|
293
|
+
* @returns The validated input
|
|
294
|
+
*/
|
|
295
|
+
protected validateInput<T>(input: T): T {
|
|
296
|
+
return input;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Validate output before returning.
|
|
301
|
+
* Override in subclasses for domain-specific output validation.
|
|
302
|
+
*
|
|
303
|
+
* @param output - The output to validate
|
|
304
|
+
* @returns The validated output
|
|
305
|
+
*/
|
|
306
|
+
protected validateOutput<T>(output: T): T {
|
|
307
|
+
return output;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
|
+
// ERROR CLASSIFICATION (addresses #143)
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Classify an error into the appropriate BridgeError subtype.
|
|
316
|
+
*
|
|
317
|
+
* This method:
|
|
318
|
+
* - Passes through existing BridgeError instances
|
|
319
|
+
* - Uses context state to determine appropriate error type
|
|
320
|
+
* - Pattern-matches error messages for classification
|
|
321
|
+
*
|
|
322
|
+
* @param error - The error to classify
|
|
323
|
+
* @returns A BridgeError instance
|
|
324
|
+
*/
|
|
325
|
+
protected classifyError(error: unknown): BridgeError {
|
|
326
|
+
// Pass through existing BridgeErrors
|
|
327
|
+
if (error instanceof BridgeError) {
|
|
328
|
+
return error;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
|
+
const cause = error instanceof Error ? error : undefined;
|
|
333
|
+
|
|
334
|
+
// Context state takes precedence
|
|
335
|
+
if (this._state === 'disposed') {
|
|
336
|
+
return new BridgeDisposedError(message, { cause });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Pattern matching for classification
|
|
340
|
+
const lowerMessage = message.toLowerCase();
|
|
341
|
+
|
|
342
|
+
// Timeout patterns
|
|
343
|
+
if (
|
|
344
|
+
lowerMessage.includes('timeout') ||
|
|
345
|
+
lowerMessage.includes('etimedout') ||
|
|
346
|
+
lowerMessage.includes('timed out') ||
|
|
347
|
+
lowerMessage.includes('aborted')
|
|
348
|
+
) {
|
|
349
|
+
return new BridgeTimeoutError(message, { cause });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Protocol patterns
|
|
353
|
+
if (
|
|
354
|
+
lowerMessage.includes('protocol') ||
|
|
355
|
+
lowerMessage.includes('invalid json') ||
|
|
356
|
+
lowerMessage.includes('parse error') ||
|
|
357
|
+
lowerMessage.includes('unexpected token') ||
|
|
358
|
+
lowerMessage.includes('not found') ||
|
|
359
|
+
error instanceof ValidationError
|
|
360
|
+
) {
|
|
361
|
+
return new BridgeProtocolError(message, { cause });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Default to execution error
|
|
365
|
+
return new BridgeExecutionError(message, { cause });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
369
|
+
// BOUNDED EXECUTION (addresses #141, #147, #148)
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Execute an operation with bounded execution guarantees.
|
|
374
|
+
*
|
|
375
|
+
* This method:
|
|
376
|
+
* - Auto-initializes if not ready
|
|
377
|
+
* - Enforces timeout limits
|
|
378
|
+
* - Supports retry on retryable errors
|
|
379
|
+
* - Respects abort signals
|
|
380
|
+
* - Validates results if a validator is provided
|
|
381
|
+
*
|
|
382
|
+
* @param operation - The async operation to execute
|
|
383
|
+
* @param options - Execution options (timeout, retries, etc.)
|
|
384
|
+
* @returns The operation result
|
|
385
|
+
* @throws BridgeTimeoutError if the operation times out
|
|
386
|
+
* @throws BridgeDisposedError if the context is disposed
|
|
387
|
+
* @throws BridgeError for other failures
|
|
388
|
+
*/
|
|
389
|
+
protected async execute<T>(
|
|
390
|
+
operation: () => Promise<T>,
|
|
391
|
+
options: ExecuteOptions<T> = {}
|
|
392
|
+
): Promise<T> {
|
|
393
|
+
// Auto-initialize if needed
|
|
394
|
+
if (this._state !== 'ready') {
|
|
395
|
+
await this.init();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check disposed state after potential init
|
|
399
|
+
if (this._state === 'disposed') {
|
|
400
|
+
throw new BridgeDisposedError('Context disposed');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const { timeoutMs = 30000, retries = 0, retryDelayMs = 100, validate, signal } = options;
|
|
404
|
+
|
|
405
|
+
let lastError: BridgeError | undefined;
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
408
|
+
// Check for abort before each attempt
|
|
409
|
+
if (signal?.aborted) {
|
|
410
|
+
throw new BridgeTimeoutError('Operation aborted');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const result = await this.withTimeout(operation(), timeoutMs, signal);
|
|
415
|
+
return validate ? validate(result) : result;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
lastError = this.classifyError(error);
|
|
418
|
+
|
|
419
|
+
// Retry if appropriate
|
|
420
|
+
if (attempt < retries && this.isRetryable(lastError)) {
|
|
421
|
+
await this.delay(retryDelayMs * (attempt + 1));
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
throw lastError;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Should not reach here, but TypeScript doesn't know that
|
|
430
|
+
/* istanbul ignore next */
|
|
431
|
+
throw lastError ?? new BridgeExecutionError('Unexpected execution flow');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Wrap a promise with a timeout.
|
|
436
|
+
*
|
|
437
|
+
* @param promise - The promise to wrap
|
|
438
|
+
* @param ms - Timeout in milliseconds (0 or negative disables timeout)
|
|
439
|
+
* @param signal - Optional abort signal
|
|
440
|
+
* @returns The promise result
|
|
441
|
+
* @throws BridgeTimeoutError if the timeout expires
|
|
442
|
+
*/
|
|
443
|
+
private async withTimeout<T>(promise: Promise<T>, ms: number, signal?: AbortSignal): Promise<T> {
|
|
444
|
+
// Check if already aborted
|
|
445
|
+
if (signal?.aborted) {
|
|
446
|
+
throw new BridgeTimeoutError('Operation aborted');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// No timeout if ms is 0 or negative, but still honor abort signal
|
|
450
|
+
if (ms <= 0 || !Number.isFinite(ms)) {
|
|
451
|
+
if (!signal) {
|
|
452
|
+
return promise;
|
|
453
|
+
}
|
|
454
|
+
// Wrap promise to honor abort signal even without timeout
|
|
455
|
+
return new Promise<T>((resolve, reject) => {
|
|
456
|
+
const abortHandler = (): void => {
|
|
457
|
+
reject(new BridgeTimeoutError('Operation aborted'));
|
|
458
|
+
};
|
|
459
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
460
|
+
promise
|
|
461
|
+
.then(result => {
|
|
462
|
+
signal.removeEventListener('abort', abortHandler);
|
|
463
|
+
resolve(result);
|
|
464
|
+
})
|
|
465
|
+
.catch(error => {
|
|
466
|
+
signal.removeEventListener('abort', abortHandler);
|
|
467
|
+
reject(error);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return new Promise<T>((resolve, reject) => {
|
|
473
|
+
const timer = setTimeout(() => {
|
|
474
|
+
reject(new BridgeTimeoutError(`Operation timed out after ${ms}ms`));
|
|
475
|
+
}, ms);
|
|
476
|
+
|
|
477
|
+
// Handle external abort signal
|
|
478
|
+
const abortHandler = (): void => {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
reject(new BridgeTimeoutError('Operation aborted'));
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
if (signal) {
|
|
484
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
promise
|
|
488
|
+
.then(result => {
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
signal?.removeEventListener('abort', abortHandler);
|
|
491
|
+
resolve(result);
|
|
492
|
+
})
|
|
493
|
+
.catch(error => {
|
|
494
|
+
clearTimeout(timer);
|
|
495
|
+
signal?.removeEventListener('abort', abortHandler);
|
|
496
|
+
reject(error);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Determine if an error is retryable.
|
|
503
|
+
* Override in subclasses to customize retry logic.
|
|
504
|
+
*
|
|
505
|
+
* @param error - The error to check
|
|
506
|
+
* @returns True if the operation should be retried
|
|
507
|
+
*/
|
|
508
|
+
protected isRetryable(error: BridgeError): boolean {
|
|
509
|
+
// Timeout and connection errors are typically retryable
|
|
510
|
+
if (error instanceof BridgeTimeoutError) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const message = error.message.toLowerCase();
|
|
515
|
+
return (
|
|
516
|
+
message.includes('econnreset') ||
|
|
517
|
+
message.includes('econnrefused') ||
|
|
518
|
+
message.includes('epipe') ||
|
|
519
|
+
message.includes('connection reset')
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Delay for a specified duration.
|
|
525
|
+
*
|
|
526
|
+
* @param ms - Milliseconds to delay
|
|
527
|
+
*/
|
|
528
|
+
private delay(ms: number): Promise<void> {
|
|
529
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
533
|
+
// RESOURCE OWNERSHIP (addresses #144, #148)
|
|
534
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Track a disposable resource for automatic cleanup.
|
|
538
|
+
*
|
|
539
|
+
* When the context is disposed, all tracked resources will be
|
|
540
|
+
* disposed automatically.
|
|
541
|
+
*
|
|
542
|
+
* @param resource - The resource to track
|
|
543
|
+
* @returns The same resource (for chaining)
|
|
544
|
+
*/
|
|
545
|
+
protected trackResource<T extends Disposable>(resource: T): T {
|
|
546
|
+
this._resources.add(resource);
|
|
547
|
+
return resource;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Stop tracking a resource.
|
|
552
|
+
*
|
|
553
|
+
* Use this when a resource is disposed manually and should
|
|
554
|
+
* not be disposed again during context disposal.
|
|
555
|
+
*
|
|
556
|
+
* @param resource - The resource to untrack
|
|
557
|
+
* @returns True if the resource was being tracked
|
|
558
|
+
*/
|
|
559
|
+
protected untrackResource(resource: Disposable): boolean {
|
|
560
|
+
return this._resources.delete(resource);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Number of currently tracked resources.
|
|
565
|
+
*/
|
|
566
|
+
protected get resourceCount(): number {
|
|
567
|
+
return this._resources.size;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
571
|
+
// ABSTRACT METHODS (RuntimeExecution interface)
|
|
572
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Call a Python function.
|
|
576
|
+
*/
|
|
577
|
+
abstract call<T = unknown>(
|
|
578
|
+
module: string,
|
|
579
|
+
functionName: string,
|
|
580
|
+
args: unknown[],
|
|
581
|
+
kwargs?: Record<string, unknown>
|
|
582
|
+
): Promise<T>;
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Instantiate a Python class.
|
|
586
|
+
*/
|
|
587
|
+
abstract instantiate<T = unknown>(
|
|
588
|
+
module: string,
|
|
589
|
+
className: string,
|
|
590
|
+
args: unknown[],
|
|
591
|
+
kwargs?: Record<string, unknown>
|
|
592
|
+
): Promise<T>;
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Call a method on a Python instance.
|
|
596
|
+
*/
|
|
597
|
+
abstract callMethod<T = unknown>(
|
|
598
|
+
handle: string,
|
|
599
|
+
methodName: string,
|
|
600
|
+
args: unknown[],
|
|
601
|
+
kwargs?: Record<string, unknown>
|
|
602
|
+
): Promise<T>;
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Dispose a Python instance.
|
|
606
|
+
*/
|
|
607
|
+
abstract disposeInstance(handle: string): Promise<void>;
|
|
608
|
+
}
|