tywrap 0.3.0 → 0.5.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 +40 -5
- package/dist/cli.js +24 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -13
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +0 -1
- package/dist/core/analyzer.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +7 -7
- package/dist/runtime/base.js +8 -8
- package/dist/runtime/bounded-context.d.ts +15 -31
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +16 -15
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/http.d.ts +24 -5
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +57 -12
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +33 -7
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +144 -120
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +5 -4
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +5 -4
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +2 -18
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -29
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +2 -18
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +3 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts +12 -0
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -0
- package/dist/runtime/pyodide-bootstrap-core.generated.js +12 -0
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +7 -37
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +90 -214
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/pyodide.d.ts +25 -5
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +59 -10
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/{bridge-protocol.d.ts → rpc-client.d.ts} +59 -46
- package/dist/runtime/rpc-client.d.ts.map +1 -0
- package/dist/runtime/{bridge-protocol.js → rpc-client.js} +60 -42
- package/dist/runtime/rpc-client.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +8 -20
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +58 -59
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +8 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +10 -3
- package/dist/tywrap.js.map +1 -1
- package/package.json +22 -18
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/python_bridge.py +85 -702
- package/runtime/safe_codec.py +10 -2
- package/runtime/tywrap_bridge_core.py +875 -0
- package/src/cli.ts +27 -6
- package/src/config/index.ts +28 -15
- package/src/core/analyzer.ts +1 -2
- package/src/dev.ts +983 -0
- package/src/index.ts +7 -7
- package/src/runtime/base.ts +8 -8
- package/src/runtime/bounded-context.ts +17 -56
- package/src/runtime/http.ts +85 -13
- package/src/runtime/node.ts +172 -178
- package/src/runtime/optimized-node.ts +5 -4
- package/src/runtime/pooled-transport.ts +2 -57
- package/src/runtime/process-io.ts +3 -55
- package/src/runtime/pyodide-bootstrap-core.generated.ts +12 -0
- package/src/runtime/pyodide-io.ts +92 -243
- package/src/runtime/pyodide.ts +87 -10
- package/src/runtime/{bridge-protocol.ts → rpc-client.ts} +76 -49
- package/src/runtime/worker-pool.ts +67 -88
- package/src/types/index.ts +20 -12
- package/src/tywrap.ts +17 -3
- package/dist/runtime/bridge-core.d.ts +0 -65
- package/dist/runtime/bridge-core.d.ts.map +0 -1
- package/dist/runtime/bridge-core.js +0 -379
- package/dist/runtime/bridge-core.js.map +0 -1
- package/dist/runtime/bridge-protocol.d.ts.map +0 -1
- package/dist/runtime/bridge-protocol.js.map +0 -1
- package/src/runtime/bridge-core.ts +0 -494
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* RpcClient - the single correlated-RPC client for JS<->Python communication.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* It HOLDS a Transport and a codec (SafeCodec) and is, in turn, HELD by the
|
|
5
|
+
* bridge facades (NodeBridge/HttpBridge/PyodideBridge) via composition — it is
|
|
6
|
+
* NOT a base class bridges extend. It owns the one place where the wire frame
|
|
7
|
+
* is built and correlated: id generation, {id, protocol} stamping, codec
|
|
8
|
+
* encode/decode, and transport.send. It composes DisposableBase to obtain its
|
|
9
|
+
* lifecycle (init/dispose) and bounded execution (timeout/retry/abort), but it
|
|
10
|
+
* carries no PythonRuntime contract obligation — the facade implements
|
|
11
|
+
* PythonRuntime and delegates the four RPC methods to this client.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Why composition (not inheritance): inheritance previously forced the RPC
|
|
14
|
+
* contract onto a lifecycle base, leaking throwing RPC stubs into transports.
|
|
15
|
+
* Holding one RpcClient keeps exactly one encode/decode/correlation site and
|
|
16
|
+
* lets transports stay pure byte-movers.
|
|
15
17
|
*
|
|
16
18
|
* @see https://github.com/bbopen/tywrap/issues/149
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import type { BridgeInfo } from '../types/index.js';
|
|
20
22
|
|
|
21
|
-
import {
|
|
23
|
+
import { DisposableBase, type ExecuteOptions } from './bounded-context.js';
|
|
22
24
|
import { BridgeProtocolError } from './errors.js';
|
|
23
25
|
import { SafeCodec, type CodecOptions } from './safe-codec.js';
|
|
24
26
|
import { TYWRAP_PROTOCOL_VERSION } from './protocol.js';
|
|
@@ -37,9 +39,9 @@ export interface GetBridgeInfoOptions {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
* Configuration options for
|
|
42
|
+
* Configuration options for RpcClient.
|
|
41
43
|
*/
|
|
42
|
-
export interface
|
|
44
|
+
export interface RpcClientOptions {
|
|
43
45
|
/** The transport to use for communication */
|
|
44
46
|
transport: Transport;
|
|
45
47
|
|
|
@@ -174,43 +176,37 @@ function validateBridgeInfoPayload(value: unknown): BridgeInfo {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
// =============================================================================
|
|
177
|
-
//
|
|
179
|
+
// RPC CLIENT
|
|
178
180
|
// =============================================================================
|
|
179
181
|
|
|
180
182
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
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
|
|
183
|
+
* RpcClient holds a SafeCodec + Transport and composes DisposableBase for
|
|
184
|
+
* lifecycle/bounded-execution. It is the one correlated-RPC client; bridge
|
|
185
|
+
* facades HOLD an instance and delegate their PythonRuntime methods to it.
|
|
194
186
|
*
|
|
195
187
|
* @example
|
|
196
188
|
* ```typescript
|
|
197
|
-
* class NodeBridge extends
|
|
189
|
+
* class NodeBridge extends DisposableBase implements PythonRuntime {
|
|
190
|
+
* private readonly rpc: RpcClient;
|
|
198
191
|
* constructor(options: NodeBridgeOptions) {
|
|
192
|
+
* super();
|
|
199
193
|
* const transport = new ProcessIO(options);
|
|
200
|
-
*
|
|
194
|
+
* this.rpc = new RpcClient({ transport, defaultTimeoutMs: options.timeout });
|
|
195
|
+
* this.trackResource(this.rpc);
|
|
201
196
|
* }
|
|
197
|
+
* // call/instantiate/callMethod/disposeInstance delegate to this.rpc.*
|
|
202
198
|
* }
|
|
203
199
|
* ```
|
|
204
200
|
*/
|
|
205
|
-
export class
|
|
201
|
+
export class RpcClient extends DisposableBase {
|
|
206
202
|
/** Codec instance for validation and serialization */
|
|
207
|
-
|
|
203
|
+
readonly codec: SafeCodec;
|
|
208
204
|
|
|
209
205
|
/** Transport instance for message passing */
|
|
210
|
-
|
|
206
|
+
readonly transport: Transport;
|
|
211
207
|
|
|
212
208
|
/** Default timeout for operations in milliseconds */
|
|
213
|
-
|
|
209
|
+
readonly defaultTimeoutMs: number;
|
|
214
210
|
|
|
215
211
|
/** Counter for generating unique request IDs */
|
|
216
212
|
private requestId = 0;
|
|
@@ -219,11 +215,11 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
219
215
|
private bridgeInfoCache?: BridgeInfo;
|
|
220
216
|
|
|
221
217
|
/**
|
|
222
|
-
* Create a new
|
|
218
|
+
* Create a new RpcClient instance.
|
|
223
219
|
*
|
|
224
220
|
* @param options - Configuration options including transport and codec settings
|
|
225
221
|
*/
|
|
226
|
-
constructor(options:
|
|
222
|
+
constructor(options: RpcClientOptions) {
|
|
227
223
|
super();
|
|
228
224
|
this.codec = new SafeCodec(options.codec);
|
|
229
225
|
this.transport = options.transport;
|
|
@@ -238,26 +234,22 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
238
234
|
// ===========================================================================
|
|
239
235
|
|
|
240
236
|
/**
|
|
241
|
-
* Initialize the
|
|
242
|
-
*
|
|
243
|
-
* Initializes the underlying transport. Subclasses can override this
|
|
244
|
-
* to add additional initialization logic, but must call super.doInit().
|
|
237
|
+
* Initialize the client by initializing the underlying transport.
|
|
238
|
+
* Driven by the holding facade's lifecycle (facade.init() -> rpc.init()).
|
|
245
239
|
*/
|
|
246
240
|
protected async doInit(): Promise<void> {
|
|
247
241
|
await this.transport.init();
|
|
248
242
|
}
|
|
249
243
|
|
|
250
244
|
/**
|
|
251
|
-
* Dispose the
|
|
245
|
+
* Dispose the client.
|
|
252
246
|
*
|
|
253
|
-
* The transport is tracked as a resource and
|
|
254
|
-
*
|
|
255
|
-
* but should not need to dispose the transport manually.
|
|
247
|
+
* The transport is tracked as a resource and is disposed automatically by
|
|
248
|
+
* DisposableBase. Here we only clear the cached bridge info.
|
|
256
249
|
*/
|
|
257
250
|
protected async doDispose(): Promise<void> {
|
|
258
251
|
this.bridgeInfoCache = undefined;
|
|
259
|
-
// Transport is tracked and will be disposed by
|
|
260
|
-
// Subclasses can override to add additional cleanup
|
|
252
|
+
// Transport is tracked and will be disposed by DisposableBase.
|
|
261
253
|
}
|
|
262
254
|
|
|
263
255
|
// ===========================================================================
|
|
@@ -281,7 +273,7 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
281
273
|
* @throws BridgeExecutionError if Python returns an error
|
|
282
274
|
* @throws BridgeTimeoutError if the operation times out
|
|
283
275
|
*/
|
|
284
|
-
|
|
276
|
+
async sendMessage<T>(
|
|
285
277
|
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
286
278
|
options?: ExecuteOptions<T>
|
|
287
279
|
): Promise<T> {
|
|
@@ -321,7 +313,7 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
321
313
|
* @throws BridgeExecutionError if Python returns an error
|
|
322
314
|
* @throws BridgeTimeoutError if the operation times out
|
|
323
315
|
*/
|
|
324
|
-
|
|
316
|
+
async sendMessageAsync<T>(
|
|
325
317
|
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
326
318
|
options?: ExecuteOptions<T>
|
|
327
319
|
): Promise<T> {
|
|
@@ -347,6 +339,41 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
347
339
|
}, options);
|
|
348
340
|
}
|
|
349
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Send a message over an EXPLICIT transport (not this.transport) with the
|
|
344
|
+
* same id-generation + codec encode/decode as the normal send path, but
|
|
345
|
+
* WITHOUT this.execute() and WITHOUT auto-init.
|
|
346
|
+
*
|
|
347
|
+
* This is the warmup path: NodeBridge runs warmup commands inside
|
|
348
|
+
* transport.init() (worker spawn -> onWorkerReady), which happens DURING
|
|
349
|
+
* rpc.init(). Routing those through this.execute() would auto-init this same
|
|
350
|
+
* client and re-await the in-flight init() that is itself waiting on warmup,
|
|
351
|
+
* deadlocking. So sendOn deliberately skips lifecycle: it only unifies the
|
|
352
|
+
* id counter + codec, never the state machine.
|
|
353
|
+
*
|
|
354
|
+
* @param transport - The specific worker transport to send on
|
|
355
|
+
* @param message - The protocol message (without id/protocol; stamped here)
|
|
356
|
+
* @param opts - Optional timeout / abort signal
|
|
357
|
+
*/
|
|
358
|
+
async sendOn<T>(
|
|
359
|
+
transport: Transport,
|
|
360
|
+
message: Omit<ProtocolMessage, 'id' | 'protocol'>,
|
|
361
|
+
opts?: { timeoutMs?: number; signal?: AbortSignal }
|
|
362
|
+
): Promise<T> {
|
|
363
|
+
const fullMessage: ProtocolMessage = {
|
|
364
|
+
...message,
|
|
365
|
+
id: this.generateId(),
|
|
366
|
+
protocol: PROTOCOL_ID,
|
|
367
|
+
};
|
|
368
|
+
const encoded = this.codec.encodeRequest(fullMessage);
|
|
369
|
+
const responseStr = await transport.send(
|
|
370
|
+
encoded,
|
|
371
|
+
opts?.timeoutMs ?? this.defaultTimeoutMs,
|
|
372
|
+
opts?.signal
|
|
373
|
+
);
|
|
374
|
+
return this.codec.decodeResponseAsync<T>(responseStr);
|
|
375
|
+
}
|
|
376
|
+
|
|
350
377
|
/**
|
|
351
378
|
* Generate a unique request ID.
|
|
352
379
|
*
|
|
@@ -358,7 +385,7 @@ export class BridgeProtocol extends BoundedContext {
|
|
|
358
385
|
}
|
|
359
386
|
|
|
360
387
|
// ===========================================================================
|
|
361
|
-
//
|
|
388
|
+
// RPC METHODS (delegated to by the facade's PythonRuntime implementation)
|
|
362
389
|
// ===========================================================================
|
|
363
390
|
|
|
364
391
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @see https://github.com/bbopen/tywrap/issues/149
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { DisposableBase } from './bounded-context.js';
|
|
11
11
|
import { BridgeTimeoutError, BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
12
12
|
import type { Transport } from './transport.js';
|
|
13
13
|
|
|
@@ -105,7 +105,7 @@ interface QueuedWaiter {
|
|
|
105
105
|
* await pool.dispose();
|
|
106
106
|
* ```
|
|
107
107
|
*/
|
|
108
|
-
export class WorkerPool extends
|
|
108
|
+
export class WorkerPool extends DisposableBase {
|
|
109
109
|
private readonly options: Omit<
|
|
110
110
|
Required<WorkerPoolOptions>,
|
|
111
111
|
'onWorkerReady' | 'onReplacementWorkerReady'
|
|
@@ -162,13 +162,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
162
162
|
*/
|
|
163
163
|
protected async doInit(): Promise<void> {
|
|
164
164
|
// Pre-spawn minimum workers if configured
|
|
165
|
-
|
|
166
|
-
const spawns: Promise<PooledWorker>[] = [];
|
|
167
|
-
for (let i = 0; i < this.options.minWorkers; i++) {
|
|
168
|
-
spawns.push(this.createWorker());
|
|
169
|
-
}
|
|
170
|
-
await Promise.all(spawns);
|
|
171
|
-
}
|
|
165
|
+
await this.fillToMinimumWorkers();
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
/**
|
|
@@ -224,7 +218,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
224
218
|
*/
|
|
225
219
|
async acquire(): Promise<PooledWorker> {
|
|
226
220
|
// Check for disposed state
|
|
227
|
-
if (this.isDisposed) {
|
|
221
|
+
if (this.isDisposed || this.state === 'disposing') {
|
|
228
222
|
throw new BridgeExecutionError('Pool has been disposed');
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -243,7 +237,12 @@ export class WorkerPool extends BoundedContext {
|
|
|
243
237
|
this.pendingCreations++;
|
|
244
238
|
try {
|
|
245
239
|
const newWorker = await this.createWorker();
|
|
240
|
+
if (this.isShuttingDown()) {
|
|
241
|
+
this.removeWorker(newWorker);
|
|
242
|
+
throw new BridgeExecutionError('Pool has been disposed');
|
|
243
|
+
}
|
|
246
244
|
newWorker.inFlightCount++;
|
|
245
|
+
this.publishAvailableWorker(newWorker);
|
|
247
246
|
return newWorker;
|
|
248
247
|
} finally {
|
|
249
248
|
this.pendingCreations--;
|
|
@@ -270,16 +269,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
270
269
|
|
|
271
270
|
// Decrement in-flight count (minimum 0)
|
|
272
271
|
worker.inFlightCount = Math.max(0, worker.inFlightCount - 1);
|
|
273
|
-
|
|
274
|
-
// If there are waiters and this worker has capacity, fulfill the first waiter
|
|
275
|
-
if (this.waitQueue.length > 0 && worker.inFlightCount < this.options.maxConcurrentPerWorker) {
|
|
276
|
-
const waiter = this.waitQueue.shift();
|
|
277
|
-
if (waiter) {
|
|
278
|
-
clearTimeout(waiter.timer);
|
|
279
|
-
worker.inFlightCount++;
|
|
280
|
-
waiter.resolve(worker);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
272
|
+
this.publishAvailableWorker(worker);
|
|
283
273
|
}
|
|
284
274
|
|
|
285
275
|
/**
|
|
@@ -331,14 +321,8 @@ export class WorkerPool extends BoundedContext {
|
|
|
331
321
|
* - Process exited unexpectedly
|
|
332
322
|
* - Pipe errors (EPIPE)
|
|
333
323
|
* - Connection reset errors (ECONNRESET)
|
|
334
|
-
* - Request timeouts (the worker may still be busy with an uncancellable request)
|
|
335
324
|
*/
|
|
336
325
|
private isFatalWorkerError(error: unknown): boolean {
|
|
337
|
-
// If a request times out, the underlying transport may still be executing it.
|
|
338
|
-
// Quarantine this worker so subsequent requests don't get stuck behind it.
|
|
339
|
-
if (error instanceof BridgeTimeoutError) {
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
326
|
if (error instanceof BridgeProtocolError) {
|
|
343
327
|
const msg = error.message.toLowerCase();
|
|
344
328
|
return (
|
|
@@ -365,7 +349,9 @@ export class WorkerPool extends BoundedContext {
|
|
|
365
349
|
worker.transport.dispose().catch(() => {
|
|
366
350
|
// Ignore disposal errors for dead workers
|
|
367
351
|
});
|
|
368
|
-
this.
|
|
352
|
+
if (this.state === 'ready') {
|
|
353
|
+
this.scheduleReplacementWorker();
|
|
354
|
+
}
|
|
369
355
|
}
|
|
370
356
|
}
|
|
371
357
|
|
|
@@ -405,6 +391,30 @@ export class WorkerPool extends BoundedContext {
|
|
|
405
391
|
return this.workers.find(w => w.inFlightCount < this.options.maxConcurrentPerWorker);
|
|
406
392
|
}
|
|
407
393
|
|
|
394
|
+
private getMinimumWorkerDeficit(): number {
|
|
395
|
+
return Math.max(0, this.options.minWorkers - (this.workers.length + this.pendingCreations));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private isShuttingDown(): boolean {
|
|
399
|
+
return this.isDisposed || this.state === 'disposing';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async fillToMinimumWorkers(): Promise<void> {
|
|
403
|
+
await this.spawnWorkers(this.getMinimumWorkerDeficit());
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private async spawnWorkers(count: number): Promise<void> {
|
|
407
|
+
if (count === 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.pendingCreations += count;
|
|
411
|
+
try {
|
|
412
|
+
await Promise.all(Array.from({ length: count }, () => this.spawnWorkerToPool()));
|
|
413
|
+
} finally {
|
|
414
|
+
this.pendingCreations = Math.max(0, this.pendingCreations - count);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
408
418
|
/**
|
|
409
419
|
* Replace a removed worker in the background so the next caller does not pay
|
|
410
420
|
* the full worker cold-start penalty after a timeout or crash.
|
|
@@ -419,11 +429,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
419
429
|
|
|
420
430
|
this.pendingCreations++;
|
|
421
431
|
const replacementReady = this.options.onReplacementWorkerReady ?? this.options.onWorkerReady;
|
|
422
|
-
this.
|
|
423
|
-
.then(worker => {
|
|
424
|
-
// Reuse release() to wake queued callers if one is waiting.
|
|
425
|
-
this.release(worker);
|
|
426
|
-
})
|
|
432
|
+
this.spawnWorkerToPool(replacementReady)
|
|
427
433
|
.catch(() => {
|
|
428
434
|
// Ignore background replacement failures. A later acquire() can retry.
|
|
429
435
|
})
|
|
@@ -432,6 +438,35 @@ export class WorkerPool extends BoundedContext {
|
|
|
432
438
|
});
|
|
433
439
|
}
|
|
434
440
|
|
|
441
|
+
private async spawnWorkerToPool(onWorkerReady = this.options.onWorkerReady): Promise<void> {
|
|
442
|
+
if (this.isShuttingDown()) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const worker = await this.createWorker(onWorkerReady);
|
|
447
|
+
if (this.isShuttingDown()) {
|
|
448
|
+
this.removeWorker(worker);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.publishAvailableWorker(worker);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private publishAvailableWorker(worker: PooledWorker): void {
|
|
456
|
+
while (
|
|
457
|
+
this.waitQueue.length > 0 &&
|
|
458
|
+
worker.inFlightCount < this.options.maxConcurrentPerWorker
|
|
459
|
+
) {
|
|
460
|
+
const waiter = this.waitQueue.shift();
|
|
461
|
+
if (!waiter) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
clearTimeout(waiter.timer);
|
|
465
|
+
worker.inFlightCount++;
|
|
466
|
+
waiter.resolve(worker);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
435
470
|
/**
|
|
436
471
|
* Create a new worker and add it to the pool.
|
|
437
472
|
*
|
|
@@ -500,60 +535,4 @@ export class WorkerPool extends BoundedContext {
|
|
|
500
535
|
this.waitQueue.push({ resolve, reject, timer });
|
|
501
536
|
});
|
|
502
537
|
}
|
|
503
|
-
|
|
504
|
-
// ===========================================================================
|
|
505
|
-
// RUNTIME EXECUTION (Not implemented - WorkerPool is just for worker management)
|
|
506
|
-
// ===========================================================================
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Not implemented - WorkerPool does not execute Python calls directly.
|
|
510
|
-
* Use the BridgeProtocol layer with a pooled worker's transport.
|
|
511
|
-
*/
|
|
512
|
-
async call<T = unknown>(
|
|
513
|
-
_module: string,
|
|
514
|
-
_functionName: string,
|
|
515
|
-
_args: unknown[],
|
|
516
|
-
_kwargs?: Record<string, unknown>
|
|
517
|
-
): Promise<T> {
|
|
518
|
-
throw new BridgeExecutionError(
|
|
519
|
-
'WorkerPool does not implement call() - use withWorker() to get a transport'
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Not implemented - WorkerPool does not execute Python calls directly.
|
|
525
|
-
*/
|
|
526
|
-
async instantiate<T = unknown>(
|
|
527
|
-
_module: string,
|
|
528
|
-
_className: string,
|
|
529
|
-
_args: unknown[],
|
|
530
|
-
_kwargs?: Record<string, unknown>
|
|
531
|
-
): Promise<T> {
|
|
532
|
-
throw new BridgeExecutionError(
|
|
533
|
-
'WorkerPool does not implement instantiate() - use withWorker() to get a transport'
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Not implemented - WorkerPool does not execute Python calls directly.
|
|
539
|
-
*/
|
|
540
|
-
async callMethod<T = unknown>(
|
|
541
|
-
_handle: string,
|
|
542
|
-
_methodName: string,
|
|
543
|
-
_args: unknown[],
|
|
544
|
-
_kwargs?: Record<string, unknown>
|
|
545
|
-
): Promise<T> {
|
|
546
|
-
throw new BridgeExecutionError(
|
|
547
|
-
'WorkerPool does not implement callMethod() - use withWorker() to get a transport'
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Not implemented - WorkerPool does not execute Python calls directly.
|
|
553
|
-
*/
|
|
554
|
-
async disposeInstance(_handle: string): Promise<void> {
|
|
555
|
-
throw new BridgeExecutionError(
|
|
556
|
-
'WorkerPool does not implement disposeInstance() - use withWorker() to get a transport'
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
538
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -304,7 +304,6 @@ export interface TywrapOptions {
|
|
|
304
304
|
output: OutputConfig;
|
|
305
305
|
runtime: RuntimeConfig;
|
|
306
306
|
performance: PerformanceConfig;
|
|
307
|
-
development: DevelopmentConfig;
|
|
308
307
|
types?: TypeMappingConfig;
|
|
309
308
|
debug?: boolean;
|
|
310
309
|
}
|
|
@@ -320,7 +319,6 @@ export interface PythonModuleConfig {
|
|
|
320
319
|
excludePatterns?: string[];
|
|
321
320
|
alias?: string;
|
|
322
321
|
typeHints: 'strict' | 'loose' | 'ignore';
|
|
323
|
-
watch?: boolean;
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
export interface OutputConfig {
|
|
@@ -360,24 +358,22 @@ export interface PerformanceConfig {
|
|
|
360
358
|
compression: 'auto' | 'gzip' | 'brotli' | 'none';
|
|
361
359
|
}
|
|
362
360
|
|
|
363
|
-
export interface DevelopmentConfig {
|
|
364
|
-
hotReload: boolean;
|
|
365
|
-
sourceMap: boolean;
|
|
366
|
-
validation: 'runtime' | 'compile' | 'both' | 'none';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
361
|
export type TypePreset = 'numpy' | 'pandas' | 'pydantic' | 'stdlib' | 'scipy' | 'torch' | 'sklearn';
|
|
370
362
|
|
|
371
363
|
export interface TypeMappingConfig {
|
|
372
364
|
presets?: TypePreset[];
|
|
373
365
|
}
|
|
374
366
|
|
|
367
|
+
/** Known bridge backends. Each speaks the identical "tywrap/1" protocol. */
|
|
368
|
+
export type BridgeBackend = 'python-subprocess' | 'pyodide' | 'http';
|
|
369
|
+
|
|
375
370
|
export interface BridgeInfo {
|
|
376
371
|
protocol: string;
|
|
377
372
|
protocolVersion: number;
|
|
378
|
-
bridge:
|
|
373
|
+
bridge: BridgeBackend;
|
|
379
374
|
pythonVersion: string;
|
|
380
|
-
|
|
375
|
+
/** OS process id for subprocess backends; null for in-WASM (Pyodide). */
|
|
376
|
+
pid: number | null;
|
|
381
377
|
codecFallback: 'json' | 'none';
|
|
382
378
|
arrowAvailable: boolean;
|
|
383
379
|
scipyAvailable: boolean;
|
|
@@ -432,8 +428,14 @@ export interface GenerationMetadata {
|
|
|
432
428
|
optimizations: string[];
|
|
433
429
|
}
|
|
434
430
|
|
|
435
|
-
//
|
|
436
|
-
|
|
431
|
+
// PythonRuntime — the four cross-boundary RPC methods generated wrappers call.
|
|
432
|
+
// This is the contract that, after the composition rework, is implemented ONLY
|
|
433
|
+
// by the bridge facades (NodeBridge/HttpBridge/PyodideBridge); the facades
|
|
434
|
+
// satisfy it by delegating to an owned RpcClient. It deliberately carries NO
|
|
435
|
+
// lifecycle method — dispose() is a separate lifecycle concern (see Disposable
|
|
436
|
+
// in runtime/disposable.ts), so transports and the lifecycle base class
|
|
437
|
+
// (DisposableBase) never have to stub these methods.
|
|
438
|
+
export interface PythonRuntime {
|
|
437
439
|
call<T = unknown>(
|
|
438
440
|
module: string,
|
|
439
441
|
functionName: string,
|
|
@@ -456,7 +458,13 @@ export interface RuntimeExecution {
|
|
|
456
458
|
): Promise<T>;
|
|
457
459
|
|
|
458
460
|
disposeInstance(handle: string): Promise<void>;
|
|
461
|
+
}
|
|
459
462
|
|
|
463
|
+
// Runtime bridge interface — the four RPC methods plus lifecycle dispose().
|
|
464
|
+
// Kept as PythonRuntime + dispose() so getRuntimeBridge() and every existing
|
|
465
|
+
// `RuntimeExecution` reference (registry, dev.ts) compile with zero churn. The
|
|
466
|
+
// RuntimeExecution -> PythonRuntime symbol rename is deferred to the T9 pass.
|
|
467
|
+
export interface RuntimeExecution extends PythonRuntime {
|
|
460
468
|
dispose(): Promise<void>;
|
|
461
469
|
}
|
|
462
470
|
|
package/src/tywrap.ts
CHANGED
|
@@ -86,9 +86,16 @@ export interface GenerateRunOptions {
|
|
|
86
86
|
check?: boolean;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export interface GenerateFailure {
|
|
90
|
+
module: string;
|
|
91
|
+
code: 'ir-unavailable';
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
export interface GenerateResult {
|
|
90
96
|
written: string[];
|
|
91
97
|
warnings: string[];
|
|
98
|
+
failures: GenerateFailure[];
|
|
92
99
|
/**
|
|
93
100
|
* Only set when `GenerateRunOptions.check === true`.
|
|
94
101
|
* Lists files that are missing or differ from what would be generated.
|
|
@@ -123,6 +130,7 @@ export async function generate(
|
|
|
123
130
|
const written: string[] = [];
|
|
124
131
|
const outOfDate: string[] = [];
|
|
125
132
|
const warnings: string[] = [];
|
|
133
|
+
const failures: GenerateFailure[] = [];
|
|
126
134
|
const outputDir = resolvedOptions.output.dir;
|
|
127
135
|
const caching = resolvedOptions.performance.caching;
|
|
128
136
|
const cacheDir = '.tywrap/cache';
|
|
@@ -173,7 +181,13 @@ export async function generate(
|
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
if (!ir) {
|
|
176
|
-
|
|
184
|
+
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
185
|
+
warnings.push(message);
|
|
186
|
+
failures.push({
|
|
187
|
+
module: moduleKey,
|
|
188
|
+
code: 'ir-unavailable',
|
|
189
|
+
message,
|
|
190
|
+
});
|
|
177
191
|
continue;
|
|
178
192
|
}
|
|
179
193
|
|
|
@@ -324,10 +338,10 @@ export async function generate(
|
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
if (checkMode) {
|
|
327
|
-
return { written: [], warnings, outOfDate };
|
|
341
|
+
return { written: [], warnings, failures, outOfDate };
|
|
328
342
|
}
|
|
329
343
|
|
|
330
|
-
return { written, warnings };
|
|
344
|
+
return { written, warnings, failures };
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
/**
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { BridgeProtocolError, BridgeTimeoutError } from './errors.js';
|
|
2
|
-
export type RpcMethod = 'call' | 'instantiate' | 'call_method' | 'dispose_instance' | 'meta';
|
|
3
|
-
export interface RpcRequest {
|
|
4
|
-
id: number;
|
|
5
|
-
protocol: string;
|
|
6
|
-
method: RpcMethod;
|
|
7
|
-
params: unknown;
|
|
8
|
-
}
|
|
9
|
-
export interface RpcResponse<T = unknown> {
|
|
10
|
-
id: number;
|
|
11
|
-
protocol: string;
|
|
12
|
-
result?: T;
|
|
13
|
-
error?: {
|
|
14
|
-
type: string;
|
|
15
|
-
message: string;
|
|
16
|
-
traceback?: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export interface BridgeCoreTransport {
|
|
20
|
-
write: (data: string) => void;
|
|
21
|
-
}
|
|
22
|
-
export interface BridgeCoreOptions {
|
|
23
|
-
timeoutMs: number;
|
|
24
|
-
maxLineLength?: number;
|
|
25
|
-
maxStderrBytes?: number;
|
|
26
|
-
protocol?: string;
|
|
27
|
-
protocolVersion?: number;
|
|
28
|
-
decodeValue?: (value: unknown) => Promise<unknown>;
|
|
29
|
-
onFatalError?: (error: BridgeProtocolError) => void;
|
|
30
|
-
onTimeout?: (error: BridgeTimeoutError) => void;
|
|
31
|
-
}
|
|
32
|
-
export declare class BridgeCore {
|
|
33
|
-
private readonly transport;
|
|
34
|
-
private readonly options;
|
|
35
|
-
private readonly timedOutRequests;
|
|
36
|
-
private readonly pending;
|
|
37
|
-
private nextId;
|
|
38
|
-
private stdoutBuffer;
|
|
39
|
-
private stderrBuffer;
|
|
40
|
-
private protocolError;
|
|
41
|
-
constructor(transport: BridgeCoreTransport, options: BridgeCoreOptions);
|
|
42
|
-
getStderrTail(): string;
|
|
43
|
-
getPendingCount(): number;
|
|
44
|
-
clear(): void;
|
|
45
|
-
send<T>(payload: Omit<RpcRequest, 'id' | 'protocol'>): Promise<T>;
|
|
46
|
-
handleStdoutData(chunk: Buffer | string): void;
|
|
47
|
-
handleStderrData(chunk: Buffer | string): void;
|
|
48
|
-
handleProcessExit(): void;
|
|
49
|
-
handleProcessError(err: Error): void;
|
|
50
|
-
private handleResponseLine;
|
|
51
|
-
private errorFrom;
|
|
52
|
-
private normalizeErrorPayload;
|
|
53
|
-
private describeInvalidErrorPayload;
|
|
54
|
-
private failRequest;
|
|
55
|
-
private rejectAll;
|
|
56
|
-
private handleProtocolError;
|
|
57
|
-
private handleFatalError;
|
|
58
|
-
}
|
|
59
|
-
export declare function validateBridgeInfo(info: unknown): void;
|
|
60
|
-
export declare function getPathKey(...envs: Array<Record<string, string | undefined>>): string;
|
|
61
|
-
export declare function ensurePythonEncoding(env: NodeJS.ProcessEnv): void;
|
|
62
|
-
export declare function ensureJsonFallback(env: NodeJS.ProcessEnv, enabled: boolean): void;
|
|
63
|
-
export declare function getMaxLineLengthFromEnv(env: NodeJS.ProcessEnv): number | undefined;
|
|
64
|
-
export declare function normalizeEnv(baseEnv: Record<string, string | undefined>, overrides: Record<string, string | undefined>): NodeJS.ProcessEnv;
|
|
65
|
-
//# sourceMappingURL=bridge-core.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-core.d.ts","sourceRoot":"","sources":["../../src/runtime/bridge-core.ts"],"names":[],"mappings":"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"}
|