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.
Files changed (101) hide show
  1. package/README.md +40 -5
  2. package/dist/cli.js +24 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +19 -13
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/core/analyzer.d.ts.map +1 -1
  8. package/dist/core/analyzer.js +0 -1
  9. package/dist/core/analyzer.js.map +1 -1
  10. package/dist/dev.d.ts +57 -0
  11. package/dist/dev.d.ts.map +1 -0
  12. package/dist/dev.js +743 -0
  13. package/dist/dev.js.map +1 -0
  14. package/dist/index.d.ts +5 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/runtime/base.d.ts +7 -7
  19. package/dist/runtime/base.js +8 -8
  20. package/dist/runtime/bounded-context.d.ts +15 -31
  21. package/dist/runtime/bounded-context.d.ts.map +1 -1
  22. package/dist/runtime/bounded-context.js +16 -15
  23. package/dist/runtime/bounded-context.js.map +1 -1
  24. package/dist/runtime/http.d.ts +24 -5
  25. package/dist/runtime/http.d.ts.map +1 -1
  26. package/dist/runtime/http.js +57 -12
  27. package/dist/runtime/http.js.map +1 -1
  28. package/dist/runtime/node.d.ts +33 -7
  29. package/dist/runtime/node.d.ts.map +1 -1
  30. package/dist/runtime/node.js +144 -120
  31. package/dist/runtime/node.js.map +1 -1
  32. package/dist/runtime/optimized-node.d.ts +5 -4
  33. package/dist/runtime/optimized-node.d.ts.map +1 -1
  34. package/dist/runtime/optimized-node.js +5 -4
  35. package/dist/runtime/optimized-node.js.map +1 -1
  36. package/dist/runtime/pooled-transport.d.ts +2 -18
  37. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  38. package/dist/runtime/pooled-transport.js +2 -29
  39. package/dist/runtime/pooled-transport.js.map +1 -1
  40. package/dist/runtime/process-io.d.ts +2 -18
  41. package/dist/runtime/process-io.d.ts.map +1 -1
  42. package/dist/runtime/process-io.js +3 -30
  43. package/dist/runtime/process-io.js.map +1 -1
  44. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts +12 -0
  45. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -0
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js +12 -0
  47. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -0
  48. package/dist/runtime/pyodide-io.d.ts +7 -37
  49. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  50. package/dist/runtime/pyodide-io.js +90 -214
  51. package/dist/runtime/pyodide-io.js.map +1 -1
  52. package/dist/runtime/pyodide.d.ts +25 -5
  53. package/dist/runtime/pyodide.d.ts.map +1 -1
  54. package/dist/runtime/pyodide.js +59 -10
  55. package/dist/runtime/pyodide.js.map +1 -1
  56. package/dist/runtime/{bridge-protocol.d.ts → rpc-client.d.ts} +59 -46
  57. package/dist/runtime/rpc-client.d.ts.map +1 -0
  58. package/dist/runtime/{bridge-protocol.js → rpc-client.js} +60 -42
  59. package/dist/runtime/rpc-client.js.map +1 -0
  60. package/dist/runtime/worker-pool.d.ts +8 -20
  61. package/dist/runtime/worker-pool.d.ts.map +1 -1
  62. package/dist/runtime/worker-pool.js +58 -59
  63. package/dist/runtime/worker-pool.js.map +1 -1
  64. package/dist/types/index.d.ts +8 -10
  65. package/dist/types/index.d.ts.map +1 -1
  66. package/dist/tywrap.d.ts +6 -0
  67. package/dist/tywrap.d.ts.map +1 -1
  68. package/dist/tywrap.js +10 -3
  69. package/dist/tywrap.js.map +1 -1
  70. package/package.json +22 -18
  71. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  72. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  73. package/runtime/python_bridge.py +85 -702
  74. package/runtime/safe_codec.py +10 -2
  75. package/runtime/tywrap_bridge_core.py +875 -0
  76. package/src/cli.ts +27 -6
  77. package/src/config/index.ts +28 -15
  78. package/src/core/analyzer.ts +1 -2
  79. package/src/dev.ts +983 -0
  80. package/src/index.ts +7 -7
  81. package/src/runtime/base.ts +8 -8
  82. package/src/runtime/bounded-context.ts +17 -56
  83. package/src/runtime/http.ts +85 -13
  84. package/src/runtime/node.ts +172 -178
  85. package/src/runtime/optimized-node.ts +5 -4
  86. package/src/runtime/pooled-transport.ts +2 -57
  87. package/src/runtime/process-io.ts +3 -55
  88. package/src/runtime/pyodide-bootstrap-core.generated.ts +12 -0
  89. package/src/runtime/pyodide-io.ts +92 -243
  90. package/src/runtime/pyodide.ts +87 -10
  91. package/src/runtime/{bridge-protocol.ts → rpc-client.ts} +76 -49
  92. package/src/runtime/worker-pool.ts +67 -88
  93. package/src/types/index.ts +20 -12
  94. package/src/tywrap.ts +17 -3
  95. package/dist/runtime/bridge-core.d.ts +0 -65
  96. package/dist/runtime/bridge-core.d.ts.map +0 -1
  97. package/dist/runtime/bridge-core.js +0 -379
  98. package/dist/runtime/bridge-core.js.map +0 -1
  99. package/dist/runtime/bridge-protocol.d.ts.map +0 -1
  100. package/dist/runtime/bridge-protocol.js.map +0 -1
  101. package/src/runtime/bridge-core.ts +0 -494
