UNPKG

torotask

Version:

Task queue processing in NodeJS based on BullMQ and Redis

201 lines 8.61 kB
import { QueueBase } from 'bullmq'; import { TaskQueueEvents } from './queue-events.js'; import { TaskQueue } from './queue.js'; import { TaskWorker } from './worker.js'; export class TaskWorkerQueue extends TaskQueue { options; queueEvents; workerOptions; worker; // Store listeners to remove them later workerEventHandlers = {}; constructor(taskClient, name, options) { const { processor, validator, workerOptions, ...queueOptions } = options ?? {}; super(taskClient, name, queueOptions); this.options = options; this.queueEvents = new TaskQueueEvents(taskClient, this.name); this.workerOptions = workerOptions; } /** * Method to process a job. * Subclasses must implement this method to define the job handling logic. * * @param job The BullMQ job object. * @returns A promise that resolves with the result of the job. */ process(_job, _token) { // This method should be implemented by subclasses throw new Error('process method not implemented'); } getWorkerOptions() { return this.workerOptions ?? {}; } /** * Starts a dedicated BullMQ Worker for this queue, if one is not already running. * Forwards worker events to this BaseQueue instance, prefixing them with 'worker:'. */ async startWorker(options) { if (this.worker) { this.logger.warn('Worker already started for this queue. Returning existing instance.'); return this.worker; } // Clear any stale listeners if somehow start is called without stop this.removeAllWorkerListeners(); const mergedOptions = { ...this.getWorkerOptions(), ...(options ?? {}), }; this.logger.info({ workerOptions: mergedOptions }, 'Starting worker'); const newWorker = new TaskWorker(this.taskClient, this.name, this.options?.processor ?? this.process.bind(this), this.options?.validator, mergedOptions); // --- Event Forwarding (with prefixing) --- // Define handlers that emit events from `this` (the BaseQueue instance) this.workerEventHandlers = { active: (job, prev) => { this.logger.debug({ jobId: job.id, prev }, 'Worker event: active'); // Emit with 'worker:' prefix this.emit('worker:active', job, prev); }, completed: (job, result, prev) => { this.logger.debug({ jobId: job.id, result }, 'Worker event: completed'); this.emit('worker:completed', job, result, prev); }, drained: () => { this.logger.debug('Worker event: drained'); this.emit('worker:drained'); }, error: (error) => { // This is for errors *within the worker itself*, not job failures this.logger.error({ err: error }, 'Worker event: error'); this.emit('worker:error', error); }, failed: (job, error, prev) => { // Job might be undefined if failure happens before job is retrieved this.logger.warn({ jobId: job?.id, err: error, prev }, 'Worker event: failed'); this.emit('worker:failed', job, error, prev); }, paused: () => { this.logger.debug('Worker event: paused'); this.emit('worker:paused'); }, progress: (job, progress) => { // Progress can be number | object this.logger.debug({ jobId: job.id, progress }, 'Worker event: progress'); this.emit('worker:progress', job, progress); }, ready: () => { this.logger.debug('Worker event: ready'); this.emit('worker:ready'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any resumed: () => { this.logger.debug('Worker event: resumed'); this.emit('worker:resumed'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any stalled: (jobId, prev) => { this.logger.warn({ jobId, prev }, 'Worker event: stalled'); this.emit('worker:stalled', jobId, prev); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any closed: () => { this.logger.debug('Worker event: closed'); this.emit('worker:closed'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any closing: (msg) => { this.logger.debug({ msg }, 'Worker event: closing'); this.emit('worker:closing', msg); }, }; for (const eventName of Object.keys(this.workerEventHandlers)) { const handler = this.workerEventHandlers[eventName]; if (handler) { // Explicitly type the handler function newWorker.on(eventName, handler); } } // --- End Event Forwarding --- this.worker = newWorker; this.logger.info('Worker started successfully and event forwarding enabled'); return this.worker; } /** Helper to remove all attached worker listeners */ removeAllWorkerListeners() { if (this.worker && Object.keys(this.workerEventHandlers).length > 0) { this.logger.debug('Removing worker event listeners'); // Detach listeners using Object.keys for (const eventName of Object.keys(this.workerEventHandlers)) { const handler = this.workerEventHandlers[eventName]; if (handler) { // Explicitly type the handler function this.worker.off(eventName, handler); } } this.workerEventHandlers = {}; // Clear stored handlers } } /** * Stops the worker instance associated with this queue, if it is running. * Removes event listeners before closing. */ async stopWorker() { if (this.worker) { this.logger.info('Stopping worker and removing listeners...'); // Remove listeners before closing this.removeAllWorkerListeners(); try { await this.worker.close(); this.logger.info('Worker stopped successfully.'); } catch (error) { this.logger.error({ err: error instanceof Error ? error : new Error(String(error)) }, 'Error stopping worker'); } finally { this.worker = undefined; } } } /** * Closes the underlying BullMQ Worker (if started), Queue, and QueueEvents instances, * and removes the instance from the static registry. * Should be called during application shutdown. */ async close() { this.logger.debug('Closing Queue resources (worker, queue, events)'); const closePromises = []; // Stop the worker first using the new method // Use await here to ensure worker stops before queue closes if order matters await this.stopWorker(); // Proceed with closing queue and events this.logger.debug('Closing queue events...'); closePromises.push(this.queueEvents.close()); this.logger.debug('Closing queue...'); closePromises.push(super.close()); // Close the queue try { await Promise.all(closePromises); this.logger.debug('WorkerQueue resources closed successfully'); } catch (error) { this.logger.error({ err: error instanceof Error ? error : new Error(String(error)) }, 'Error closing WorkerQueue resources'); // Note: Instance was already removed from registry } } // --- Update Event Emitter Signatures --- // Override emit, on, off, once to use the new combined listener interface emit(event, ...args) { return QueueBase.prototype.emit.call(this, event, ...args); } off(eventName, listener) { QueueBase.prototype.off.call(this, eventName, listener); return this; } on(event, listener) { QueueBase.prototype.on.call(this, event, listener); return this; } once(event, listener) { QueueBase.prototype.once.call(this, event, listener); return this; } } //# sourceMappingURL=worker-queue.js.map