tywrap 0.4.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 +1 -0
- package/dist/index.d.ts +3 -3
- 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 +139 -151
- package/dist/runtime/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 +2 -19
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +2 -30
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +8 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +10 -10
- 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/index.ts +6 -6
- 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 +165 -217
- 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 +2 -58
- package/src/types/index.ts +20 -4
- 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
package/src/runtime/node.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node.js runtime bridge
|
|
2
|
+
* Node.js runtime bridge.
|
|
3
3
|
*
|
|
4
|
-
* NodeBridge
|
|
5
|
-
*
|
|
4
|
+
* NodeBridge is a thin facade: it extends DisposableBase (lifecycle/resources)
|
|
5
|
+
* and implements PythonRuntime by HOLDING an RpcClient. It uses ProcessIO
|
|
6
|
+
* transports with optional pooling for concurrent Python execution.
|
|
6
7
|
*
|
|
7
8
|
* @see https://github.com/bbopen/tywrap/issues/149
|
|
8
9
|
*/
|
|
@@ -12,17 +13,18 @@ import { delimiter, isAbsolute, join, resolve } from 'node:path';
|
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { createRequire } from 'node:module';
|
|
14
15
|
|
|
16
|
+
import type { PythonRuntime, BridgeInfo } from '../types/index.js';
|
|
15
17
|
import { autoRegisterArrowDecoder } from '../utils/codec.js';
|
|
16
18
|
import { getDefaultPythonPath } from '../utils/python.js';
|
|
17
19
|
import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
|
|
18
20
|
import { globalCache } from '../utils/cache.js';
|
|
19
21
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
+
import { DisposableBase } from './bounded-context.js';
|
|
23
|
+
import { RpcClient, type GetBridgeInfoOptions } from './rpc-client.js';
|
|
24
|
+
import { BridgeCodecError, BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
22
25
|
import { ProcessIO } from './process-io.js';
|
|
23
26
|
import { PooledTransport } from './pooled-transport.js';
|
|
24
27
|
import type { CodecOptions } from './safe-codec.js';
|
|
25
|
-
import { PROTOCOL_ID } from './transport.js';
|
|
26
28
|
import type { PooledWorker } from './worker-pool.js';
|
|
27
29
|
|
|
28
30
|
// =============================================================================
|
|
@@ -303,9 +305,10 @@ function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']):
|
|
|
303
305
|
* await pooledBridge.init();
|
|
304
306
|
* ```
|
|
305
307
|
*/
|
|
306
|
-
export class NodeBridge extends
|
|
308
|
+
export class NodeBridge extends DisposableBase implements PythonRuntime {
|
|
307
309
|
private readonly resolvedOptions: ResolvedOptions;
|
|
308
310
|
private readonly pooledTransport: PooledTransport;
|
|
311
|
+
private readonly rpc: RpcClient;
|
|
309
312
|
|
|
310
313
|
/**
|
|
311
314
|
* Create a new NodeBridge instance.
|
|
@@ -343,11 +346,19 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
343
346
|
// Build environment for ProcessIO
|
|
344
347
|
const processEnv = buildProcessEnv(resolvedOptions);
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
// Why a late-bound holder: PooledTransport copies onWorkerReady eagerly at
|
|
350
|
+
// construction (before super()), but the callback needs the RpcClient, and
|
|
351
|
+
// a derived constructor cannot touch `this` before super(). So the closure
|
|
352
|
+
// captures a mutable holder whose .rpc is assigned right after super(). The
|
|
353
|
+
// closure body only runs at worker-spawn time during init(), by which point
|
|
354
|
+
// rpc is set. Warmup goes through rpc.sendOn (NOT rpc.execute): it runs
|
|
355
|
+
// inside transport.init() during rpc.init(), so it must not auto-init.
|
|
356
|
+
const rpcHolder: { rpc?: RpcClient } = {};
|
|
357
|
+
const onWorkerReady = createWorkerReadyCallback(
|
|
358
|
+
rpcHolder,
|
|
359
|
+
resolvedOptions.warmupCommands,
|
|
360
|
+
resolvedOptions.timeoutMs
|
|
361
|
+
);
|
|
351
362
|
|
|
352
363
|
// Create pooled transport with ProcessIO workers
|
|
353
364
|
const transport = new PooledTransport({
|
|
@@ -366,17 +377,21 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
366
377
|
onReplacementWorkerReady: onWorkerReady,
|
|
367
378
|
});
|
|
368
379
|
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
super();
|
|
381
|
+
|
|
382
|
+
this.resolvedOptions = resolvedOptions;
|
|
383
|
+
this.pooledTransport = transport;
|
|
384
|
+
this.rpc = new RpcClient({
|
|
371
385
|
transport,
|
|
372
386
|
codec: resolvedOptions.codec,
|
|
373
387
|
defaultTimeoutMs: resolvedOptions.timeoutMs,
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
|
|
388
|
+
});
|
|
389
|
+
// Track the RpcClient (which itself tracks the transport): one disposal
|
|
390
|
+
// chain facade -> rpc -> transport, and rpc.doDispose clears its info cache.
|
|
391
|
+
this.trackResource(this.rpc);
|
|
377
392
|
|
|
378
|
-
|
|
379
|
-
|
|
393
|
+
// Publish the rpc into the holder so the warmup closure can reach it.
|
|
394
|
+
rpcHolder.rpc = this.rpc;
|
|
380
395
|
}
|
|
381
396
|
|
|
382
397
|
// ===========================================================================
|
|
@@ -387,9 +402,10 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
387
402
|
* Initialize the bridge.
|
|
388
403
|
*
|
|
389
404
|
* Validates the bridge script exists, registers Arrow decoder,
|
|
390
|
-
* and initializes the
|
|
405
|
+
* and initializes the held RpcClient (which initializes the transport pool,
|
|
406
|
+
* running warmup commands per-worker).
|
|
391
407
|
*/
|
|
392
|
-
protected
|
|
408
|
+
protected async doInit(): Promise<void> {
|
|
393
409
|
// Validate script exists
|
|
394
410
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- script path is user-configured
|
|
395
411
|
if (!existsSync(this.resolvedOptions.scriptPath)) {
|
|
@@ -404,18 +420,29 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
404
420
|
loader: () => require('apache-arrow'),
|
|
405
421
|
});
|
|
406
422
|
|
|
407
|
-
// Initialize
|
|
408
|
-
await
|
|
423
|
+
// Initialize the RpcClient (which initializes transport and runs warmup).
|
|
424
|
+
await this.rpc.init();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* No facade-specific teardown: the RpcClient (and through it the transport
|
|
429
|
+
* pool) is tracked as a resource and disposed automatically by DisposableBase.
|
|
430
|
+
*/
|
|
431
|
+
protected async doDispose(): Promise<void> {
|
|
432
|
+
// Intentionally empty; tracked resources handle disposal.
|
|
409
433
|
}
|
|
410
434
|
|
|
411
435
|
// ===========================================================================
|
|
412
|
-
//
|
|
436
|
+
// RPC METHODS (delegate to the held RpcClient)
|
|
413
437
|
// ===========================================================================
|
|
414
438
|
|
|
415
439
|
/**
|
|
416
|
-
*
|
|
440
|
+
* Call a Python function, with optional result caching.
|
|
441
|
+
*
|
|
442
|
+
* Cache lookup stays FIRST so cache hits return without forcing init,
|
|
443
|
+
* preserving the pre-composition behavior.
|
|
417
444
|
*/
|
|
418
|
-
|
|
445
|
+
async call<T = unknown>(
|
|
419
446
|
module: string,
|
|
420
447
|
functionName: string,
|
|
421
448
|
args: unknown[],
|
|
@@ -433,7 +460,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
433
460
|
|
|
434
461
|
// Execute and cache if pure function
|
|
435
462
|
const startTime = performance.now();
|
|
436
|
-
|
|
463
|
+
await this.ensureReady();
|
|
464
|
+
const result = await this.rpc.call<T>(module, functionName, args, kwargs);
|
|
437
465
|
const duration = performance.now() - startTime;
|
|
438
466
|
|
|
439
467
|
if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
|
|
@@ -447,7 +475,52 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
447
475
|
}
|
|
448
476
|
|
|
449
477
|
// No caching - direct call
|
|
450
|
-
|
|
478
|
+
await this.ensureReady();
|
|
479
|
+
return this.rpc.call<T>(module, functionName, args, kwargs);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async instantiate<T = unknown>(
|
|
483
|
+
module: string,
|
|
484
|
+
className: string,
|
|
485
|
+
args: unknown[],
|
|
486
|
+
kwargs?: Record<string, unknown>
|
|
487
|
+
): Promise<T> {
|
|
488
|
+
await this.ensureReady();
|
|
489
|
+
return this.rpc.instantiate<T>(module, className, args, kwargs);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async callMethod<T = unknown>(
|
|
493
|
+
handle: string,
|
|
494
|
+
methodName: string,
|
|
495
|
+
args: unknown[],
|
|
496
|
+
kwargs?: Record<string, unknown>
|
|
497
|
+
): Promise<T> {
|
|
498
|
+
await this.ensureReady();
|
|
499
|
+
return this.rpc.callMethod<T>(handle, methodName, args, kwargs);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async disposeInstance(handle: string): Promise<void> {
|
|
503
|
+
await this.ensureReady();
|
|
504
|
+
return this.rpc.disposeInstance(handle);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Fetch bridge diagnostics and feature availability.
|
|
509
|
+
*/
|
|
510
|
+
async getBridgeInfo(options?: GetBridgeInfoOptions): Promise<BridgeInfo> {
|
|
511
|
+
await this.ensureReady();
|
|
512
|
+
return this.rpc.getBridgeInfo(options);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Ensure the facade is initialized before delegating an RPC. Replicates the
|
|
517
|
+
* auto-init that BoundedContext.execute() gave for free, so the facade's
|
|
518
|
+
* own doInit pre-work (script check, Arrow decoder) runs before any RPC.
|
|
519
|
+
*/
|
|
520
|
+
private async ensureReady(): Promise<void> {
|
|
521
|
+
if (!this.isReady) {
|
|
522
|
+
await this.init();
|
|
523
|
+
}
|
|
451
524
|
}
|
|
452
525
|
|
|
453
526
|
// ===========================================================================
|
|
@@ -556,216 +629,91 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
556
629
|
// =============================================================================
|
|
557
630
|
|
|
558
631
|
/**
|
|
559
|
-
*
|
|
632
|
+
* Wrap an error from rpc.sendOn() with the warmup command's context label,
|
|
633
|
+
* preserving the exact phrasing the warmup tests assert on. The single
|
|
634
|
+
* RpcClient codec+id-counter is reused (via sendOn); only this label wrapping
|
|
635
|
+
* is bespoke, and only because warmup surfaces command-specific diagnostics.
|
|
636
|
+
*
|
|
637
|
+
* Maps codec/protocol error shapes to the historical messages:
|
|
638
|
+
* - encode failure (BridgeCodecError, e.g. BigInt) -> "failed to encode request"
|
|
639
|
+
* - missing result/error envelope (BridgeProtocolError) -> "malformed response envelope"
|
|
640
|
+
* - Python error (BridgeExecutionError "Type: message") -> "failed: Type: message"
|
|
641
|
+
* - anything else (transport/send) -> "failed to send"
|
|
560
642
|
*/
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function sendWarmupRequest(
|
|
567
|
-
worker: PooledWorker,
|
|
568
|
-
timeoutMs: number,
|
|
569
|
-
requestId: number,
|
|
570
|
-
label: string,
|
|
571
|
-
messagePayload: Record<string, unknown>
|
|
572
|
-
): Promise<void> {
|
|
573
|
-
let message: string;
|
|
574
|
-
try {
|
|
575
|
-
message = JSON.stringify({
|
|
576
|
-
id: requestId,
|
|
577
|
-
protocol: PROTOCOL_ID,
|
|
578
|
-
...messagePayload,
|
|
579
|
-
});
|
|
580
|
-
} catch (error) {
|
|
581
|
-
throw new BridgeExecutionError(
|
|
582
|
-
`${label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
let response: string;
|
|
588
|
-
try {
|
|
589
|
-
response = await worker.transport.send(message, timeoutMs);
|
|
590
|
-
} catch (error) {
|
|
591
|
-
throw new BridgeExecutionError(
|
|
592
|
-
`${label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
593
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
let parsed: unknown;
|
|
598
|
-
try {
|
|
599
|
-
parsed = JSON.parse(response);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
throw new BridgeExecutionError(`${label} returned invalid JSON response`, {
|
|
602
|
-
cause: error instanceof Error ? error : undefined,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
643
|
+
function wrapWarmupError(label: string, error: unknown): BridgeExecutionError {
|
|
644
|
+
const cause = error instanceof Error ? error : undefined;
|
|
645
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
646
|
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
`${label} returned malformed response envelope for request ${requestId}`
|
|
609
|
-
);
|
|
647
|
+
if (error instanceof BridgeCodecError && error.codecPhase === 'encode') {
|
|
648
|
+
return new BridgeExecutionError(`${label} failed to encode request: ${message}`, { cause });
|
|
610
649
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown warmup error';
|
|
619
|
-
throw new BridgeExecutionError(`${label} failed: ${errType}: ${errMessage}`);
|
|
650
|
+
if (error instanceof BridgeProtocolError) {
|
|
651
|
+
// The codec rejects an envelope that has neither "result" nor "error".
|
|
652
|
+
// Distinguish that specific malformed-envelope case from transport-level
|
|
653
|
+
// protocol errors (e.g. garbled stdout), whose original message must be
|
|
654
|
+
// preserved so callers still see the underlying "Protocol error" text.
|
|
655
|
+
if (/missing "result" or "error"/.test(message)) {
|
|
656
|
+
return new BridgeExecutionError(`${label} returned malformed response envelope`, { cause });
|
|
620
657
|
}
|
|
621
|
-
|
|
622
|
-
throw new BridgeExecutionError(`${label} failed with malformed error payload`);
|
|
658
|
+
return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
|
|
623
659
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
`${label} returned malformed response envelope for request ${requestId}`
|
|
628
|
-
);
|
|
660
|
+
if (error instanceof BridgeExecutionError) {
|
|
661
|
+
// Python-side error already formatted as "Type: message" by the codec.
|
|
662
|
+
return new BridgeExecutionError(`${label} failed: ${message}`, { cause });
|
|
629
663
|
}
|
|
664
|
+
return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
|
|
630
665
|
}
|
|
631
666
|
|
|
632
667
|
/**
|
|
633
|
-
*
|
|
668
|
+
* Build the per-worker onWorkerReady callback. It runs at worker-spawn time
|
|
669
|
+
* during init(); by then rpcHolder.rpc is set (assigned right after super()).
|
|
634
670
|
*
|
|
635
|
-
* The callback
|
|
636
|
-
*
|
|
671
|
+
* The callback first does a readiness `meta` probe, then runs each warmup
|
|
672
|
+
* command. Both go through rpc.sendOn (raw encode/send/decode, NO auto-init)
|
|
673
|
+
* so they cannot re-await the in-flight rpc.init().
|
|
637
674
|
*/
|
|
638
|
-
function
|
|
675
|
+
function createWorkerReadyCallback(
|
|
676
|
+
rpcHolder: { rpc?: RpcClient },
|
|
639
677
|
warmupCommands: WarmupCommand[],
|
|
640
678
|
timeoutMs: number
|
|
641
679
|
): (worker: PooledWorker) => Promise<void> {
|
|
642
680
|
return async (worker: PooledWorker) => {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
worker,
|
|
647
|
-
{
|
|
648
|
-
module: cmd.module,
|
|
649
|
-
functionName: cmd.functionName,
|
|
650
|
-
args: cmd.args ?? [],
|
|
651
|
-
},
|
|
652
|
-
timeoutMs,
|
|
653
|
-
{
|
|
654
|
-
label: `Warmup command #${index + 1} (${commandLabel})`,
|
|
655
|
-
invalidJsonMessage: 'returned invalid JSON response',
|
|
656
|
-
malformedEnvelopeMessage: requestId =>
|
|
657
|
-
`returned malformed response envelope for request ${requestId}`,
|
|
658
|
-
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
659
|
-
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
660
|
-
}
|
|
661
|
-
);
|
|
681
|
+
const rpc = rpcHolder.rpc;
|
|
682
|
+
if (!rpc) {
|
|
683
|
+
throw new BridgeExecutionError('Worker warmup attempted before RpcClient was wired');
|
|
662
684
|
}
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
685
|
|
|
666
|
-
function createWorkerReadyCallback(
|
|
667
|
-
timeoutMs: number,
|
|
668
|
-
extraWarmup?: (worker: PooledWorker) => Promise<void>
|
|
669
|
-
): (worker: PooledWorker) => Promise<void> {
|
|
670
|
-
return async (worker: PooledWorker) => {
|
|
671
686
|
const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
|
|
672
|
-
await sendWarmupRequest(worker, readyTimeoutMs, generateWarmupId(), 'Worker warmup check', {
|
|
673
|
-
method: 'meta',
|
|
674
|
-
params: {},
|
|
675
|
-
});
|
|
676
687
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
module: string;
|
|
683
|
-
functionName: string;
|
|
684
|
-
args: unknown[];
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
interface WorkerCallErrorMessages {
|
|
688
|
-
label: string;
|
|
689
|
-
invalidJsonMessage: string;
|
|
690
|
-
malformedEnvelopeMessage: (requestId: number) => string;
|
|
691
|
-
pythonErrorMessage: (errorType: string, errorMessage: string) => string;
|
|
692
|
-
malformedErrorPayloadMessage: string;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
async function executeWorkerCall(
|
|
696
|
-
worker: PooledWorker,
|
|
697
|
-
request: WorkerCallSpec,
|
|
698
|
-
timeoutMs: number,
|
|
699
|
-
messages: WorkerCallErrorMessages
|
|
700
|
-
): Promise<unknown> {
|
|
701
|
-
const requestId = generateWarmupId();
|
|
702
|
-
let message: string;
|
|
703
|
-
try {
|
|
704
|
-
message = JSON.stringify({
|
|
705
|
-
id: requestId,
|
|
706
|
-
protocol: PROTOCOL_ID,
|
|
707
|
-
method: 'call',
|
|
708
|
-
params: {
|
|
709
|
-
module: request.module,
|
|
710
|
-
functionName: request.functionName,
|
|
711
|
-
args: request.args,
|
|
712
|
-
kwargs: {},
|
|
713
|
-
},
|
|
714
|
-
});
|
|
715
|
-
} catch (error) {
|
|
716
|
-
throw new BridgeExecutionError(
|
|
717
|
-
`${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
718
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
let response: string;
|
|
723
|
-
try {
|
|
724
|
-
response = await worker.transport.send(message, timeoutMs);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
throw new BridgeExecutionError(
|
|
727
|
-
`${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
728
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
let parsed: unknown;
|
|
733
|
-
try {
|
|
734
|
-
parsed = JSON.parse(response);
|
|
735
|
-
} catch (error) {
|
|
736
|
-
throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
|
|
737
|
-
cause: error instanceof Error ? error : undefined,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
742
|
-
throw new BridgeExecutionError(
|
|
743
|
-
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
748
|
-
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
749
|
-
const errorPayload = envelope.error;
|
|
750
|
-
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
751
|
-
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
752
|
-
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
753
|
-
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
|
|
754
|
-
throw new BridgeExecutionError(
|
|
755
|
-
`${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
|
|
756
|
-
);
|
|
688
|
+
// Readiness probe (mirrors getBridgeInfo's meta request, per-worker).
|
|
689
|
+
try {
|
|
690
|
+
await rpc.sendOn(worker.transport, { method: 'meta', params: {} }, { timeoutMs: readyTimeoutMs });
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throw wrapWarmupError('Worker warmup check', error);
|
|
757
693
|
}
|
|
758
694
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
695
|
+
// User-provided warmup commands.
|
|
696
|
+
for (const [index, cmd] of warmupCommands.entries()) {
|
|
697
|
+
const label = `Warmup command #${index + 1} (${cmd.module}.${cmd.functionName})`;
|
|
698
|
+
try {
|
|
699
|
+
await rpc.sendOn(
|
|
700
|
+
worker.transport,
|
|
701
|
+
{
|
|
702
|
+
method: 'call',
|
|
703
|
+
params: {
|
|
704
|
+
module: cmd.module,
|
|
705
|
+
functionName: cmd.functionName,
|
|
706
|
+
args: cmd.args ?? [],
|
|
707
|
+
kwargs: {},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
{ timeoutMs }
|
|
711
|
+
);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
throw wrapWarmupError(label, error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
769
717
|
}
|
|
770
718
|
|
|
771
719
|
// =============================================================================
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @see https://github.com/bbopen/tywrap/issues/149
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { DisposableBase } from './bounded-context.js';
|
|
12
12
|
import { BridgeDisposedError, BridgeExecutionError } from './errors.js';
|
|
13
13
|
import type { Transport } from './transport.js';
|
|
14
14
|
import { WorkerPool, type PooledWorker } from './worker-pool.js';
|
|
@@ -82,7 +82,7 @@ export interface PooledTransportOptions {
|
|
|
82
82
|
* await transport.dispose();
|
|
83
83
|
* ```
|
|
84
84
|
*/
|
|
85
|
-
export class PooledTransport extends
|
|
85
|
+
export class PooledTransport extends DisposableBase implements Transport {
|
|
86
86
|
private readonly poolOptions: Omit<
|
|
87
87
|
Required<PooledTransportOptions>,
|
|
88
88
|
'onWorkerReady' | 'onReplacementWorkerReady'
|
|
@@ -205,59 +205,4 @@ export class PooledTransport extends BoundedContext implements Transport {
|
|
|
205
205
|
get totalInFlight(): number {
|
|
206
206
|
return this.pool?.totalInFlight ?? 0;
|
|
207
207
|
}
|
|
208
|
-
|
|
209
|
-
// ===========================================================================
|
|
210
|
-
// RUNTIME EXECUTION (Not implemented - PooledTransport is just a transport)
|
|
211
|
-
// ===========================================================================
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Not implemented - PooledTransport is a transport, use BridgeProtocol.
|
|
215
|
-
*/
|
|
216
|
-
async call<T = unknown>(
|
|
217
|
-
_module: string,
|
|
218
|
-
_functionName: string,
|
|
219
|
-
_args: unknown[],
|
|
220
|
-
_kwargs?: Record<string, unknown>
|
|
221
|
-
): Promise<T> {
|
|
222
|
-
throw new BridgeExecutionError(
|
|
223
|
-
'PooledTransport is a transport, use BridgeProtocol for operations'
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Not implemented - PooledTransport is a transport, use BridgeProtocol.
|
|
229
|
-
*/
|
|
230
|
-
async instantiate<T = unknown>(
|
|
231
|
-
_module: string,
|
|
232
|
-
_className: string,
|
|
233
|
-
_args: unknown[],
|
|
234
|
-
_kwargs?: Record<string, unknown>
|
|
235
|
-
): Promise<T> {
|
|
236
|
-
throw new BridgeExecutionError(
|
|
237
|
-
'PooledTransport is a transport, use BridgeProtocol for operations'
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Not implemented - PooledTransport is a transport, use BridgeProtocol.
|
|
243
|
-
*/
|
|
244
|
-
async callMethod<T = unknown>(
|
|
245
|
-
_handle: string,
|
|
246
|
-
_methodName: string,
|
|
247
|
-
_args: unknown[],
|
|
248
|
-
_kwargs?: Record<string, unknown>
|
|
249
|
-
): Promise<T> {
|
|
250
|
-
throw new BridgeExecutionError(
|
|
251
|
-
'PooledTransport is a transport, use BridgeProtocol for operations'
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Not implemented - PooledTransport is a transport, use BridgeProtocol.
|
|
257
|
-
*/
|
|
258
|
-
async disposeInstance(_handle: string): Promise<void> {
|
|
259
|
-
throw new BridgeExecutionError(
|
|
260
|
-
'PooledTransport is a transport, use BridgeProtocol for operations'
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
208
|
}
|
|
@@ -14,13 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { spawn, type ChildProcess } from 'child_process';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
BridgeDisposedError,
|
|
20
|
-
BridgeProtocolError,
|
|
21
|
-
BridgeTimeoutError,
|
|
22
|
-
BridgeExecutionError,
|
|
23
|
-
} from './errors.js';
|
|
17
|
+
import { DisposableBase } from './bounded-context.js';
|
|
18
|
+
import { BridgeDisposedError, BridgeProtocolError, BridgeTimeoutError } from './errors.js';
|
|
24
19
|
import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
|
|
25
20
|
import type { Transport } from './transport.js';
|
|
26
21
|
|
|
@@ -162,7 +157,7 @@ function extractMessageId(json: string): number | null {
|
|
|
162
157
|
* await transport.dispose();
|
|
163
158
|
* ```
|
|
164
159
|
*/
|
|
165
|
-
export class ProcessIO extends
|
|
160
|
+
export class ProcessIO extends DisposableBase implements Transport {
|
|
166
161
|
// Configuration
|
|
167
162
|
private readonly pythonPath: string;
|
|
168
163
|
private readonly bridgeScript: string;
|
|
@@ -372,53 +367,6 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
372
367
|
this.requestCount = 0;
|
|
373
368
|
}
|
|
374
369
|
|
|
375
|
-
// ===========================================================================
|
|
376
|
-
// ABSTRACT METHOD STUBS (from BoundedContext)
|
|
377
|
-
// ===========================================================================
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Not implemented - ProcessIO is a transport, not a full bridge.
|
|
381
|
-
*/
|
|
382
|
-
call<T = unknown>(
|
|
383
|
-
_module: string,
|
|
384
|
-
_functionName: string,
|
|
385
|
-
_args: unknown[],
|
|
386
|
-
_kwargs?: Record<string, unknown>
|
|
387
|
-
): Promise<T> {
|
|
388
|
-
throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Not implemented - ProcessIO is a transport, not a full bridge.
|
|
393
|
-
*/
|
|
394
|
-
instantiate<T = unknown>(
|
|
395
|
-
_module: string,
|
|
396
|
-
_className: string,
|
|
397
|
-
_args: unknown[],
|
|
398
|
-
_kwargs?: Record<string, unknown>
|
|
399
|
-
): Promise<T> {
|
|
400
|
-
throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Not implemented - ProcessIO is a transport, not a full bridge.
|
|
405
|
-
*/
|
|
406
|
-
callMethod<T = unknown>(
|
|
407
|
-
_handle: string,
|
|
408
|
-
_methodName: string,
|
|
409
|
-
_args: unknown[],
|
|
410
|
-
_kwargs?: Record<string, unknown>
|
|
411
|
-
): Promise<T> {
|
|
412
|
-
throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Not implemented - ProcessIO is a transport, not a full bridge.
|
|
417
|
-
*/
|
|
418
|
-
disposeInstance(_handle: string): Promise<void> {
|
|
419
|
-
throw new BridgeExecutionError('ProcessIO is a transport, use BridgeProtocol for operations');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
370
|
// ===========================================================================
|
|
423
371
|
// PROCESS MANAGEMENT
|
|
424
372
|
// ===========================================================================
|