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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WorkerPool - Manages multiple Transport instances for concurrent request handling.
2
+ * TransportPool - Manages multiple Transport instances for concurrent request handling.
3
3
  *
4
4
  * Provides semaphore-based concurrency control with configurable limits per worker
5
5
  * and a wait queue for callers when all workers are at capacity.
@@ -16,9 +16,9 @@ import type { Transport } from './transport.js';
16
16
  // =============================================================================
17
17
 
18
18
  /**
19
- * Configuration options for the WorkerPool.
19
+ * Configuration options for the TransportPool.
20
20
  */
21
- export interface WorkerPoolOptions {
21
+ export interface TransportPoolOptions {
22
22
  /** Factory function to create transports */
23
23
  createTransport: () => Transport;
24
24
 
@@ -38,20 +38,20 @@ export interface WorkerPoolOptions {
38
38
  * Callback invoked after each worker is created and initialized.
39
39
  * Use this for per-worker warmup (e.g., importing modules, running setup).
40
40
  */
41
- onWorkerReady?: (worker: PooledWorker) => Promise<void>;
41
+ onWorkerReady?: (worker: TransportLease) => Promise<void>;
42
42
 
43
43
  /**
44
44
  * Optional callback used only for background replacement workers after a
45
45
  * fatal timeout/crash. This lets callers publish a replacement only after it
46
46
  * is proven ready, without charging hidden work to normal request startup.
47
47
  */
48
- onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
48
+ onReplacementWorkerReady?: (worker: TransportLease) => Promise<void>;
49
49
  }
50
50
 
51
51
  /**
52
52
  * A pooled worker with its transport and current in-flight request count.
53
53
  */
54
- export interface PooledWorker {
54
+ export interface TransportLease {
55
55
  /** The underlying transport instance */
56
56
  transport: Transport;
57
57
 
@@ -64,7 +64,7 @@ export interface PooledWorker {
64
64
  */
65
65
  interface QueuedWaiter {
66
66
  /** Resolve function to fulfill the promise with a worker */
67
- resolve: (worker: PooledWorker) => void;
67
+ resolve: (worker: TransportLease) => void;
68
68
 
69
69
  /** Reject function to reject the promise with an error */
70
70
  reject: (error: Error) => void;
@@ -88,8 +88,8 @@ interface QueuedWaiter {
88
88
  *
89
89
  * @example
90
90
  * ```typescript
91
- * const pool = new WorkerPool({
92
- * createTransport: () => new ProcessIO({ pythonPath: 'python3' }),
91
+ * const pool = new TransportPool({
92
+ * createTransport: () => new SubprocessTransport({ pythonPath: 'python3' }),
93
93
  * maxWorkers: 4,
94
94
  * maxConcurrentPerWorker: 2,
95
95
  * queueTimeoutMs: 5000,
@@ -105,25 +105,25 @@ interface QueuedWaiter {
105
105
  * await pool.dispose();
106
106
  * ```
107
107
  */
108
- export class WorkerPool extends DisposableBase {
108
+ export class TransportPool extends DisposableBase {
109
109
  private readonly options: Omit<
110
- Required<WorkerPoolOptions>,
110
+ Required<TransportPoolOptions>,
111
111
  'onWorkerReady' | 'onReplacementWorkerReady'
112
112
  > & {
113
- onWorkerReady?: (worker: PooledWorker) => Promise<void>;
114
- onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
113
+ onWorkerReady?: (worker: TransportLease) => Promise<void>;
114
+ onReplacementWorkerReady?: (worker: TransportLease) => Promise<void>;
115
115
  };
116
- private readonly workers: PooledWorker[] = [];
116
+ private readonly workers: TransportLease[] = [];
117
117
  private readonly waitQueue: QueuedWaiter[] = [];
118
118
  /** Tracks workers being created to prevent race condition in acquire() */
119
119
  private pendingCreations = 0;
120
120
 
121
121
  /**
122
- * Create a new WorkerPool.
122
+ * Create a new TransportPool.
123
123
  *
124
124
  * @param options - Pool configuration options
125
125
  */
126
- constructor(options: WorkerPoolOptions) {
126
+ constructor(options: TransportPoolOptions) {
127
127
  super();
128
128
 
129
129
  // Validate required options
@@ -216,7 +216,7 @@ export class WorkerPool extends DisposableBase {
216
216
  * @throws BridgeTimeoutError if queue timeout expires
217
217
  * @throws BridgeExecutionError if pool is disposed while waiting
218
218
  */
219
- async acquire(): Promise<PooledWorker> {
219
+ async acquire(): Promise<TransportLease> {
220
220
  // Check for disposed state
221
221
  if (this.isDisposed || this.state === 'disposing') {
222
222
  throw new BridgeExecutionError('Pool has been disposed');
@@ -261,7 +261,7 @@ export class WorkerPool extends DisposableBase {
261
261
  *
262
262
  * @param worker - The worker to release
263
263
  */
264
- release(worker: PooledWorker): void {
264
+ release(worker: TransportLease): void {
265
265
  // Validate the worker belongs to this pool
266
266
  if (!this.workers.includes(worker)) {
267
267
  return;
@@ -288,7 +288,7 @@ export class WorkerPool extends DisposableBase {
288
288
  * });
289
289
  * ```
290
290
  */
291
- async withWorker<T>(fn: (worker: PooledWorker) => Promise<T>): Promise<T> {
291
+ async withWorker<T>(fn: (worker: TransportLease) => Promise<T>): Promise<T> {
292
292
  const worker = await this.acquire();
293
293
  let workerRemoved = false;
294
294
 
@@ -341,7 +341,7 @@ export class WorkerPool extends DisposableBase {
341
341
  * This is called when a worker is detected as dead (crashed, pipe error, etc.).
342
342
  * The worker's transport is disposed in the background.
343
343
  */
344
- private removeWorker(worker: PooledWorker): void {
344
+ private removeWorker(worker: TransportLease): void {
345
345
  const index = this.workers.indexOf(worker);
346
346
  if (index !== -1) {
347
347
  this.workers.splice(index, 1);
@@ -387,7 +387,7 @@ export class WorkerPool extends DisposableBase {
387
387
  /**
388
388
  * Find an available worker with capacity for another request.
389
389
  */
390
- private findAvailableWorker(): PooledWorker | undefined {
390
+ private findAvailableWorker(): TransportLease | undefined {
391
391
  return this.workers.find(w => w.inFlightCount < this.options.maxConcurrentPerWorker);
392
392
  }
393
393
 
@@ -452,7 +452,7 @@ export class WorkerPool extends DisposableBase {
452
452
  this.publishAvailableWorker(worker);
453
453
  }
454
454
 
455
- private publishAvailableWorker(worker: PooledWorker): void {
455
+ private publishAvailableWorker(worker: TransportLease): void {
456
456
  while (
457
457
  this.waitQueue.length > 0 &&
458
458
  worker.inFlightCount < this.options.maxConcurrentPerWorker
@@ -473,13 +473,13 @@ export class WorkerPool extends DisposableBase {
473
473
  * If onWorkerReady is configured, calls it after the transport is initialized.
474
474
  * This is useful for per-worker warmup (importing modules, running setup).
475
475
  */
476
- private async createWorker(onWorkerReady = this.options.onWorkerReady): Promise<PooledWorker> {
476
+ private async createWorker(onWorkerReady = this.options.onWorkerReady): Promise<TransportLease> {
477
477
  const transport = this.options.createTransport();
478
478
 
479
479
  // Initialize the transport
480
480
  await transport.init();
481
481
 
482
- const worker: PooledWorker = {
482
+ const worker: TransportLease = {
483
483
  transport,
484
484
  inFlightCount: 0,
485
485
  };
@@ -512,8 +512,8 @@ export class WorkerPool extends DisposableBase {
512
512
  /**
513
513
  * Wait in queue for a worker to become available.
514
514
  */
515
- private waitForWorker(): Promise<PooledWorker> {
516
- return new Promise<PooledWorker>((resolve, reject) => {
515
+ private waitForWorker(): Promise<TransportLease> {
516
+ return new Promise<TransportLease>((resolve, reject) => {
517
517
  const timer = setTimeout(() => {
518
518
  // Remove this waiter from the queue
519
519
  const index = this.waitQueue.findIndex(w => w.timer === timer);
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Provides an abstract I/O channel for all bridge communications across
5
5
  * the JS-Python boundary. Concrete implementations handle different runtimes:
6
- * - ProcessIO: Child process with stdio streams (Node.js)
7
- * - HttpIO: HTTP POST requests (remote Python server)
8
- * - PyodideIO: In-memory Pyodide calls (browser/WASM)
6
+ * - SubprocessTransport: Child process with stdio streams (Node.js)
7
+ * - HttpTransport: HTTP POST requests (remote Python server)
8
+ * - PyodideTransport: In-memory Pyodide calls (browser/WASM)
9
9
  *
10
10
  * @see https://github.com/bbopen/tywrap/issues/149
11
11
  */
@@ -16,9 +16,16 @@ import type { Disposable } from './disposable.js';
16
16
  // PROTOCOL CONSTANTS
17
17
  // =============================================================================
18
18
 
19
- /** Protocol identifier for tywrap communication */
19
+ /** Protocol identifier for tywrap communication. Single source of truth for the version. */
20
20
  export const PROTOCOL_ID = 'tywrap/1';
21
21
 
22
+ /**
23
+ * Numeric protocol version negotiated with the Python bridge. Derived from the
24
+ * trailing number of {@link PROTOCOL_ID} so the two cannot drift — bump
25
+ * PROTOCOL_ID alone and this follows.
26
+ */
27
+ export const TYWRAP_PROTOCOL_VERSION = Number.parseInt(PROTOCOL_ID.split('/')[1] ?? '', 10);
28
+
22
29
  // =============================================================================
23
30
  // PROTOCOL TYPES
24
31
  // =============================================================================
@@ -112,13 +119,13 @@ export interface ProtocolResponse {
112
119
  * 4. Call `dispose()` to release resources
113
120
  *
114
121
  * Implementations:
115
- * - ProcessIO: Spawns a Python child process, communicates via stdio
116
- * - HttpIO: Sends HTTP POST requests to a Python server
117
- * - PyodideIO: Calls Pyodide directly in-memory (WASM)
122
+ * - SubprocessTransport: Spawns a Python child process, communicates via stdio
123
+ * - HttpTransport: Sends HTTP POST requests to a Python server
124
+ * - PyodideTransport: Calls Pyodide directly in-memory (WASM)
118
125
  *
119
126
  * @example
120
127
  * ```typescript
121
- * const transport = new ProcessIO({ pythonPath: 'python3' });
128
+ * const transport = new SubprocessTransport({ pythonPath: 'python3' });
122
129
  * await transport.init();
123
130
  *
124
131
  * const response = await transport.send(
@@ -310,7 +310,14 @@ export interface TywrapOptions {
310
310
 
311
311
  export interface PythonModuleConfig {
312
312
  version?: string;
313
- runtime: RuntimeStrategy;
313
+ /**
314
+ * @deprecated Dead input. The per-module runtime is never read during code
315
+ * generation — the active runtime is resolved from the top-level
316
+ * {@link RuntimeConfig} (`runtime.node`, etc.). Kept optional so existing
317
+ * configs that still set it continue to validate; it has no effect and will
318
+ * be removed in a future major release.
319
+ */
320
+ runtime?: RuntimeStrategy;
314
321
  functions?: string[];
315
322
  classes?: string[];
316
323
  /** Exclude specific exports by exact name. */
@@ -343,11 +350,13 @@ export interface PyodideConfig {
343
350
  export interface NodeConfig {
344
351
  pythonPath?: string;
345
352
  virtualEnv?: string;
353
+ /** Per-call timeout in milliseconds for the subprocess bridge. */
346
354
  timeout?: number;
347
355
  }
348
356
 
349
357
  export interface HttpConfig {
350
358
  baseURL: string;
359
+ /** Per-request timeout in milliseconds for the HTTP bridge. */
351
360
  timeout?: number;
352
361
  headers?: Record<string, string>;
353
362
  }
package/src/tywrap.ts CHANGED
@@ -52,30 +52,6 @@ export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<Tywr
52
52
  };
53
53
  }
54
54
 
55
- export default tywrap;
56
-
57
- /**
58
- * Append minimal tsd tests for a generated module (optional dev aid)
59
- */
60
- export async function emitTypeTestsForModule(
61
- moduleName: string,
62
- outDir = 'test-d/generated'
63
- ): Promise<void> {
64
- const { writeFile, mkdir } = await import('fs/promises');
65
- const { join } = await import('path');
66
- try {
67
- await mkdir(outDir, { recursive: true });
68
- } catch {}
69
- const filePath = join(outDir, `${moduleName}.test-d.ts`);
70
- const content = `import { expectType } from 'tsd';
71
- import * as mod from '../../generated/${moduleName}.generated.ts';
72
-
73
- // Opportunistic: ensure module namespace is an object
74
- expectType<Record<string, unknown>>(mod);
75
- `;
76
- await writeFile(filePath, content, 'utf-8');
77
- }
78
-
79
55
  export interface GenerateRunOptions {
80
56
  /**
81
57
  * If true, do not write files; instead compare generated output to what's on disk.
@@ -47,7 +47,7 @@ export interface CacheConfig {
47
47
  debug?: boolean;
48
48
  }
49
49
 
50
- export class IntelligentCache {
50
+ export class ArtifactCache {
51
51
  private memoryCache = new Map<string, CacheEntry>();
52
52
  private stats = {
53
53
  hits: 0,
@@ -149,75 +149,73 @@ export class IntelligentCache {
149
149
  hash.update(prefix);
150
150
  hash.update('\0');
151
151
 
152
- // Add inputs
152
+ // Add inputs. Each input contributes a type tag, an optional payload, and a
153
+ // boundary separator so that "1" vs 1 and ["a","bc"] vs ["ab","c"] never collide.
153
154
  for (const input of inputs) {
154
- // Why: disambiguate types so "1" and 1 don't collide, and keep input boundaries so
155
- // ["a", "bc"] doesn't collide with ["ab", "c"].
156
- if (input === undefined) {
157
- hash.update('undef:');
158
- hash.update('\0');
159
- continue;
155
+ const { tag, payload } = ArtifactCache.encodeKeyInput(input);
156
+ hash.update(tag);
157
+ if (payload !== undefined) {
158
+ hash.update(payload);
160
159
  }
161
- if (input === null) {
162
- hash.update('null:');
163
- hash.update('\0');
164
- continue;
165
- }
166
- if (typeof input === 'string') {
167
- hash.update('str:');
168
- hash.update(input);
169
- hash.update('\0');
170
- continue;
171
- }
172
- if (typeof input === 'number') {
173
- hash.update('num:');
174
- hash.update(String(input));
175
- hash.update('\0');
176
- continue;
177
- }
178
- if (typeof input === 'boolean') {
179
- hash.update('bool:');
180
- hash.update(input ? '1' : '0');
181
- hash.update('\0');
182
- continue;
183
- }
184
- if (typeof input === 'bigint') {
185
- hash.update('bigint:');
186
- hash.update(input.toString());
187
- hash.update('\0');
188
- continue;
189
- }
190
- if (Buffer.isBuffer(input)) {
191
- hash.update('buf:');
192
- hash.update(input);
193
- hash.update('\0');
194
- continue;
195
- }
196
-
197
- if (typeof input === 'symbol') {
198
- hash.update('sym:');
199
- hash.update(input.toString());
200
- hash.update('\0');
201
- continue;
202
- }
203
-
204
- hash.update('json:');
205
- let json: string;
206
- try {
207
- json =
208
- JSON.stringify(input, (_key, value) =>
209
- typeof value === 'symbol' ? value.toString() : value
210
- ) ?? 'undefined';
211
- } catch {
212
- json = String(input);
213
- }
214
- hash.update(json);
215
160
  hash.update('\0');
216
161
  }
217
162
 
218
163
  return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
219
164
  }
220
165
 
166
+ /**
167
+ * Encode a single key input as a type tag plus optional payload.
168
+ * Why: disambiguate types so "1" and 1 don't collide, and keep input boundaries so
169
+ * ["a", "bc"] doesn't collide with ["ab", "c"].
170
+ */
171
+ private static encodeKeyInput(input: unknown): {
172
+ tag: string;
173
+ payload?: string | Buffer;
174
+ } {
175
+ if (input === undefined) {
176
+ return { tag: 'undef:' };
177
+ }
178
+ if (input === null) {
179
+ return { tag: 'null:' };
180
+ }
181
+ if (typeof input === 'string') {
182
+ return { tag: 'str:', payload: input };
183
+ }
184
+ if (typeof input === 'number') {
185
+ return { tag: 'num:', payload: String(input) };
186
+ }
187
+ if (typeof input === 'boolean') {
188
+ return { tag: 'bool:', payload: input ? '1' : '0' };
189
+ }
190
+ if (typeof input === 'bigint') {
191
+ return { tag: 'bigint:', payload: input.toString() };
192
+ }
193
+ if (Buffer.isBuffer(input)) {
194
+ return { tag: 'buf:', payload: input };
195
+ }
196
+ if (typeof input === 'symbol') {
197
+ return { tag: 'sym:', payload: input.toString() };
198
+ }
199
+
200
+ return { tag: 'json:', payload: ArtifactCache.stringifyKeyInput(input) };
201
+ }
202
+
203
+ /**
204
+ * Stringify an arbitrary key input for hashing, normalizing symbols and
205
+ * falling back to String() when JSON serialization fails.
206
+ */
207
+ private static stringifyKeyInput(input: unknown): string {
208
+ try {
209
+ return (
210
+ JSON.stringify(input, (_key, value) =>
211
+ typeof value === 'symbol' ? value.toString() : value
212
+ ) ?? 'undefined'
213
+ );
214
+ } catch {
215
+ return String(input);
216
+ }
217
+ }
218
+
221
219
  /**
222
220
  * Get entry from cache with performance tracking
223
221
  */
@@ -685,4 +683,4 @@ export class IntelligentCache {
685
683
  }
686
684
 
687
685
  // Export singleton instance
688
- export const globalCache = new IntelligentCache();
686
+ export const globalCache = new ArtifactCache();