tywrap 0.2.0 → 0.3.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 (163) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +23 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +23 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/base.d.ts +17 -7
  33. package/dist/runtime/base.d.ts.map +1 -1
  34. package/dist/runtime/base.js +18 -1
  35. package/dist/runtime/base.js.map +1 -1
  36. package/dist/runtime/bounded-context.d.ts +252 -0
  37. package/dist/runtime/bounded-context.d.ts.map +1 -0
  38. package/dist/runtime/bounded-context.js +454 -0
  39. package/dist/runtime/bounded-context.js.map +1 -0
  40. package/dist/runtime/bridge-core.d.ts +1 -0
  41. package/dist/runtime/bridge-core.d.ts.map +1 -1
  42. package/dist/runtime/bridge-core.js +52 -17
  43. package/dist/runtime/bridge-core.js.map +1 -1
  44. package/dist/runtime/bridge-protocol.d.ts +184 -0
  45. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  46. package/dist/runtime/bridge-protocol.js +344 -0
  47. package/dist/runtime/bridge-protocol.js.map +1 -0
  48. package/dist/runtime/disposable.d.ts +40 -0
  49. package/dist/runtime/disposable.d.ts.map +1 -0
  50. package/dist/runtime/disposable.js +49 -0
  51. package/dist/runtime/disposable.js.map +1 -0
  52. package/dist/runtime/errors.d.ts +10 -0
  53. package/dist/runtime/errors.d.ts.map +1 -1
  54. package/dist/runtime/errors.js +9 -0
  55. package/dist/runtime/errors.js.map +1 -1
  56. package/dist/runtime/http-io.d.ts +91 -0
  57. package/dist/runtime/http-io.d.ts.map +1 -0
  58. package/dist/runtime/http-io.js +195 -0
  59. package/dist/runtime/http-io.js.map +1 -0
  60. package/dist/runtime/http.d.ts +48 -13
  61. package/dist/runtime/http.d.ts.map +1 -1
  62. package/dist/runtime/http.js +60 -73
  63. package/dist/runtime/http.js.map +1 -1
  64. package/dist/runtime/node.d.ts +97 -130
  65. package/dist/runtime/node.d.ts.map +1 -1
  66. package/dist/runtime/node.js +392 -521
  67. package/dist/runtime/node.js.map +1 -1
  68. package/dist/runtime/optimized-node.d.ts +1 -1
  69. package/dist/runtime/optimized-node.d.ts.map +1 -1
  70. package/dist/runtime/optimized-node.js +1 -1
  71. package/dist/runtime/optimized-node.js.map +1 -1
  72. package/dist/runtime/pooled-transport.d.ts +135 -0
  73. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  74. package/dist/runtime/pooled-transport.js +177 -0
  75. package/dist/runtime/pooled-transport.js.map +1 -0
  76. package/dist/runtime/process-io.d.ts +209 -0
  77. package/dist/runtime/process-io.d.ts.map +1 -0
  78. package/dist/runtime/process-io.js +729 -0
  79. package/dist/runtime/process-io.js.map +1 -0
  80. package/dist/runtime/pyodide-io.d.ts +154 -0
  81. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  82. package/dist/runtime/pyodide-io.js +459 -0
  83. package/dist/runtime/pyodide-io.js.map +1 -0
  84. package/dist/runtime/pyodide.d.ts +51 -19
  85. package/dist/runtime/pyodide.d.ts.map +1 -1
  86. package/dist/runtime/pyodide.js +57 -186
  87. package/dist/runtime/pyodide.js.map +1 -1
  88. package/dist/runtime/safe-codec.d.ts +103 -0
  89. package/dist/runtime/safe-codec.d.ts.map +1 -0
  90. package/dist/runtime/safe-codec.js +519 -0
  91. package/dist/runtime/safe-codec.js.map +1 -0
  92. package/dist/runtime/transport.d.ts +191 -0
  93. package/dist/runtime/transport.d.ts.map +1 -0
  94. package/dist/runtime/transport.js +86 -0
  95. package/dist/runtime/transport.js.map +1 -0
  96. package/dist/runtime/validators.d.ts +120 -0
  97. package/dist/runtime/validators.d.ts.map +1 -0
  98. package/dist/runtime/validators.js +229 -0
  99. package/dist/runtime/validators.js.map +1 -0
  100. package/dist/runtime/worker-pool.d.ts +208 -0
  101. package/dist/runtime/worker-pool.d.ts.map +1 -0
  102. package/dist/runtime/worker-pool.js +419 -0
  103. package/dist/runtime/worker-pool.js.map +1 -0
  104. package/dist/types/index.d.ts +51 -1
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/tywrap.d.ts.map +1 -1
  107. package/dist/tywrap.js +227 -328
  108. package/dist/tywrap.js.map +1 -1
  109. package/dist/utils/cache.d.ts.map +1 -1
  110. package/dist/utils/cache.js +53 -5
  111. package/dist/utils/cache.js.map +1 -1
  112. package/dist/utils/codec.d.ts.map +1 -1
  113. package/dist/utils/codec.js +114 -1
  114. package/dist/utils/codec.js.map +1 -1
  115. package/dist/utils/ir-cache.d.ts +28 -0
  116. package/dist/utils/ir-cache.d.ts.map +1 -0
  117. package/dist/utils/ir-cache.js +29 -0
  118. package/dist/utils/ir-cache.js.map +1 -0
  119. package/dist/utils/parallel-processor.d.ts +1 -1
  120. package/dist/utils/parallel-processor.d.ts.map +1 -1
  121. package/dist/utils/parallel-processor.js +111 -40
  122. package/dist/utils/parallel-processor.js.map +1 -1
  123. package/dist/utils/runtime.d.ts +5 -1
  124. package/dist/utils/runtime.d.ts.map +1 -1
  125. package/dist/utils/runtime.js +112 -18
  126. package/dist/utils/runtime.js.map +1 -1
  127. package/package.json +40 -18
  128. package/runtime/python_bridge.py +108 -15
  129. package/runtime/safe_codec.py +344 -0
  130. package/src/cli.ts +61 -2
  131. package/src/config/index.ts +63 -17
  132. package/src/core/analyzer.ts +52 -15
  133. package/src/core/annotation-parser.ts +500 -0
  134. package/src/core/discovery.ts +26 -12
  135. package/src/core/generator.ts +956 -116
  136. package/src/core/mapper.ts +158 -51
  137. package/src/index.ts +78 -6
  138. package/src/runtime/base.ts +18 -26
  139. package/src/runtime/bounded-context.ts +608 -0
  140. package/src/runtime/bridge-core.ts +58 -18
  141. package/src/runtime/bridge-protocol.ts +483 -0
  142. package/src/runtime/disposable.ts +65 -0
  143. package/src/runtime/errors.ts +14 -0
  144. package/src/runtime/http-io.ts +245 -0
  145. package/src/runtime/http.ts +76 -115
  146. package/src/runtime/node.ts +563 -676
  147. package/src/runtime/optimized-node.ts +4 -1
  148. package/src/runtime/pooled-transport.ts +263 -0
  149. package/src/runtime/process-io.ts +930 -0
  150. package/src/runtime/pyodide-io.ts +551 -0
  151. package/src/runtime/pyodide.ts +75 -215
  152. package/src/runtime/safe-codec.ts +651 -0
  153. package/src/runtime/transport.ts +278 -0
  154. package/src/runtime/validators.ts +252 -0
  155. package/src/runtime/worker-pool.ts +559 -0
  156. package/src/types/global.d.ts +11 -1
  157. package/src/types/index.ts +63 -0
  158. package/src/tywrap.ts +279 -358
  159. package/src/utils/cache.ts +59 -5
  160. package/src/utils/codec.ts +122 -1
  161. package/src/utils/ir-cache.ts +51 -0
  162. package/src/utils/parallel-processor.ts +119 -43
  163. package/src/utils/runtime.ts +129 -18
