import { Queue, type ConnectionOptions, type QueueOptions } from 'bullmq'; import type { Config } from './config.js'; import type { Channel, SendJob } from './types.js'; export const QUEUE_NAMES: Record = { fcm: 'tcef-fcm', email: 'tcef-email', }; /** * BullMQ connection options. We hand BullMQ plain options (not a shared ioredis * instance) so it manages its own blocking/non-blocking connections and sets * maxRetriesPerRequest:null on workers itself. */ export function redisConnection(cfg: Config): ConnectionOptions { return { host: cfg.redis.host, port: cfg.redis.port, password: cfg.redis.password, db: cfg.redis.db, }; } export function createQueues(cfg: Config): Record> { const opts: QueueOptions = { connection: redisConnection(cfg), defaultJobOptions: { attempts: cfg.retries.attempts, backoff: { type: 'exponential', delay: cfg.retries.backoffMs }, removeOnComplete: { count: 1000 }, removeOnFail: false, // keep failures = dead-letter for inspection }, }; return { fcm: new Queue(QUEUE_NAMES.fcm, opts), email: new Queue(QUEUE_NAMES.email, opts), }; } /** * Job id = `${channel}_${notificationId}` so BullMQ itself dedupes enqueues: * re-polling the same pending doc will not create a second job. * (BullMQ rejects custom job ids containing ':', hence the underscore.) */ export function jobId(job: SendJob): string { return `${job.channel}_${job.notificationId}`; }