tywrap 0.5.1 → 0.6.1

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 (178) hide show
  1. package/dist/cli.js +8 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +236 -147
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts.map +1 -1
  7. package/dist/core/annotation-parser.js +112 -97
  8. package/dist/core/annotation-parser.js.map +1 -1
  9. package/dist/core/discovery.d.ts +17 -0
  10. package/dist/core/discovery.d.ts.map +1 -1
  11. package/dist/core/discovery.js +85 -49
  12. package/dist/core/discovery.js.map +1 -1
  13. package/dist/core/emit-call.d.ts +58 -0
  14. package/dist/core/emit-call.d.ts.map +1 -0
  15. package/dist/core/emit-call.js +103 -0
  16. package/dist/core/emit-call.js.map +1 -0
  17. package/dist/core/generator.d.ts +2 -0
  18. package/dist/core/generator.d.ts.map +1 -1
  19. package/dist/core/generator.js +54 -211
  20. package/dist/core/generator.js.map +1 -1
  21. package/dist/core/mapper.d.ts.map +1 -1
  22. package/dist/core/mapper.js +118 -104
  23. package/dist/core/mapper.js.map +1 -1
  24. package/dist/core/validation.d.ts +23 -0
  25. package/dist/core/validation.d.ts.map +1 -1
  26. package/dist/core/validation.js +52 -48
  27. package/dist/core/validation.js.map +1 -1
  28. package/dist/dev.d.ts.map +1 -1
  29. package/dist/dev.js +175 -133
  30. package/dist/dev.js.map +1 -1
  31. package/dist/index.d.ts +6 -25
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +9 -31
  34. package/dist/index.js.map +1 -1
  35. package/dist/runtime/bounded-context.d.ts +2 -2
  36. package/dist/runtime/bounded-context.js +2 -2
  37. package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +26 -11
  38. package/dist/runtime/bridge-codec.d.ts.map +1 -0
  39. package/dist/runtime/{safe-codec.js → bridge-codec.js} +69 -72
  40. package/dist/runtime/bridge-codec.js.map +1 -0
  41. package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
  42. package/dist/runtime/http-transport.d.ts.map +1 -0
  43. package/dist/runtime/{http-io.js → http-transport.js} +3 -3
  44. package/dist/runtime/http-transport.js.map +1 -0
  45. package/dist/runtime/http.d.ts +3 -3
  46. package/dist/runtime/http.d.ts.map +1 -1
  47. package/dist/runtime/http.js +4 -4
  48. package/dist/runtime/http.js.map +1 -1
  49. package/dist/runtime/index.d.ts +9 -1
  50. package/dist/runtime/index.d.ts.map +1 -1
  51. package/dist/runtime/index.js +9 -1
  52. package/dist/runtime/index.js.map +1 -1
  53. package/dist/runtime/node.d.ts +6 -6
  54. package/dist/runtime/node.d.ts.map +1 -1
  55. package/dist/runtime/node.js +7 -7
  56. package/dist/runtime/node.js.map +1 -1
  57. package/dist/runtime/pooled-transport.d.ts +11 -11
  58. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  59. package/dist/runtime/pooled-transport.js +10 -10
  60. package/dist/runtime/pooled-transport.js.map +1 -1
  61. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  62. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  63. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  64. package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
  65. package/dist/runtime/pyodide-transport.d.ts.map +1 -0
  66. package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
  67. package/dist/runtime/pyodide-transport.js.map +1 -0
  68. package/dist/runtime/pyodide.d.ts +3 -3
  69. package/dist/runtime/pyodide.d.ts.map +1 -1
  70. package/dist/runtime/pyodide.js +5 -5
  71. package/dist/runtime/pyodide.js.map +1 -1
  72. package/dist/runtime/rpc-client.d.ts +23 -5
  73. package/dist/runtime/rpc-client.d.ts.map +1 -1
  74. package/dist/runtime/rpc-client.js +36 -31
  75. package/dist/runtime/rpc-client.js.map +1 -1
  76. package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +25 -9
  77. package/dist/runtime/subprocess-transport.d.ts.map +1 -0
  78. package/dist/runtime/{process-io.js → subprocess-transport.js} +60 -41
  79. package/dist/runtime/subprocess-transport.js.map +1 -0
  80. package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
  81. package/dist/runtime/transport-pool.d.ts.map +1 -0
  82. package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
  83. package/dist/runtime/transport-pool.js.map +1 -0
  84. package/dist/runtime/transport.d.ts +14 -8
  85. package/dist/runtime/transport.d.ts.map +1 -1
  86. package/dist/runtime/transport.js +10 -4
  87. package/dist/runtime/transport.js.map +1 -1
  88. package/dist/types/index.d.ts +10 -1
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/tywrap.d.ts +0 -5
  91. package/dist/tywrap.d.ts.map +1 -1
  92. package/dist/tywrap.js +0 -20
  93. package/dist/tywrap.js.map +1 -1
  94. package/dist/utils/cache.d.ts +13 -2
  95. package/dist/utils/cache.d.ts.map +1 -1
  96. package/dist/utils/cache.js +52 -60
  97. package/dist/utils/cache.js.map +1 -1
  98. package/dist/utils/codec.d.ts +1 -1
  99. package/dist/utils/codec.d.ts.map +1 -1
  100. package/dist/utils/codec.js +181 -182
  101. package/dist/utils/codec.js.map +1 -1
  102. package/dist/utils/python.d.ts.map +1 -1
  103. package/dist/utils/python.js +28 -17
  104. package/dist/utils/python.js.map +1 -1
  105. package/dist/utils/runtime.d.ts.map +1 -1
  106. package/dist/utils/runtime.js +22 -16
  107. package/dist/utils/runtime.js.map +1 -1
  108. package/dist/version.d.ts +12 -0
  109. package/dist/version.d.ts.map +1 -0
  110. package/dist/version.js +12 -0
  111. package/dist/version.js.map +1 -0
  112. package/package.json +3 -2
  113. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  114. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  115. package/runtime/python_bridge.py +54 -6
  116. package/runtime/safe_codec.py +8 -8
  117. package/runtime/tywrap_bridge_core.py +159 -23
  118. package/src/cli.ts +8 -12
  119. package/src/config/index.ts +271 -165
  120. package/src/core/annotation-parser.ts +135 -123
  121. package/src/core/discovery.ts +102 -54
  122. package/src/core/emit-call.ts +171 -0
  123. package/src/core/generator.ts +67 -263
  124. package/src/core/mapper.ts +125 -110
  125. package/src/core/validation.ts +84 -48
  126. package/src/dev.ts +237 -153
  127. package/src/index.ts +11 -68
  128. package/src/runtime/bounded-context.ts +2 -2
  129. package/src/runtime/{safe-codec.ts → bridge-codec.ts} +83 -95
  130. package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
  131. package/src/runtime/http.ts +5 -5
  132. package/src/runtime/index.ts +23 -1
  133. package/src/runtime/node.ts +15 -15
  134. package/src/runtime/pooled-transport.ts +15 -15
  135. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  136. package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
  137. package/src/runtime/pyodide.ts +6 -6
  138. package/src/runtime/rpc-client.ts +51 -40
  139. package/src/runtime/{process-io.ts → subprocess-transport.ts} +77 -47
  140. package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
  141. package/src/runtime/transport.ts +15 -8
  142. package/src/types/index.ts +10 -1
  143. package/src/tywrap.ts +0 -24
  144. package/src/utils/cache.ts +61 -63
  145. package/src/utils/codec.ts +217 -196
  146. package/src/utils/python.ts +33 -20
  147. package/src/utils/runtime.ts +24 -13
  148. package/src/version.ts +12 -0
  149. package/dist/runtime/http-io.d.ts.map +0 -1
  150. package/dist/runtime/http-io.js.map +0 -1
  151. package/dist/runtime/optimized-node.d.ts +0 -23
  152. package/dist/runtime/optimized-node.d.ts.map +0 -1
  153. package/dist/runtime/optimized-node.js +0 -23
  154. package/dist/runtime/optimized-node.js.map +0 -1
  155. package/dist/runtime/process-io.d.ts.map +0 -1
  156. package/dist/runtime/process-io.js.map +0 -1
  157. package/dist/runtime/protocol.d.ts +0 -3
  158. package/dist/runtime/protocol.d.ts.map +0 -1
  159. package/dist/runtime/protocol.js +0 -3
  160. package/dist/runtime/protocol.js.map +0 -1
  161. package/dist/runtime/pyodide-io.d.ts.map +0 -1
  162. package/dist/runtime/pyodide-io.js.map +0 -1
  163. package/dist/runtime/safe-codec.d.ts.map +0 -1
  164. package/dist/runtime/safe-codec.js.map +0 -1
  165. package/dist/runtime/worker-pool.d.ts.map +0 -1
  166. package/dist/runtime/worker-pool.js.map +0 -1
  167. package/dist/utils/bundle-optimizer.d.ts +0 -182
  168. package/dist/utils/bundle-optimizer.d.ts.map +0 -1
  169. package/dist/utils/bundle-optimizer.js +0 -680
  170. package/dist/utils/bundle-optimizer.js.map +0 -1
  171. package/dist/utils/memory-profiler.d.ts +0 -123
  172. package/dist/utils/memory-profiler.d.ts.map +0 -1
  173. package/dist/utils/memory-profiler.js +0 -426
  174. package/dist/utils/memory-profiler.js.map +0 -1
  175. package/src/runtime/optimized-node.ts +0 -25
  176. package/src/runtime/protocol.ts +0 -2
  177. package/src/utils/bundle-optimizer.ts +0 -876
  178. package/src/utils/memory-profiler.ts +0 -582