@@ -147,18 +147,72 @@ export class IntelligentCache {
147
147
 
148
148
  // Add prefix
149
149
  hash.update(prefix);
150
+ hash.update('\0');
150
151
 
151
152
  // Add inputs
152
153
  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;
160
+ }
161
+ if (input === null) {
162
+ hash.update('null:');
163
+ hash.update('\0');
164
+ continue;
165
+ }
153
166
  if (typeof input === 'string') {
167
+ hash.update('str:');
154
168
  hash.update(input);
155
- } else if (Buffer.isBuffer(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:');
156
192
  hash.update(input);
157
- } else if (input !== undefined && input !== null) {
158
- hash.update(JSON.stringify(input));
159
- } else {
160
- hash.update('null');
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);
161
213
  }
214
+ hash.update(json);
215
+ hash.update('\0');
162
216
  }
163
217
 
164
218
  return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
@@ -243,6 +243,97 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
243
243
  );
244
244
  }
245
245
 
246
+ /**
247
+ * Convert a typed array (Int32Array, Float64Array, BigInt64Array, etc.) to a plain JS array.
248
+ *
249
+ * Why: Arrow's column.toArray() returns typed arrays, but we need plain arrays for
250
+ * JSON-compatible output and proper nested array reshaping.
251
+ *
252
+ * @param arr - Typed array or plain array
253
+ * @returns Plain JavaScript array with values converted (BigInt → Number where safe)
254
+ */
255
+ function typedArrayToPlain(arr: unknown): unknown[] {
256
+ if (Array.isArray(arr)) {
257
+ return arr;
258
+ }
259
+ // Handle typed arrays (Int32Array, Float64Array, BigInt64Array, etc.)
260
+ if (ArrayBuffer.isView(arr) && 'length' in arr) {
261
+ const values = Array.from(arr as unknown as ArrayLike<unknown>);
262
+ return values.map(value => {
263
+ // Convert BigInt to Number if within safe integer range
264
+ if (typeof value === 'bigint') {
265
+ if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
266
+ return Number(value);
267
+ }
268
+ }
269
+ return value; // Keep BigInt when too large (or preserve non-BigInt values)
270
+ });
271
+ }
272
+ // Fallback: check if iterable before converting
273
+ if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
274
+ return Array.from(arr as Iterable<unknown>);
275
+ }
276
+ // Non-iterable: return empty array (shouldn't happen with valid Arrow data)
277
+ return [];
278
+ }
279
+
280
+ /**
281
+ * Extract values from an Arrow table as a plain JavaScript array.
282
+ *
283
+ * Why: Arrow decoding returns Table objects, not raw arrays. We need to extract
284
+ * the column values and convert any typed arrays to plain arrays.
285
+ */
286
+ function extractArrowValues(data: unknown): unknown[] | null {
287
+ if (Array.isArray(data)) {
288
+ return data;
289
+ }
290
+ // Arrow table - extract values from first column
291
+ const table = data as ArrowTable & { getChildAt?: (i: number) => { toArray?: () => unknown } };
292
+ if (typeof table.getChildAt === 'function') {
293
+ const column = table.getChildAt(0);
294
+ if (column && typeof column.toArray === 'function') {
295
+ return typedArrayToPlain(column.toArray());
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+
301
+ /**
302
+ * Reshape a flat array into a multi-dimensional nested array.
303
+ *
304
+ * Why: PyArrow's pa.array() only handles 1D arrays, so we flatten multi-dimensional
305
+ * arrays before Arrow encoding and reshape after decoding. This maintains Arrow's
306
+ * binary efficiency while working with current arrow-js (which doesn't yet support
307
+ * FixedShapeTensorArray). See: https://github.com/apache/arrow-js/issues/115
308
+ *
309
+ * @param flat - Flat array of values (must be a plain array, not typed array)
310
+ * @param shape - Target shape, e.g., [2, 3] for a 2x3 matrix
311
+ * @returns Nested array with the specified shape
312
+ */
313
+ function reshapeArray(flat: unknown[], shape: readonly number[]): unknown {
314
+ if (shape.length === 0) {
315
+ return flat[0];
316
+ }
317
+ if (shape.length === 1) {
318
+ return flat;
319
+ }
320
+
321
+ const first = shape[0];
322
+ if (first === undefined) {
323
+ return [];
324
+ }
325
+ const rest = shape.slice(1);
326
+ const chunkSize = rest.reduce((a, b) => a * b, 1);
327
+ const result: unknown[] = [];
328
+
329
+ for (let i = 0; i < first; i++) {
330
+ const chunk = flat.slice(i * chunkSize, (i + 1) * chunkSize);
331
+ result.push(reshapeArray(chunk, rest));
332
+ }
333
+
334
+ return result;
335
+ }
336
+
246
337
  // Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
247
338
  const CODEC_VERSION = 1;
248
339
 
@@ -326,13 +417,43 @@ function decodeEnvelopeCore<T>(
326
417
 
327
418
  if (marker === 'ndarray') {
328
419
  const encoding = (value as { encoding?: unknown }).encoding;
420
+ const shapeValue = (value as { shape?: unknown }).shape;
421
+ const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
422
+
329
423
  if (encoding === 'arrow') {
330
424
  const b64 = (value as { b64?: unknown }).b64;
331
425
  if (typeof b64 !== 'string') {
332
426
  throw new Error('Invalid ndarray envelope: missing b64');
333
427
  }
334
428
  const bytes = fromBase64(b64);
335
- return decodeArrow(bytes);
429
+ const decoded = decodeArrow(bytes);
430
+
431
+ // Extract values from Arrow table and reshape if needed
432
+ // Arrow only handles 1D arrays, so we flatten on encode and reshape here
433
+ // Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
434
+ // Skip reshape for: 1D arrays (shape.length === 1) - return as-is
435
+ if (isPromiseLike(decoded)) {
436
+ return decoded.then(data => {
437
+ const values = extractArrowValues(data);
438
+ if (!values) {
439
+ return data; // Fallback: return raw data if extraction fails
440
+ }
441
+ // Reshape scalars and multi-dimensional arrays, but not 1D
442
+ if (shape && shape.length !== 1) {
443
+ return reshapeArray(values, shape);
444
+ }
445
+ return values;
446
+ });
447
+ }
448
+ const values = extractArrowValues(decoded);
449
+ if (!values) {
450
+ return decoded; // Fallback: return raw data if extraction fails
451
+ }
452
+ // Reshape scalars and multi-dimensional arrays, but not 1D
453
+ if (shape && shape.length !== 1) {
454
+ return reshapeArray(values, shape);
455
+ }
456
+ return values;
336
457
  }
337
458
  if (encoding === 'json') {
338
459
  if (!('data' in (value as object))) {
@@ -0,0 +1,51 @@
1
+ import { hashUtils } from './runtime.js';
2
+
3
+ export interface IrCacheKeyObject {
4
+ module: string;
5
+ moduleVersion: string | null;
6
+ irVersion: string;
7
+ pythonImportPath?: readonly string[];
8
+ runtime: {
9
+ pythonPath: string;
10
+ virtualEnv: string | null;
11
+ };
12
+ output: {
13
+ format: 'esm' | 'cjs' | 'both';
14
+ declaration: boolean;
15
+ sourceMap: boolean;
16
+ };
17
+ performance: {
18
+ caching: boolean;
19
+ compression: 'auto' | 'gzip' | 'brotli' | 'none';
20
+ };
21
+ typeHints: 'strict' | 'loose' | 'ignore';
22
+ }
23
+
24
+ function stableSortForJson(value: unknown): unknown {
25
+ if (Array.isArray(value)) {
26
+ return value.map(v => stableSortForJson(v));
27
+ }
28
+ if (value && typeof value === 'object') {
29
+ const obj = value as Record<string, unknown>;
30
+ const out: Record<string, unknown> = {};
31
+ for (const key of Object.keys(obj).sort()) {
32
+ out[key] = stableSortForJson(obj[key]);
33
+ }
34
+ return out;
35
+ }
36
+ return value;
37
+ }
38
+
39
+ /**
40
+ * Compute a stable, safe filename for on-disk IR caching.
41
+ *
42
+ * Why: cache filenames must not include user-controlled module names to avoid
43
+ * path traversal or invalid filename characters (especially on Windows).
44
+ */
45
+ export async function computeIrCacheFilename(keyObject: IrCacheKeyObject): Promise<string> {
46
+ // Canonicalize to avoid cache misses when key object property insertion order differs.
47
+ const normalized = JSON.stringify(stableSortForJson(keyObject));
48
+ const digest = await hashUtils.sha256Hex(normalized);
49
+ // Hash-only filename: safe ASCII, no separators, stable length.
50
+ return `ir_${digest.slice(0, 32)}.json`;
51
+ }
@@ -127,10 +127,12 @@ export class ParallelProcessor extends EventEmitter {
127
127
  private workers = new Map<string, Worker>();
128
128
  private workerStats = new Map<string, WorkerStats>();
129
129
  private taskQueue: ParallelTask[] = [];
130
+ private queueInterval?: NodeJS.Timeout;
130
131
  private activeTasks = new Map<string, ParallelTask>();
131
132
  private pendingResults = new Map<
132
133
  string,
133
134
  {
135
+ workerId: string;
134
136
  resolve: (result: ParallelResult<unknown>) => void;
135
137
  reject: (error: Error) => void;
136
138
  timeout?: NodeJS.Timeout;
@@ -329,12 +331,16 @@ export class ParallelProcessor extends EventEmitter {
329
331
  attempts++;
330
332
 
331
333
  try {
332
- const worker = this.selectOptimalWorker();
333
- if (!worker) {
334
+ const selected = this.selectOptimalWorker();
335
+ if (!selected) {
334
336
  throw new Error('No available workers');
335
337
  }
336
338
 
337
- const result = await this.sendTaskToWorker<Data, Result>(worker, task);
339
+ const result = await this.sendTaskToWorker<Data, Result>(
340
+ selected.workerId,
341
+ selected.worker,
342
+ task
343
+ );
338
344
 
339
345
  // Cache successful results
340
346
  if (this.options.enableCaching && result.success) {
@@ -366,10 +372,16 @@ export class ParallelProcessor extends EventEmitter {
366
372
  /**
367
373
  * Select optimal worker using load balancing strategy
368
374
  */
369
- private selectOptimalWorker(): Worker | null {
370
- const availableWorkers = Array.from(this.workers.values()).filter(
371
- worker => worker.threadId > 0
372
- );
375
+ private selectOptimalWorker(): { workerId: string; worker: Worker } | null {
376
+ const availableWorkers = Array.from(this.workers.entries())
377
+ .map(([workerId, worker]) => ({ workerId, worker }))
378
+ .filter(({ workerId, worker }) => {
379
+ if (worker.threadId <= 0) {
380
+ return false;
381
+ }
382
+ const stats = this.workerStats.get(workerId);
383
+ return stats?.isActive !== false;
384
+ });
373
385
 
374
386
  if (availableWorkers.length === 0) {
375
387
  return null;
@@ -384,37 +396,37 @@ export class ParallelProcessor extends EventEmitter {
384
396
 
385
397
  case 'least-loaded': {
386
398
  // Find worker with least active tasks
387
- const workerLoads = availableWorkers.map(w => {
388
- const workerId = this.getWorkerId(w);
399
+ const workerLoads = availableWorkers.map(({ workerId, worker }) => {
389
400
  const stats = this.workerStats.get(workerId);
390
- return { worker: w, load: stats?.tasksCompleted ?? 0 };
401
+ return { workerId, worker, load: stats?.tasksCompleted ?? 0 };
391
402
  });
392
403
 
393
404
  workerLoads.sort((a, b) => a.load - b.load);
394
- return workerLoads[0]?.worker ?? null;
405
+ const first = workerLoads[0];
406
+ return first ? { workerId: first.workerId, worker: first.worker } : null;
395
407
  }
396
408
 
397
409
  case 'weighted': {
398
410
  // Weight by average task completion time
399
- const workerWeights = availableWorkers.map(w => {
400
- const workerId = this.getWorkerId(w);
411
+ const workerWeights = availableWorkers.map(({ workerId, worker }) => {
401
412
  const stats = this.workerStats.get(workerId);
402
413
  const avgTime = stats?.averageTime ?? 1000;
403
- return { worker: w, weight: 1 / avgTime };
414
+ return { workerId, worker, weight: 1 / avgTime };
404
415
  });
405
416
 
406
417
  // Weighted random selection
407
418
  const totalWeight = workerWeights.reduce((sum, w) => sum + w.weight, 0);
408
419
  let random = Math.random() * totalWeight;
409
420
 
410
- for (const { worker: candidate, weight } of workerWeights) {
421
+ for (const { workerId, worker: candidate, weight } of workerWeights) {
411
422
  random -= weight;
412
423
  if (random <= 0) {
413
- return candidate;
424
+ return { workerId, worker: candidate };
414
425
  }
415
426
  }
416
427
 
417
- return workerWeights[0]?.worker ?? null;
428
+ const fallback = workerWeights[0];
429
+ return fallback ? { workerId: fallback.workerId, worker: fallback.worker } : null;
418
430
  }
419
431
 
420
432
  default:
@@ -426,17 +438,29 @@ export class ParallelProcessor extends EventEmitter {
426
438
  * Send task to specific worker
427
439
  */
428
440
  private async sendTaskToWorker<Data, Result>(
441
+ workerId: string,
429
442
  worker: Worker,
430
443
  task: ParallelTask<Data>
431
444
  ): Promise<ParallelResult<Result>> {
432
445
  return new Promise((resolve, reject) => {
446
+ if (this.disposed) {
447
+ reject(new Error('Processor has been disposed'));
448
+ return;
449
+ }
450
+
433
451
  const timeoutMs = task.timeout ?? this.options.taskTimeout;
434
452
  const timeout = setTimeout(() => {
453
+ const pending = this.pendingResults.get(task.id);
454
+ if (!pending) {
455
+ return;
456
+ }
435
457
  this.pendingResults.delete(task.id);
436
- reject(new Error(`Task ${task.id} timed out after ${timeoutMs}ms`));
458
+ this.activeTasks.delete(task.id);
459
+ pending.reject(new Error(`Task ${task.id} timed out after ${timeoutMs}ms`));
437
460
  }, timeoutMs);
438
461
 
439
462
  this.pendingResults.set(task.id, {
463
+ workerId,
440
464
  resolve: resolve as unknown as (result: ParallelResult<unknown>) => void,
441
465
  reject,
442
466
  timeout,
@@ -444,10 +468,17 @@ export class ParallelProcessor extends EventEmitter {
444
468
 
445
469
  this.activeTasks.set(task.id, task);
446
470
 
447
- worker.postMessage({
448
- type: 'task',
449
- task,
450
- });
471
+ try {
472
+ worker.postMessage({
473
+ type: 'task',
474
+ task,
475
+ });
476
+ } catch (error: unknown) {
477
+ this.pendingResults.delete(task.id);
478
+ this.activeTasks.delete(task.id);
479
+ clearTimeout(timeout);
480
+ reject(error instanceof Error ? error : new Error(String(error)));
481
+ }
451
482
  });
452
483
  }
453
484
 
@@ -472,8 +503,9 @@ export class ParallelProcessor extends EventEmitter {
472
503
 
473
504
  // Setup worker error handling
474
505
  worker.on('error', error => {
475
- log.error('Worker error', { workerId, error: String(error) });
476
- this.handleWorkerError(workerId, error);
506
+ const workerError = error instanceof Error ? error : new Error(String(error));
507
+ log.error('Worker error', { workerId, error: String(workerError) });
508
+ this.handleWorkerError(workerId, workerError);
477
509
  });
478
510
 
479
511
  // Setup worker exit handling
@@ -554,15 +586,25 @@ export class ParallelProcessor extends EventEmitter {
554
586
  stats.isActive = false;
555
587
  }
556
588
 
557
- // Reject pending tasks for this worker
589
+ // Reject only tasks owned by this worker.
590
+ const ownedTasks: string[] = [];
558
591
  for (const [taskId, pending] of this.pendingResults) {
559
- const task = this.activeTasks.get(taskId);
560
- if (task) {
561
- pending.reject(new Error(`Worker ${workerId} error: ${error.message}`));
562
- this.pendingResults.delete(taskId);
563
- this.activeTasks.delete(taskId);
592
+ if (pending.workerId === workerId) {
593
+ ownedTasks.push(taskId);
564
594
  }
565
595
  }
596
+ for (const taskId of ownedTasks) {
597
+ const pending = this.pendingResults.get(taskId);
598
+ if (!pending) {
599
+ continue;
600
+ }
601
+ if (pending.timeout) {
602
+ clearTimeout(pending.timeout);
603
+ }
604
+ pending.reject(new Error(`Worker ${workerId} error: ${error.message}`));
605
+ this.pendingResults.delete(taskId);
606
+ this.activeTasks.delete(taskId);
607
+ }
566
608
 
567
609
  this.emit('worker_error', workerId, error);
568
610
  }
@@ -578,6 +620,26 @@ export class ParallelProcessor extends EventEmitter {
578
620
  stats.isActive = false;
579
621
  }
580
622
 
623
+ // Reject any tasks that were in-flight on this worker.
624
+ const ownedTasks: string[] = [];
625
+ for (const [taskId, pending] of this.pendingResults) {
626
+ if (pending.workerId === workerId) {
627
+ ownedTasks.push(taskId);
628
+ }
629
+ }
630
+ for (const taskId of ownedTasks) {
631
+ const pending = this.pendingResults.get(taskId);
632
+ if (!pending) {
633
+ continue;
634
+ }
635
+ if (pending.timeout) {
636
+ clearTimeout(pending.timeout);
637
+ }
638
+ pending.reject(new Error(`Worker ${workerId} exited with code ${code}`));
639
+ this.pendingResults.delete(taskId);
640
+ this.activeTasks.delete(taskId);
641
+ }
642
+
581
643
  // Respawn worker if not disposing
582
644
  if (!this.disposed && this.workers.size < this.options.maxWorkers) {
583
645
  this.spawnWorker(`${workerId}_respawn_${Date.now()}`).catch(error => {
@@ -593,7 +655,10 @@ export class ParallelProcessor extends EventEmitter {
593
655
  */
594
656
  private processQueue(): void {
595
657
  // Simple queue processing - could be enhanced with priority scheduling
596
- setInterval(() => {
658
+ if (this.queueInterval) {
659
+ return;
660
+ }
661
+ this.queueInterval = setInterval(() => {
597
662
  if (this.taskQueue.length > 0 && this.workers.size > 0) {
598
663
  const task = this.taskQueue.shift();
599
664
  if (task) {
@@ -603,6 +668,8 @@ export class ParallelProcessor extends EventEmitter {
603
668
  }
604
669
  }
605
670
  }, 100);
671
+ // Don't keep the event loop alive just for an idle queue pump.
672
+ this.queueInterval.unref?.();
606
673
  }
607
674
 
608
675
  /**
@@ -645,15 +712,6 @@ export class ParallelProcessor extends EventEmitter {
645
712
  return chunks;
646
713
  }
647
714
 
648
- private getWorkerId(worker: Worker): string {
649
- for (const [id, w] of this.workers) {
650
- if (w === worker) {
651
- return id;
652
- }
653
- }
654
- return 'unknown';
655
- }
656
-
657
715
  private sleep(ms: number): Promise<void> {
658
716
  return new Promise(resolve => setTimeout(resolve, ms));
659
717
  }
@@ -670,6 +728,22 @@ export class ParallelProcessor extends EventEmitter {
670
728
 
671
729
  this.debugLog('🛑 Disposing parallel processor...');
672
730
 
731
+ if (this.queueInterval) {
732
+ clearInterval(this.queueInterval);
733
+ this.queueInterval = undefined;
734
+ }
735
+
736
+ // Reject any pending results immediately so callers don't hang.
737
+ const disposedError = new Error('Processor has been disposed');
738
+ for (const pending of this.pendingResults.values()) {
739
+ if (pending.timeout) {
740
+ clearTimeout(pending.timeout);
741
+ }
742
+ pending.reject(disposedError);
743
+ }
744
+ this.pendingResults.clear();
745
+ this.activeTasks.clear();
746
+
673
747
  // Terminate all workers
674
748
  const terminationPromises = Array.from(this.workers.values()).map(worker => {
675
749
  return new Promise<void>(resolve => {
@@ -683,7 +757,11 @@ export class ParallelProcessor extends EventEmitter {
683
757
  resolve();
684
758
  });
685
759
 
686
- worker.postMessage({ type: 'shutdown' });
760
+ try {
761
+ worker.postMessage({ type: 'shutdown' });
762
+ } catch {
763
+ // Ignore: worker may already be dead.
764
+ }
687
765
  });
688
766
  });
689
767
 
@@ -693,8 +771,6 @@ export class ParallelProcessor extends EventEmitter {
693
771
  this.workers.clear();
694
772
  this.workerStats.clear();
695
773
  this.taskQueue.length = 0;
696
- this.activeTasks.clear();
697
- this.pendingResults.clear();
698
774
 
699
775
  this.removeAllListeners();
700
776