@elysiumjs/heracles 0.4.3 → 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/queue.ts DELETED
@@ -1,567 +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 { EventHandler } from '@elysiumjs/core';
16
- import type { JobClass, JobOverlapBehavior } from './job';
17
- import type { JobStatusInfo, Transport, TransportClass, TransportEvent } from './transport';
18
-
19
- import { Event, InteractsWithConsole } from '@elysiumjs/core';
20
- import { map, uid } from 'radash';
21
-
22
- import { Job, JobStatus } from './job';
23
- import { TransportMode } from './transport';
24
- import { ThreadTransport } from './transports/thread.transport';
25
-
26
- /**
27
- * Options for dispatching a job to a queue.
28
- * @author Axel Nana <axel.nana@workbud.com>
29
- */
30
- export type JobDispatchOptions = {
31
- /**
32
- * Schedule the job to run at a specific time.
33
- */
34
- scheduledFor?: Date;
35
-
36
- /**
37
- * Priority of this job (lower number = higher priority).
38
- * @default 10
39
- */
40
- priority?: number;
41
-
42
- /**
43
- * Maximum number of retries for this job.
44
- * Overrides both the job class and queue settings.
45
- */
46
- maxRetries?: number;
47
-
48
- /**
49
- * Delay between retries in milliseconds.
50
- * Overrides both the job class and queue settings.
51
- */
52
- retryDelay?: number;
53
-
54
- /**
55
- * Optional job ID to use for this dispatch.
56
- * If not provided, a unique ID will be generated.
57
- */
58
- jobId?: string;
59
-
60
- /**
61
- * A unique ID for this specific dispatch of the job.
62
- * If not provided, one will be generated.
63
- */
64
- dispatchId?: string;
65
-
66
- /**
67
- * Defines how the job handles overlap with other instances of the same job ID.
68
- * Overrides the setting from the job class.
69
- */
70
- overlapBehavior?: JobOverlapBehavior;
71
-
72
- /**
73
- * When using NO_OVERLAP behavior, specifies the delay in milliseconds
74
- * between sequential executions of jobs with the same ID.
75
- * Overrides the setting from the job class.
76
- */
77
- overlapDelay?: number;
78
- };
79
-
80
- /**
81
- * Configuration options for a Queue.
82
- * @author Axel Nana <axel.nana@workbud.com>
83
- */
84
- export type QueueOptions = {
85
- /**
86
- * The name of the queue.
87
- */
88
- name: string;
89
-
90
- /**
91
- * The transport class to use for this queue.
92
- */
93
- transport?: TransportClass;
94
-
95
- /**
96
- * The maximum number of concurrent jobs to process in this queue.
97
- * @default 1
98
- */
99
- concurrency?: number;
100
-
101
- /**
102
- * The maximum number of retries for failed jobs.
103
- * @default 0
104
- */
105
- maxRetries?: number;
106
-
107
- /**
108
- * The delay in milliseconds between retries.
109
- * @default 1000
110
- */
111
- retryDelay?: number;
112
-
113
- /**
114
- * Whether to pause processing when an error occurs.
115
- * @default false
116
- */
117
- pauseOnError?: boolean;
118
-
119
- /**
120
- * Configuration options for the transport.
121
- * This is passed to the transport constructor.
122
- */
123
- transportOptions?: Record<string, any>;
124
- };
125
-
126
- /**
127
- * Generated ID when dispatching jobs in queues.
128
- * @author Axel Nana <axel.nana@workbud.com>
129
- */
130
- export type JobDispatchId = {
131
- /**
132
- * The job ID.
133
- */
134
- jobId: string;
135
-
136
- /**
137
- * The dispatch ID.
138
- */
139
- dispatchId: string;
140
- };
141
-
142
- /**
143
- * Queue class for dispatching jobs to workers.
144
- * @author Axel Nana <axel.nana@icloud.com>
145
- */
146
- export class Queue extends InteractsWithConsole {
147
- /**
148
- * Map of all registered queues.
149
- */
150
- private static queues: Map<string, Queue> = new Map();
151
-
152
- /**
153
- * Checks whether a queue exists.
154
- * @param name The name of the queue.
155
- * @returns `true` if the queue exists, `false` otherwise.
156
- */
157
- public static exists(name: string): boolean {
158
- return this.queues.has(name);
159
- }
160
-
161
- /**
162
- * Get a queue by name, or create it if it doesn't exist.
163
- *
164
- * @param name The name of the queue.
165
- * @param config Optional configuration for the queue if it needs to be created.
166
- * @returns The queue instance.
167
- */
168
- public static get(name: string, config?: Omit<QueueOptions, 'name'>): Queue {
169
- if (!this.queues.has(name)) {
170
- const queue = new Queue({
171
- name,
172
- ...config,
173
- transport: config?.transport ?? ThreadTransport
174
- });
175
- this.queues.set(name, queue);
176
- }
177
-
178
- return this.queues.get(name)!;
179
- }
180
-
181
- /**
182
- * Get all registered queues.
183
- *
184
- * @returns A map of all registered queues.
185
- */
186
- public static getAll(): Map<string, Queue> {
187
- return new Map(this.queues);
188
- }
189
-
190
- /**
191
- * The name of this queue
192
- */
193
- public readonly name: string;
194
-
195
- /**
196
- * The transport used by this queue
197
- */
198
- private readonly transport: Transport;
199
-
200
- /**
201
- * The transport class for this queue.
202
- */
203
- private transportClass?: TransportClass;
204
-
205
- /**
206
- * The transport options for this queue. These are passed to the transport constructor.
207
- */
208
- private transportOptions?: Record<string, any>;
209
-
210
- /**
211
- * The queue configuration options.
212
- */
213
- private options: QueueOptions;
214
-
215
- /**
216
- * Array of job IDs currently being processed
217
- */
218
- private jobs: Array<JobDispatchId> = [];
219
-
220
- /**
221
- * Creates a new Queue instance.
222
- *
223
- * @param config The queue configuration.
224
- */
225
- constructor(config: QueueOptions) {
226
- super();
227
-
228
- this.name = config.name;
229
- this.transportClass = config.transport;
230
- this.transportOptions = config.transportOptions;
231
-
232
- // Store queue options for worker configuration
233
- this.options = {
234
- concurrency: config.concurrency ?? 1,
235
- maxRetries: config.maxRetries ?? 0,
236
- retryDelay: config.retryDelay ?? 1000,
237
- pauseOnError: config.pauseOnError ?? false,
238
- ...config
239
- };
240
-
241
- // Initialize the transport
242
- this.transport = new (this.transportClass ?? ThreadTransport)(
243
- TransportMode.PRODUCER,
244
- this.transportOptions
245
- );
246
-
247
- // Register message handler for job status updates
248
- this.transport.onMessage(this.handleTransportMessage.bind(this));
249
- }
250
-
251
- public on<TData, TSource = any>(
252
- event: TransportEvent['type'],
253
- handler: EventHandler<TData, TSource> | never
254
- ) {
255
- Event.on(`elysium:heracles:queue:${this.name}:${event}`, handler);
256
- }
257
-
258
- public once<TData, TSource = any>(
259
- event: TransportEvent['type'],
260
- handler: EventHandler<TData, TSource> | never
261
- ) {
262
- Event.once(`elysium:heracles:queue:${this.name}:${event}`, handler);
263
- }
264
-
265
- private emit<TData, TSource = any>(
266
- event: TransportEvent['type'],
267
- data: TData,
268
- source: TSource | null = null
269
- ): void {
270
- Event.emit(`elysium:heracles:queue:${this.name}:${event}`, data, source);
271
- }
272
-
273
- /**
274
- * Handle messages from the transport
275
- *
276
- * @param message The message from the transport
277
- */
278
- private async handleTransportMessage(message: TransportEvent): Promise<void> {
279
- switch (message.type) {
280
- case 'job:status':
281
- case 'job:result':
282
- // Update job status for both status and result messages
283
- if (message.queue === this.name) {
284
- // For result messages, log completion
285
- if (message.type === 'job:result') {
286
- this.info(`Job ${message.jobId} completed with status: ${message.status}`);
287
- }
288
-
289
- // Get the existing status if available
290
- const existingStatus = await this.getJobStatus({
291
- jobId: message.jobId,
292
- dispatchId: message.dispatchId
293
- });
294
-
295
- // Preserve creation time if we already have it
296
- const createdAt = existingStatus?.createdAt ?? new Date().toISOString();
297
-
298
- // Update job status
299
- this.updateJobStatus(message.jobId, message.dispatchId, {
300
- jobId: message.jobId,
301
- dispatchId: message.dispatchId,
302
- queue: message.queue,
303
- status: message.status,
304
- error: message.error,
305
- retries: message.retries ?? existingStatus?.retries ?? 0,
306
- createdAt: createdAt,
307
- startedAt: (message as any).startedAt ?? existingStatus?.startedAt,
308
- completedAt: message.completedAt ?? existingStatus?.completedAt,
309
- updatedAt: message.updatedAt ?? new Date().toISOString()
310
- });
311
- }
312
- break;
313
- }
314
-
315
- this.emit(message.type, message, this);
316
- }
317
-
318
- /**
319
- * Update the status of a job.
320
- *
321
- * @param jobId The ID of the job.
322
- * @param dispatchID The dispatch ID of the job.
323
- * @param status The new status information.
324
- */
325
- private updateJobStatus(jobId: string, dispatchId: string, status: JobStatusInfo): void {
326
- // Perform cleanup for completed jobs
327
- if (
328
- status.status === JobStatus.COMPLETED ||
329
- status.status === JobStatus.FAILED ||
330
- status.status === JobStatus.CANCELLED
331
- ) {
332
- // TODO: In the future, this could trigger metrics collection or other cleanup tasks
333
- this.debug(`Job ${jobId} (${dispatchId}) reached terminal state: ${status.status}`);
334
- }
335
- }
336
-
337
- /**
338
- * Dispatch a job to this queue
339
- *
340
- * @param name The name of the job to dispatch
341
- * @param args The arguments to pass to the job constructor
342
- * @param options Optional dispatch options
343
- * @returns A promise that resolves to the created job instance
344
- */
345
- public async dispatch<T extends Job, TClass extends JobClass<T> = JobClass<T>>(
346
- job: TClass,
347
- name: string,
348
- args: ConstructorParameters<TClass> = [] as any,
349
- options: JobDispatchOptions = {}
350
- ): Promise<JobDispatchId> {
351
- // Create initial timestamp
352
- const now = new Date();
353
- const createdAt = now.toISOString();
354
-
355
- // Generate or use provided job ID
356
- const jobId = options.jobId ?? job.generateJobId(...args);
357
-
358
- // Generate or use provided dispatch ID
359
- const dispatchId = options.dispatchId ?? `dispatch_${Date.now()}_${uid(8)}`;
360
-
361
- // Initialize job status
362
- const initialStatus: JobStatusInfo = {
363
- jobId,
364
- dispatchId,
365
- queue: this.name,
366
- status:
367
- options.scheduledFor && options.scheduledFor > now
368
- ? JobStatus.SCHEDULED_FOR_RETRY
369
- : JobStatus.PENDING,
370
- retries: 0,
371
- createdAt: createdAt,
372
- updatedAt: createdAt
373
- };
374
-
375
- this.updateJobStatus(jobId, dispatchId, initialStatus);
376
- this.debug(
377
- `Dispatching job ${jobId} (dispatch: ${dispatchId}, name: ${name}) to queue '${this.name}'`
378
- );
379
-
380
- // Use transport for dispatching
381
- try {
382
- const message: TransportEvent = {
383
- type: 'job:process',
384
- job: name,
385
- jobId,
386
- dispatchId,
387
- args,
388
- queue: this.name,
389
- options: {
390
- scheduledFor: options.scheduledFor?.toISOString(),
391
- priority: options.priority,
392
- maxRetries: options.maxRetries,
393
- retryDelay: options.retryDelay,
394
- overlapBehavior: options.overlapBehavior,
395
- overlapDelay: options.overlapDelay
396
- }
397
- };
398
-
399
- await this.transport.send(message);
400
- return { jobId: initialStatus.jobId, dispatchId };
401
- } catch (error: any) {
402
- this.error(`Failed to dispatch job ${name} to queue '${this.name}': ${error.message}`);
403
-
404
- // Update job status to failed
405
- this.updateJobStatus(jobId, dispatchId, {
406
- jobId: initialStatus.jobId,
407
- dispatchId,
408
- queue: this.name,
409
- status: JobStatus.FAILED,
410
- error: error.message,
411
- retries: 0,
412
- createdAt: initialStatus.createdAt,
413
- completedAt: new Date().toISOString(),
414
- updatedAt: new Date().toISOString()
415
- });
416
-
417
- throw error;
418
- }
419
- }
420
-
421
- /**
422
- * Cancel a job in this queue.
423
- *
424
- * @param id The ID of the dispatched job to cancel.
425
- * @returns A promise that resolves to true if the job was found and canceled.
426
- */
427
- public async cancelJob(id: JobDispatchId): Promise<boolean> {
428
- this.debug(`Canceling job ${id.jobId} (${id.dispatchId}) in queue '${this.name}'`);
429
-
430
- // Use transport interface
431
- try {
432
- const message: TransportEvent = {
433
- type: 'job:cancel',
434
- jobId: id.jobId,
435
- dispatchId: id.dispatchId,
436
- queue: this.name
437
- };
438
-
439
- await this.transport.send(message);
440
- return true;
441
- } catch (error) {
442
- this.error(`Failed to cancel job ${id.jobId}: ${error}`);
443
- return false;
444
- }
445
- }
446
-
447
- /**
448
- * Cancel all jobs in this queue.
449
- *
450
- * @returns A promise that resolves to the number of jobs canceled.
451
- */
452
- public async cancelAllJobs(): Promise<number> {
453
- this.debug(`Canceling all jobs in queue '${this.name}'`);
454
-
455
- try {
456
- const message: TransportEvent = {
457
- type: 'job:cancelAll',
458
- queue: this.name
459
- };
460
-
461
- await this.transport.send(message);
462
- } catch (error) {
463
- this.error(`Failed to cancel all jobs: ${error}`);
464
- }
465
-
466
- // We don't know how many were canceled
467
- return 0;
468
- }
469
-
470
- /**
471
- * Get the status of a job.
472
- *
473
- * @param id The ID of the job.
474
- * @returns A promise that resolves to the job status or null if not found
475
- */
476
- public async getJobStatus(id: JobDispatchId): Promise<JobStatusInfo | null> {
477
- // Try to get from transport
478
- try {
479
- const status = await this.transport.getJobStatus(id.jobId, id.dispatchId, this.name);
480
- if (status) {
481
- this.debug(
482
- `Retrieved job ${id.jobId} (${id.dispatchId}) status from transport: ${status.status}`
483
- );
484
- return status;
485
- }
486
- } catch (error) {
487
- this.warning(`Failed to get job status from transport: ${error}`);
488
- }
489
-
490
- return null;
491
- }
492
-
493
- /**
494
- * Get all jobs in this queue
495
- *
496
- * @returns An array of job IDs
497
- */
498
- public getAllJobs(): Array<JobDispatchId> {
499
- return Array.from(this.jobs);
500
- }
501
-
502
- /**
503
- * Get jobs by status
504
- *
505
- * @param status The status to filter by
506
- * @returns An array of jobs with the specified status
507
- */
508
- public async getJobsByStatus(status: JobStatus): Promise<JobDispatchId[]> {
509
- const result: JobDispatchId[] = [];
510
-
511
- for (const [jobId, statusInfo] of await map(
512
- this.jobs,
513
- async (id) => [id, await this.getJobStatus(id)] as [JobDispatchId, JobStatusInfo | null]
514
- )) {
515
- if (statusInfo?.status === status) {
516
- result.push(jobId);
517
- }
518
- }
519
-
520
- return result;
521
- }
522
-
523
- /**
524
- * Start the queue's transport.
525
- *
526
- * @returns A promise that resolves when the transport has started.
527
- */
528
- public async start(): Promise<void> {
529
- if (this.transport) {
530
- await this.transport.start();
531
- this.info(`Queue '${this.name}' transport started`);
532
- }
533
- }
534
-
535
- /**
536
- * Stop the queue's transport.
537
- *
538
- * @returns A promise that resolves when the transport has stopped.
539
- */
540
- public async stop(): Promise<void> {
541
- if (this.transport) {
542
- await this.transport.stop();
543
- this.info(`Queue '${this.name}' transport stopped`);
544
- }
545
- }
546
-
547
- /**
548
- * Get the queue options.
549
- *
550
- * @returns The queue options.
551
- */
552
- public getOptions(): QueueOptions {
553
- return { ...this.options };
554
- }
555
-
556
- /**
557
- * Update the queue options.
558
- *
559
- * @param options The new options
560
- */
561
- public updateOptions(options: Partial<QueueOptions>): void {
562
- this.options = {
563
- ...this.options,
564
- ...options
565
- };
566
- }
567
- }