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
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.
|
|
@@ -340,16 +343,22 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
340
343
|
codec: options.codec,
|
|
341
344
|
warmupCommands,
|
|
342
345
|
};
|
|
343
|
-
|
|
344
346
|
// Build environment for ProcessIO
|
|
345
347
|
const processEnv = buildProcessEnv(resolvedOptions);
|
|
346
348
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
);
|
|
353
362
|
|
|
354
363
|
// Create pooled transport with ProcessIO workers
|
|
355
364
|
const transport = new PooledTransport({
|
|
@@ -364,21 +373,25 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
364
373
|
minWorkers: resolvedOptions.minProcesses,
|
|
365
374
|
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
366
375
|
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
367
|
-
onWorkerReady
|
|
368
|
-
onReplacementWorkerReady:
|
|
376
|
+
onWorkerReady,
|
|
377
|
+
onReplacementWorkerReady: onWorkerReady,
|
|
369
378
|
});
|
|
370
379
|
|
|
371
|
-
|
|
372
|
-
|
|
380
|
+
super();
|
|
381
|
+
|
|
382
|
+
this.resolvedOptions = resolvedOptions;
|
|
383
|
+
this.pooledTransport = transport;
|
|
384
|
+
this.rpc = new RpcClient({
|
|
373
385
|
transport,
|
|
374
386
|
codec: resolvedOptions.codec,
|
|
375
387
|
defaultTimeoutMs: resolvedOptions.timeoutMs,
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
|
|
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);
|
|
379
392
|
|
|
380
|
-
|
|
381
|
-
|
|
393
|
+
// Publish the rpc into the holder so the warmup closure can reach it.
|
|
394
|
+
rpcHolder.rpc = this.rpc;
|
|
382
395
|
}
|
|
383
396
|
|
|
384
397
|
// ===========================================================================
|
|
@@ -389,9 +402,10 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
389
402
|
* Initialize the bridge.
|
|
390
403
|
*
|
|
391
404
|
* Validates the bridge script exists, registers Arrow decoder,
|
|
392
|
-
* and initializes the
|
|
405
|
+
* and initializes the held RpcClient (which initializes the transport pool,
|
|
406
|
+
* running warmup commands per-worker).
|
|
393
407
|
*/
|
|
394
|
-
protected
|
|
408
|
+
protected async doInit(): Promise<void> {
|
|
395
409
|
// Validate script exists
|
|
396
410
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- script path is user-configured
|
|
397
411
|
if (!existsSync(this.resolvedOptions.scriptPath)) {
|
|
@@ -406,18 +420,29 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
406
420
|
loader: () => require('apache-arrow'),
|
|
407
421
|
});
|
|
408
422
|
|
|
409
|
-
// Initialize
|
|
410
|
-
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.
|
|
411
433
|
}
|
|
412
434
|
|
|
413
435
|
// ===========================================================================
|
|
414
|
-
//
|
|
436
|
+
// RPC METHODS (delegate to the held RpcClient)
|
|
415
437
|
// ===========================================================================
|
|
416
438
|
|
|
417
439
|
/**
|
|
418
|
-
*
|
|
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.
|
|
419
444
|
*/
|
|
420
|
-
|
|
445
|
+
async call<T = unknown>(
|
|
421
446
|
module: string,
|
|
422
447
|
functionName: string,
|
|
423
448
|
args: unknown[],
|
|
@@ -435,7 +460,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
435
460
|
|
|
436
461
|
// Execute and cache if pure function
|
|
437
462
|
const startTime = performance.now();
|
|
438
|
-
|
|
463
|
+
await this.ensureReady();
|
|
464
|
+
const result = await this.rpc.call<T>(module, functionName, args, kwargs);
|
|
439
465
|
const duration = performance.now() - startTime;
|
|
440
466
|
|
|
441
467
|
if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
|
|
@@ -449,7 +475,52 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
449
475
|
}
|
|
450
476
|
|
|
451
477
|
// No caching - direct call
|
|
452
|
-
|
|
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
|
+
}
|
|
453
524
|
}
|
|
454
525
|
|
|
455
526
|
// ===========================================================================
|
|
@@ -558,168 +629,91 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
558
629
|
// =============================================================================
|
|
559
630
|
|
|
560
631
|
/**
|
|
561
|
-
*
|
|
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"
|
|
562
642
|
*/
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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);
|
|
646
|
+
|
|
647
|
+
if (error instanceof BridgeCodecError && error.codecPhase === 'encode') {
|
|
648
|
+
return new BridgeExecutionError(`${label} failed to encode request: ${message}`, { cause });
|
|
649
|
+
}
|
|
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 });
|
|
657
|
+
}
|
|
658
|
+
return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
|
|
659
|
+
}
|
|
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 });
|
|
663
|
+
}
|
|
664
|
+
return new BridgeExecutionError(`${label} failed to send: ${message}`, { cause });
|
|
566
665
|
}
|
|
567
666
|
|
|
568
667
|
/**
|
|
569
|
-
*
|
|
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()).
|
|
570
670
|
*
|
|
571
|
-
* The callback
|
|
572
|
-
*
|
|
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().
|
|
573
674
|
*/
|
|
574
|
-
function
|
|
675
|
+
function createWorkerReadyCallback(
|
|
676
|
+
rpcHolder: { rpc?: RpcClient },
|
|
575
677
|
warmupCommands: WarmupCommand[],
|
|
576
678
|
timeoutMs: number
|
|
577
679
|
): (worker: PooledWorker) => Promise<void> {
|
|
578
680
|
return async (worker: PooledWorker) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
worker,
|
|
583
|
-
{
|
|
584
|
-
module: cmd.module,
|
|
585
|
-
functionName: cmd.functionName,
|
|
586
|
-
args: cmd.args ?? [],
|
|
587
|
-
},
|
|
588
|
-
timeoutMs,
|
|
589
|
-
{
|
|
590
|
-
label: `Warmup command #${index + 1} (${commandLabel})`,
|
|
591
|
-
invalidJsonMessage: 'returned invalid JSON response',
|
|
592
|
-
malformedEnvelopeMessage: requestId =>
|
|
593
|
-
`returned malformed response envelope for request ${requestId}`,
|
|
594
|
-
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
595
|
-
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
596
|
-
}
|
|
597
|
-
);
|
|
681
|
+
const rpc = rpcHolder.rpc;
|
|
682
|
+
if (!rpc) {
|
|
683
|
+
throw new BridgeExecutionError('Worker warmup attempted before RpcClient was wired');
|
|
598
684
|
}
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
685
|
|
|
602
|
-
|
|
603
|
-
timeoutMs: number,
|
|
604
|
-
extraWarmup?: (worker: PooledWorker) => Promise<void>
|
|
605
|
-
): (worker: PooledWorker) => Promise<void> {
|
|
606
|
-
return async (worker: PooledWorker) => {
|
|
607
|
-
const result = await executeWorkerCall(
|
|
608
|
-
worker,
|
|
609
|
-
{
|
|
610
|
-
module: 'builtins',
|
|
611
|
-
functionName: 'len',
|
|
612
|
-
args: [[]],
|
|
613
|
-
},
|
|
614
|
-
Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS),
|
|
615
|
-
{
|
|
616
|
-
label: 'Worker readiness probe (builtins.len)',
|
|
617
|
-
invalidJsonMessage: 'returned invalid JSON response',
|
|
618
|
-
malformedEnvelopeMessage: requestId =>
|
|
619
|
-
`returned malformed response envelope for request ${requestId}`,
|
|
620
|
-
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
621
|
-
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
622
|
-
}
|
|
623
|
-
);
|
|
686
|
+
const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
|
|
624
687
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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);
|
|
629
693
|
}
|
|
630
694
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
request: WorkerCallSpec,
|
|
652
|
-
timeoutMs: number,
|
|
653
|
-
messages: WorkerCallErrorMessages
|
|
654
|
-
): Promise<unknown> {
|
|
655
|
-
const requestId = generateWarmupId();
|
|
656
|
-
let message: string;
|
|
657
|
-
try {
|
|
658
|
-
message = JSON.stringify({
|
|
659
|
-
id: requestId,
|
|
660
|
-
protocol: PROTOCOL_ID,
|
|
661
|
-
method: 'call',
|
|
662
|
-
params: {
|
|
663
|
-
module: request.module,
|
|
664
|
-
functionName: request.functionName,
|
|
665
|
-
args: request.args,
|
|
666
|
-
kwargs: {},
|
|
667
|
-
},
|
|
668
|
-
});
|
|
669
|
-
} catch (error) {
|
|
670
|
-
throw new BridgeExecutionError(
|
|
671
|
-
`${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
672
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
let response: string;
|
|
677
|
-
try {
|
|
678
|
-
response = await worker.transport.send(message, timeoutMs);
|
|
679
|
-
} catch (error) {
|
|
680
|
-
throw new BridgeExecutionError(
|
|
681
|
-
`${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
682
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
let parsed: unknown;
|
|
687
|
-
try {
|
|
688
|
-
parsed = JSON.parse(response);
|
|
689
|
-
} catch (error) {
|
|
690
|
-
throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
|
|
691
|
-
cause: error instanceof Error ? error : undefined,
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
696
|
-
throw new BridgeExecutionError(
|
|
697
|
-
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
702
|
-
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
703
|
-
const errorPayload = envelope.error;
|
|
704
|
-
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
705
|
-
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
706
|
-
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
707
|
-
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
|
|
708
|
-
throw new BridgeExecutionError(
|
|
709
|
-
`${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
|
|
710
|
-
);
|
|
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
|
+
}
|
|
711
715
|
}
|
|
712
|
-
|
|
713
|
-
throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (!('result' in envelope)) {
|
|
717
|
-
throw new BridgeExecutionError(
|
|
718
|
-
`${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
return envelope.result;
|
|
716
|
+
};
|
|
723
717
|
}
|
|
724
718
|
|
|
725
719
|
// =============================================================================
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @deprecated Import from './node.js' instead.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* now supports both single-process mode (default) and multi-process
|
|
6
|
-
* via the minProcesses/maxProcesses options.
|
|
4
|
+
* Backward-compat shim for older deep imports. NodeBridge is the public API,
|
|
5
|
+
* and it now supports both single-process mode (default) and multi-process
|
|
6
|
+
* pooling via the minProcesses/maxProcesses options.
|
|
7
7
|
*
|
|
8
8
|
* Migration:
|
|
9
9
|
* ```typescript
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* const bridge = new NodeBridge({ minProcesses: 2, maxProcesses: 4 });
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
-
* This file is
|
|
19
|
+
* This file is not exposed through the package exports map and is maintained
|
|
20
|
+
* for backward compatibility only.
|
|
20
21
|
*/
|
|
21
22
|
export {
|
|
22
23
|
NodeBridge as OptimizedNodeBridge,
|
|
@@ -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
|
// ===========================================================================
|