@warlock.js/notifications 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/cjs/index.cjs +1034 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/channels/database-channel.d.mts +10 -0
- package/esm/channels/database-channel.d.mts.map +1 -0
- package/esm/channels/database-channel.mjs +19 -0
- package/esm/channels/database-channel.mjs.map +1 -0
- package/esm/channels/index.d.mts +2 -0
- package/esm/channels/index.mjs +4 -0
- package/esm/channels/mail-channel.d.mts +11 -0
- package/esm/channels/mail-channel.d.mts.map +1 -0
- package/esm/channels/mail-channel.mjs +43 -0
- package/esm/channels/mail-channel.mjs.map +1 -0
- package/esm/config.d.mts +46 -0
- package/esm/config.d.mts.map +1 -0
- package/esm/config.mjs +32 -0
- package/esm/config.mjs.map +1 -0
- package/esm/contracts/channel.contract.d.mts +38 -0
- package/esm/contracts/channel.contract.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +5 -0
- package/esm/contracts/notification.contract.d.mts +18 -0
- package/esm/contracts/notification.contract.d.mts.map +1 -0
- package/esm/contracts/preference-provider.contract.d.mts +20 -0
- package/esm/contracts/preference-provider.contract.d.mts.map +1 -0
- package/esm/contracts/queue-dispatcher.contract.d.mts +23 -0
- package/esm/contracts/queue-dispatcher.contract.d.mts.map +1 -0
- package/esm/contracts/rate-limiter.contract.d.mts +23 -0
- package/esm/contracts/rate-limiter.contract.d.mts.map +1 -0
- package/esm/dispatch/define-channel.d.mts +37 -0
- package/esm/dispatch/define-channel.d.mts.map +1 -0
- package/esm/dispatch/define-channel.mjs +39 -0
- package/esm/dispatch/define-channel.mjs.map +1 -0
- package/esm/dispatch/define-notification.d.mts +22 -0
- package/esm/dispatch/define-notification.d.mts.map +1 -0
- package/esm/dispatch/define-notification.mjs +125 -0
- package/esm/dispatch/define-notification.mjs.map +1 -0
- package/esm/dispatch/dispatch-id.mjs +15 -0
- package/esm/dispatch/dispatch-id.mjs.map +1 -0
- package/esm/dispatch/dispatcher.mjs +120 -0
- package/esm/dispatch/dispatcher.mjs.map +1 -0
- package/esm/dispatch/index.d.mts +4 -0
- package/esm/dispatch/index.mjs +6 -0
- package/esm/dispatch/notifications-event-bus.d.mts +18 -0
- package/esm/dispatch/notifications-event-bus.d.mts.map +1 -0
- package/esm/dispatch/notifications-event-bus.mjs +58 -0
- package/esm/dispatch/notifications-event-bus.mjs.map +1 -0
- package/esm/dispatch/notify.d.mts +20 -0
- package/esm/dispatch/notify.d.mts.map +1 -0
- package/esm/dispatch/notify.mjs +80 -0
- package/esm/dispatch/notify.mjs.map +1 -0
- package/esm/errors.d.mts +32 -0
- package/esm/errors.d.mts.map +1 -0
- package/esm/errors.mjs +56 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/in-app/base-notifications-repository.d.mts +82 -0
- package/esm/in-app/base-notifications-repository.d.mts.map +1 -0
- package/esm/in-app/base-notifications-repository.mjs +187 -0
- package/esm/in-app/base-notifications-repository.mjs.map +1 -0
- package/esm/in-app/column-map.d.mts +65 -0
- package/esm/in-app/column-map.d.mts.map +1 -0
- package/esm/in-app/column-map.mjs +28 -0
- package/esm/in-app/column-map.mjs.map +1 -0
- package/esm/in-app/database-notification.d.mts +30 -0
- package/esm/in-app/database-notification.d.mts.map +1 -0
- package/esm/in-app/database-notification.mjs +70 -0
- package/esm/in-app/database-notification.mjs.map +1 -0
- package/esm/in-app/in-app.d.mts +56 -0
- package/esm/in-app/in-app.d.mts.map +1 -0
- package/esm/in-app/in-app.mjs +67 -0
- package/esm/in-app/in-app.mjs.map +1 -0
- package/esm/in-app/index.d.mts +4 -0
- package/esm/in-app/index.mjs +6 -0
- package/esm/index.d.mts +25 -0
- package/esm/index.mjs +23 -0
- package/esm/migration/index.d.mts +1 -0
- package/esm/migration/index.mjs +3 -0
- package/esm/migration/notification-columns.d.mts +13 -0
- package/esm/migration/notification-columns.d.mts.map +1 -0
- package/esm/migration/notification-columns.mjs +53 -0
- package/esm/migration/notification-columns.mjs.map +1 -0
- package/esm/queue/herald-queue.d.mts +10 -0
- package/esm/queue/herald-queue.d.mts.map +1 -0
- package/esm/queue/herald-queue.mjs +14 -0
- package/esm/queue/herald-queue.mjs.map +1 -0
- package/esm/queue/index.d.mts +3 -0
- package/esm/queue/index.mjs +5 -0
- package/esm/queue/load-herald.d.mts +6 -0
- package/esm/queue/load-herald.d.mts.map +1 -0
- package/esm/queue/load-herald.mjs +35 -0
- package/esm/queue/load-herald.mjs.map +1 -0
- package/esm/queue/notifications-worker.d.mts +9 -0
- package/esm/queue/notifications-worker.d.mts.map +1 -0
- package/esm/queue/notifications-worker.mjs +46 -0
- package/esm/queue/notifications-worker.mjs.map +1 -0
- package/esm/types.d.mts +130 -0
- package/esm/types.d.mts.map +1 -0
- package/llms-full.txt +994 -0
- package/llms.txt +17 -0
- package/package.json +40 -0
- package/skills/README.md +23 -0
- package/skills/configure-notifications/SKILL.md +138 -0
- package/skills/define-channel/SKILL.md +107 -0
- package/skills/define-notification/SKILL.md +133 -0
- package/skills/notifications-basics/SKILL.md +94 -0
- package/skills/observe-notifications/SKILL.md +66 -0
- package/skills/queue-notifications/SKILL.md +81 -0
- package/skills/send-ad-hoc/SKILL.md +95 -0
- package/skills/use-in-app/SKILL.md +168 -0
- package/skills/write-notification-migration/SKILL.md +70 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region ../@warlock.js/notifications/src/dispatch/define-channel.ts
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper that narrows the channel type to its payload. Use it when
|
|
4
|
+
* you want TypeScript to enforce the `send` payload shape against the
|
|
5
|
+
* payload type you declared.
|
|
6
|
+
*
|
|
7
|
+
* Built-in channels (`mail`, `database`) ship as factories; custom channels
|
|
8
|
+
* are usually defined with this helper.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { defineChannel } from "@warlock.js/notifications";
|
|
12
|
+
*
|
|
13
|
+
* type DiscordPayload = { content: string };
|
|
14
|
+
*
|
|
15
|
+
* export const discordChannel = () =>
|
|
16
|
+
* defineChannel<DiscordPayload>({
|
|
17
|
+
* name: "discord",
|
|
18
|
+
* route: (n) => n.get("discord_webhook"),
|
|
19
|
+
* async send({ payload, route }) {
|
|
20
|
+
* await fetch(route as string, {
|
|
21
|
+
* method: "POST",
|
|
22
|
+
* headers: { "content-type": "application/json" },
|
|
23
|
+
* body: JSON.stringify(payload),
|
|
24
|
+
* });
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Then teach the registry:
|
|
29
|
+
* declare module "@warlock.js/notifications" {
|
|
30
|
+
* interface NotificationChannels { discord: DiscordPayload }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
function defineChannel(channel) {
|
|
34
|
+
return channel;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { defineChannel };
|
|
39
|
+
//# sourceMappingURL=define-channel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-channel.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/define-channel.ts"],"sourcesContent":["import type { Channel } from \"../contracts\";\n\n/**\n * Identity helper that narrows the channel type to its payload. Use it when\n * you want TypeScript to enforce the `send` payload shape against the\n * payload type you declared.\n *\n * Built-in channels (`mail`, `database`) ship as factories; custom channels\n * are usually defined with this helper.\n *\n * @example\n * import { defineChannel } from \"@warlock.js/notifications\";\n *\n * type DiscordPayload = { content: string };\n *\n * export const discordChannel = () =>\n * defineChannel<DiscordPayload>({\n * name: \"discord\",\n * route: (n) => n.get(\"discord_webhook\"),\n * async send({ payload, route }) {\n * await fetch(route as string, {\n * method: \"POST\",\n * headers: { \"content-type\": \"application/json\" },\n * body: JSON.stringify(payload),\n * });\n * },\n * });\n *\n * // Then teach the registry:\n * declare module \"@warlock.js/notifications\" {\n * interface NotificationChannels { discord: DiscordPayload }\n * }\n */\nexport function defineChannel<P>(channel: Channel<P>): Channel<P> {\n return channel;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,cAAiB,SAAiC;CAChE,OAAO;AACT"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ChannelName, Notifiable, NotificationChannels, RenderContext, SendOptions } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/notifications/src/dispatch/define-notification.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-channel renderer return type. The `database` renderer may OMIT `type`
|
|
6
|
+
* — it inherits from `NotificationDef.type` at dispatch time.
|
|
7
|
+
*/
|
|
8
|
+
type RendererReturn<C extends ChannelName> = C extends "database" ? Omit<NotificationChannels["database"], "type"> : NotificationChannels[C];
|
|
9
|
+
type Renderer<Data, C extends ChannelName> = (data: Data, to: Notifiable, ctx: RenderContext) => RendererReturn<C>;
|
|
10
|
+
type NotificationDef<Data> = {
|
|
11
|
+
type: string;
|
|
12
|
+
via: ChannelName[] | ((data: Data, to: Notifiable) => ChannelName[]);
|
|
13
|
+
} & { [C in ChannelName]?: Renderer<Data, C> };
|
|
14
|
+
interface DefinedNotification<Data> {
|
|
15
|
+
send(to: Notifiable | Notifiable[], data: Data, options?: SendOptions): Promise<void>;
|
|
16
|
+
queue(to: Notifiable | Notifiable[], data: Data, options?: SendOptions): Promise<void>;
|
|
17
|
+
only(...channels: ChannelName[]): DefinedNotification<Data>;
|
|
18
|
+
}
|
|
19
|
+
declare function defineNotification<Data>(def: NotificationDef<Data>): DefinedNotification<Data>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DefinedNotification, NotificationDef, defineNotification };
|
|
22
|
+
//# sourceMappingURL=define-notification.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-notification.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/define-notification.ts"],"mappings":";;;;;AA4CkB;;KASb,cAAA,WAAyB,WAAA,IAAe,CAAA,sBACzC,IAAA,CAAK,oBAAA,wBACL,oBAAA,CAAqB,CAAA;AAAA,KAEpB,QAAA,iBAAyB,WAAA,KAC5B,IAAA,EAAM,IAAA,EACN,EAAA,EAAI,UAAA,EACJ,GAAA,EAAK,aAAA,KACF,cAAA,CAAe,CAAA;AAAA,KAER,eAAA;EACV,IAAA;EACA,GAAA,EAAK,WAAA,OAAkB,IAAA,EAAM,IAAA,EAAM,EAAA,EAAI,UAAA,KAAe,WAAA;AAAA,YAEhD,WAAA,IAAe,QAAA,CAAS,IAAA,EAAM,CAAA;AAAA,UAGrB,mBAAA;EACf,IAAA,CAAK,EAAA,EAAI,UAAA,GAAa,UAAA,IAAc,IAAA,EAAM,IAAA,EAAM,OAAA,GAAU,WAAA,GAAc,OAAA;EACxE,KAAA,CAAM,EAAA,EAAI,UAAA,GAAa,UAAA,IAAc,IAAA,EAAM,IAAA,EAAM,OAAA,GAAU,WAAA,GAAc,OAAA;EACzE,IAAA,IAAQ,QAAA,EAAU,WAAA,KAAgB,mBAAA,CAAoB,IAAA;AAAA;AAAA,iBAYxC,kBAAA,OAAyB,GAAA,EAAK,eAAA,CAAgB,IAAA,IAAQ,mBAAA,CAAoB,IAAA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError } from "../errors.mjs";
|
|
2
|
+
import { getNotificationConfig } from "../config.mjs";
|
|
3
|
+
import { newDispatchId } from "./dispatch-id.mjs";
|
|
4
|
+
import { emit } from "./notifications-event-bus.mjs";
|
|
5
|
+
import { dispatchChannel } from "./dispatcher.mjs";
|
|
6
|
+
import { log } from "@warlock.js/logger";
|
|
7
|
+
|
|
8
|
+
//#region ../@warlock.js/notifications/src/dispatch/define-notification.ts
|
|
9
|
+
/**
|
|
10
|
+
* `defineNotification` — reusable, type-safe, multi-channel notification.
|
|
11
|
+
*
|
|
12
|
+
* Functional, not class-based: pass `via` + a renderer per channel + a
|
|
13
|
+
* stable `type`. The returned object exposes `.send` / `.queue` / `.only`.
|
|
14
|
+
*
|
|
15
|
+
* Renderer signature is `(data, to, ctx)` where `ctx` carries `locale` and
|
|
16
|
+
* `meta` from `SendOptions`. The database renderer's `type` is OPTIONAL — the
|
|
17
|
+
* dispatcher injects `def.type`, keeping the type defined in ONE place.
|
|
18
|
+
*
|
|
19
|
+
* ## Error policy
|
|
20
|
+
* - **Config errors** (missing renderer, no queue dispatcher, unknown channel)
|
|
21
|
+
* are programmer mistakes → they REJECT `.send()`/`.queue()` (and a static
|
|
22
|
+
* `via` with a missing renderer throws at definition time). Consistent with
|
|
23
|
+
* `notify`, which rejects `ChannelNotFoundError`.
|
|
24
|
+
* - **Transport errors** (a channel's `send` throwing) stay ISOLATED — emitted
|
|
25
|
+
* as a `failed` event, never aborting sibling channels/recipients.
|
|
26
|
+
* - **`via()` / preferences throws** for one recipient are logged and that
|
|
27
|
+
* recipient is dropped (the rest of a fan-out still go).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* export const orderShipped = defineNotification<{ order: Order }>({
|
|
31
|
+
* type: "order.shipped",
|
|
32
|
+
* via: ["mail", "database"],
|
|
33
|
+
* mail: ({ order }, to) => ({ subject: `#${order.number} shipped`, html: "…" }),
|
|
34
|
+
* database: ({ order }) => ({ title: "Your order shipped", payload: { id: order.id } }),
|
|
35
|
+
* });
|
|
36
|
+
* await orderShipped.send(user, { order });
|
|
37
|
+
* await orderShipped.queue([buyer, salesRep], { order }, { delay: "10m" });
|
|
38
|
+
* await orderShipped.only("mail").send(user, { order });
|
|
39
|
+
*/
|
|
40
|
+
/** Errors that represent a misconfiguration and must surface (reject), not isolate. */
|
|
41
|
+
function isConfigError(error) {
|
|
42
|
+
return error instanceof MissingRendererError || error instanceof NoQueueDispatcherError || error instanceof ChannelNotFoundError;
|
|
43
|
+
}
|
|
44
|
+
function defineNotification(def) {
|
|
45
|
+
const make = (restrict) => {
|
|
46
|
+
if (Array.isArray(def.via)) {
|
|
47
|
+
const channels = restrict ? def.via.filter((c) => restrict.includes(c)) : def.via;
|
|
48
|
+
for (const channel of channels) if (!def[channel]) throw new MissingRendererError(def.type, channel);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
send(to, data, options) {
|
|
52
|
+
return dispatchAll(def, to, data, options ?? {}, "send", restrict);
|
|
53
|
+
},
|
|
54
|
+
queue(to, data, options) {
|
|
55
|
+
return dispatchAll(def, to, data, options ?? {}, "queue", restrict);
|
|
56
|
+
},
|
|
57
|
+
only(...channels) {
|
|
58
|
+
return make(channels);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
return make();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fan-out to all recipients. Each recipient dispatches independently; one
|
|
66
|
+
* recipient's transport failure never aborts the others. CONFIG errors,
|
|
67
|
+
* however, propagate — they reject the returned promise.
|
|
68
|
+
*/
|
|
69
|
+
async function dispatchAll(def, to, data, options, mode, restrict) {
|
|
70
|
+
const recipients = Array.isArray(to) ? to : [to];
|
|
71
|
+
rethrowConfigErrors(await Promise.allSettled(recipients.map((recipient) => dispatchToRecipient(def, recipient, data, options, mode, restrict))));
|
|
72
|
+
}
|
|
73
|
+
async function dispatchToRecipient(def, to, data, options, mode, restrict) {
|
|
74
|
+
const config = getNotificationConfig();
|
|
75
|
+
let channels;
|
|
76
|
+
try {
|
|
77
|
+
channels = typeof def.via === "function" ? def.via(data, to) : [...def.via];
|
|
78
|
+
if (restrict) channels = channels.filter((c) => restrict.includes(c));
|
|
79
|
+
if (!options.force && config.preferences) {
|
|
80
|
+
const allowed = await config.preferences.resolveChannels(to, def.type, channels);
|
|
81
|
+
for (const channel of channels.filter((c) => !allowed.includes(c))) await emit("skipped", {
|
|
82
|
+
dispatchId: newDispatchId(),
|
|
83
|
+
channel,
|
|
84
|
+
notifiable: to,
|
|
85
|
+
reason: "preference",
|
|
86
|
+
options
|
|
87
|
+
});
|
|
88
|
+
channels = channels.filter((c) => allowed.includes(c));
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
log.error("notifications", `resolve.${def.type}`, error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const renderCtx = {
|
|
95
|
+
locale: options.locale,
|
|
96
|
+
meta: options.meta
|
|
97
|
+
};
|
|
98
|
+
rethrowConfigErrors(await Promise.allSettled(channels.map((name) => {
|
|
99
|
+
const renderer = def[name];
|
|
100
|
+
if (!renderer) return Promise.reject(new MissingRendererError(def.type, name));
|
|
101
|
+
let payload = renderer(data, to, renderCtx);
|
|
102
|
+
if (name === "database" && payload && typeof payload === "object" && !("type" in payload)) payload = {
|
|
103
|
+
...payload,
|
|
104
|
+
type: def.type
|
|
105
|
+
};
|
|
106
|
+
return dispatchChannel({
|
|
107
|
+
channelName: name,
|
|
108
|
+
payload,
|
|
109
|
+
to,
|
|
110
|
+
notificationType: def.type,
|
|
111
|
+
mode,
|
|
112
|
+
options
|
|
113
|
+
});
|
|
114
|
+
})));
|
|
115
|
+
}
|
|
116
|
+
/** Collect config-class rejections and rethrow (single or AggregateError). */
|
|
117
|
+
function rethrowConfigErrors(results) {
|
|
118
|
+
const configErrors = results.filter((r) => r.status === "rejected" && isConfigError(r.reason)).map((r) => r.reason);
|
|
119
|
+
if (configErrors.length === 1) throw configErrors[0];
|
|
120
|
+
if (configErrors.length > 1) throw new AggregateError(configErrors, "Multiple notification configuration errors");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
export { defineNotification };
|
|
125
|
+
//# sourceMappingURL=define-notification.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-notification.mjs","names":[],"sources":["../../../../../../../@warlock.js/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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/notifications/src/dispatch/dispatch-id.ts
|
|
4
|
+
/**
|
|
5
|
+
* A unique id for one (channel, recipient) dispatch. `sending` and its terminal
|
|
6
|
+
* `sent` / `failed` event share it so observers can pair them — for tracing
|
|
7
|
+
* spans, latency, or spotting a `sending` with no terminal (a hung send).
|
|
8
|
+
*/
|
|
9
|
+
function newDispatchId() {
|
|
10
|
+
return randomUUID();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { newDispatchId };
|
|
15
|
+
//# sourceMappingURL=dispatch-id.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-id.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/dispatch-id.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\n/**\n * A unique id for one (channel, recipient) dispatch. `sending` and its terminal\n * `sent` / `failed` event share it so observers can pair them — for tracing\n * spans, latency, or spotting a `sending` with no terminal (a hung send).\n */\nexport function newDispatchId(): string {\n return randomUUID();\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,gBAAwB;CACtC,OAAO,WAAW;AACpB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ChannelNotFoundError, NoQueueDispatcherError, UnresolvableRouteError } from "../errors.mjs";
|
|
2
|
+
import { getNotificationConfig } from "../config.mjs";
|
|
3
|
+
import { newDispatchId } from "./dispatch-id.mjs";
|
|
4
|
+
import { emit } from "./notifications-event-bus.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../@warlock.js/notifications/src/dispatch/dispatcher.ts
|
|
7
|
+
/**
|
|
8
|
+
* Internal dispatch core — shared by `defineNotification` and `notify.<channel>`.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* 1. Look up the channel in the configured registry (throws if missing).
|
|
12
|
+
* 2. Run the rate-limit gate (if a notification type is known) — drop with a
|
|
13
|
+
* `skipped` event when the limiter refuses.
|
|
14
|
+
* 3. Resolve the route: raw string > channel.route(notifiable) > { id }.
|
|
15
|
+
* 4. Inject the per-send `idempotencyKey` into the database payload.
|
|
16
|
+
* 5. Dispatch (sync via channel.send OR async via queue.dispatch).
|
|
17
|
+
* 6. Emit `sent` on success, `failed` on throw.
|
|
18
|
+
*
|
|
19
|
+
* Per-channel failure isolation is the CALLER's job (it wraps a list of
|
|
20
|
+
* `dispatchChannel` calls in `Promise.allSettled`).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Dispatch ONE rendered payload to ONE recipient through ONE channel.
|
|
24
|
+
* Does NOT consult `PreferenceProvider` — that gate runs at the caller level.
|
|
25
|
+
*
|
|
26
|
+
* Throws `ChannelNotFoundError` (channel not registered) and
|
|
27
|
+
* `UnresolvableRouteError` (channel's `route()` returned undefined) — both are
|
|
28
|
+
* configuration/data errors that should surface loudly. Transport failures
|
|
29
|
+
* (channel.send throwing) are emitted as `failed` AND rethrown.
|
|
30
|
+
*/
|
|
31
|
+
async function dispatchChannel(args) {
|
|
32
|
+
const config = getNotificationConfig();
|
|
33
|
+
const channel = config.channels[args.channelName];
|
|
34
|
+
if (!channel) throw new ChannelNotFoundError(args.channelName);
|
|
35
|
+
const dispatchId = newDispatchId();
|
|
36
|
+
if (args.to && args.notificationType && config.rateLimit) {
|
|
37
|
+
if (!await config.rateLimit.allow(args.to, args.channelName, args.notificationType)) {
|
|
38
|
+
await emit("skipped", {
|
|
39
|
+
dispatchId,
|
|
40
|
+
channel: args.channelName,
|
|
41
|
+
notifiable: args.to,
|
|
42
|
+
reason: "rate-limit",
|
|
43
|
+
options: args.options
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const route = resolveRoute(args, channel.route);
|
|
49
|
+
let payload = args.payload;
|
|
50
|
+
if (args.channelName === "database" && args.options.idempotencyKey && payload && typeof payload === "object") payload = {
|
|
51
|
+
...payload,
|
|
52
|
+
idempotencyKey: args.options.idempotencyKey
|
|
53
|
+
};
|
|
54
|
+
await emit("sending", {
|
|
55
|
+
dispatchId,
|
|
56
|
+
channel: args.channelName,
|
|
57
|
+
notifiable: args.to,
|
|
58
|
+
payload,
|
|
59
|
+
options: args.options
|
|
60
|
+
});
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
if (args.mode === "queue") {
|
|
64
|
+
if (!config.queue) throw new NoQueueDispatcherError(args.channelName);
|
|
65
|
+
await config.queue.dispatch({
|
|
66
|
+
channel: args.channelName,
|
|
67
|
+
route,
|
|
68
|
+
payload,
|
|
69
|
+
options: args.options
|
|
70
|
+
});
|
|
71
|
+
} else await channel.send({
|
|
72
|
+
payload,
|
|
73
|
+
route,
|
|
74
|
+
notifiable: args.to,
|
|
75
|
+
options: args.options
|
|
76
|
+
});
|
|
77
|
+
await emit("sent", {
|
|
78
|
+
dispatchId,
|
|
79
|
+
channel: args.channelName,
|
|
80
|
+
notifiable: args.to,
|
|
81
|
+
payload,
|
|
82
|
+
options: args.options,
|
|
83
|
+
durationMs: Date.now() - startedAt
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await emit("failed", {
|
|
87
|
+
dispatchId,
|
|
88
|
+
channel: args.channelName,
|
|
89
|
+
notifiable: args.to,
|
|
90
|
+
payload,
|
|
91
|
+
error,
|
|
92
|
+
options: args.options,
|
|
93
|
+
durationMs: Date.now() - startedAt
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Route resolution priority:
|
|
100
|
+
* 1. raw route string (ad-hoc `notify.mail("x@y.com", …)`)
|
|
101
|
+
* 2. the channel's `route(notifiable)` resolver
|
|
102
|
+
* 3. `{ id: notifiable.id }` — ONLY when the channel declares no resolver
|
|
103
|
+
*
|
|
104
|
+
* When a channel HAS a resolver but it returns undefined (e.g. a mail
|
|
105
|
+
* recipient with no email), we throw `UnresolvableRouteError` rather than
|
|
106
|
+
* silently coercing to `{ id }` — that would hand a string-route channel an
|
|
107
|
+
* object and fail deep inside the transport.
|
|
108
|
+
*/
|
|
109
|
+
function resolveRoute(args, resolver) {
|
|
110
|
+
if (args.rawRoute !== void 0) return args.rawRoute;
|
|
111
|
+
if (!args.to) throw new UnresolvableRouteError(args.channelName);
|
|
112
|
+
if (!resolver) return { id: args.to.id };
|
|
113
|
+
const resolved = resolver(args.to);
|
|
114
|
+
if (resolved === void 0) throw new UnresolvableRouteError(args.channelName, args.to.id);
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { dispatchChannel };
|
|
120
|
+
//# sourceMappingURL=dispatcher.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/dispatcher.ts"],"sourcesContent":["/**\n * Internal dispatch core — shared by `defineNotification` and `notify.<channel>`.\n *\n * Responsibilities:\n * 1. Look up the channel in the configured registry (throws if missing).\n * 2. Run the rate-limit gate (if a notification type is known) — drop with a\n * `skipped` event when the limiter refuses.\n * 3. Resolve the route: raw string > channel.route(notifiable) > { id }.\n * 4. Inject the per-send `idempotencyKey` into the database payload.\n * 5. Dispatch (sync via channel.send OR async via queue.dispatch).\n * 6. Emit `sent` on success, `failed` on throw.\n *\n * Per-channel failure isolation is the CALLER's job (it wraps a list of\n * `dispatchChannel` calls in `Promise.allSettled`).\n */\nimport { getNotificationConfig } from \"../config\";\nimport { ChannelNotFoundError, NoQueueDispatcherError, UnresolvableRouteError } from \"../errors\";\nimport type { ChannelName, Id, Notifiable, SendOptions } from \"../types\";\nimport { newDispatchId } from \"./dispatch-id\";\nimport { emit } from \"./notifications-event-bus\";\n\nexport type DispatchMode = \"send\" | \"queue\";\n\nexport type DispatchChannelArgs = {\n channelName: string;\n payload: unknown;\n /** The recipient model — undefined when dispatching to a raw route string. */\n to: Notifiable | undefined;\n /** Raw route override (ad-hoc `notify.mail(\"x@y.com\", …)`). */\n rawRoute?: string;\n /** Notification type — drives the rate-limit gate. May be undefined. */\n notificationType: string | undefined;\n mode: DispatchMode;\n options: SendOptions;\n};\n\n/**\n * Dispatch ONE rendered payload to ONE recipient through ONE channel.\n * Does NOT consult `PreferenceProvider` — that gate runs at the caller level.\n *\n * Throws `ChannelNotFoundError` (channel not registered) and\n * `UnresolvableRouteError` (channel's `route()` returned undefined) — both are\n * configuration/data errors that should surface loudly. Transport failures\n * (channel.send throwing) are emitted as `failed` AND rethrown.\n */\nexport async function dispatchChannel(args: DispatchChannelArgs): Promise<void> {\n const config = getNotificationConfig();\n const channel = config.channels[args.channelName as ChannelName];\n\n if (!channel) {\n throw new ChannelNotFoundError(args.channelName);\n }\n\n // One id for this (channel, recipient) dispatch — `sending` and its terminal\n // `sent` / `failed` share it so observers can pair them.\n const dispatchId = newDispatchId();\n\n // Rate-limit gate. Only applies when we have a notifiable + a notification\n // type (otherwise there's nothing to rate-limit on). `force` does NOT\n // bypass — rate limits are a safety valve, not a UX preference.\n //\n // NOTE (Phase-2 seam): for mode \"queue\" the budget is consumed at ENQUEUE\n // time here, not at delivery. When the herald worker lands it should run\n // this gate at delivery instead; see RateLimiter docstring.\n if (args.to && args.notificationType && config.rateLimit) {\n const allowed = await config.rateLimit.allow(\n args.to,\n args.channelName as ChannelName,\n args.notificationType,\n );\n\n if (!allowed) {\n await emit(\"skipped\", {\n dispatchId,\n channel: args.channelName,\n notifiable: args.to,\n reason: \"rate-limit\",\n options: args.options,\n });\n return;\n }\n }\n\n const route = resolveRoute(args, channel.route);\n\n // Inject idempotency key into the database payload (so the repo can dedupe).\n let payload = args.payload;\n if (\n args.channelName === \"database\" &&\n args.options.idempotencyKey &&\n payload &&\n typeof payload === \"object\"\n ) {\n payload = { ...(payload as object), idempotencyKey: args.options.idempotencyKey };\n }\n\n // `sending` fires after the gates pass + the route resolves, BEFORE the\n // transport — awaited so it always precedes the send. Observers can't abort\n // the send, but a slow handler delays it; keep them fast.\n await emit(\"sending\", {\n dispatchId,\n channel: args.channelName,\n notifiable: args.to,\n payload,\n options: args.options,\n });\n\n const startedAt = Date.now();\n\n try {\n if (args.mode === \"queue\") {\n if (!config.queue) {\n throw new NoQueueDispatcherError(args.channelName);\n }\n await config.queue.dispatch({\n channel: args.channelName,\n route,\n payload,\n options: args.options,\n });\n } else {\n await channel.send({\n payload: payload as never,\n route,\n notifiable: args.to,\n options: args.options,\n });\n }\n\n await emit(\"sent\", {\n dispatchId,\n channel: args.channelName,\n notifiable: args.to,\n payload,\n options: args.options,\n durationMs: Date.now() - startedAt,\n });\n } catch (error) {\n await emit(\"failed\", {\n dispatchId,\n channel: args.channelName,\n notifiable: args.to,\n payload,\n error: error as Error,\n options: args.options,\n durationMs: Date.now() - startedAt,\n });\n throw error;\n }\n}\n\n/**\n * Route resolution priority:\n * 1. raw route string (ad-hoc `notify.mail(\"x@y.com\", …)`)\n * 2. the channel's `route(notifiable)` resolver\n * 3. `{ id: notifiable.id }` — ONLY when the channel declares no resolver\n *\n * When a channel HAS a resolver but it returns undefined (e.g. a mail\n * recipient with no email), we throw `UnresolvableRouteError` rather than\n * silently coercing to `{ id }` — that would hand a string-route channel an\n * object and fail deep inside the transport.\n */\nfunction resolveRoute(\n args: DispatchChannelArgs,\n resolver: ((notifiable: Notifiable) => string | { id: Id } | undefined) | undefined,\n): string | { id: Id } {\n if (args.rawRoute !== undefined) {\n return args.rawRoute;\n }\n\n if (!args.to) {\n throw new UnresolvableRouteError(args.channelName);\n }\n\n // No resolver declared → the channel addresses by id (database/internal).\n if (!resolver) {\n return { id: args.to.id };\n }\n\n const resolved = resolver(args.to);\n\n if (resolved === undefined) {\n throw new UnresolvableRouteError(args.channelName, args.to.id);\n }\n\n return resolved;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,eAAsB,gBAAgB,MAA0C;CAC9E,MAAM,SAAS,sBAAsB;CACrC,MAAM,UAAU,OAAO,SAAS,KAAK;CAErC,IAAI,CAAC,SACH,MAAM,IAAI,qBAAqB,KAAK,WAAW;CAKjD,MAAM,aAAa,cAAc;CASjC,IAAI,KAAK,MAAM,KAAK,oBAAoB,OAAO,WAO7C;MAAI,CAAC,MANiB,OAAO,UAAU,MACrC,KAAK,IACL,KAAK,aACL,KAAK,gBACP,GAEc;GACZ,MAAM,KAAK,WAAW;IACpB;IACA,SAAS,KAAK;IACd,YAAY,KAAK;IACjB,QAAQ;IACR,SAAS,KAAK;GAChB,CAAC;GACD;EACF;;CAGF,MAAM,QAAQ,aAAa,MAAM,QAAQ,KAAK;CAG9C,IAAI,UAAU,KAAK;CACnB,IACE,KAAK,gBAAgB,cACrB,KAAK,QAAQ,kBACb,WACA,OAAO,YAAY,UAEnB,UAAU;EAAE,GAAI;EAAoB,gBAAgB,KAAK,QAAQ;CAAe;CAMlF,MAAM,KAAK,WAAW;EACpB;EACA,SAAS,KAAK;EACd,YAAY,KAAK;EACjB;EACA,SAAS,KAAK;CAChB,CAAC;CAED,MAAM,YAAY,KAAK,IAAI;CAE3B,IAAI;EACF,IAAI,KAAK,SAAS,SAAS;GACzB,IAAI,CAAC,OAAO,OACV,MAAM,IAAI,uBAAuB,KAAK,WAAW;GAEnD,MAAM,OAAO,MAAM,SAAS;IAC1B,SAAS,KAAK;IACd;IACA;IACA,SAAS,KAAK;GAChB,CAAC;EACH,OACE,MAAM,QAAQ,KAAK;GACR;GACT;GACA,YAAY,KAAK;GACjB,SAAS,KAAK;EAChB,CAAC;EAGH,MAAM,KAAK,QAAQ;GACjB;GACA,SAAS,KAAK;GACd,YAAY,KAAK;GACjB;GACA,SAAS,KAAK;GACd,YAAY,KAAK,IAAI,IAAI;EAC3B,CAAC;CACH,SAAS,OAAO;EACd,MAAM,KAAK,UAAU;GACnB;GACA,SAAS,KAAK;GACd,YAAY,KAAK;GACjB;GACO;GACP,SAAS,KAAK;GACd,YAAY,KAAK,IAAI,IAAI;EAC3B,CAAC;EACD,MAAM;CACR;AACF;;;;;;;;;;;;AAaA,SAAS,aACP,MACA,UACqB;CACrB,IAAI,KAAK,aAAa,QACpB,OAAO,KAAK;CAGd,IAAI,CAAC,KAAK,IACR,MAAM,IAAI,uBAAuB,KAAK,WAAW;CAInD,IAAI,CAAC,UACH,OAAO,EAAE,IAAI,KAAK,GAAG,GAAG;CAG1B,MAAM,WAAW,SAAS,KAAK,EAAE;CAEjC,IAAI,aAAa,QACf,MAAM,IAAI,uBAAuB,KAAK,aAAa,KAAK,GAAG,EAAE;CAG/D,OAAO;AACT"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NotificationEvents } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/notifications/src/dispatch/notifications-event-bus.d.ts
|
|
4
|
+
type EventName = keyof NotificationEvents;
|
|
5
|
+
type Handler<E extends EventName> = (data: NotificationEvents[E]) => void | Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Public observability surface.
|
|
8
|
+
*
|
|
9
|
+
* `on(event, handler)` returns an unsubscribe function. `off` exists for
|
|
10
|
+
* symmetry with libraries that hold handler references for later removal.
|
|
11
|
+
*/
|
|
12
|
+
declare const notifications: {
|
|
13
|
+
on<E extends EventName>(event: E, handler: Handler<E>): () => void;
|
|
14
|
+
off<E extends EventName>(event: E, handler: Handler<E>): void;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { notifications };
|
|
18
|
+
//# sourceMappingURL=notifications-event-bus.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications-event-bus.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/notifications-event-bus.ts"],"mappings":";;;KAkBK,SAAA,SAAkB,kBAAkB;AAAA,KACpC,OAAA,WAAkB,SAAA,KAAc,IAAA,EAAM,kBAAA,CAAmB,CAAA,aAAc,OAAA;AAHzB;;;;AAEV;AAAA;AAFU,cAkBtC,aAAA;eACE,SAAA,EAAS,KAAA,EAAS,CAAA,EAAC,OAAA,EAAW,OAAA,CAAQ,CAAA;gBAMrC,SAAA,EAAS,KAAA,EAAS,CAAA,EAAC,OAAA,EAAW,OAAA,CAAQ,CAAA;AAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { log } from "@warlock.js/logger";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/notifications/src/dispatch/notifications-event-bus.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed event bus for notifications observability. Emits `sent`, `failed`,
|
|
6
|
+
* and `skipped` events per (channel, recipient) — fan-out emits N events.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally minimal — a `Map<event, Set<handler>>` keeps tests trivial
|
|
9
|
+
* and avoids pulling event-emitter machinery for what is fundamentally
|
|
10
|
+
* pub/sub. Handler exceptions are logged-and-swallowed so one bad listener
|
|
11
|
+
* cannot break the dispatcher.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* notifications.on("sent", ({ channel }) => metrics.inc(`notif.${channel}.sent`));
|
|
15
|
+
* notifications.on("skipped", ({ reason }) => metrics.inc(`notif.skipped.${reason}`));
|
|
16
|
+
* const off = notifications.on("failed", logErr);
|
|
17
|
+
* off(); // unsubscribe
|
|
18
|
+
*/
|
|
19
|
+
const handlers = {
|
|
20
|
+
sending: /* @__PURE__ */ new Set(),
|
|
21
|
+
sent: /* @__PURE__ */ new Set(),
|
|
22
|
+
failed: /* @__PURE__ */ new Set(),
|
|
23
|
+
skipped: /* @__PURE__ */ new Set()
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Public observability surface.
|
|
27
|
+
*
|
|
28
|
+
* `on(event, handler)` returns an unsubscribe function. `off` exists for
|
|
29
|
+
* symmetry with libraries that hold handler references for later removal.
|
|
30
|
+
*/
|
|
31
|
+
const notifications = {
|
|
32
|
+
on(event, handler) {
|
|
33
|
+
handlers[event].add(handler);
|
|
34
|
+
return () => {
|
|
35
|
+
handlers[event].delete(handler);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
off(event, handler) {
|
|
39
|
+
handlers[event].delete(handler);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Internal emit — used by the dispatcher. Not exported from the package.
|
|
44
|
+
* Handlers run in registration order; an awaited Promise.allSettled isolates
|
|
45
|
+
* each listener so one throw cannot block the others.
|
|
46
|
+
*/
|
|
47
|
+
async function emit(event, data) {
|
|
48
|
+
const set = handlers[event];
|
|
49
|
+
if (set.size === 0) return;
|
|
50
|
+
const results = await Promise.allSettled(Array.from(set).map(async (handler) => {
|
|
51
|
+
await handler(data);
|
|
52
|
+
}));
|
|
53
|
+
for (const result of results) if (result.status === "rejected") log.error("notifications", "event-handler", result.reason);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { emit, notifications };
|
|
58
|
+
//# sourceMappingURL=notifications-event-bus.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications-event-bus.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/notifications-event-bus.ts"],"sourcesContent":["/**\n * Typed event bus for notifications observability. Emits `sent`, `failed`,\n * and `skipped` events per (channel, recipient) — fan-out emits N events.\n *\n * Intentionally minimal — a `Map<event, Set<handler>>` keeps tests trivial\n * and avoids pulling event-emitter machinery for what is fundamentally\n * pub/sub. Handler exceptions are logged-and-swallowed so one bad listener\n * cannot break the dispatcher.\n *\n * @example\n * notifications.on(\"sent\", ({ channel }) => metrics.inc(`notif.${channel}.sent`));\n * notifications.on(\"skipped\", ({ reason }) => metrics.inc(`notif.skipped.${reason}`));\n * const off = notifications.on(\"failed\", logErr);\n * off(); // unsubscribe\n */\nimport { log } from \"@warlock.js/logger\";\nimport type { NotificationEvents } from \"../types\";\n\ntype EventName = keyof NotificationEvents;\ntype Handler<E extends EventName> = (data: NotificationEvents[E]) => void | Promise<void>;\n\nconst handlers: { [E in EventName]: Set<Handler<E>> } = {\n sending: new Set(),\n sent: new Set(),\n failed: new Set(),\n skipped: new Set(),\n};\n\n/**\n * Public observability surface.\n *\n * `on(event, handler)` returns an unsubscribe function. `off` exists for\n * symmetry with libraries that hold handler references for later removal.\n */\nexport const notifications = {\n on<E extends EventName>(event: E, handler: Handler<E>): () => void {\n handlers[event].add(handler as Handler<EventName>);\n return () => {\n handlers[event].delete(handler as Handler<EventName>);\n };\n },\n off<E extends EventName>(event: E, handler: Handler<E>): void {\n handlers[event].delete(handler as Handler<EventName>);\n },\n};\n\n/**\n * Internal emit — used by the dispatcher. Not exported from the package.\n * Handlers run in registration order; an awaited Promise.allSettled isolates\n * each listener so one throw cannot block the others.\n */\nexport async function emit<E extends EventName>(\n event: E,\n data: NotificationEvents[E],\n): Promise<void> {\n const set = handlers[event];\n if (set.size === 0) return;\n\n // The `async` wrapper turns SYNC handler throws into promise rejections so\n // `Promise.allSettled` can isolate them. Without it, a `throw` inside the\n // handler escapes the array map before allSettled gets a chance to catch.\n const results = await Promise.allSettled(\n Array.from(set).map(async (handler) => {\n await (handler as Handler<E>)(data);\n }),\n );\n\n for (const result of results) {\n if (result.status === \"rejected\") {\n log.error(\"notifications\", \"event-handler\", result.reason);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAM,WAAkD;CACtD,yBAAS,IAAI,IAAI;CACjB,sBAAM,IAAI,IAAI;CACd,wBAAQ,IAAI,IAAI;CAChB,yBAAS,IAAI,IAAI;AACnB;;;;;;;AAQA,MAAa,gBAAgB;CAC3B,GAAwB,OAAU,SAAiC;EACjE,SAAS,MAAM,CAAC,IAAI,OAA6B;EACjD,aAAa;GACX,SAAS,MAAM,CAAC,OAAO,OAA6B;EACtD;CACF;CACA,IAAyB,OAAU,SAA2B;EAC5D,SAAS,MAAM,CAAC,OAAO,OAA6B;CACtD;AACF;;;;;;AAOA,eAAsB,KACpB,OACA,MACe;CACf,MAAM,MAAM,SAAS;CACrB,IAAI,IAAI,SAAS,GAAG;CAKpB,MAAM,UAAU,MAAM,QAAQ,WAC5B,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,OAAO,YAAY;EACrC,MAAO,QAAuB,IAAI;CACpC,CAAC,CACH;CAEA,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,WAAW,YACpB,IAAI,MAAM,iBAAiB,iBAAiB,OAAO,MAAM;AAG/D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChannelName, Notifiable, NotificationChannels, SendOptions } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/notifications/src/dispatch/notify.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Public shape of `notify`. The mapped portion gives `notify.<channel>` per
|
|
6
|
+
* registered channel; `channel(name)` is the runtime escape.
|
|
7
|
+
*/
|
|
8
|
+
type Notify = {
|
|
9
|
+
channel(name: string): {
|
|
10
|
+
send(to: Notifiable | string, payload: unknown, options?: SendOptions): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
} & { [C in ChannelName]: (to: Notifiable | string, payload: NotificationChannels[C], options?: SendOptions) => Promise<void> };
|
|
13
|
+
/**
|
|
14
|
+
* Build the `notify` proxy. `channel(name)` returns a `{ send }` object;
|
|
15
|
+
* any other property access returns a one-shot dispatcher for that channel.
|
|
16
|
+
*/
|
|
17
|
+
declare const notify: Notify;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { Notify, notify };
|
|
20
|
+
//# sourceMappingURL=notify.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/notify.ts"],"mappings":";;;;;AA8BA;;KAAY,MAAA;EACV,OAAA,CAAQ,IAAA;IACN,IAAA,CACE,EAAA,EAAI,UAAA,WACJ,OAAA,WACA,OAAA,GAAU,WAAA,GACT,OAAA;EAAA;AAAA,YAGC,WAAA,IACJ,EAAA,EAAI,UAAA,WACJ,OAAA,EAAS,oBAAA,CAAqB,CAAA,GAC9B,OAAA,GAAU,WAAA,KACP,OAAA;;;;;cA0EM,MAAA,EAAQ,MAuBnB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getNotificationConfig } from "../config.mjs";
|
|
2
|
+
import { newDispatchId } from "./dispatch-id.mjs";
|
|
3
|
+
import { emit } from "./notifications-event-bus.mjs";
|
|
4
|
+
import { dispatchChannel } from "./dispatcher.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../@warlock.js/notifications/src/dispatch/notify.ts
|
|
7
|
+
/**
|
|
8
|
+
* `notify` — ad-hoc, per-channel send facade.
|
|
9
|
+
*
|
|
10
|
+
* A Proxy keyed by the channel registry. `notify.<channel>(to, payload,
|
|
11
|
+
* options?)` works for ANY channel name registered in
|
|
12
|
+
* `NotificationChannels` — built-in or custom (via declaration merging).
|
|
13
|
+
*
|
|
14
|
+
* For multi-channel sends, use `defineNotification` — that's the reusable
|
|
15
|
+
* pattern and the only mental model for "send through several channels at
|
|
16
|
+
* once" (no inline multi-channel overload here on purpose).
|
|
17
|
+
*
|
|
18
|
+
* `notify.channel(name)` is the runtime escape when the channel name isn't a
|
|
19
|
+
* compile-time literal.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* await notify.mail(user, { subject: "Welcome", html: "<p>Hi!</p>" });
|
|
23
|
+
* await notify.database(user, { type: "welcome", title: "Welcome!" });
|
|
24
|
+
* await notify.mail("guest@example.com", { subject: "…", html: "…" }); // raw target
|
|
25
|
+
* await notify.channel(dynamicName).send(user, payload); // dynamic name
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Per-call dispatch for `notify.<channel>`. Mirrors `defineNotification`'s
|
|
29
|
+
* pipeline but without rendering: payload is already concrete.
|
|
30
|
+
*
|
|
31
|
+
* Preferences gate runs ONLY when a notification `type` is known — explicit
|
|
32
|
+
* via `options.type`, or implicit from a database payload's `type` field.
|
|
33
|
+
* Without a type we have nothing to gate on, and ad-hoc sends bypass
|
|
34
|
+
* preferences silently (use `defineNotification` if you want guaranteed
|
|
35
|
+
* gating).
|
|
36
|
+
*/
|
|
37
|
+
async function notifyImpl(channelName, to, payload, options = {}) {
|
|
38
|
+
const isRawTarget = typeof to === "string";
|
|
39
|
+
const notifiable = isRawTarget ? void 0 : to;
|
|
40
|
+
let notificationType = options.type;
|
|
41
|
+
if (notificationType === void 0 && payload && typeof payload === "object" && "type" in payload && typeof payload.type === "string") notificationType = payload.type;
|
|
42
|
+
if (notifiable && notificationType && !options.force) {
|
|
43
|
+
const config = getNotificationConfig();
|
|
44
|
+
if (config.preferences) {
|
|
45
|
+
if (!(await config.preferences.resolveChannels(notifiable, notificationType, [channelName])).includes(channelName)) {
|
|
46
|
+
await emit("skipped", {
|
|
47
|
+
dispatchId: newDispatchId(),
|
|
48
|
+
channel: channelName,
|
|
49
|
+
notifiable,
|
|
50
|
+
reason: "preference",
|
|
51
|
+
options
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await dispatchChannel({
|
|
58
|
+
channelName,
|
|
59
|
+
payload,
|
|
60
|
+
to: notifiable,
|
|
61
|
+
rawRoute: isRawTarget ? to : void 0,
|
|
62
|
+
notificationType,
|
|
63
|
+
mode: "send",
|
|
64
|
+
options
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the `notify` proxy. `channel(name)` returns a `{ send }` object;
|
|
69
|
+
* any other property access returns a one-shot dispatcher for that channel.
|
|
70
|
+
*/
|
|
71
|
+
const notify = new Proxy({}, { get(_target, prop) {
|
|
72
|
+
if (typeof prop === "symbol") return;
|
|
73
|
+
if (prop === "then" || prop === "catch" || prop === "finally" || prop === "toJSON") return;
|
|
74
|
+
if (prop === "channel") return (name) => ({ send: (to, payload, options) => notifyImpl(name, to, payload, options) });
|
|
75
|
+
return (to, payload, options) => notifyImpl(prop, to, payload, options);
|
|
76
|
+
} });
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { notify };
|
|
80
|
+
//# sourceMappingURL=notify.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/dispatch/notify.ts"],"sourcesContent":["/**\n * `notify` — ad-hoc, per-channel send facade.\n *\n * A Proxy keyed by the channel registry. `notify.<channel>(to, payload,\n * options?)` works for ANY channel name registered in\n * `NotificationChannels` — built-in or custom (via declaration merging).\n *\n * For multi-channel sends, use `defineNotification` — that's the reusable\n * pattern and the only mental model for \"send through several channels at\n * once\" (no inline multi-channel overload here on purpose).\n *\n * `notify.channel(name)` is the runtime escape when the channel name isn't a\n * compile-time literal.\n *\n * @example\n * await notify.mail(user, { subject: \"Welcome\", html: \"<p>Hi!</p>\" });\n * await notify.database(user, { type: \"welcome\", title: \"Welcome!\" });\n * await notify.mail(\"guest@example.com\", { subject: \"…\", html: \"…\" }); // raw target\n * await notify.channel(dynamicName).send(user, payload); // dynamic name\n */\nimport { getNotificationConfig } from \"../config\";\nimport type { ChannelName, Notifiable, NotificationChannels, SendOptions } from \"../types\";\nimport { newDispatchId } from \"./dispatch-id\";\nimport { dispatchChannel } from \"./dispatcher\";\nimport { emit } from \"./notifications-event-bus\";\n\n/**\n * Public shape of `notify`. The mapped portion gives `notify.<channel>` per\n * registered channel; `channel(name)` is the runtime escape.\n */\nexport type Notify = {\n channel(name: string): {\n send(\n to: Notifiable | string,\n payload: unknown,\n options?: SendOptions,\n ): Promise<void>;\n };\n} & {\n [C in ChannelName]: (\n to: Notifiable | string,\n payload: NotificationChannels[C],\n options?: SendOptions,\n ) => Promise<void>;\n};\n\n/**\n * Per-call dispatch for `notify.<channel>`. Mirrors `defineNotification`'s\n * pipeline but without rendering: payload is already concrete.\n *\n * Preferences gate runs ONLY when a notification `type` is known — explicit\n * via `options.type`, or implicit from a database payload's `type` field.\n * Without a type we have nothing to gate on, and ad-hoc sends bypass\n * preferences silently (use `defineNotification` if you want guaranteed\n * gating).\n */\nasync function notifyImpl(\n channelName: string,\n to: Notifiable | string,\n payload: unknown,\n options: SendOptions = {},\n): Promise<void> {\n const isRawTarget = typeof to === \"string\";\n const notifiable: Notifiable | undefined = isRawTarget ? undefined : to;\n\n // Derive the type: explicit `options.type` wins, then the payload's\n // `type` field (database channel convention).\n let notificationType: string | undefined = options.type;\n if (\n notificationType === undefined &&\n payload &&\n typeof payload === \"object\" &&\n \"type\" in payload &&\n typeof (payload as { type: unknown }).type === \"string\"\n ) {\n notificationType = (payload as { type: string }).type;\n }\n\n // Preferences gate — only when notifiable + type are both available.\n if (notifiable && notificationType && !options.force) {\n const config = getNotificationConfig();\n\n if (config.preferences) {\n const allowed = await config.preferences.resolveChannels(\n notifiable,\n notificationType,\n [channelName as ChannelName],\n );\n\n if (!allowed.includes(channelName as ChannelName)) {\n await emit(\"skipped\", {\n dispatchId: newDispatchId(),\n channel: channelName,\n notifiable,\n reason: \"preference\",\n options,\n });\n return;\n }\n }\n }\n\n await dispatchChannel({\n channelName,\n payload,\n to: notifiable,\n rawRoute: isRawTarget ? to : undefined,\n notificationType,\n mode: \"send\",\n options,\n });\n}\n\n/**\n * Build the `notify` proxy. `channel(name)` returns a `{ send }` object;\n * any other property access returns a one-shot dispatcher for that channel.\n */\nexport const notify: Notify = new Proxy({} as Notify, {\n get(_target, prop) {\n if (typeof prop === \"symbol\") {\n return undefined;\n }\n\n // Don't masquerade as a thenable — awaiting or Promise-wrapping `notify`\n // must NOT invoke notify.then(...) as a channel send. Also keep common\n // serialization probes from being treated as channel names.\n if (prop === \"then\" || prop === \"catch\" || prop === \"finally\" || prop === \"toJSON\") {\n return undefined;\n }\n\n if (prop === \"channel\") {\n return (name: string) => ({\n send: (to: Notifiable | string, payload: unknown, options?: SendOptions) =>\n notifyImpl(name, to, payload, options),\n });\n }\n\n return (to: Notifiable | string, payload: unknown, options?: SendOptions) =>\n notifyImpl(prop, to, payload, options);\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,eAAe,WACb,aACA,IACA,SACA,UAAuB,CAAC,GACT;CACf,MAAM,cAAc,OAAO,OAAO;CAClC,MAAM,aAAqC,cAAc,SAAY;CAIrE,IAAI,mBAAuC,QAAQ;CACnD,IACE,qBAAqB,UACrB,WACA,OAAO,YAAY,YACnB,UAAU,WACV,OAAQ,QAA8B,SAAS,UAE/C,mBAAoB,QAA6B;CAInD,IAAI,cAAc,oBAAoB,CAAC,QAAQ,OAAO;EACpD,MAAM,SAAS,sBAAsB;EAErC,IAAI,OAAO,aAOT;OAAI,EAAC,MANiB,OAAO,YAAY,gBACvC,YACA,kBACA,CAAC,WAA0B,CAC7B,EAEY,CAAC,SAAS,WAA0B,GAAG;IACjD,MAAM,KAAK,WAAW;KACpB,YAAY,cAAc;KAC1B,SAAS;KACT;KACA,QAAQ;KACR;IACF,CAAC;IACD;GACF;;CAEJ;CAEA,MAAM,gBAAgB;EACpB;EACA;EACA,IAAI;EACJ,UAAU,cAAc,KAAK;EAC7B;EACA,MAAM;EACN;CACF,CAAC;AACH;;;;;AAMA,MAAa,SAAiB,IAAI,MAAM,CAAC,GAAa,EACpD,IAAI,SAAS,MAAM;CACjB,IAAI,OAAO,SAAS,UAClB;CAMF,IAAI,SAAS,UAAU,SAAS,WAAW,SAAS,aAAa,SAAS,UACxE;CAGF,IAAI,SAAS,WACX,QAAQ,UAAkB,EACxB,OAAO,IAAyB,SAAkB,YAChD,WAAW,MAAM,IAAI,SAAS,OAAO,EACzC;CAGF,QAAQ,IAAyB,SAAkB,YACjD,WAAW,MAAM,IAAI,SAAS,OAAO;AACzC,EACF,CAAC"}
|
package/esm/errors.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region ../@warlock.js/notifications/src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Domain errors for `@warlock.js/notifications`. All extend `Error` directly
|
|
4
|
+
* (no framework HttpError) so the package stays runtime-agnostic — the host
|
|
5
|
+
* app maps them to its own error shape via `instanceof` if needed.
|
|
6
|
+
*
|
|
7
|
+
* Each captures a clean V8 stack (drops the Error-constructor frame) to match
|
|
8
|
+
* the cascade convention.
|
|
9
|
+
*/
|
|
10
|
+
declare class NotificationsNotConfiguredError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
declare class ChannelNotFoundError extends Error {
|
|
14
|
+
constructor(channelName: string);
|
|
15
|
+
}
|
|
16
|
+
declare class NoQueueDispatcherError extends Error {
|
|
17
|
+
constructor(channelName: string);
|
|
18
|
+
}
|
|
19
|
+
declare class MissingRendererError extends Error {
|
|
20
|
+
constructor(notificationType: string, channelName: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when a channel's `route()` resolver runs but returns `undefined` for
|
|
24
|
+
* a recipient (e.g. a mail recipient with no email). Distinct from "the
|
|
25
|
+
* channel declares no resolver" — that case falls back to `{ id }`.
|
|
26
|
+
*/
|
|
27
|
+
declare class UnresolvableRouteError extends Error {
|
|
28
|
+
constructor(channelName: string, recipientId?: unknown);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, UnresolvableRouteError };
|
|
32
|
+
//# sourceMappingURL=errors.d.mts.map
|