tywrap 0.3.0 → 0.4.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 (44) hide show
  1. package/README.md +39 -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 +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/runtime/node.d.ts.map +1 -1
  18. package/dist/runtime/node.js +53 -17
  19. package/dist/runtime/node.js.map +1 -1
  20. package/dist/runtime/optimized-node.d.ts +5 -4
  21. package/dist/runtime/optimized-node.d.ts.map +1 -1
  22. package/dist/runtime/optimized-node.js +5 -4
  23. package/dist/runtime/optimized-node.js.map +1 -1
  24. package/dist/runtime/worker-pool.d.ts +6 -1
  25. package/dist/runtime/worker-pool.d.ts.map +1 -1
  26. package/dist/runtime/worker-pool.js +56 -29
  27. package/dist/runtime/worker-pool.js.map +1 -1
  28. package/dist/types/index.d.ts +0 -7
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/tywrap.d.ts +6 -0
  31. package/dist/tywrap.d.ts.map +1 -1
  32. package/dist/tywrap.js +10 -3
  33. package/dist/tywrap.js.map +1 -1
  34. package/package.json +17 -13
  35. package/src/cli.ts +27 -6
  36. package/src/config/index.ts +28 -15
  37. package/src/core/analyzer.ts +1 -2
  38. package/src/dev.ts +983 -0
  39. package/src/index.ts +1 -1
  40. package/src/runtime/node.ts +74 -28
  41. package/src/runtime/optimized-node.ts +5 -4
  42. package/src/runtime/worker-pool.ts +65 -30
  43. package/src/types/index.ts +0 -8
  44. package/src/tywrap.ts +17 -3
package/src/index.ts CHANGED
@@ -121,7 +121,6 @@ export type {
121
121
  NodeConfig,
122
122
  HttpConfig,
123
123
  PerformanceConfig,
124
- DevelopmentConfig,
125
124
  TypeMappingConfig,
126
125
  TypePreset,
127
126
  BridgeInfo,
@@ -135,6 +134,7 @@ export type {
135
134
  // Main API
136
135
  export { tywrap } from './tywrap.js';
137
136
  export { generate } from './tywrap.js';
137
+ export type { GenerateFailure, GenerateResult, GenerateRunOptions } from './tywrap.js';
138
138
 
139
139
  // Runtime detection utilities
140
140
  export { detectRuntime, isNodejs, isDeno, isBun, isBrowser } from './utils/runtime.js';
@@ -340,16 +340,14 @@ export class NodeBridge extends BridgeProtocol {
340
340
  codec: options.codec,
341
341
  warmupCommands,
342
342
  };
343
-
344
343
  // Build environment for ProcessIO
345
344
  const processEnv = buildProcessEnv(resolvedOptions);
346
345
 
347
- // Create warmup callback for per-worker initialization
348
346
  const userWarmup =
349
347
  resolvedOptions.warmupCommands.length > 0
350
348
  ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
351
349
  : undefined;
352
- const replacementWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
350
+ const onWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
353
351
 
354
352
  // Create pooled transport with ProcessIO workers
355
353
  const transport = new PooledTransport({
@@ -364,8 +362,8 @@ export class NodeBridge extends BridgeProtocol {
364
362
  minWorkers: resolvedOptions.minProcesses,
365
363
  queueTimeoutMs: resolvedOptions.queueTimeoutMs,
366
364
  maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
367
- onWorkerReady: userWarmup,
368
- onReplacementWorkerReady: replacementWorkerReady,
365
+ onWorkerReady,
366
+ onReplacementWorkerReady: onWorkerReady,
369
367
  });
370
368
 
371
369
  // Initialize BridgeProtocol with pooled transport
@@ -565,6 +563,72 @@ function generateWarmupId(): number {
565
563
  return ++warmupRequestId;
566
564
  }
567
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
+ }
605
+
606
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
607
+ throw new BridgeExecutionError(
608
+ `${label} returned malformed response envelope for request ${requestId}`
609
+ );
610
+ }
611
+
612
+ const envelope = parsed as { result?: unknown; error?: unknown };
613
+ if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
614
+ const errorPayload = envelope.error;
615
+ if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
616
+ const err = errorPayload as { type?: unknown; message?: unknown };
617
+ const errType = typeof err.type === 'string' ? err.type : 'Error';
618
+ const errMessage = typeof err.message === 'string' ? err.message : 'Unknown warmup error';
619
+ throw new BridgeExecutionError(`${label} failed: ${errType}: ${errMessage}`);
620
+ }
621
+
622
+ throw new BridgeExecutionError(`${label} failed with malformed error payload`);
623
+ }
624
+
625
+ if (!('result' in envelope)) {
626
+ throw new BridgeExecutionError(
627
+ `${label} returned malformed response envelope for request ${requestId}`
628
+ );
629
+ }
630
+ }
631
+
568
632
  /**
569
633
  * Create a callback that runs warmup commands on each worker.
570
634
  *
@@ -604,29 +668,11 @@ function createWorkerReadyCallback(
604
668
  extraWarmup?: (worker: PooledWorker) => Promise<void>
605
669
  ): (worker: PooledWorker) => Promise<void> {
606
670
  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
- );
624
-
625
- if (result !== 0) {
626
- throw new BridgeExecutionError(
627
- `Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`
628
- );
629
- }
671
+ 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
+ });
630
676
 
631
677
  await extraWarmup?.(worker);
632
678
  };
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @deprecated Import from './node.js' instead.
3
3
  *
4
- * OptimizedNodeBridge has been unified with NodeBridge. The NodeBridge class
5
- * now supports both single-process mode (default) and multi-process pooling
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 maintained for backward compatibility only.
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,
@@ -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
  *
@@ -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,12 +358,6 @@ 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 {
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
  /**