{"version":3,"file":"define-notification.mjs","names":[],"sources":["../../../../../../../notifications/src/dispatch/define-notification.ts"],"sourcesContent":["/**\n * `defineNotification` — reusable, type-safe, multi-channel notification.\n *\n * Functional, not class-based: pass `via` + a renderer per channel + a\n * stable `type`. The returned object exposes `.send` / `.queue` / `.only`.\n *\n * Renderer signature is `(data, to, ctx)` where `ctx` carries `locale` and\n * `meta` from `SendOptions`. The database renderer's `type` is OPTIONAL — the\n * dispatcher injects `def.type`, keeping the type defined in ONE place.\n *\n * ## Error policy\n * - **Config errors** (missing renderer, no queue dispatcher, unknown channel)\n *   are programmer mistakes → they REJECT `.send()`/`.queue()` (and a static\n *   `via` with a missing renderer throws at definition time). Consistent with\n *   `notify`, which rejects `ChannelNotFoundError`.\n * - **Transport errors** (a channel's `send` throwing) stay ISOLATED — emitted\n *   as a `failed` event, never aborting sibling channels/recipients.\n * - **`via()` / preferences throws** for one recipient are logged and that\n *   recipient is dropped (the rest of a fan-out still go).\n *\n * @example\n *   export const orderShipped = defineNotification<{ order: Order }>({\n *     type: \"order.shipped\",\n *     via: [\"mail\", \"database\"],\n *     mail: ({ order }, to) => ({ subject: `#${order.number} shipped`, html: \"…\" }),\n *     database: ({ order }) => ({ title: \"Your order shipped\", payload: { id: order.id } }),\n *   });\n *   await orderShipped.send(user, { order });\n *   await orderShipped.queue([buyer, salesRep], { order }, { delay: \"10m\" });\n *   await orderShipped.only(\"mail\").send(user, { order });\n */\nimport { log } from \"@warlock.js/logger\";\nimport { getNotificationConfig } from \"../config\";\nimport {\n  ChannelNotFoundError,\n  MissingRendererError,\n  NoQueueDispatcherError,\n} from \"../errors\";\nimport type {\n  ChannelName,\n  Notifiable,\n  NotificationChannels,\n  RenderContext,\n  SendOptions,\n} from \"../types\";\nimport { newDispatchId } from \"./dispatch-id\";\nimport { dispatchChannel, type DispatchMode } from \"./dispatcher\";\nimport { emit } from \"./notifications-event-bus\";\n\n/**\n * Per-channel renderer return type. The `database` renderer may OMIT `type`\n * — it inherits from `NotificationDef.type` at dispatch time.\n */\ntype RendererReturn<C extends ChannelName> = C extends \"database\"\n  ? Omit<NotificationChannels[\"database\"], \"type\">\n  : NotificationChannels[C];\n\ntype Renderer<Data, C extends ChannelName> = (\n  data: Data,\n  to: Notifiable,\n  ctx: RenderContext,\n) => RendererReturn<C>;\n\nexport type NotificationDef<Data> = {\n  type: string;\n  via: ChannelName[] | ((data: Data, to: Notifiable) => ChannelName[]);\n} & {\n  [C in ChannelName]?: Renderer<Data, C>;\n};\n\nexport interface DefinedNotification<Data> {\n  send(to: Notifiable | Notifiable[], data: Data, options?: SendOptions): Promise<void>;\n  queue(to: Notifiable | Notifiable[], data: Data, options?: SendOptions): Promise<void>;\n  only(...channels: ChannelName[]): DefinedNotification<Data>;\n}\n\n/** Errors that represent a misconfiguration and must surface (reject), not isolate. */\nfunction isConfigError(error: unknown): boolean {\n  return (\n    error instanceof MissingRendererError ||\n    error instanceof NoQueueDispatcherError ||\n    error instanceof ChannelNotFoundError\n  );\n}\n\nexport function defineNotification<Data>(def: NotificationDef<Data>): DefinedNotification<Data> {\n  const make = (restrict?: ChannelName[]): DefinedNotification<Data> => {\n    // Validate at definition time when the channel set is static — a missing\n    // renderer is a programmer error, surfaced loudly at import.\n    if (Array.isArray(def.via)) {\n      const channels = restrict ? def.via.filter((c) => restrict.includes(c)) : def.via;\n      for (const channel of channels) {\n        if (!def[channel]) {\n          throw new MissingRendererError(def.type, channel);\n        }\n      }\n    }\n\n    return {\n      send(to, data, options) {\n        return dispatchAll(def, to, data, options ?? {}, \"send\", restrict);\n      },\n      queue(to, data, options) {\n        return dispatchAll(def, to, data, options ?? {}, \"queue\", restrict);\n      },\n      only(...channels) {\n        return make(channels);\n      },\n    };\n  };\n\n  return make();\n}\n\n/**\n * Fan-out to all recipients. Each recipient dispatches independently; one\n * recipient's transport failure never aborts the others. CONFIG errors,\n * however, propagate — they reject the returned promise.\n */\nasync function dispatchAll<Data>(\n  def: NotificationDef<Data>,\n  to: Notifiable | Notifiable[],\n  data: Data,\n  options: SendOptions,\n  mode: DispatchMode,\n  restrict?: ChannelName[],\n): Promise<void> {\n  const recipients = Array.isArray(to) ? to : [to];\n\n  const results = await Promise.allSettled(\n    recipients.map((recipient) =>\n      dispatchToRecipient(def, recipient, data, options, mode, restrict),\n    ),\n  );\n\n  rethrowConfigErrors(results);\n}\n\nasync function dispatchToRecipient<Data>(\n  def: NotificationDef<Data>,\n  to: Notifiable,\n  data: Data,\n  options: SendOptions,\n  mode: DispatchMode,\n  restrict?: ChannelName[],\n): Promise<void> {\n  const config = getNotificationConfig();\n\n  // Resolve channels (via + preferences). A throw here drops THIS recipient\n  // with a log — it's a per-recipient data problem, not a config error.\n  let channels: ChannelName[];\n  try {\n    channels = typeof def.via === \"function\" ? def.via(data, to) : [...def.via];\n\n    if (restrict) {\n      channels = channels.filter((c) => restrict.includes(c));\n    }\n\n    if (!options.force && config.preferences) {\n      const allowed = await config.preferences.resolveChannels(to, def.type, channels);\n      for (const channel of channels.filter((c) => !allowed.includes(c))) {\n        await emit(\"skipped\", {\n          dispatchId: newDispatchId(),\n          channel,\n          notifiable: to,\n          reason: \"preference\",\n          options,\n        });\n      }\n      channels = channels.filter((c) => allowed.includes(c));\n    }\n  } catch (error) {\n    log.error(\"notifications\", `resolve.${def.type}`, error as Error);\n    return;\n  }\n\n  const renderCtx: RenderContext = { locale: options.locale, meta: options.meta };\n\n  const results = await Promise.allSettled(\n    channels.map((name) => {\n      const renderer = def[name] as Renderer<Data, ChannelName> | undefined;\n      if (!renderer) {\n        // Dynamic via chose a channel with no renderer — surface it.\n        return Promise.reject(new MissingRendererError(def.type, name));\n      }\n\n      let payload: unknown = renderer(data, to, renderCtx);\n\n      // Default the database channel's `type` from `def.type` when omitted.\n      if (name === \"database\" && payload && typeof payload === \"object\" && !(\"type\" in payload)) {\n        payload = { ...(payload as object), type: def.type };\n      }\n\n      return dispatchChannel({\n        channelName: name,\n        payload,\n        to,\n        notificationType: def.type,\n        mode,\n        options,\n      });\n    }),\n  );\n\n  rethrowConfigErrors(results);\n}\n\n/** Collect config-class rejections and rethrow (single or AggregateError). */\nfunction rethrowConfigErrors(results: PromiseSettledResult<unknown>[]): void {\n  const configErrors = results\n    .filter((r): r is PromiseRejectedResult => r.status === \"rejected\" && isConfigError(r.reason))\n    .map((r) => r.reason);\n\n  if (configErrors.length === 1) {\n    throw configErrors[0];\n  }\n  if (configErrors.length > 1) {\n    throw new AggregateError(configErrors, \"Multiple notification configuration errors\");\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAS,cAAc,OAAyB;CAC9C,OACE,iBAAiB,wBACjB,iBAAiB,0BACjB,iBAAiB;AAErB;AAEA,SAAgB,mBAAyB,KAAuD;CAC9F,MAAM,QAAQ,aAAwD;EAGpE,IAAI,MAAM,QAAQ,IAAI,GAAG,GAAG;GAC1B,MAAM,WAAW,WAAW,IAAI,IAAI,QAAQ,MAAM,SAAS,SAAS,CAAC,CAAC,IAAI,IAAI;GAC9E,KAAK,MAAM,WAAW,UACpB,IAAI,CAAC,IAAI,UACP,MAAM,IAAI,qBAAqB,IAAI,MAAM,OAAO;EAGtD;EAEA,OAAO;GACL,KAAK,IAAI,MAAM,SAAS;IACtB,OAAO,YAAY,KAAK,IAAI,MAAM,WAAW,CAAC,GAAG,QAAQ,QAAQ;GACnE;GACA,MAAM,IAAI,MAAM,SAAS;IACvB,OAAO,YAAY,KAAK,IAAI,MAAM,WAAW,CAAC,GAAG,SAAS,QAAQ;GACpE;GACA,KAAK,GAAG,UAAU;IAChB,OAAO,KAAK,QAAQ;GACtB;EACF;CACF;CAEA,OAAO,KAAK;AACd;;;;;;AAOA,eAAe,YACb,KACA,IACA,MACA,SACA,MACA,UACe;CACf,MAAM,aAAa,MAAM,QAAQ,EAAE,IAAI,KAAK,CAAC,EAAE;CAQ/C,oBAAoB,MANE,QAAQ,WAC5B,WAAW,KAAK,cACd,oBAAoB,KAAK,WAAW,MAAM,SAAS,MAAM,QAAQ,CACnE,CACF,CAE2B;AAC7B;AAEA,eAAe,oBACb,KACA,IACA,MACA,SACA,MACA,UACe;CACf,MAAM,SAAS,sBAAsB;CAIrC,IAAI;CACJ,IAAI;EACF,WAAW,OAAO,IAAI,QAAQ,aAAa,IAAI,IAAI,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,GAAG;EAE1E,IAAI,UACF,WAAW,SAAS,QAAQ,MAAM,SAAS,SAAS,CAAC,CAAC;EAGxD,IAAI,CAAC,QAAQ,SAAS,OAAO,aAAa;GACxC,MAAM,UAAU,MAAM,OAAO,YAAY,gBAAgB,IAAI,IAAI,MAAM,QAAQ;GAC/E,KAAK,MAAM,WAAW,SAAS,QAAQ,MAAM,CAAC,QAAQ,SAAS,CAAC,CAAC,GAC/D,MAAM,KAAK,WAAW;IACpB,YAAY,cAAc;IAC1B;IACA,YAAY;IACZ,QAAQ;IACR;GACF,CAAC;GAEH,WAAW,SAAS,QAAQ,MAAM,QAAQ,SAAS,CAAC,CAAC;EACvD;CACF,SAAS,OAAO;EACd,IAAI,MAAM,iBAAiB,WAAW,IAAI,QAAQ,KAAc;EAChE;CACF;CAEA,MAAM,YAA2B;EAAE,QAAQ,QAAQ;EAAQ,MAAM,QAAQ;CAAK;CA4B9E,oBAAoB,MA1BE,QAAQ,WAC5B,SAAS,KAAK,SAAS;EACrB,MAAM,WAAW,IAAI;EACrB,IAAI,CAAC,UAEH,OAAO,QAAQ,OAAO,IAAI,qBAAqB,IAAI,MAAM,IAAI,CAAC;EAGhE,IAAI,UAAmB,SAAS,MAAM,IAAI,SAAS;EAGnD,IAAI,SAAS,cAAc,WAAW,OAAO,YAAY,YAAY,EAAE,UAAU,UAC/E,UAAU;GAAE,GAAI;GAAoB,MAAM,IAAI;EAAK;EAGrD,OAAO,gBAAgB;GACrB,aAAa;GACb;GACA;GACA,kBAAkB,IAAI;GACtB;GACA;EACF,CAAC;CACH,CAAC,CACH,CAE2B;AAC7B;;AAGA,SAAS,oBAAoB,SAAgD;CAC3E,MAAM,eAAe,QAClB,QAAQ,MAAkC,EAAE,WAAW,cAAc,cAAc,EAAE,MAAM,CAAC,CAAC,CAC7F,KAAK,MAAM,EAAE,MAAM;CAEtB,IAAI,aAAa,WAAW,GAC1B,MAAM,aAAa;CAErB,IAAI,aAAa,SAAS,GACxB,MAAM,IAAI,eAAe,cAAc,4CAA4C;AAEvF"}