@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/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
+ }