package/src/dev.ts CHANGED
@@ -335,42 +335,44 @@ async function listExistingManagedFiles(outputDir: string): Promise<Set<string>>
335
335
  return managedFiles;
336
336
  }
337
337
 
338
+ async function listChildDirectories(current: string): Promise<string[]> {
339
+ let entries;
340
+ try {
341
+ entries = await readdir(current, { withFileTypes: true });
342
+ } catch {
343
+ return [];
344
+ }
345
+
346
+ return entries
347
+ .filter(entry => entry.isDirectory())
348
+ .map(entry => join(current, entry.name));
349
+ }
350
+
351
+ async function isWatchableDirectory(current: string, ignoredPaths: string[]): Promise<boolean> {
352
+ if (shouldIgnoreWatchPath(current, ignoredPaths)) {
353
+ return false;
354
+ }
355
+
356
+ try {
357
+ const currentStats = await stat(current);
358
+ return currentStats.isDirectory();
359
+ } catch {
360
+ return false;
361
+ }
362
+ }
363
+
338
364
  async function collectWatchDirectories(root: string, ignoredPaths: string[]): Promise<string[]> {
339
365
  const queue = [resolve(root)];
340
366
  const directories: string[] = [];
341
367
 
342
368
  while (queue.length > 0) {
343
369
  const current = queue.shift()!;
344
- if (shouldIgnoreWatchPath(current, ignoredPaths)) {
345
- continue;
346
- }
347
-
348
- let currentStats;
349
- try {
350
- currentStats = await stat(current);
351
- } catch {
352
- continue;
353
- }
354
-
355
- if (!currentStats.isDirectory()) {
370
+ if (!(await isWatchableDirectory(current, ignoredPaths))) {
356
371
  continue;
357
372
  }
358
373
 
359
374
  directories.push(current);
360
-
361
- let entries;
362
- try {
363
- entries = await readdir(current, { withFileTypes: true });
364
- } catch {
365
- continue;
366
- }
367
-
368
- for (const entry of entries) {
369
- if (!entry.isDirectory()) {
370
- continue;
371
- }
372
- queue.push(join(current, entry.name));
373
- }
375
+ queue.push(...(await listChildDirectories(current)));
374
376
  }
375
377
 
376
378
  return normalizePaths(directories);
@@ -444,7 +446,51 @@ async function promoteManagedFiles(
444
446
  }
445
447
  }
446
448
 
447
- async function resolveWatchTargets(config: TywrapOptions): Promise<WatchTarget[]> {
449
+ interface WatchTargetPayload {
450
+ missing?: boolean;
451
+ origin?: string | null;
452
+ locations?: string[];
453
+ has_location?: boolean;
454
+ }
455
+
456
+ interface WatchPythonEnv {
457
+ pythonPath: string;
458
+ env: { PYTHONPATH: string } | undefined;
459
+ timeoutMs: number;
460
+ }
461
+
462
+ const WATCH_TARGET_RESOLUTION_SCRIPT = [
463
+ 'import importlib',
464
+ 'import importlib.util',
465
+ 'import json',
466
+ 'import sys',
467
+ '',
468
+ 'module_name = sys.argv[1]',
469
+ 'spec = importlib.util.find_spec(module_name)',
470
+ 'if spec is None:',
471
+ " print(json.dumps({'missing': True}))",
472
+ ' raise SystemExit(0)',
473
+ '',
474
+ 'module_path = None',
475
+ 'module_origin = spec.origin',
476
+ 'if spec.submodule_search_locations:',
477
+ ' try:',
478
+ ' module = importlib.import_module(module_name)',
479
+ ' except Exception:',
480
+ ' module = None',
481
+ ' if module is not None:',
482
+ ' module_path = getattr(module, "__path__", None)',
483
+ ' module_origin = getattr(module, "__file__", None) or spec.origin',
484
+ 'payload = {',
485
+ " 'missing': False,",
486
+ " 'origin': module_origin,",
487
+ " 'locations': list(module_path) if module_path is not None else list(spec.submodule_search_locations or []),",
488
+ " 'has_location': bool(spec.has_location),",
489
+ '}',
490
+ 'print(json.dumps(payload))',
491
+ ].join('\n');
492
+
493
+ async function resolveWatchPythonEnv(config: TywrapOptions): Promise<WatchPythonEnv> {
448
494
  const pythonPath = await resolvePythonExecutable({
449
495
  pythonPath: config.runtime?.node?.pythonPath,
450
496
  virtualEnv: config.runtime?.node?.virtualEnv,
@@ -460,104 +506,100 @@ async function resolveWatchTargets(config: TywrapOptions): Promise<WatchTarget[]
460
506
  const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
461
507
  const timeoutMs = config.runtime?.node?.timeout ?? 30000;
462
508
 
463
- const watchTargets: WatchTarget[] = [];
464
-
465
- for (const moduleName of Object.keys(config.pythonModules ?? {})) {
466
- const resolutionScript = [
467
- 'import importlib',
468
- 'import importlib.util',
469
- 'import json',
470
- 'import sys',
471
- '',
472
- 'module_name = sys.argv[1]',
473
- 'spec = importlib.util.find_spec(module_name)',
474
- 'if spec is None:',
475
- " print(json.dumps({'missing': True}))",
476
- ' raise SystemExit(0)',
477
- '',
478
- 'module_path = None',
479
- 'module_origin = spec.origin',
480
- 'if spec.submodule_search_locations:',
481
- ' try:',
482
- ' module = importlib.import_module(module_name)',
483
- ' except Exception:',
484
- ' module = None',
485
- ' if module is not None:',
486
- ' module_path = getattr(module, "__path__", None)',
487
- ' module_origin = getattr(module, "__file__", None) or spec.origin',
488
- 'payload = {',
489
- " 'missing': False,",
490
- " 'origin': module_origin,",
491
- " 'locations': list(module_path) if module_path is not None else list(spec.submodule_search_locations or []),",
492
- " 'has_location': bool(spec.has_location),",
493
- '}',
494
- 'print(json.dumps(payload))',
495
- ].join('\n');
496
-
497
- const result = await processUtils.exec(pythonPath, ['-c', resolutionScript, moduleName], {
498
- timeoutMs,
499
- env,
500
- });
509
+ return { pythonPath, env, timeoutMs };
510
+ }
501
511
 
502
- if (result.code !== 0) {
503
- throw new Error(
504
- `Failed to resolve watch target for module "${moduleName}": ${result.stderr.trim() || result.stdout.trim() || 'unknown error'}`
505
- );
512
+ async function resolveWatchTargetPayload(
513
+ moduleName: string,
514
+ pythonEnv: WatchPythonEnv
515
+ ): Promise<WatchTargetPayload> {
516
+ const result = await processUtils.exec(
517
+ pythonEnv.pythonPath,
518
+ ['-c', WATCH_TARGET_RESOLUTION_SCRIPT, moduleName],
519
+ {
520
+ timeoutMs: pythonEnv.timeoutMs,
521
+ env: pythonEnv.env,
506
522
  }
523
+ );
507
524
 
508
- let payload: {
509
- missing?: boolean;
510
- origin?: string | null;
511
- locations?: string[];
512
- has_location?: boolean;
513
- };
514
- try {
515
- payload = JSON.parse(result.stdout) as {
516
- missing?: boolean;
517
- origin?: string | null;
518
- locations?: string[];
519
- has_location?: boolean;
520
- };
521
- } catch (error) {
525
+ if (result.code !== 0) {
526
+ throw new Error(
527
+ `Failed to resolve watch target for module "${moduleName}": ${result.stderr.trim() || result.stdout.trim() || 'unknown error'}`
528
+ );
529
+ }
530
+
531
+ try {
532
+ return JSON.parse(result.stdout) as WatchTargetPayload;
533
+ } catch (error) {
534
+ throw new Error(
535
+ `Failed to parse watch target for module "${moduleName}": ${(error as Error).message}`
536
+ );
537
+ }
538
+ }
539
+
540
+ async function resolvePackageWatchTargets(
541
+ moduleName: string,
542
+ packageLocations: string[]
543
+ ): Promise<WatchTarget[]> {
544
+ const targets: WatchTarget[] = [];
545
+ for (const location of packageLocations) {
546
+ const packageDir = resolve(location);
547
+ const stats = await stat(packageDir);
548
+ if (!stats.isDirectory()) {
522
549
  throw new Error(
523
- `Failed to parse watch target for module "${moduleName}": ${(error as Error).message}`
550
+ `Module "${moduleName}" resolved to "${packageDir}", but it is not a package directory`
524
551
  );
525
552
  }
553
+ targets.push({ moduleName, path: packageDir, kind: 'directory' });
554
+ }
555
+ return targets;
556
+ }
526
557
 
527
- if (payload.missing) {
528
- throw new Error(`Module "${moduleName}" could not be resolved for watching`);
529
- }
558
+ async function resolveFileWatchTarget(
559
+ moduleName: string,
560
+ payload: WatchTargetPayload
561
+ ): Promise<WatchTarget> {
562
+ if (typeof payload.origin !== 'string' || !payload.origin.endsWith('.py')) {
563
+ throw new Error(
564
+ `Module "${moduleName}" is not backed by a local Python source file or package directory and cannot be watched`
565
+ );
566
+ }
530
567
 
531
- const packageLocations = Array.isArray(payload.locations) ? payload.locations : [];
532
- if (packageLocations.length > 0) {
533
- for (const location of packageLocations) {
534
- const packageDir = resolve(location);
535
- const stats = await stat(packageDir);
536
- if (!stats.isDirectory()) {
537
- throw new Error(
538
- `Module "${moduleName}" resolved to "${packageDir}", but it is not a package directory`
539
- );
540
- }
541
- watchTargets.push({ moduleName, path: packageDir, kind: 'directory' });
542
- }
543
- continue;
544
- }
568
+ const modulePath = resolve(payload.origin);
569
+ const stats = await stat(modulePath);
570
+ if (!stats.isFile()) {
571
+ throw new Error(
572
+ `Module "${moduleName}" resolved to "${modulePath}", but it is not a Python source file`
573
+ );
574
+ }
545
575
 
546
- if (typeof payload.origin !== 'string' || !payload.origin.endsWith('.py')) {
547
- throw new Error(
548
- `Module "${moduleName}" is not backed by a local Python source file or package directory and cannot be watched`
549
- );
550
- }
576
+ return { moduleName, path: modulePath, kind: 'file' };
577
+ }
551
578
 
552
- const modulePath = resolve(payload.origin);
553
- const stats = await stat(modulePath);
554
- if (!stats.isFile()) {
555
- throw new Error(
556
- `Module "${moduleName}" resolved to "${modulePath}", but it is not a Python source file`
557
- );
558
- }
579
+ async function resolveModuleWatchTargets(
580
+ moduleName: string,
581
+ pythonEnv: WatchPythonEnv
582
+ ): Promise<WatchTarget[]> {
583
+ const payload = await resolveWatchTargetPayload(moduleName, pythonEnv);
584
+
585
+ if (payload.missing) {
586
+ throw new Error(`Module "${moduleName}" could not be resolved for watching`);
587
+ }
559
588
 
560
- watchTargets.push({ moduleName, path: modulePath, kind: 'file' });
589
+ const packageLocations = Array.isArray(payload.locations) ? payload.locations : [];
590
+ if (packageLocations.length > 0) {
591
+ return resolvePackageWatchTargets(moduleName, packageLocations);
592
+ }
593
+
594
+ return [await resolveFileWatchTarget(moduleName, payload)];
595
+ }
596
+
597
+ async function resolveWatchTargets(config: TywrapOptions): Promise<WatchTarget[]> {
598
+ const pythonEnv = await resolveWatchPythonEnv(config);
599
+
600
+ const watchTargets: WatchTarget[] = [];
601
+ for (const moduleName of Object.keys(config.pythonModules ?? {})) {
602
+ watchTargets.push(...(await resolveModuleWatchTargets(moduleName, pythonEnv)));
561
603
  }
562
604
 
563
605
  return watchTargets;
@@ -786,6 +828,28 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
786
828
  return watcherRefreshPromise;
787
829
  };
788
830
 
831
+ interface ReloadState {
832
+ stage: StageResult | null;
833
+ nextBridge: T | null;
834
+ preparedWatchers: PreparedWatchers | null;
835
+ }
836
+
837
+ interface PreparedReload {
838
+ stage: StageResult;
839
+ preparedWatchers: PreparedWatchers;
840
+ nextIgnoredPaths: string[];
841
+ watchPaths: string[];
842
+ }
843
+
844
+ const disposeReloadFailure = async (state: ReloadState): Promise<void> => {
845
+ if (state.preparedWatchers) {
846
+ closePreparedWatchers(state.preparedWatchers);
847
+ }
848
+ if (state.nextBridge) {
849
+ await state.nextBridge.dispose().catch(() => {});
850
+ }
851
+ };
852
+
789
853
  const runReload = async (trigger: { path?: string; manual: boolean }): Promise<boolean> => {
790
854
  if (closed) {
791
855
  return false;
@@ -793,20 +857,18 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
793
857
 
794
858
  emit({ type: 'reload-start', path: trigger.path, manual: trigger.manual });
795
859
 
796
- let stage: StageResult | null = null;
797
- let nextBridge: T | null = null;
798
- let preparedWatchers: PreparedWatchers | null = null;
860
+ const state: ReloadState = { stage: null, nextBridge: null, preparedWatchers: null };
799
861
 
800
862
  const abortReload = async (
801
863
  managerToDispose: ManagedBridgeReloader<T> | null = null
802
864
  ): Promise<boolean> => {
803
- if (preparedWatchers) {
804
- closePreparedWatchers(preparedWatchers);
805
- preparedWatchers = null;
865
+ if (state.preparedWatchers) {
866
+ closePreparedWatchers(state.preparedWatchers);
867
+ state.preparedWatchers = null;
806
868
  }
807
- if (nextBridge) {
808
- await nextBridge.dispose().catch(() => {});
809
- nextBridge = null;
869
+ if (state.nextBridge) {
870
+ await state.nextBridge.dispose().catch(() => {});
871
+ state.nextBridge = null;
810
872
  }
811
873
  if (managerToDispose) {
812
874
  await managerToDispose.dispose().catch(() => {});
@@ -814,10 +876,13 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
814
876
  return false;
815
877
  };
816
878
 
817
- try {
818
- stage = await generateToStage(configFile);
879
+ // Generate the next stage, prepare watchers, and warm the next bridge.
880
+ // Returns null when the session closed mid-flight (caller aborts).
881
+ const prepareReload = async (): Promise<PreparedReload | null> => {
882
+ const stage = await generateToStage(configFile);
883
+ state.stage = stage;
819
884
  if (closed) {
820
- return abortReload();
885
+ return null;
821
886
  }
822
887
  currentBridgeConfig = stage.config;
823
888
  const nextIgnoredPaths = buildIgnoredPaths(stage.outputDir);
@@ -828,17 +893,50 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
828
893
  ...extraWatchSources,
829
894
  ]);
830
895
  const watchPaths = normalizePaths(watchSources.map(source => source.path));
831
- preparedWatchers = await prepareWatchers(watchSources, nextIgnoredPaths);
896
+ const preparedWatchers = await prepareWatchers(watchSources, nextIgnoredPaths);
897
+ state.preparedWatchers = preparedWatchers;
832
898
  if (closed) {
833
- return abortReload();
899
+ return null;
834
900
  }
835
901
 
836
902
  if (bridgeManager) {
837
- nextBridge = await bridgeManager.prepareNextBridge();
903
+ state.nextBridge = await bridgeManager.prepareNextBridge();
904
+ if (closed) {
905
+ return null;
906
+ }
907
+ }
908
+
909
+ return { stage, preparedWatchers, nextIgnoredPaths, watchPaths };
910
+ };
911
+
912
+ // Activate (or create) the bridge for the freshly prepared stage.
913
+ // Returns the manager to dispose (or null) when the session closed
914
+ // mid-flight, otherwise null; sets `closedDuringActivation` accordingly.
915
+ let closedDuringActivation = false;
916
+ const activateReloadBridge = async (): Promise<ManagedBridgeReloader<T> | null> => {
917
+ if (bridgeManager && state.nextBridge) {
918
+ await bridgeManager.activatePreparedBridge(state.nextBridge);
919
+ state.nextBridge = null;
920
+ closedDuringActivation = closed;
921
+ return null;
922
+ }
923
+ if (!bridgeManager) {
924
+ const createdBridgeManager = await ManagedBridgeReloader.create(createBridgeForSession);
838
925
  if (closed) {
839
- return abortReload();
926
+ closedDuringActivation = true;
927
+ return createdBridgeManager;
840
928
  }
929
+ bridgeManager = createdBridgeManager;
930
+ }
931
+ return null;
932
+ };
933
+
934
+ try {
935
+ const prepared = await prepareReload();
936
+ if (!prepared) {
937
+ return abortReload();
841
938
  }
939
+ const { stage } = prepared;
842
940
 
843
941
  const previousManagedFiles =
844
942
  activeOutputDir === stage.outputDir
@@ -853,29 +951,20 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
853
951
  return abortReload();
854
952
  }
855
953
 
856
- if (bridgeManager && nextBridge) {
857
- await bridgeManager.activatePreparedBridge(nextBridge);
858
- nextBridge = null;
859
- if (closed) {
860
- return abortReload();
861
- }
862
- } else if (!bridgeManager) {
863
- const createdBridgeManager = await ManagedBridgeReloader.create(createBridgeForSession);
864
- if (closed) {
865
- return abortReload(createdBridgeManager);
866
- }
867
- bridgeManager = createdBridgeManager;
954
+ const managerToDispose = await activateReloadBridge();
955
+ if (closedDuringActivation) {
956
+ return abortReload(managerToDispose);
868
957
  }
869
958
 
870
959
  managedFiles = promotedManagedFiles;
871
960
  activeOutputDir = stage.outputDir;
872
- ignoredPaths = nextIgnoredPaths;
873
- commitWatchers(preparedWatchers);
961
+ ignoredPaths = prepared.nextIgnoredPaths;
962
+ commitWatchers(prepared.preparedWatchers);
874
963
  emit({
875
964
  type: 'reload-success',
876
965
  path: trigger.path,
877
966
  manual: trigger.manual,
878
- paths: watchPaths,
967
+ paths: prepared.watchPaths,
879
968
  written: [...stage.files.keys()].sort(),
880
969
  warnings: stage.warnings,
881
970
  });
@@ -883,12 +972,7 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
883
972
  return true;
884
973
  } catch (error) {
885
974
  lastReloadError = error instanceof Error ? error : new Error(String(error));
886
- if (preparedWatchers) {
887
- closePreparedWatchers(preparedWatchers);
888
- }
889
- if (nextBridge) {
890
- await nextBridge.dispose().catch(() => {});
891
- }
975
+ await disposeReloadFailure(state);
892
976
  emit({
893
977
  type: 'reload-error',
894
978
  path: trigger.path,
@@ -897,8 +981,8 @@ export async function startNodeWatchSession<T extends RuntimeExecution & Disposa
897
981
  });
898
982
  return false;
899
983
  } finally {
900
- if (stage) {
901
- await rm(stage.tempDir, { recursive: true, force: true });
984
+ if (state.stage) {
985
+ await rm(state.stage.tempDir, { recursive: true, force: true });
902
986
  }
903
987
  }
904
988
  };
package/src/index.ts CHANGED
@@ -3,70 +3,20 @@
3
3
  *
4
4
  * @description Build-time code generation system that makes Python libraries
5
5
  * feel native in TypeScript with zero runtime overhead
6
+ *
7
+ * This is the package root. It intentionally exposes only the stable,
8
+ * consumer-facing surface. Runtime plumbing (codec, transport, bridge
9
+ * implementations, disposables, validators) lives behind the subpath
10
+ * entrypoints: `tywrap/runtime`, `tywrap/node`, `tywrap/pyodide`, `tywrap/http`.
6
11
  */
7
12
 
8
13
  import { tywrap } from './tywrap.js';
9
14
 
15
+ // Configuration
10
16
  export type { TywrapConfig, ResolvedTywrapConfig } from './config/index.js';
11
17
  export { defineConfig, resolveConfig } from './config/index.js';
12
- // DisposableBase - lifecycle/resource base for cross-boundary components
13
- export {
14
- DisposableBase,
15
- type ContextState,
16
- type ExecuteOptions,
17
- } from './runtime/bounded-context.js';
18
- // RpcClient - the single correlated-RPC client (codec + transport), held by bridges
19
- export { RpcClient, type RpcClientOptions } from './runtime/rpc-client.js';
20
- // SafeCodec - validation and serialization for JS<->Python boundary
21
- export { SafeCodec, type CodecOptions } from './runtime/safe-codec.js';
22
- // Transport - abstract I/O channel interface
23
- export type {
24
- Transport,
25
- TransportOptions,
26
- ProtocolMessage,
27
- ProtocolResponse,
28
- } from './runtime/transport.js';
29
- export {
30
- PROTOCOL_ID,
31
- isTransport,
32
- isProtocolMessage,
33
- isProtocolResponse,
34
- } from './runtime/transport.js';
35
- // Transport implementations
36
- export { ProcessIO, type ProcessIOOptions } from './runtime/process-io.js';
37
- export { HttpIO, type HttpIOOptions } from './runtime/http-io.js';
38
- export { PyodideIO, type PyodideIOOptions } from './runtime/pyodide-io.js';
39
- // WorkerPool - concurrent transport management
40
- export { WorkerPool, type WorkerPoolOptions, type PooledWorker } from './runtime/worker-pool.js';
41
- // PooledTransport - Transport adapter that wraps WorkerPool
42
- export { PooledTransport, type PooledTransportOptions } from './runtime/pooled-transport.js';
43
- export type { Disposable } from './runtime/disposable.js';
44
- export { isDisposable, safeDispose, disposeAll } from './runtime/disposable.js';
45
- export {
46
- ValidationError,
47
- isFiniteNumber,
48
- isPositiveNumber,
49
- isNonNegativeNumber,
50
- isNonEmptyString,
51
- isPlainObject,
52
- assertFiniteNumber,
53
- assertPositive,
54
- assertNonNegative,
55
- assertString,
56
- assertNonEmptyString,
57
- assertArray,
58
- assertObject,
59
- containsSpecialFloat,
60
- assertNoSpecialFloats,
61
- sanitizeForFilename,
62
- containsPathTraversal,
63
- } from './runtime/validators.js';
64
-
65
- /**
66
- * @deprecated Use DisposableBase instead. RuntimeBridge will be removed in the next major version.
67
- */
68
- export { RuntimeBridge } from './runtime/base.js';
69
18
 
19
+ // Bridge error hierarchy
70
20
  export {
71
21
  BridgeError,
72
22
  BridgeCodecError,
@@ -75,12 +25,6 @@ export {
75
25
  BridgeDisposedError,
76
26
  BridgeExecutionError,
77
27
  } from './runtime/errors.js';
78
- export { getRuntimeBridge, setRuntimeBridge, clearRuntimeBridge } from './runtime/index.js';
79
-
80
- // Runtime-specific exports (Bridge facades that HOLD an RpcClient)
81
- export { NodeBridge, type NodeBridgeOptions } from './runtime/node.js';
82
- export { PyodideBridge, type PyodideBridgeOptions } from './runtime/pyodide.js';
83
- export { HttpBridge, type HttpBridgeOptions } from './runtime/http.js';
84
28
 
85
29
  // Core types
86
30
  export type {
@@ -138,6 +82,8 @@ export type { GenerateFailure, GenerateResult, GenerateRunOptions } from './tywr
138
82
 
139
83
  // Runtime detection utilities
140
84
  export { detectRuntime, isNodejs, isDeno, isBun, isBrowser } from './utils/runtime.js';
85
+
86
+ // Arrow/codec public helpers
141
87
  export {
142
88
  decodeValue,
143
89
  decodeValueAsync,
@@ -146,11 +92,8 @@ export {
146
92
  clearArrowDecoder,
147
93
  } from './utils/codec.js';
148
94
 
149
- // Version info
150
- export const VERSION = '0.3.0';
95
+ // Version info — single-sourced from package.json via scripts/generate-version.mjs
96
+ export { VERSION } from './version.js';
151
97
 
152
- /**
153
- * Quick setup function for getting started
154
- */
155
98
  // Default export for convenience
156
99
  export default tywrap;
@@ -6,8 +6,8 @@
6
6
  * ownership tracking. It carries ZERO RPC methods by design — the
7
7
  * cross-boundary RPC contract (call/instantiate/callMethod/disposeInstance)
8
8
  * is PythonRuntime, implemented only by the bridge facades, never by a base
9
- * class or a transport. This separation is what keeps transports (ProcessIO,
10
- * PooledTransport, WorkerPool, PyodideIO) and RpcClient from having to stub
9
+ * class or a transport. This separation is what keeps transports (SubprocessTransport,
10
+ * PooledTransport, TransportPool, PyodideTransport) and RpcClient from having to stub
11
11
  * RPC methods they do not implement.
12
12
  *
13
13
  * Reused by RpcClient and all transports.