@@ -1,24 +1,26 @@
1
1
  /**
2
- * BridgeProtocol - Unified abstraction for JS<->Python communication.
2
+ * RpcClient - the single correlated-RPC client for JS<->Python communication.
3
3
  *
4
- * Combines BoundedContext + SafeCodec + Transport into a single base class
5
- * that handles all cross-boundary concerns:
6
- * - Lifecycle management (init/dispose state machine)
7
- * - Request/response encoding with validation
8
- * - Transport-agnostic message passing
9
- * - Bounded execution (timeout, retry, abort)
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
- * Subclasses (NodeBridge, HttpBridge, PyodideBridge) only need to:
12
- * 1. Create their transport in their constructor
13
- * 2. Pass it to super() via BridgeProtocolOptions
14
- * 3. Optionally override doInit() and doDispose() for additional setup/teardown
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 { BoundedContext, type ExecuteOptions } from './bounded-context.js';
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 BridgeProtocol.
42
+ * Configuration options for RpcClient.
41
43
  */
42
- export interface BridgeProtocolOptions {
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
- // BRIDGE PROTOCOL BASE CLASS
179
+ // RPC CLIENT
178
180
  // =============================================================================
179
181
 
180
182
  /**
181
- * BridgeProtocol combines BoundedContext + SafeCodec + Transport
182
- * into a unified abstraction for all JS<->Python communication.
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 BridgeProtocol {
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
- * super({ transport, defaultTimeoutMs: options.timeout });
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 BridgeProtocol extends BoundedContext {
201
+ export class RpcClient extends DisposableBase {
206
202
  /** Codec instance for validation and serialization */
207
- protected readonly codec: SafeCodec;
203
+ readonly codec: SafeCodec;
208
204
 
209
205
  /** Transport instance for message passing */
210
- protected readonly transport: Transport;
206
+ readonly transport: Transport;
211
207
 
212
208
  /** Default timeout for operations in milliseconds */
213
- protected readonly defaultTimeoutMs: number;
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 BridgeProtocol instance.
218
+ * Create a new RpcClient instance.
223
219
  *
224
220
  * @param options - Configuration options including transport and codec settings
225
221
  */
226
- constructor(options: BridgeProtocolOptions) {
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 protocol.
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 protocol.
245
+ * Dispose the client.
252
246
  *
253
- * The transport is tracked as a resource and will be disposed automatically
254
- * by BoundedContext. Subclasses can override this to add additional cleanup,
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 BoundedContext
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
- protected async sendMessage<T>(
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
- protected async sendMessageAsync<T>(
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
- // RUNTIME EXECUTION INTERFACE
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 { BoundedContext } from './bounded-context.js';
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 BoundedContext {
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
- if (this.options.minWorkers > 0) {
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.scheduleReplacementWorker();
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.createWorker(replacementReady)
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
  }
@@ -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: 'python-subprocess';
373
+ bridge: BridgeBackend;
379
374
  pythonVersion: string;
380
- pid: number;
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
- // Runtime bridge interface
436
- export interface RuntimeExecution {
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
- warnings.push(`No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`);
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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"}