/** * apps/worker — BullMQ queue names + enqueue helper. * * The seven queues and their owners (see README for the event→queue map): * files → file pipeline (hash, mime check, scan, metadata) * search-index → explicit (re)index ops; stream events are handled by the * search-index StreamConsumer instead * webhooks → outbound webhook dispatch (retry-safe via BullMQ attempts) * automations → automation run jobs (schedule repeatables land here too) * email → outbound mail (dev-safe no-op without SMTP_HOST) * ai → embeddings / generation (no-op without AI_API_KEY) * maintenance → retention, reconciliation, DLQ reports, session cleanup * * Retry policy (every job): 8 attempts, exponential backoff, base 5s, ×2 per * attempt. BullMQ's exponential backoff has no explicit delay cap; with base * 5s and 8 attempts the per-retry delays are 5s → 10s → 20s → 40s → 80s → * 160s → 320s (≈5 min), which satisfies the "base 5s ×2, cap 5min" policy. * removeOnComplete keeps the last 1000 completed jobs for observability; * failed jobs are kept (removeOnFail false) for the DLQ reports. */ import { Queue, type Job, type JobsOptions } from "bullmq"; import { randomUUID } from "node:crypto"; import type { Redis } from "ioredis"; export const QUEUES = { files: "files", searchIndex: "search-index", webhooks: "webhooks", automations: "automations", email: "email", ai: "ai", maintenance: "maintenance" } as const; export type QueueName = (typeof QUEUES)[keyof typeof QUEUES]; /** Default job options — applied to EVERY enqueue unless overridden. */ export function defaultJobOpts(partial: JobsOptions = {}): JobsOptions { return { attempts: 8, backoff: { type: "exponential", delay: 5_000 }, removeOnComplete: { age: 1_000 }, removeOnFail: false, jobId: partial.jobId ?? randomUUID(), ...partial }; } /** * Enqueue a job on a queue. Job id defaults to a fresh UUID (idempotency * hooks may pass an explicit jobId — BullMQ dedups identical jobIds). */ export async function enqueue( connection: Redis, queue: QueueName, data: Record, opts: JobsOptions = {} ): Promise { const q = new Queue(queue, { connection }); try { // BullMQ's add(name, data, opts): the JOB NAME is the queue name (a // plain string) and `data` is the payload. return await q.add(queue, data, defaultJobOpts(opts)); } finally { await q.close(); } } /** Enqueue with an explicit stable job id (schedule-triggered runs, …). */ export async function enqueueWithId( connection: Redis, queue: QueueName, jobId: string, data: Record, opts: JobsOptions = {} ): Promise { return enqueue(connection, queue, data, { ...opts, jobId }); }