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.
- package/README.md +65 -31
- package/dist/cli.js +49 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -7
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +47 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +114 -1
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +40 -18
- package/runtime/python_bridge.py +108 -15
- package/runtime/safe_codec.py +344 -0
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/index.ts +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/src/utils/cache.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
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
|
package/src/utils/codec.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
333
|
-
if (!
|
|
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>(
|
|
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.
|
|
371
|
-
worker => worker
|
|
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(
|
|
388
|
-
const workerId = this.getWorkerId(w);
|
|
399
|
+
const workerLoads = availableWorkers.map(({ workerId, worker }) => {
|
|
389
400
|
const stats = this.workerStats.get(workerId);
|
|
390
|
-
return { worker
|
|
401
|
+
return { workerId, worker, load: stats?.tasksCompleted ?? 0 };
|
|
391
402
|
});
|
|
392
403
|
|
|
393
404
|
workerLoads.sort((a, b) => a.load - b.load);
|
|
394
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
|
589
|
+
// Reject only tasks owned by this worker.
|
|
590
|
+
const ownedTasks: string[] = [];
|
|
558
591
|
for (const [taskId, pending] of this.pendingResults) {
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|