torotask
Version:
Task queue processing in NodeJS based on BullMQ and Redis
200 lines • 8.44 kB
JavaScript
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.
* @param _token An optional token for 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');
},
resumed: () => {
this.logger.debug('Worker event: resumed');
this.emit('worker:resumed');
},
stalled: (jobId, prev) => {
this.logger.warn({ jobId, prev }, 'Worker event: stalled');
this.emit('worker:stalled', jobId, prev);
},
closed: () => {
this.logger.debug('Worker event: closed');
this.emit('worker:closed');
},
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
// @ts-expect-error - Intentionally widening the event interface
emit(event, ...args) {
return super.emit(event, ...args);
}
// @ts-expect-error - Intentionally widening the event interface
off(eventName, listener) {
super.off(eventName, listener);
return this;
}
// @ts-expect-error - Intentionally widening the event interface
on(event, listener) {
super.on(event, listener);
return this;
}
// @ts-expect-error - Intentionally widening the event interface
once(event, listener) {
super.once(event, listener);
return this;
}
}
//# sourceMappingURL=worker-queue.js.map