@elysiumjs/core 0.4.2 → 0.5.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/worker.ts DELETED
@@ -1,849 +0,0 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { Class } from 'type-fest';
16
- import type { Job } from './job';
17
-
18
- import { uid } from 'radash';
19
-
20
- import { InteractsWithConsole } from './console';
21
- import { JobStatus } from './job';
22
- import { Service } from './service';
23
- import { Symbols } from './utils';
24
-
25
- const BunWorker = globalThis.Worker;
26
- type BunWorker = InstanceType<typeof BunWorker>;
27
-
28
- /**
29
- * Configuration options for a job queue.
30
- * @author Axel Nana <axel.nana@workbud.com>
31
- */
32
- export interface QueueOptions {
33
- /**
34
- * The name of the queue.
35
- */
36
- name: string;
37
-
38
- /**
39
- * The maximum number of concurrent jobs to process in this queue.
40
- * @default 1
41
- */
42
- concurrency?: number;
43
-
44
- /**
45
- * The maximum number of retries for failed jobs.
46
- * @default 0
47
- */
48
- maxRetries?: number;
49
-
50
- /**
51
- * The delay in milliseconds between retries.
52
- * @default 1000
53
- */
54
- retryDelay?: number;
55
-
56
- /**
57
- * Whether to pause processing when an error occurs.
58
- * @default false
59
- */
60
- pauseOnError?: boolean;
61
- }
62
-
63
- /**
64
- * Represents a job in the queue with its metadata.
65
- * @author Axel Nana <axel.nana@workbud.com>
66
- */
67
- interface QueuedJob {
68
- /**
69
- * The job instance.
70
- */
71
- job: Job;
72
-
73
- /**
74
- * The number of times this job has been retried.
75
- */
76
- retries: number;
77
-
78
- /**
79
- * The queue this job belongs to.
80
- */
81
- queueName: string;
82
- }
83
-
84
- /**
85
- * Represents the state of a queue.
86
- * @author Axel Nana <axel.nana@workbud.com>
87
- */
88
- interface QueueState {
89
- /**
90
- * The queue configuration options.
91
- */
92
- options: QueueOptions;
93
-
94
- /**
95
- * The jobs waiting to be processed.
96
- */
97
- jobs: QueuedJob[];
98
-
99
- /**
100
- * The number of jobs currently being processed.
101
- */
102
- processing: number;
103
-
104
- /**
105
- * Whether the queue is currently paused.
106
- */
107
- paused: boolean;
108
-
109
- /**
110
- * Whether the queue is currently being drained (completing existing jobs but not accepting new ones).
111
- */
112
- draining: boolean;
113
- }
114
-
115
- /**
116
- * Worker class that manages multiple job queues and processes them in parallel.
117
- * Each queue processes its jobs sequentially by default, but can be configured for concurrency.
118
- * @author Axel Nana <axel.nana@workbud.com>
119
- */
120
- export class Worker extends InteractsWithConsole {
121
- readonly #id: string;
122
-
123
- /**
124
- * The queues managed by this worker.
125
- */
126
- private queues: Map<string, QueueState> = new Map();
127
-
128
- /**
129
- * The default queue name.
130
- */
131
- private defaultQueueName: string = 'default';
132
-
133
- public static spawn(
134
- thread: BunWorker,
135
- queues: string[] = ['default'],
136
- options?: Omit<QueueOptions, 'name'>
137
- ): Worker {
138
- const worker = new this();
139
-
140
- worker.debug(`Worker ${worker.id} starting with queues: ${queues.join(', ')}`);
141
-
142
- // Create each queue
143
- for (const queue of queues) {
144
- worker.createQueue({
145
- name: queue,
146
- concurrency: options?.concurrency ?? 1,
147
- maxRetries: options?.maxRetries ?? 5,
148
- retryDelay: options?.retryDelay ?? 5000,
149
- pauseOnError: options?.pauseOnError ?? false
150
- });
151
- }
152
-
153
- // Listen for messages from the main thread
154
- thread.addEventListener('message', async (event) => {
155
- const { type = 'job:process', jobId, job: jobName, args, queue = 'default' } = event.data;
156
-
157
- // Handle different message types
158
- switch (type) {
159
- case 'job:cancel':
160
- // Cancel a specific job
161
- const canceled = worker.cancelJob(jobId, queue);
162
- thread.postMessage({
163
- type: 'job:canceled',
164
- jobId,
165
- queue,
166
- success: canceled,
167
- count: canceled ? 1 : 0
168
- });
169
- break;
170
-
171
- case 'job:cancelAll':
172
- // Cancel all jobs in a queue
173
- const canceledCount = worker.cancelAllJobs(queue);
174
- thread.postMessage({
175
- type: 'jobs:canceled',
176
- queue,
177
- count: canceledCount
178
- });
179
- break;
180
-
181
- case 'job:process':
182
- worker.debug(`Worker ${worker.id} received job ${jobName} for queue ${queue}`);
183
-
184
- // Handle regular job submission
185
- try {
186
- // Get the job class from the service container
187
- const JobClass = Service.get(jobName) as Class<Job>;
188
-
189
- if (!JobClass) {
190
- worker.error(`Job class ${jobName} not found`);
191
- throw new Error(`Job class ${jobName} not found`);
192
- }
193
-
194
- // Create a new job instance with the provided arguments
195
- const job = new JobClass(...(args || []));
196
-
197
- // Add the job to the specified queue
198
- worker.addJob(job, queue);
199
-
200
- // Acknowledge receipt of the job
201
- thread.postMessage({
202
- type: 'job:received',
203
- jobId: job.id,
204
- queue
205
- });
206
- } catch (error: any) {
207
- worker.trace(
208
- { ...error, ...(options ?? {}) },
209
- `Error while adding job '${jobName}' to queue '${queue}' with worker ${worker.id}`
210
- );
211
-
212
- // Report the error back to the main thread
213
- thread.postMessage({
214
- type: 'job:error',
215
- jobName,
216
- error: error.message
217
- });
218
- }
219
- break;
220
- }
221
- });
222
-
223
- // Handle errors
224
- thread.addEventListener('error', (e) => {
225
- worker.trace({ ...e.error, filename: e.filename }, `Worker ${worker.id} error:`);
226
- });
227
-
228
- // Handle unhandled promise rejections
229
- thread.addEventListener('unhandledrejection', (e: any) => {
230
- worker.trace(e, `Worker ${worker.id} unhandled rejection:`);
231
- });
232
-
233
- // Report that the worker is ready
234
- thread.postMessage({
235
- type: 'worker:ready',
236
- id: worker.id,
237
- queues
238
- });
239
-
240
- return worker;
241
- }
242
-
243
- /**
244
- * Creates a new Worker instance.
245
- */
246
- constructor() {
247
- super();
248
-
249
- this.#id = uid(24);
250
-
251
- // Initialize the default queue
252
- this.createQueue({ name: this.defaultQueueName });
253
- }
254
-
255
- /**
256
- * Gets the worker ID.
257
- * @returns The worker ID.
258
- */
259
- public get id(): string {
260
- return this.#id;
261
- }
262
-
263
- /**
264
- * Creates a new queue with the specified options.
265
- *
266
- * @param options The queue configuration options.
267
- * @returns The Worker instance for method chaining.
268
- */
269
- public createQueue(options: QueueOptions): Worker {
270
- if (this.queues.has(options.name)) {
271
- this.debug(`Queue with name '${options.name}' already exists`);
272
- return this;
273
- }
274
-
275
- const queueState: QueueState = {
276
- options: {
277
- ...options,
278
- concurrency: options.concurrency || 1,
279
- maxRetries: options.maxRetries || 0,
280
- retryDelay: options.retryDelay || 1000,
281
- pauseOnError: options.pauseOnError || false
282
- },
283
- jobs: [],
284
- processing: 0,
285
- paused: false,
286
- draining: false
287
- };
288
-
289
- this.queues.set(options.name, queueState);
290
- this.debug(`Created queue '${options.name}'`);
291
-
292
- return this;
293
- }
294
-
295
- /**
296
- * Adds a job to the specified queue or the default queue if not specified.
297
- *
298
- * @param job The job to add to the queue.
299
- * @param queueName Optional name of the queue to add the job to. Defaults to the default queue.
300
- * @returns The Worker instance for method chaining.
301
- * @throws Error if the specified queue does not exist.
302
- */
303
- public addJob(job: Job, queueName: string = this.defaultQueueName): Worker {
304
- const queue = this.queues.get(queueName);
305
-
306
- if (!queue) {
307
- this.error(`Queue '${queueName}' does not exist`);
308
- return this;
309
- }
310
-
311
- if (queue.draining) {
312
- this.warning(`Queue '${queueName}' is draining, job ${job.id} will not be added`);
313
- return this;
314
- }
315
-
316
- const queuedJob: QueuedJob = {
317
- job,
318
- retries: 0,
319
- queueName
320
- };
321
-
322
- queue.jobs.push(queuedJob);
323
- this.debug(`Added job ${job.id} to queue '${queueName}'`);
324
-
325
- // Start processing if the queue is not paused and not already at max concurrency
326
- if (!queue.paused && queue.processing < queue.options.concurrency!) {
327
- this.processNextJob(queueName);
328
- }
329
-
330
- return this;
331
- }
332
-
333
- /**
334
- * Pauses a queue, preventing it from processing new jobs.
335
- *
336
- * @param queueName The name of the queue to pause. If not specified, pauses all queues.
337
- * @returns The Worker instance for method chaining.
338
- */
339
- public pause(queueName?: string): Worker {
340
- if (queueName) {
341
- const queue = this.queues.get(queueName);
342
- if (queue) {
343
- queue.paused = true;
344
- this.info(`Paused queue '${queueName}'`);
345
- }
346
- } else {
347
- // Pause all queues
348
- for (const [name, queue] of this.queues.entries()) {
349
- this.pause(name);
350
- }
351
- }
352
-
353
- return this;
354
- }
355
-
356
- /**
357
- * Resumes a paused queue.
358
- *
359
- * @param queueName The name of the queue to resume. If not specified, resumes all queues.
360
- * @returns The Worker instance for method chaining.
361
- */
362
- public resume(queueName?: string): Worker {
363
- if (queueName) {
364
- const queue = this.queues.get(queueName);
365
- if (queue) {
366
- queue.paused = false;
367
- this.info(`Resumed queue '${queueName}'`);
368
- // Start processing jobs if there are any waiting
369
- this.processQueueJobs(queueName);
370
- }
371
- } else {
372
- // Resume all queues
373
- for (const [name] of this.queues.entries()) {
374
- this.resume(name);
375
- }
376
- }
377
-
378
- return this;
379
- }
380
-
381
- /**
382
- * Drains a queue, completing all current jobs but not accepting new ones.
383
- *
384
- * @param queueName The name of the queue to drain. If not specified, drains all queues.
385
- * @returns The Worker instance for method chaining.
386
- */
387
- public drain(queueName?: string): Worker {
388
- if (queueName) {
389
- const queue = this.queues.get(queueName);
390
- if (queue) {
391
- queue.draining = true;
392
- this.info(`Draining queue '${queueName}'`);
393
- }
394
- } else {
395
- // Drain all queues
396
- for (const [name, queue] of this.queues.entries()) {
397
- this.drain(name);
398
- }
399
- }
400
-
401
- return this;
402
- }
403
-
404
- /**
405
- * Clears all jobs from a queue.
406
- *
407
- * @param queueName The name of the queue to clear. If not specified, clears all queues.
408
- * @returns The Worker instance for method chaining.
409
- */
410
- public clear(queueName?: string): Worker {
411
- if (queueName) {
412
- const queue = this.queues.get(queueName);
413
- if (queue) {
414
- const jobCount = queue.jobs.length;
415
- queue.jobs = [];
416
- this.info(`Cleared ${jobCount} jobs from queue '${queueName}'`);
417
- }
418
- } else {
419
- // Clear all queues
420
- for (const [name, queue] of this.queues.entries()) {
421
- const jobCount = queue.jobs.length;
422
- queue.jobs = [];
423
- this.info(`Cleared ${jobCount} jobs from queue '${name}'`);
424
- }
425
- }
426
- return this;
427
- }
428
-
429
- /**
430
- * Gets the number of jobs in a queue.
431
- *
432
- * @param queueName The name of the queue to get the size of.
433
- * @returns The number of jobs in the queue.
434
- * @throws Error if the specified queue does not exist.
435
- */
436
- public size(queueName: string = this.defaultQueueName): number {
437
- const queue = this.queues.get(queueName);
438
- if (!queue) {
439
- throw new Error(`Queue '${queueName}' does not exist`);
440
- }
441
- return queue.jobs.length;
442
- }
443
-
444
- /**
445
- * Gets the total number of jobs across all queues.
446
- *
447
- * @returns The total number of jobs.
448
- */
449
- public totalSize(): number {
450
- let total = 0;
451
- for (const queue of this.queues.values()) {
452
- total += queue.jobs.length;
453
- }
454
- return total;
455
- }
456
-
457
- /**
458
- * Processes the next job in the specified queue.
459
- *
460
- * @param queueName The name of the queue to process.
461
- */
462
- private async processNextJob(queueName: string): Promise<void> {
463
- const queue = this.queues.get(queueName);
464
- if (!queue || queue.paused || queue.processing >= queue.options.concurrency!) {
465
- return;
466
- }
467
-
468
- const queuedJob = queue.jobs.shift();
469
- if (!queuedJob) {
470
- return;
471
- }
472
-
473
- // Skip cancelled jobs
474
- if (queuedJob.job.status === JobStatus.CANCELLED) {
475
- this.info(`Skipping cancelled job ${queuedJob.job.id}`);
476
- queue.processing--;
477
- this.processQueueJobs(queueName);
478
- return;
479
- }
480
-
481
- queue.processing++;
482
-
483
- try {
484
- this.debug(`Processing job ${queuedJob.job.id} from queue '${queueName}'`);
485
- await queuedJob.job.run();
486
-
487
- if (queuedJob.job.status === JobStatus.FAILED) {
488
- // Handle job failure
489
- if (queuedJob.retries < queue.options.maxRetries!) {
490
- // Retry the job
491
- queuedJob.retries++;
492
- this.warning(
493
- `Job ${queuedJob.job.id} failed, retrying (${queuedJob.retries}/${queue.options.maxRetries})`
494
- );
495
-
496
- // Add the job back to the queue after the retry delay
497
- setTimeout(() => {
498
- // Check if job was cancelled during the delay
499
- if (queuedJob.job.status !== JobStatus.CANCELLED) {
500
- queue.jobs.push(queuedJob);
501
- this.processQueueJobs(queueName);
502
- } else {
503
- this.info(`Skipping cancelled job ${queuedJob.job.id} during retry delay`);
504
- }
505
- }, queue.options.retryDelay!);
506
- } else {
507
- this.error(`Job ${queuedJob.job.id} failed after ${queuedJob.retries} retries`);
508
-
509
- // Pause the queue if configured to do so
510
- if (queue.options.pauseOnError) {
511
- this.pause(queueName);
512
- this.warning(`Queue '${queueName}' paused due to job failure`);
513
- }
514
- }
515
- } else if (queuedJob.job.status === JobStatus.COMPLETED) {
516
- this.success(`Job ${queuedJob.job.id} completed successfully`);
517
- } else {
518
- this.info(`Job ${queuedJob.job.id} cancelled while running`);
519
- }
520
- } catch (error) {
521
- this.error(`Unexpected error processing job ${queuedJob.job.id}: ${error}`);
522
-
523
- // Pause the queue if configured to do so
524
- if (queue.options.pauseOnError) {
525
- this.pause(queueName);
526
- this.warning(`Queue '${queueName}' paused due to unexpected error`);
527
- }
528
- } finally {
529
- queue.processing--;
530
-
531
- // Process the next job if there are any waiting
532
- this.processQueueJobs(queueName);
533
- }
534
- }
535
-
536
- /**
537
- * Processes jobs in a queue up to the concurrency limit.
538
- *
539
- * @param queueName The name of the queue to process.
540
- */
541
- private processQueueJobs(queueName: string): void {
542
- const queue = this.queues.get(queueName);
543
- if (!queue || queue.paused) {
544
- return;
545
- }
546
-
547
- // Start processing jobs up to the concurrency limit
548
- while (queue.jobs.length > 0 && queue.processing < queue.options.concurrency!) {
549
- this.processNextJob(queueName);
550
- }
551
- }
552
-
553
- /**
554
- * Gets statistics about all queues.
555
- *
556
- * @returns An object containing statistics about each queue.
557
- */
558
- public getStats(): Record<
559
- string,
560
- { waiting: number; processing: number; paused: boolean; draining: boolean }
561
- > {
562
- const stats: Record<
563
- string,
564
- { waiting: number; processing: number; paused: boolean; draining: boolean }
565
- > = {};
566
-
567
- for (const [name, queue] of this.queues.entries()) {
568
- stats[name] = {
569
- waiting: queue.jobs.length,
570
- processing: queue.processing,
571
- paused: queue.paused,
572
- draining: queue.draining
573
- };
574
- }
575
-
576
- return stats;
577
- }
578
-
579
- /**
580
- * Cancels a job by its ID.
581
- *
582
- * @param jobId The ID of the job to cancel.
583
- * @param queueName Optional queue name to search in. If not provided, searches all queues.
584
- * @returns `true` if the job was found and canceled, false otherwise.
585
- */
586
- public cancelJob(jobId: string, queueName?: string): boolean {
587
- if (queueName) {
588
- // Search in specific queue
589
- return this.cancelJobInQueue(jobId, queueName);
590
- } else {
591
- // Search in all queues
592
- let canceled = false;
593
- for (const [name] of this.queues.entries()) {
594
- if (this.cancelJobInQueue(jobId, name)) {
595
- canceled = true;
596
- break;
597
- }
598
- }
599
- return canceled;
600
- }
601
- }
602
-
603
- /**
604
- * Cancels a job in a specific queue.
605
- *
606
- * @param jobId The ID of the job to cancel.
607
- * @param queueName The name of the queue to search in.
608
- * @returns `true` if the job was found and canceled, false otherwise.
609
- */
610
- private cancelJobInQueue(jobId: string, queueName: string): boolean {
611
- const queue = this.queues.get(queueName);
612
- if (!queue) {
613
- return false;
614
- }
615
-
616
- // Find the job in the queue
617
- const jobIndex = queue.jobs.findIndex((queuedJob) => queuedJob.job.id === jobId);
618
- if (jobIndex >= 0) {
619
- // Mark the job as canceled
620
- queue.jobs[jobIndex].job.cancel();
621
- this.info(`Canceled job ${jobId} in queue '${queueName}'`);
622
- return true;
623
- }
624
-
625
- return false;
626
- }
627
-
628
- /**
629
- * Cancels all jobs in a queue.
630
- *
631
- * @param queueName The name of the queue to cancel jobs from. If not provided, cancels jobs in all queues.
632
- * @returns The number of jobs canceled.
633
- */
634
- public cancelAllJobs(queueName?: string): number {
635
- let canceledCount = 0;
636
-
637
- if (queueName) {
638
- // Cancel jobs in specific queue
639
- const queue = this.queues.get(queueName);
640
- if (queue) {
641
- canceledCount = queue.jobs.length;
642
- queue.jobs.forEach((queuedJob) => {
643
- queuedJob.job.cancel();
644
- });
645
- this.info(`Canceled ${canceledCount} jobs in queue '${queueName}'`);
646
- }
647
- } else {
648
- // Cancel jobs in all queues
649
- for (const [name, queue] of this.queues.entries()) {
650
- const queueCanceledCount = queue.jobs.length;
651
- queue.jobs.forEach((queuedJob) => {
652
- queuedJob.job.cancel();
653
- });
654
- canceledCount += queueCanceledCount;
655
- this.info(`Canceled ${queueCanceledCount} jobs in queue '${name}'`);
656
- }
657
- }
658
-
659
- return canceledCount;
660
- }
661
- }
662
-
663
- /**
664
- * A pool of worker threads.
665
- * @author Axel Nana <axel.nana@workbud.com>
666
- */
667
- export class WorkerPool extends InteractsWithConsole {
668
- private workers: BunWorker[] = [];
669
- private currentWorkerIndex = 0;
670
- private queueWorkerMap: Map<string, BunWorker[]> = new Map();
671
-
672
- public static readonly instance = new WorkerPool();
673
-
674
- private constructor() {
675
- super();
676
- }
677
-
678
- /**
679
- * Initialize the worker pool.
680
- */
681
- public async init() {
682
- // noop for now
683
- }
684
-
685
- /**
686
- * Add a new worker to the pool.
687
- * @param queues The queues to listen from the worker.
688
- */
689
- public async addWorker(queues: string[] = []): Promise<void> {
690
- let worker: BunWorker;
691
-
692
- if (ELYSIUM_BUILD) {
693
- worker = new BunWorker('./index.js', {
694
- argv: ['work', ...queues.map((queue) => `--queue=${queue}`)]
695
- });
696
- } else {
697
- // Preload all job files to make them available in worker thread.
698
- const jobFiles = await Array.fromAsync(new Bun.Glob('./src/**/*.job.ts').scan());
699
-
700
- worker = new BunWorker('./index.ts', {
701
- preload: jobFiles,
702
- argv: ['work', ...queues.map((queue) => `--queue=${queue}`)]
703
- });
704
- }
705
-
706
- worker.addEventListener('error', (e) => {
707
- this.trace({
708
- message: 'An error occurred inside a worker process',
709
- stack: e.message,
710
- name: 'WorkerError'
711
- });
712
-
713
- this.removeWorker(worker);
714
- this.addWorker(queues);
715
- });
716
-
717
- worker.addEventListener('messageerror', (e) => {
718
- this.error(`Worker message error: ${e}`);
719
- });
720
-
721
- worker.addEventListener('message', (e) => {
722
- if (e.data.type === 'worker:ready') {
723
- this.debug(`Worker ${e.data.id} ready with queues: ${e.data.queues.join(', ')}`);
724
- } else if (e.data.type === 'job:received') {
725
- this.debug(`Job ${e.data.jobId} received by worker for queue ${e.data.queue}`);
726
- } else if (e.data.type === 'job:error') {
727
- this.error(`Error processing job ${e.data.jobName}: ${e.data.error}`);
728
- } else {
729
- this.debug(`Worker message: ${JSON.stringify(e.data)}`);
730
- }
731
- });
732
-
733
- this.workers.push(worker);
734
-
735
- // Map queues to workers for efficient job distribution
736
- for (const queue of queues) {
737
- if (!this.queueWorkerMap.has(queue)) {
738
- this.queueWorkerMap.set(queue, []);
739
- }
740
- this.queueWorkerMap.get(queue)!.push(worker);
741
- }
742
- }
743
-
744
- /**
745
- * Removes a worker from the pool.
746
- * @param w The worker to be removed. If not provided, the last worker will be removed.
747
- */
748
- public removeWorker(w?: BunWorker): void {
749
- const worker = w ?? this.workers.pop();
750
-
751
- if (!worker) return;
752
-
753
- worker.terminate();
754
-
755
- // Remove worker from queue mappings
756
- for (const [queue, workers] of this.queueWorkerMap.entries()) {
757
- this.queueWorkerMap.set(
758
- queue,
759
- workers.filter((w) => w !== worker)
760
- );
761
- }
762
-
763
- if (w) {
764
- this.workers = this.workers.filter((workerItem) => workerItem !== worker);
765
- }
766
- }
767
-
768
- /**
769
- * Terminates all workers in the pool.
770
- */
771
- public terminate() {
772
- while (this.workers.length > 0) {
773
- this.removeWorker();
774
- }
775
-
776
- this.queueWorkerMap.clear();
777
- }
778
-
779
- /**
780
- * Runs a job in a worker.
781
- * @param job The job class to run.
782
- * @param args The class constructor arguments to pass to the job.
783
- */
784
- public runJob<T extends Job, TClass extends Class<T>>(
785
- job: TClass,
786
- ...args: ConstructorParameters<TClass>
787
- ): void {
788
- if (!Reflect.hasMetadata(Symbols.job, job)) {
789
- throw new Error(`Job ${job.name} is not marked with @job() decorator`);
790
- }
791
-
792
- const { name: jobName, queue = 'default' } = Reflect.getMetadata(Symbols.job, job);
793
-
794
- // Get workers for this queue
795
- const queueWorkers = this.queueWorkerMap.get(queue) || this.workers;
796
-
797
- if (queueWorkers.length === 0) {
798
- throw new Error(`No workers available for queue '${queue}'`);
799
- }
800
-
801
- // Use round-robin to distribute jobs among workers for this queue
802
- const workerIndex = this.currentWorkerIndex % queueWorkers.length;
803
- const worker = queueWorkers[workerIndex];
804
-
805
- worker.postMessage({ job: jobName, args, queue });
806
-
807
- // Update the index for next job
808
- this.currentWorkerIndex = (this.currentWorkerIndex + 1) % this.workers.length;
809
- }
810
-
811
- /**
812
- * Cancels a job in the worker pool.
813
- *
814
- * @param jobId The ID of the job to cancel.
815
- * @param queue Optional queue name to search in.
816
- * @returns `true` if a cancellation message was sent to workers.
817
- */
818
- public cancelJob(jobId: string, queue?: string): boolean {
819
- // We need to send a cancellation message to all workers
820
- // since we don't know which worker has the job
821
- for (const worker of this.workers) {
822
- worker.postMessage({
823
- type: 'job:cancel',
824
- jobId,
825
- queue
826
- });
827
- }
828
-
829
- return this.workers.length > 0;
830
- }
831
-
832
- /**
833
- * Cancels all jobs in a queue.
834
- *
835
- * @param queue Optional queue name to cancel jobs from. If not provided, cancels jobs in all queues.
836
- * @returns `true` if a cancellation message was sent to workers.
837
- */
838
- public cancelAllJobs(queue?: string): boolean {
839
- // Send cancellation message to all workers
840
- for (const worker of this.workers) {
841
- worker.postMessage({
842
- type: 'job:cancelAll',
843
- queue
844
- });
845
- }
846
-
847
- return this.workers.length > 0;
848
- }
849
- }