UNPKG

zeroant-factory

Version:
179 lines (164 loc) 6.36 kB
import { type ZeroantContext } from './zeroant.context.js' import Queue, { type Job } from 'bull' import { type ConfigFactory } from './config.factory.js' import { type RedisOptions } from 'ioredis' interface WorkerConfigOptions { redisUrl: string redisOptions?: RedisOptions } type FnWorkerConfigOptions = (config: ConfigFactory) => WorkerConfigOptions export interface WorkerOptions extends Queue.JobOptions { concurrency?: number | 10 config?: WorkerConfigOptions | FnWorkerConfigOptions } export type WorkerProcessor = (job: Queue.Job, done: Queue.DoneCallback, log: (row: string) => Promise<any>) => Promise<void> export const WorkerConfig = (options?: { name: string; options?: WorkerOptions }) => { return function <T extends WorkerFactoryConstructor<WorkerFactory<any, any>>>(target: T): T { if (options?.name !== undefined) { Object.defineProperty(target, 'name', { value: options?.name, configurable: false, writable: false }) } Object.defineProperty(target, 'options', { value: options?.options ?? { delay: 0, removeOnComplete: true, concurrency: WorkerFactory.concurrency }, configurable: false, writable: false }) return target } } export type WorkerFactoryConstructor<T extends WorkerFactory<any, any>> = new (context: ZeroantContext<ConfigFactory>) => T export interface OnCreate<F = any> { onCreate: (processors: Map<F, WorkerProcessor>) => void } export abstract class WorkerFactory<T, F extends string> { static concurrency = 10 #queue: Queue.Queue #processors = new Map<F, WorkerProcessor>() constructor(readonly context: ZeroantContext<ConfigFactory>) { switch (typeof this.options.config) { case 'function': { const config = this.options.config(this.context.config) this.#queue = new Queue<T>(this.name, config.redisUrl as never, { redis: config?.redisOptions ?? {}, defaultJobOptions: this.options ?? {} }) break } case 'object': { const config = this.options.config this.#queue = new Queue<T>(this.name, config.redisUrl as never, { redis: config?.redisOptions ?? {}, defaultJobOptions: this.options ?? {} }) break } default: { if (process.env.REDIS_URI !== undefined) { this.#queue = new Queue<T>(this.name, process.env.REDIS_URI, { redis: {}, defaultJobOptions: this.options ?? {} }) break } this.#queue = new Queue<T>(this.name, { defaultJobOptions: this.options ?? {} }) } } this.onCreate(this.#processors) } get options(): WorkerOptions { return (this.constructor as any).options } get name(): string { return this.constructor.name } get instance(): Queue.Queue<T> { return this.#queue } /** * @description this is the default processor you can push to a different event name by the below code * ```ts * * onCreate (processors: Map<OrderWorkerName, WorkerProcessor>): void { * * processors.set(OrderWorkerName.fulfillment, this.fulfillment) * * } * * this.push(OrderWorkerName.fulfillment, {..data.}, {..opts.}) * * */ abstract processor(job: Queue.Job, done: Queue.DoneCallback, log: (row: string) => Promise<any>): Promise<void> onCreate(processors: Map<F, WorkerProcessor>): void {} async restart(jobId: number) { const job = await this.#queue.getJob(jobId) if (job === null) { return } // await job.releaseLock() // await job.moveToFailed({ message: 'restarted' }, true) await job.retry() } async run(concurrency?: number) { console.log(`${this.name} default worker started successfully ${new Date().toLocaleString()}`) const wrapper = this._wrapper(async (job: Queue.Job, done: Queue.DoneCallback, log: (row: string) => Promise<any>) => { await this.processor(job, done, log) }) await Promise.all([ ...Array.from(this.#processors.entries()).map(async ([name, processor]) => { console.log(`${this.name} ${name} worker started successfully ${new Date().toLocaleString()}`) const wrapper = this._wrapper(async (job: Queue.Job, done: Queue.DoneCallback, log: (row: string) => Promise<any>) => { await processor(job, done, log) }) await this.#queue.process(name, concurrency ?? this.options.concurrency ?? WorkerFactory.concurrency, wrapper) }) ]) await this.#queue.process(concurrency ?? this.options.concurrency ?? WorkerFactory.concurrency, wrapper) } async add(data?: T, options?: Queue.JobOptions): Promise<Job<T>> { console.log('pushing to queue', this.name) return await this.#queue.add(data, options) } async push(name: F, data?: T, options?: Queue.JobOptions): Promise<Job<T>> { console.log('pushing to queue', this.name, name) return await this.#queue.add(name, data, options ?? {}) } addListener(eventName: string | symbol, listener: (...args: any[]) => void): Queue.Queue<T> { return this.#queue.addListener(eventName, listener) } async addBulk(bulkData: Array<{ name?: any; data: T; opts?: Omit<Queue.JobOptions, 'repeat'> | undefined }>): Promise<Job[]> { return await this.#queue.addBulk(bulkData) } _wrapper(callback: (job: Queue.Job, done: Queue.DoneCallback, log: (row: string) => Promise<any>) => Promise<void>) { return async (job: Queue.Job, done: Queue.DoneCallback): Promise<void> => { const start = Date.now() const { id, name } = job const data = job.data const log = job.log.bind(job) console.log('[-------------------------------------------]') console.log(`Processing ${this.name}->${name} Job: ${id} `) console.log(`${this.name}->${name} Worker Processor data ----->`, JSON.stringify(data)) try { await callback(job, done, log) } catch (caught: any) { console.log(`${this.name}->${name} Worker Error -----> `, caught) await log(caught.message) done(caught) } console.log(`Job ${this.name}->${name} ${id} ran for ${(Date.now() - start) / 1000}s`) console.log('[-------------------------------------------]') } } getQueue(): Queue.Queue<T> { return this.#queue } }