tywrap 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -32
- package/dist/cli.js +70 -5
- 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 +62 -20
- 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 +46 -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/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- 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 +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- 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.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- 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 +10 -16
- 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 +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -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/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- 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
|
@@ -258,22 +258,16 @@ function typedArrayToPlain(arr: unknown): unknown[] {
|
|
|
258
258
|
}
|
|
259
259
|
// Handle typed arrays (Int32Array, Float64Array, BigInt64Array, etc.)
|
|
260
260
|
if (ArrayBuffer.isView(arr) && 'length' in arr) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
for (let i = 0; i < typedArr.length; i++) {
|
|
264
|
-
const val = typedArr[i];
|
|
261
|
+
const values = Array.from(arr as unknown as ArrayLike<unknown>);
|
|
262
|
+
return values.map(value => {
|
|
265
263
|
// Convert BigInt to Number if within safe integer range
|
|
266
|
-
if (typeof
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
} else {
|
|
270
|
-
result.push(val); // Keep as BigInt if too large
|
|
264
|
+
if (typeof value === 'bigint') {
|
|
265
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
266
|
+
return Number(value);
|
|
271
267
|
}
|
|
272
|
-
} else {
|
|
273
|
-
result.push(val);
|
|
274
268
|
}
|
|
275
|
-
|
|
276
|
-
|
|
269
|
+
return value; // Keep BigInt when too large (or preserve non-BigInt values)
|
|
270
|
+
});
|
|
277
271
|
}
|
|
278
272
|
// Fallback: check if iterable before converting
|
|
279
273
|
if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
|
|
@@ -324,8 +318,10 @@ function reshapeArray(flat: unknown[], shape: readonly number[]): unknown {
|
|
|
324
318
|
return flat;
|
|
325
319
|
}
|
|
326
320
|
|
|
327
|
-
|
|
328
|
-
|
|
321
|
+
const first = shape[0];
|
|
322
|
+
if (first === undefined) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
329
325
|
const rest = shape.slice(1);
|
|
330
326
|
const chunkSize = rest.reduce((a, b) => a * b, 1);
|
|
331
327
|
const result: unknown[] = [];
|
|
@@ -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
|
|
package/src/utils/runtime.ts
CHANGED
|
@@ -432,7 +432,8 @@ export const processUtils = {
|
|
|
432
432
|
*/
|
|
433
433
|
async exec(
|
|
434
434
|
command: string,
|
|
435
|
-
args: string[] = []
|
|
435
|
+
args: string[] = [],
|
|
436
|
+
options: { timeoutMs?: number; env?: Record<string, string | undefined>; cwd?: string } = {}
|
|
436
437
|
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
437
438
|
const runtime = detectRuntime();
|
|
438
439
|
|
|
@@ -441,28 +442,88 @@ export const processUtils = {
|
|
|
441
442
|
}
|
|
442
443
|
|
|
443
444
|
if (runtime.name === 'deno' && Deno) {
|
|
444
|
-
const
|
|
445
|
-
const
|
|
445
|
+
const timeoutMs = options.timeoutMs;
|
|
446
|
+
const controller =
|
|
447
|
+
typeof timeoutMs === 'number' && timeoutMs > 0 ? new AbortController() : undefined;
|
|
448
|
+
const timeout =
|
|
449
|
+
controller && typeof timeoutMs === 'number'
|
|
450
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
451
|
+
: undefined;
|
|
452
|
+
const env: Record<string, string> | undefined =
|
|
453
|
+
options.env && Object.keys(options.env).length > 0
|
|
454
|
+
? Object.fromEntries(
|
|
455
|
+
Object.entries(options.env)
|
|
456
|
+
.filter(([, value]) => typeof value === 'string')
|
|
457
|
+
.map(([key, value]) => [key, value as string])
|
|
458
|
+
)
|
|
459
|
+
: undefined;
|
|
446
460
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
461
|
+
try {
|
|
462
|
+
const cmd = new Deno.Command(command, {
|
|
463
|
+
args,
|
|
464
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
465
|
+
...(env ? { env } : {}),
|
|
466
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
467
|
+
});
|
|
468
|
+
const { code, stdout, stderr } = await cmd.output();
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
code,
|
|
472
|
+
stdout: new TextDecoder().decode(stdout),
|
|
473
|
+
stderr: new TextDecoder().decode(stderr),
|
|
474
|
+
};
|
|
475
|
+
} catch (error: unknown) {
|
|
476
|
+
if (controller?.signal.aborted && typeof timeoutMs === 'number') {
|
|
477
|
+
throw new Error(`Command "${command}" timed out after ${timeoutMs}ms`);
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
} finally {
|
|
481
|
+
if (timeout) {
|
|
482
|
+
clearTimeout(timeout);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
452
485
|
}
|
|
453
486
|
|
|
454
487
|
if (runtime.name === 'bun' && Bun) {
|
|
488
|
+
const timeoutMs = options.timeoutMs;
|
|
489
|
+
const mergedEnv: Record<string, string> | undefined = options.env
|
|
490
|
+
? Object.fromEntries(
|
|
491
|
+
Object.entries({
|
|
492
|
+
...(typeof process !== 'undefined' ? process.env : {}),
|
|
493
|
+
...options.env,
|
|
494
|
+
})
|
|
495
|
+
.filter(([, value]) => typeof value === 'string')
|
|
496
|
+
.map(([key, value]) => [key, value as string])
|
|
497
|
+
)
|
|
498
|
+
: undefined;
|
|
455
499
|
const proc = Bun.spawn([command, ...args], {
|
|
456
500
|
stdout: 'pipe',
|
|
457
501
|
stderr: 'pipe',
|
|
502
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
503
|
+
...(mergedEnv ? { env: mergedEnv } : {}),
|
|
458
504
|
});
|
|
459
505
|
|
|
506
|
+
let timedOut = false;
|
|
507
|
+
const timeout =
|
|
508
|
+
typeof timeoutMs === 'number' && timeoutMs > 0
|
|
509
|
+
? setTimeout(() => {
|
|
510
|
+
timedOut = true;
|
|
511
|
+
proc.kill?.('SIGKILL');
|
|
512
|
+
}, timeoutMs)
|
|
513
|
+
: undefined;
|
|
514
|
+
|
|
460
515
|
const [stdout, stderr] = await Promise.all([
|
|
461
516
|
new Response(proc.stdout).text(),
|
|
462
517
|
new Response(proc.stderr).text(),
|
|
463
518
|
]);
|
|
464
519
|
|
|
465
520
|
await proc.exited;
|
|
521
|
+
if (timeout) {
|
|
522
|
+
clearTimeout(timeout);
|
|
523
|
+
}
|
|
524
|
+
if (timedOut && typeof timeoutMs === 'number') {
|
|
525
|
+
throw new Error(`Command "${command}" timed out after ${timeoutMs}ms`);
|
|
526
|
+
}
|
|
466
527
|
|
|
467
528
|
return {
|
|
468
529
|
code: proc.exitCode ?? 0,
|
|
@@ -476,15 +537,51 @@ export const processUtils = {
|
|
|
476
537
|
const { delimiter } = await import('node:path');
|
|
477
538
|
|
|
478
539
|
return new Promise((resolve, reject) => {
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
const env =
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
540
|
+
const cwd = options.cwd ?? process.cwd();
|
|
541
|
+
const extraPyPath = pathUtils.join(cwd, 'tywrap_ir');
|
|
542
|
+
const env: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
543
|
+
Object.entries(process.env).forEach(([key, value]) => {
|
|
544
|
+
if (value !== undefined) {
|
|
545
|
+
// eslint-disable-next-line security/detect-object-injection -- environment variable keys are user/runtime supplied
|
|
546
|
+
env[key] = value;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
Object.entries(options.env ?? {}).forEach(([key, value]) => {
|
|
550
|
+
if (value === undefined) {
|
|
551
|
+
// eslint-disable-next-line security/detect-object-injection -- environment variable keys are user/runtime supplied
|
|
552
|
+
delete env[key];
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// eslint-disable-next-line security/detect-object-injection -- environment variable keys are user/runtime supplied
|
|
556
|
+
env[key] = value;
|
|
557
|
+
});
|
|
558
|
+
const existingPyPath = env.PYTHONPATH;
|
|
559
|
+
env.PYTHONPATH = existingPyPath
|
|
560
|
+
? `${extraPyPath}${delimiter}${existingPyPath}`
|
|
561
|
+
: extraPyPath;
|
|
562
|
+
const child = spawn(command, args, { env, cwd: options.cwd });
|
|
486
563
|
let stdout = '';
|
|
487
564
|
let stderr = '';
|
|
565
|
+
let settled = false;
|
|
566
|
+
let timedOut = false;
|
|
567
|
+
const timeout =
|
|
568
|
+
typeof options.timeoutMs === 'number' && options.timeoutMs > 0
|
|
569
|
+
? setTimeout(() => {
|
|
570
|
+
timedOut = true;
|
|
571
|
+
child.kill('SIGKILL');
|
|
572
|
+
}, options.timeoutMs)
|
|
573
|
+
: undefined;
|
|
574
|
+
|
|
575
|
+
const finish = (fn: () => void): void => {
|
|
576
|
+
if (settled) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
settled = true;
|
|
580
|
+
if (timeout) {
|
|
581
|
+
clearTimeout(timeout);
|
|
582
|
+
}
|
|
583
|
+
fn();
|
|
584
|
+
};
|
|
488
585
|
|
|
489
586
|
child.stdout?.on('data', data => {
|
|
490
587
|
stdout += data.toString();
|
|
@@ -495,10 +592,23 @@ export const processUtils = {
|
|
|
495
592
|
});
|
|
496
593
|
|
|
497
594
|
child.on('close', code => {
|
|
498
|
-
|
|
595
|
+
if (timedOut) {
|
|
596
|
+
const timeoutMs = options.timeoutMs;
|
|
597
|
+
finish(() =>
|
|
598
|
+
reject(
|
|
599
|
+
new Error(
|
|
600
|
+
`Command "${command}" timed out after ${typeof timeoutMs === 'number' ? timeoutMs : 0}ms`
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
finish(() => resolve({ code: code ?? 0, stdout, stderr }));
|
|
499
607
|
});
|
|
500
608
|
|
|
501
|
-
child.on('error',
|
|
609
|
+
child.on('error', error => {
|
|
610
|
+
finish(() => reject(error));
|
|
611
|
+
});
|
|
502
612
|
});
|
|
503
613
|
}
|
|
504
614
|
|
|
@@ -611,6 +721,7 @@ export const hashUtils = {
|
|
|
611
721
|
hash = (hash << 5) + hash + text.charCodeAt(i);
|
|
612
722
|
hash |= 0;
|
|
613
723
|
}
|
|
614
|
-
|
|
724
|
+
// Match sha256 hex shape (64 chars) so callers can rely on fixed-length output.
|
|
725
|
+
return Math.abs(hash).toString(16).padStart(64, '0');
|
|
615
726
|
},
|
|
616
727
|
};
|