@computekit/core 0.1.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/dist/index.cjs +820 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +270 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.js +800 -0
- package/dist/index.js.map +1 -0
- package/dist/types-AaH5nWxG.d.cts +143 -0
- package/dist/types-AaH5nWxG.d.ts +143 -0
- package/dist/worker.cjs +148 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.cts +23 -0
- package/dist/worker.d.ts +23 -0
- package/dist/worker.js +143 -0
- package/dist/worker.js.map +1 -0
- package/package.json +61 -0
- package/src/cli.ts +246 -0
- package/src/index.test.ts +93 -0
- package/src/index.ts +232 -0
- package/src/pool.ts +591 -0
- package/src/types.ts +229 -0
- package/src/utils.ts +305 -0
- package/src/wasm.ts +205 -0
- package/src/worker/index.ts +11 -0
- package/src/worker/runtime.ts +149 -0
package/src/pool.ts
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComputeKit Worker Pool
|
|
3
|
+
* Manages a pool of Web Workers for parallel computation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ComputeKitOptions,
|
|
8
|
+
ComputeOptions,
|
|
9
|
+
ComputeProgress,
|
|
10
|
+
WorkerInfo,
|
|
11
|
+
WorkerState,
|
|
12
|
+
PoolStats,
|
|
13
|
+
WorkerMessage,
|
|
14
|
+
ExecutePayload,
|
|
15
|
+
ResultPayload,
|
|
16
|
+
ErrorPayload,
|
|
17
|
+
ProgressPayload,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
generateId,
|
|
22
|
+
createDeferred,
|
|
23
|
+
withTimeout,
|
|
24
|
+
findTransferables,
|
|
25
|
+
getHardwareConcurrency,
|
|
26
|
+
createLogger,
|
|
27
|
+
type Deferred,
|
|
28
|
+
type Logger,
|
|
29
|
+
} from './utils';
|
|
30
|
+
|
|
31
|
+
/** Task in the queue */
|
|
32
|
+
interface QueuedTask<T = unknown> {
|
|
33
|
+
id: string;
|
|
34
|
+
functionName: string;
|
|
35
|
+
input: unknown;
|
|
36
|
+
options?: ComputeOptions;
|
|
37
|
+
deferred: Deferred<T>;
|
|
38
|
+
priority: number;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
onProgress?: (progress: ComputeProgress) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Worker wrapper */
|
|
44
|
+
interface PoolWorker {
|
|
45
|
+
id: string;
|
|
46
|
+
worker: Worker;
|
|
47
|
+
state: WorkerState;
|
|
48
|
+
currentTask?: string;
|
|
49
|
+
tasksCompleted: number;
|
|
50
|
+
errors: number;
|
|
51
|
+
createdAt: number;
|
|
52
|
+
lastActiveAt: number;
|
|
53
|
+
ready: boolean;
|
|
54
|
+
readyPromise: Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registry entry for compute functions */
|
|
58
|
+
interface RegisteredFunction {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
60
|
+
fn: Function;
|
|
61
|
+
serialized: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Worker Pool - manages Web Workers for parallel computation
|
|
66
|
+
*/
|
|
67
|
+
export class WorkerPool {
|
|
68
|
+
private workers: Map<string, PoolWorker> = new Map();
|
|
69
|
+
private taskQueue: QueuedTask[] = [];
|
|
70
|
+
private pendingTasks: Map<string, QueuedTask> = new Map();
|
|
71
|
+
private functions: Map<string, RegisteredFunction> = new Map();
|
|
72
|
+
private workerUrl: string | null = null;
|
|
73
|
+
private options: Required<ComputeKitOptions>;
|
|
74
|
+
private logger: Logger;
|
|
75
|
+
private initialized = false;
|
|
76
|
+
private stats = {
|
|
77
|
+
tasksCompleted: 0,
|
|
78
|
+
tasksFailed: 0,
|
|
79
|
+
totalDuration: 0,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
constructor(options: ComputeKitOptions = {}) {
|
|
83
|
+
this.options = {
|
|
84
|
+
maxWorkers: options.maxWorkers ?? getHardwareConcurrency(),
|
|
85
|
+
timeout: options.timeout ?? 30000,
|
|
86
|
+
debug: options.debug ?? false,
|
|
87
|
+
workerPath: options.workerPath ?? '',
|
|
88
|
+
useSharedMemory: options.useSharedMemory ?? true,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.logger = createLogger('ComputeKit:Pool', this.options.debug);
|
|
92
|
+
this.logger.info('WorkerPool created with options:', this.options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initialize the worker pool
|
|
97
|
+
*/
|
|
98
|
+
async initialize(): Promise<void> {
|
|
99
|
+
if (this.initialized) return;
|
|
100
|
+
|
|
101
|
+
this.logger.info('Initializing worker pool...');
|
|
102
|
+
this.logger.info('Registered functions:', Array.from(this.functions.keys()));
|
|
103
|
+
this.workerUrl = this.createWorkerBlob();
|
|
104
|
+
|
|
105
|
+
// Create initial workers
|
|
106
|
+
const workerCount = Math.min(2, this.options.maxWorkers);
|
|
107
|
+
for (let i = 0; i < workerCount; i++) {
|
|
108
|
+
await this.createWorker();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.initialized = true;
|
|
112
|
+
this.logger.info(`Worker pool initialized with ${workerCount} workers`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private pendingRecreate: Promise<void> | null = null;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register a compute function
|
|
119
|
+
*/
|
|
120
|
+
register<TInput, TOutput>(
|
|
121
|
+
name: string,
|
|
122
|
+
fn: (input: TInput) => TOutput | Promise<TOutput>
|
|
123
|
+
): void {
|
|
124
|
+
this.logger.debug(`Registering function: ${name}`);
|
|
125
|
+
this.functions.set(name, {
|
|
126
|
+
fn,
|
|
127
|
+
serialized: fn.toString(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// If already initialized, we need to recreate workers with updated functions
|
|
131
|
+
if (this.initialized) {
|
|
132
|
+
this.pendingRecreate = this.recreateWorkers();
|
|
133
|
+
} else {
|
|
134
|
+
// If not initialized yet but workerUrl exists, revoke it so it gets recreated
|
|
135
|
+
if (this.workerUrl) {
|
|
136
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
137
|
+
this.workerUrl = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Recreate workers with updated function registry
|
|
144
|
+
*/
|
|
145
|
+
private async recreateWorkers(): Promise<void> {
|
|
146
|
+
this.logger.debug('Recreating workers with updated functions...');
|
|
147
|
+
|
|
148
|
+
// Revoke old blob URL
|
|
149
|
+
if (this.workerUrl) {
|
|
150
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create new worker blob with all functions
|
|
154
|
+
this.workerUrl = this.createWorkerBlob();
|
|
155
|
+
|
|
156
|
+
// Terminate existing idle workers and create new ones
|
|
157
|
+
const idleWorkers = Array.from(this.workers.entries()).filter(
|
|
158
|
+
([_, w]) => w.state === 'idle'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
for (const [id, poolWorker] of idleWorkers) {
|
|
162
|
+
poolWorker.worker.terminate();
|
|
163
|
+
this.workers.delete(id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create new workers
|
|
167
|
+
const workerCount = Math.max(
|
|
168
|
+
1,
|
|
169
|
+
Math.min(2, this.options.maxWorkers) - this.workers.size
|
|
170
|
+
);
|
|
171
|
+
for (let i = 0; i < workerCount; i++) {
|
|
172
|
+
await this.createWorker();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Execute a compute function
|
|
178
|
+
*/
|
|
179
|
+
async execute<TInput, TOutput>(
|
|
180
|
+
name: string,
|
|
181
|
+
input: TInput,
|
|
182
|
+
options?: ComputeOptions
|
|
183
|
+
): Promise<TOutput> {
|
|
184
|
+
// Wait for any pending worker recreation
|
|
185
|
+
if (this.pendingRecreate) {
|
|
186
|
+
await this.pendingRecreate;
|
|
187
|
+
this.pendingRecreate = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this.initialized) {
|
|
191
|
+
await this.initialize();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const fn = this.functions.get(name);
|
|
195
|
+
if (!fn) {
|
|
196
|
+
throw new Error(`Function "${name}" not registered`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const taskId = generateId();
|
|
200
|
+
const timeout = options?.timeout ?? this.options.timeout;
|
|
201
|
+
|
|
202
|
+
this.logger.debug(`Executing task ${taskId} for function "${name}"`);
|
|
203
|
+
|
|
204
|
+
// Check for abort signal
|
|
205
|
+
if (options?.signal?.aborted) {
|
|
206
|
+
throw new Error('Operation aborted');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const deferred = createDeferred<TOutput>();
|
|
210
|
+
const task: QueuedTask<TOutput> = {
|
|
211
|
+
id: taskId,
|
|
212
|
+
functionName: name,
|
|
213
|
+
input,
|
|
214
|
+
options,
|
|
215
|
+
deferred,
|
|
216
|
+
priority: options?.priority ?? 5,
|
|
217
|
+
createdAt: Date.now(),
|
|
218
|
+
onProgress: options?.onProgress,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Handle abort signal
|
|
222
|
+
if (options?.signal) {
|
|
223
|
+
options.signal.addEventListener('abort', () => {
|
|
224
|
+
this.cancelTask(taskId);
|
|
225
|
+
deferred.reject(new Error('Operation aborted'));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Add to queue
|
|
230
|
+
this.enqueue(task);
|
|
231
|
+
this.processQueue();
|
|
232
|
+
|
|
233
|
+
return withTimeout(deferred.promise, timeout, `Task "${name}" timed out`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get pool statistics
|
|
238
|
+
*/
|
|
239
|
+
getStats(): PoolStats {
|
|
240
|
+
const workers = Array.from(this.workers.values()).map(
|
|
241
|
+
(w): WorkerInfo => ({
|
|
242
|
+
id: w.id,
|
|
243
|
+
state: w.state,
|
|
244
|
+
currentTask: w.currentTask,
|
|
245
|
+
tasksCompleted: w.tasksCompleted,
|
|
246
|
+
errors: w.errors,
|
|
247
|
+
createdAt: w.createdAt,
|
|
248
|
+
lastActiveAt: w.lastActiveAt,
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
workers,
|
|
254
|
+
totalWorkers: this.workers.size,
|
|
255
|
+
activeWorkers: workers.filter((w) => w.state === 'busy').length,
|
|
256
|
+
idleWorkers: workers.filter((w) => w.state === 'idle').length,
|
|
257
|
+
queueLength: this.taskQueue.length,
|
|
258
|
+
tasksCompleted: this.stats.tasksCompleted,
|
|
259
|
+
tasksFailed: this.stats.tasksFailed,
|
|
260
|
+
averageTaskDuration:
|
|
261
|
+
this.stats.tasksCompleted > 0
|
|
262
|
+
? this.stats.totalDuration / this.stats.tasksCompleted
|
|
263
|
+
: 0,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Terminate all workers and clean up
|
|
269
|
+
*/
|
|
270
|
+
async terminate(): Promise<void> {
|
|
271
|
+
this.logger.info('Terminating worker pool...');
|
|
272
|
+
|
|
273
|
+
// Reject all pending tasks
|
|
274
|
+
for (const task of this.pendingTasks.values()) {
|
|
275
|
+
task.deferred.reject(new Error('Worker pool terminated'));
|
|
276
|
+
}
|
|
277
|
+
this.pendingTasks.clear();
|
|
278
|
+
this.taskQueue = [];
|
|
279
|
+
|
|
280
|
+
// Terminate all workers
|
|
281
|
+
for (const poolWorker of this.workers.values()) {
|
|
282
|
+
poolWorker.worker.terminate();
|
|
283
|
+
poolWorker.state = 'terminated';
|
|
284
|
+
}
|
|
285
|
+
this.workers.clear();
|
|
286
|
+
|
|
287
|
+
// Revoke blob URL
|
|
288
|
+
if (this.workerUrl) {
|
|
289
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
290
|
+
this.workerUrl = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.initialized = false;
|
|
294
|
+
this.logger.info('Worker pool terminated');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create the worker blob URL
|
|
299
|
+
*/
|
|
300
|
+
private createWorkerBlob(): string {
|
|
301
|
+
// Serialize all registered functions
|
|
302
|
+
const functionsCode = Array.from(this.functions.entries())
|
|
303
|
+
.map(([name, { serialized }]) => `"${name}": ${serialized}`)
|
|
304
|
+
.join(',\n');
|
|
305
|
+
|
|
306
|
+
this.logger.debug(
|
|
307
|
+
'Creating worker blob with functions:',
|
|
308
|
+
Array.from(this.functions.keys())
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const workerCode = `
|
|
312
|
+
const functions = {
|
|
313
|
+
${functionsCode}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
self.onmessage = function(e) {
|
|
317
|
+
const msg = e.data;
|
|
318
|
+
if (msg.type === 'execute') {
|
|
319
|
+
const fn = functions[msg.payload.functionName];
|
|
320
|
+
if (!fn) {
|
|
321
|
+
self.postMessage({ id: msg.id, type: 'error', payload: { message: 'Function not found: ' + msg.payload.functionName } });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const start = performance.now();
|
|
326
|
+
Promise.resolve(fn(msg.payload.input)).then(function(result) {
|
|
327
|
+
self.postMessage({ id: msg.id, type: 'result', payload: { data: result, duration: performance.now() - start } });
|
|
328
|
+
}).catch(function(err) {
|
|
329
|
+
self.postMessage({ id: msg.id, type: 'error', payload: { message: err.message || String(err) } });
|
|
330
|
+
});
|
|
331
|
+
} catch (err) {
|
|
332
|
+
self.postMessage({ id: msg.id, type: 'error', payload: { message: err.message || String(err) } });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
self.postMessage({ type: 'ready' });
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
340
|
+
return URL.createObjectURL(blob);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create a new worker
|
|
345
|
+
*/
|
|
346
|
+
private async createWorker(): Promise<PoolWorker> {
|
|
347
|
+
if (!this.workerUrl) {
|
|
348
|
+
this.workerUrl = this.createWorkerBlob();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const id = generateId();
|
|
352
|
+
const worker = new Worker(this.workerUrl);
|
|
353
|
+
|
|
354
|
+
// Create ready promise
|
|
355
|
+
let resolveReady: () => void;
|
|
356
|
+
const readyPromise = new Promise<void>((resolve) => {
|
|
357
|
+
resolveReady = resolve;
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const poolWorker: PoolWorker = {
|
|
361
|
+
id,
|
|
362
|
+
worker,
|
|
363
|
+
state: 'idle',
|
|
364
|
+
tasksCompleted: 0,
|
|
365
|
+
errors: 0,
|
|
366
|
+
createdAt: Date.now(),
|
|
367
|
+
lastActiveAt: Date.now(),
|
|
368
|
+
ready: false,
|
|
369
|
+
readyPromise,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Set up message handler
|
|
373
|
+
worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
|
374
|
+
if (e.data.type === 'ready') {
|
|
375
|
+
poolWorker.ready = true;
|
|
376
|
+
resolveReady!();
|
|
377
|
+
}
|
|
378
|
+
this.handleWorkerMessage(poolWorker, e.data);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
worker.onerror = (e: ErrorEvent) => {
|
|
382
|
+
this.handleWorkerError(poolWorker, e);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
this.workers.set(id, poolWorker);
|
|
386
|
+
this.logger.debug(`Created worker ${id}`);
|
|
387
|
+
|
|
388
|
+
// Wait for worker to be ready
|
|
389
|
+
await readyPromise;
|
|
390
|
+
this.logger.debug(`Worker ${id} is ready`);
|
|
391
|
+
|
|
392
|
+
return poolWorker;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Handle messages from workers
|
|
397
|
+
*/
|
|
398
|
+
private handleWorkerMessage(poolWorker: PoolWorker, message: WorkerMessage): void {
|
|
399
|
+
this.logger.debug('Received message from worker:', message);
|
|
400
|
+
const { id, type, payload } = message;
|
|
401
|
+
|
|
402
|
+
switch (type) {
|
|
403
|
+
case 'ready':
|
|
404
|
+
this.logger.debug(`Worker ${poolWorker.id} ready`);
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case 'result': {
|
|
408
|
+
const task = this.pendingTasks.get(id);
|
|
409
|
+
if (task) {
|
|
410
|
+
const resultPayload = payload as ResultPayload;
|
|
411
|
+
this.pendingTasks.delete(id);
|
|
412
|
+
poolWorker.state = 'idle';
|
|
413
|
+
poolWorker.currentTask = undefined;
|
|
414
|
+
poolWorker.tasksCompleted++;
|
|
415
|
+
poolWorker.lastActiveAt = Date.now();
|
|
416
|
+
|
|
417
|
+
this.stats.tasksCompleted++;
|
|
418
|
+
this.stats.totalDuration += resultPayload.duration;
|
|
419
|
+
|
|
420
|
+
this.logger.debug(
|
|
421
|
+
`Task ${id} completed in ${resultPayload.duration.toFixed(2)}ms`
|
|
422
|
+
);
|
|
423
|
+
task.deferred.resolve(resultPayload.data);
|
|
424
|
+
|
|
425
|
+
// Process next task
|
|
426
|
+
this.processQueue();
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
case 'error': {
|
|
432
|
+
const task = this.pendingTasks.get(id);
|
|
433
|
+
if (task) {
|
|
434
|
+
const errorPayload = payload as ErrorPayload;
|
|
435
|
+
this.pendingTasks.delete(id);
|
|
436
|
+
poolWorker.state = 'idle';
|
|
437
|
+
poolWorker.currentTask = undefined;
|
|
438
|
+
poolWorker.errors++;
|
|
439
|
+
poolWorker.lastActiveAt = Date.now();
|
|
440
|
+
|
|
441
|
+
this.stats.tasksFailed++;
|
|
442
|
+
|
|
443
|
+
const error = new Error(errorPayload.message);
|
|
444
|
+
if (errorPayload.stack) {
|
|
445
|
+
error.stack = errorPayload.stack;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.logger.error(`Task ${id} failed:`, errorPayload.message);
|
|
449
|
+
task.deferred.reject(error);
|
|
450
|
+
|
|
451
|
+
// Process next task
|
|
452
|
+
this.processQueue();
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case 'progress': {
|
|
458
|
+
const progressPayload = payload as ProgressPayload;
|
|
459
|
+
const task = this.pendingTasks.get(progressPayload.taskId);
|
|
460
|
+
if (task?.onProgress) {
|
|
461
|
+
task.onProgress(progressPayload.progress);
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Handle worker errors
|
|
470
|
+
*/
|
|
471
|
+
private handleWorkerError(poolWorker: PoolWorker, error: ErrorEvent): void {
|
|
472
|
+
this.logger.error(`Worker ${poolWorker.id} error:`, error.message);
|
|
473
|
+
poolWorker.state = 'error';
|
|
474
|
+
poolWorker.errors++;
|
|
475
|
+
|
|
476
|
+
// Reject current task if any
|
|
477
|
+
if (poolWorker.currentTask) {
|
|
478
|
+
const task = this.pendingTasks.get(poolWorker.currentTask);
|
|
479
|
+
if (task) {
|
|
480
|
+
this.pendingTasks.delete(poolWorker.currentTask);
|
|
481
|
+
task.deferred.reject(new Error(`Worker error: ${error.message}`));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Terminate and recreate the worker
|
|
486
|
+
poolWorker.worker.terminate();
|
|
487
|
+
this.workers.delete(poolWorker.id);
|
|
488
|
+
|
|
489
|
+
// Create a new worker to replace it
|
|
490
|
+
this.createWorker().then(() => this.processQueue());
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Add task to queue (priority-based)
|
|
495
|
+
*/
|
|
496
|
+
private enqueue<T>(task: QueuedTask<T>): void {
|
|
497
|
+
// Insert based on priority (higher priority first)
|
|
498
|
+
let inserted = false;
|
|
499
|
+
for (let i = 0; i < this.taskQueue.length; i++) {
|
|
500
|
+
if (task.priority > this.taskQueue[i].priority) {
|
|
501
|
+
this.taskQueue.splice(i, 0, task as QueuedTask);
|
|
502
|
+
inserted = true;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!inserted) {
|
|
507
|
+
this.taskQueue.push(task as QueuedTask);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Process queued tasks
|
|
513
|
+
*/
|
|
514
|
+
private async processQueue(): Promise<void> {
|
|
515
|
+
if (this.taskQueue.length === 0) return;
|
|
516
|
+
|
|
517
|
+
// Find an idle worker
|
|
518
|
+
let idleWorker: PoolWorker | undefined;
|
|
519
|
+
for (const worker of this.workers.values()) {
|
|
520
|
+
if (worker.state === 'idle') {
|
|
521
|
+
idleWorker = worker;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Create new worker if needed and under limit
|
|
527
|
+
if (!idleWorker && this.workers.size < this.options.maxWorkers) {
|
|
528
|
+
idleWorker = await this.createWorker();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!idleWorker) return;
|
|
532
|
+
|
|
533
|
+
// Get next task
|
|
534
|
+
const task = this.taskQueue.shift();
|
|
535
|
+
if (!task) return;
|
|
536
|
+
|
|
537
|
+
// Execute task
|
|
538
|
+
this.executeOnWorker(idleWorker, task);
|
|
539
|
+
|
|
540
|
+
// Continue processing if more tasks
|
|
541
|
+
if (this.taskQueue.length > 0) {
|
|
542
|
+
this.processQueue();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Execute task on a specific worker
|
|
548
|
+
*/
|
|
549
|
+
private executeOnWorker(poolWorker: PoolWorker, task: QueuedTask): void {
|
|
550
|
+
this.logger.debug(
|
|
551
|
+
`executeOnWorker: Starting task ${task.id} (${task.functionName}) on worker ${poolWorker.id}`
|
|
552
|
+
);
|
|
553
|
+
poolWorker.state = 'busy';
|
|
554
|
+
poolWorker.currentTask = task.id;
|
|
555
|
+
poolWorker.lastActiveAt = Date.now();
|
|
556
|
+
|
|
557
|
+
this.pendingTasks.set(task.id, task);
|
|
558
|
+
|
|
559
|
+
const message: WorkerMessage<ExecutePayload> = {
|
|
560
|
+
id: task.id,
|
|
561
|
+
type: 'execute',
|
|
562
|
+
payload: {
|
|
563
|
+
functionName: task.functionName,
|
|
564
|
+
input: task.input,
|
|
565
|
+
// Don't send options - they may contain non-cloneable objects like AbortSignal
|
|
566
|
+
},
|
|
567
|
+
timestamp: Date.now(),
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Find transferables in input
|
|
571
|
+
const transfer = findTransferables(task.input);
|
|
572
|
+
|
|
573
|
+
this.logger.debug(`Posting message to worker:`, message);
|
|
574
|
+
poolWorker.worker.postMessage(message, transfer);
|
|
575
|
+
this.logger.debug(`Message posted to worker ${poolWorker.id}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Cancel a pending task
|
|
580
|
+
*/
|
|
581
|
+
private cancelTask(taskId: string): void {
|
|
582
|
+
// Remove from queue
|
|
583
|
+
const queueIndex = this.taskQueue.findIndex((t) => t.id === taskId);
|
|
584
|
+
if (queueIndex !== -1) {
|
|
585
|
+
this.taskQueue.splice(queueIndex, 1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Remove from pending (worker will complete but result ignored)
|
|
589
|
+
this.pendingTasks.delete(taskId);
|
|
590
|
+
}
|
|
591
|
+
}
|