{"version":3,"file":"bullmq-queue.mjs","names":[],"sources":["../../../../../../../notifications/src/queue/bullmq-queue.ts"],"sourcesContent":["/**\n * BullMQ-backed `QueueDispatcher` — an alternative to `heraldQueue()` for apps\n * that already run `@warlock.js/queue` and would rather deliver notifications\n * through it than stand up a herald broker.\n *\n * `defineNotification` renders the payload + resolves the route BEFORE\n * handing a job to the dispatcher, so the job (`{ channel, route, payload,\n * options }`) is plain JSON — the same contract `heraldQueue` relies on.\n *\n * `@warlock.js/queue` is an OPTIONAL peer, lazy-loaded via `loadQueuePackage`\n * on first `.dispatch()` — never at import time — so notifications never pays\n * for `@warlock.js/queue` unless `bullmqQueue()` is actually configured.\n *\n * @example  src/config/notifications.ts — declarative; the connector registers it\n *   import { type NotificationConfig, bullmqQueue, mailChannel } from \"@warlock.js/notifications\";\n *\n *   const config: NotificationConfig = {\n *     channels: { mail: mailChannel() },\n *     queue: bullmqQueue({ attempts: 3, backoff: { type: \"exponential\", delay: 5000 } }),\n *   };\n *\n *   export default config;\n */\nimport type { JobBackoff } from \"@warlock.js/queue\";\nimport type { QueueDispatcher } from \"../contracts\";\nimport { getNotificationConfig } from \"../config\";\nimport type { ChannelName, SendOptions } from \"../types\";\nimport { loadQueuePackage } from \"./load-queue\";\n\n/** The job name notification deliveries run under. */\nexport const NOTIFICATION_JOB_NAME = \"warlock.notifications.deliver\";\n\nexport type BullmqQueueOptions = {\n  /** Queue to deliver on. Default: `@warlock.js/queue`'s default queue. */\n  queue?: string;\n  /** Attempts per delivery. Default: `queue.defaultJobOptions.attempts`, else `1`. */\n  attempts?: number;\n  /** Backoff between attempts. */\n  backoff?: JobBackoff;\n};\n\ntype NotificationJobPayload = {\n  channel: string;\n  route: unknown;\n  payload: unknown;\n  options: SendOptions;\n};\n\n/**\n * Create the `QueueDispatcher` for `NotificationConfig.queue` backed by\n * `@warlock.js/queue`.\n *\n * - `SendOptions.delay` is honoured: a number is SECONDS (notifications'\n *   convention), a string is a duration such as `\"10m\"` (`@warlock.js/queue`'s\n *   convention).\n * - A channel missing from the worker's notifications config throws — the\n *   delivery is retried per `attempts` / `backoff` like any other failure.\n */\nexport function bullmqQueue(options: BullmqQueueOptions): QueueDispatcher {\n  let jobPromise: ReturnType<typeof defineNotificationJob> | undefined;\n\n  return {\n    async dispatch(job) {\n      if (!jobPromise) {\n        jobPromise = defineNotificationJob(options);\n      }\n\n      const notificationJob = await jobPromise;\n\n      await notificationJob.dispatch(\n        { channel: job.channel, route: job.route, payload: job.payload, options: job.options },\n        { delay: job.options.delay === undefined ? undefined : notificationDelay(job.options.delay) },\n      );\n    },\n  };\n}\n\nasync function defineNotificationJob(options: BullmqQueueOptions) {\n  const { defineJob } = await loadQueuePackage();\n\n  return defineJob<NotificationJobPayload, void>({\n    name: NOTIFICATION_JOB_NAME,\n    queue: options.queue,\n    attempts: options.attempts,\n    backoff: options.backoff,\n    async handle(job) {\n      const channel = getNotificationConfig().channels[job.channel as ChannelName];\n\n      if (!channel) {\n        throw new Error(\n          `Notification channel \"${job.channel}\" is not configured in this worker's notifications config.`,\n        );\n      }\n\n      await channel.send({ payload: job.payload, route: job.route, options: job.options } as never);\n    },\n  });\n}\n\nfunction notificationDelay(delay: number | string): number {\n  return typeof delay === \"number\" ? delay * 1_000 : toMilliseconds(delay);\n}\n\n// `toMilliseconds` mirrors `@warlock.js/queue`'s duration parser but is\n// inlined here so this file doesn't need a non-lazy import from the optional\n// peer just to convert a string like \"10m\".\nfunction toMilliseconds(value: string): number {\n  const units: Record<string, number> = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };\n  const match = /^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/.exec(value.trim());\n\n  if (!match) {\n    throw new Error(\n      `Invalid duration \"${value}\": expected a number of milliseconds or a string like ` +\n        `\"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n    );\n  }\n\n  return Math.round(Number(match[1]) * units[match[2]!]!);\n}\n"],"mappings":";;;;;AA8BA,MAAa,wBAAwB;;;;;;;;;;;AA4BrC,SAAgB,YAAY,SAA8C;CACxE,IAAI;CAEJ,OAAO,EACL,MAAM,SAAS,KAAK;EAClB,IAAI,CAAC,YACH,aAAa,sBAAsB,OAAO;EAK5C,OAAM,MAFwB,WAET,CAAC,SACpB;GAAE,SAAS,IAAI;GAAS,OAAO,IAAI;GAAO,SAAS,IAAI;GAAS,SAAS,IAAI;EAAQ,GACrF,EAAE,OAAO,IAAI,QAAQ,UAAU,SAAY,SAAY,kBAAkB,IAAI,QAAQ,KAAK,EAAE,CAC9F;CACF,EACF;AACF;AAEA,eAAe,sBAAsB,SAA6B;CAChE,MAAM,EAAE,cAAc,MAAM,iBAAiB;CAE7C,OAAO,UAAwC;EAC7C,MAAM;EACN,OAAO,QAAQ;EACf,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,MAAM,OAAO,KAAK;GAChB,MAAM,UAAU,sBAAsB,CAAC,CAAC,SAAS,IAAI;GAErD,IAAI,CAAC,SACH,MAAM,IAAI,MACR,yBAAyB,IAAI,QAAQ,2DACvC;GAGF,MAAM,QAAQ,KAAK;IAAE,SAAS,IAAI;IAAS,OAAO,IAAI;IAAO,SAAS,IAAI;GAAQ,CAAU;EAC9F;CACF,CAAC;AACH;AAEA,SAAS,kBAAkB,OAAgC;CACzD,OAAO,OAAO,UAAU,WAAW,QAAQ,MAAQ,eAAe,KAAK;AACzE;AAKA,SAAS,eAAe,OAAuB;CAC7C,MAAM,QAAgC;EAAE,IAAI;EAAG,GAAG;EAAO,GAAG;EAAQ,GAAG;EAAW,GAAG;CAAW;CAChG,MAAM,QAAQ,gCAAgC,KAAK,MAAM,KAAK,CAAC;CAE/D,IAAI,CAAC,OACH,MAAM,IAAI,MACR,qBAAqB,MAAM,yFAE7B;CAGF,OAAO,KAAK,MAAM,OAAO,MAAM,EAAE,IAAI,MAAM,MAAM,GAAK;AACxD"}