@warlock.js/notifications 5.24.0 → 5.26.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 +12 -0
- package/cjs/index.cjs +1 -1
- package/cjs/rabbitmq-driver-DvWaCPES.cjs +3 -0
- package/cjs/{src-DbAWktJm.cjs → src-BVlqtCR2.cjs} +22 -6
- package/cjs/src-BVlqtCR2.cjs.map +1 -0
- package/esm/herald/src/communicators/broker-registry.mjs.map +1 -1
- package/esm/herald/src/communicators/broker.mjs.map +1 -1
- package/esm/herald/src/drivers/rabbitmq/rabbitmq-channel.mjs.map +1 -1
- package/esm/herald/src/drivers/rabbitmq/rabbitmq-driver.mjs.map +1 -1
- package/esm/herald/src/message-managers/prepare-consumer-subscription.mjs.map +1 -1
- package/esm/herald/src/utils/connect-to-broker.mjs.map +1 -1
- package/esm/seal/src/config.mjs.map +1 -1
- package/esm/seal/src/factory/validate.mjs.map +1 -1
- package/esm/seal/src/factory/validators.mjs +1 -1
- package/esm/seal/src/factory/validators.mjs.map +1 -1
- package/esm/seal/src/helpers/date-helpers.mjs.map +1 -1
- package/esm/seal/src/helpers/get-field-value.mjs.map +1 -1
- package/esm/seal/src/helpers/path-helpers.mjs.map +1 -1
- package/esm/seal/src/mutators/array-mutators.mjs.map +1 -1
- package/esm/seal/src/mutators/date-mutators.mjs.map +1 -1
- package/esm/seal/src/mutators/number-mutators.mjs.map +1 -1
- package/esm/seal/src/mutators/object-mutators.mjs.map +1 -1
- package/esm/seal/src/mutators/scalar-mutators.mjs.map +1 -1
- package/esm/seal/src/mutators/string-mutators.mjs.map +1 -1
- package/esm/seal/src/rules/array/array-rules.mjs.map +1 -1
- package/esm/seal/src/rules/color/color-rules.mjs.map +1 -1
- package/esm/seal/src/rules/common/enum.mjs.map +1 -1
- package/esm/seal/src/rules/common/equals-field-rules.mjs.map +1 -1
- package/esm/seal/src/rules/common/type-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/forbidden-if-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/present-if-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/present-unless-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/present-with-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/present-without-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/required-if-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/required-unless-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/required-with-rules.mjs.map +1 -1
- package/esm/seal/src/rules/conditional/required-without-rules.mjs.map +1 -1
- package/esm/seal/src/rules/core/equal.mjs.map +1 -1
- package/esm/seal/src/rules/core/forbidden.mjs.map +1 -1
- package/esm/seal/src/rules/core/required.mjs.map +1 -1
- package/esm/seal/src/rules/core/union.mjs.map +1 -1
- package/esm/seal/src/rules/core/when.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-comparison-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-day-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-field-comparison-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-period-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-relative-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date-special-rules.mjs.map +1 -1
- package/esm/seal/src/rules/date/date.mjs.map +1 -1
- package/esm/seal/src/rules/length/length-rules.mjs.map +1 -1
- package/esm/seal/src/rules/number/number-rules.mjs.map +1 -1
- package/esm/seal/src/rules/scalar/accepted-rule.mjs.map +1 -1
- package/esm/seal/src/rules/scalar/declined-rule.mjs.map +1 -1
- package/esm/seal/src/rules/string/alpha.mjs.map +1 -1
- package/esm/seal/src/rules/string/credit-card.mjs.map +1 -1
- package/esm/seal/src/rules/string/email.mjs.map +1 -1
- package/esm/seal/src/rules/string/pattern.mjs.map +1 -1
- package/esm/seal/src/rules/string/string-comparison.mjs.map +1 -1
- package/esm/seal/src/rules/string/strong-password-rule.mjs.map +1 -1
- package/esm/seal/src/rules/string/url.mjs.map +1 -1
- package/esm/seal/src/rules/string/without-whitespace.mjs.map +1 -1
- package/esm/seal/src/types/date-types.mjs.map +1 -1
- package/esm/seal/src/validators/any-validator.mjs.map +1 -1
- package/esm/seal/src/validators/array-validator.mjs +1 -0
- package/esm/seal/src/validators/array-validator.mjs.map +1 -1
- package/esm/seal/src/validators/base-validator.mjs +12 -1
- package/esm/seal/src/validators/base-validator.mjs.map +1 -1
- package/esm/seal/src/validators/boolean-validator.mjs +1 -0
- package/esm/seal/src/validators/boolean-validator.mjs.map +1 -1
- package/esm/seal/src/validators/date-validator.mjs +1 -0
- package/esm/seal/src/validators/date-validator.mjs.map +1 -1
- package/esm/seal/src/validators/float-validator.mjs.map +1 -1
- package/esm/seal/src/validators/int-validator.mjs.map +1 -1
- package/esm/seal/src/validators/literal-validator.mjs +1 -3
- package/esm/seal/src/validators/literal-validator.mjs.map +1 -1
- package/esm/seal/src/validators/methods/equality-conditional-methods.mjs.map +1 -1
- package/esm/seal/src/validators/methods/forbidden-methods.mjs.map +1 -1
- package/esm/seal/src/validators/methods/present-methods.mjs.map +1 -1
- package/esm/seal/src/validators/methods/required-methods.mjs.map +1 -1
- package/esm/seal/src/validators/number-validator.mjs +1 -0
- package/esm/seal/src/validators/number-validator.mjs.map +1 -1
- package/esm/seal/src/validators/numeric-validator.mjs.map +1 -1
- package/esm/seal/src/validators/object-validator.mjs +1 -1
- package/esm/seal/src/validators/object-validator.mjs.map +1 -1
- package/esm/seal/src/validators/record-validator.mjs.map +1 -1
- package/esm/seal/src/validators/scalar-validator.mjs +1 -0
- package/esm/seal/src/validators/scalar-validator.mjs.map +1 -1
- package/esm/seal/src/validators/string-validator.mjs.map +1 -1
- package/esm/seal/src/validators/tuple-validator.mjs.map +1 -1
- package/esm/seal/src/validators/union-validator.mjs.map +1 -1
- package/llms-full.txt +901 -891
- package/package.json +7 -7
- package/skills/configure-notifications/SKILL.md +138 -138
- package/skills/define-channel/SKILL.md +107 -107
- package/skills/define-notification/SKILL.md +133 -133
- package/skills/observe-notifications/SKILL.md +66 -66
- package/skills/queue-notifications/SKILL.md +110 -110
- package/skills/send-ad-hoc/SKILL.md +95 -95
- package/skills/use-in-app/SKILL.md +172 -172
- package/skills/write-notification-migration/SKILL.md +66 -66
- package/cjs/rabbitmq-driver-C0o3Pz15.cjs +0 -3
- package/cjs/src-DbAWktJm.cjs.map +0 -1
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: define-notification
|
|
3
|
-
description: 'Build a reusable multi-channel notification with `defineNotification<Data>({ type, via, ...renderers })`. `type` is the stable identifier preference/rate-limit gates use AND defaults the database channel''s `type`. `via` is `ChannelName[]` OR `(data, to) => ChannelName[]` for per-recipient channel selection. Renderers are `(data, to, ctx) => NotificationChannels[C]`; `ctx` carries `locale` + `meta` from `SendOptions`. The returned object exposes `.send(to|to[], data, options?)`, `.queue(to|to[], data, options?)`, and `.only(...channels)`. Per-recipient + per-channel `Promise.allSettled` isolates failures; the database channel''s `type` is auto-defaulted from `def.type`; `SendOptions.idempotencyKey` is injected into the database payload. Triggers: `defineNotification`, `via`, "reusable notification", "multi-channel notification", "send through several channels at once", "render per channel", `RenderContext`, `def.type`; "force on a notification", "queue with delay"; typical import `import { defineNotification } from "@warlock.js/notifications"`. Skip: single-channel ad-hoc sends — `@warlock.js/notifications/send-ad-hoc/SKILL.md`; adding a brand-new channel — `@warlock.js/notifications/define-channel/SKILL.md`; observing dispatch outcomes — `@warlock.js/notifications/observe-notifications/SKILL.md`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# `defineNotification` — reusable multi-channel notification
|
|
7
|
-
|
|
8
|
-
The reusable pattern. Define a notification once with `type` + `via` + a renderer per channel; fire it anywhere with `.send` / `.queue` / `.only`.
|
|
9
|
-
|
|
10
|
-
## Shape
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
defineNotification<Data>({
|
|
14
|
-
type: string, // REQUIRED — gate key + database default
|
|
15
|
-
via: ChannelName[] | (data, to) => ChannelName[], // static array OR per-recipient callback
|
|
16
|
-
mail?: (data, to, ctx) => MailPayload,
|
|
17
|
-
database?: (data, to, ctx) => Omit<DatabasePayload, "type">, // `type` defaulted from `def.type`
|
|
18
|
-
// ...one renderer per channel listed in `via`
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Returns `{ send, queue, only }`:
|
|
23
|
-
|
|
24
|
-
| Method | Signature | What it does |
|
|
25
|
-
|---|---|---|
|
|
26
|
-
| `.send` | `(to, data, options?)` | Render + dispatch synchronously. |
|
|
27
|
-
| `.queue` | `(to, data, options?)` | Render + enqueue. Throws if no queue dispatcher is configured. |
|
|
28
|
-
| `.only(...channels)` | returns `{send, queue, only}` | Restrict dispatch to a subset of channels. |
|
|
29
|
-
|
|
30
|
-
`to` accepts a `Notifiable` OR a `Notifiable[]`. Fan-out emits one render+dispatch per recipient with per-recipient + per-channel `Promise.allSettled` (one failure does not abort the others).
|
|
31
|
-
|
|
32
|
-
## Static `via`
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
export const welcome = defineNotification<{ name: string }>({
|
|
36
|
-
type: "welcome",
|
|
37
|
-
via: ["mail"],
|
|
38
|
-
mail: ({ name }) => ({ subject: "Welcome", html: `<p>Hi ${name}</p>` }),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
await welcome.send(user, { name: "Hasan" });
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Dynamic `via` per recipient
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
export const orderShipped = defineNotification<{ order: Order }>({
|
|
48
|
-
type: "order.shipped",
|
|
49
|
-
via: (_data, to) =>
|
|
50
|
-
to.get("telegram_chat_id")
|
|
51
|
-
? ["database", "telegram"]
|
|
52
|
-
: ["database", "mail"],
|
|
53
|
-
database: ({ order }) => ({ title: `Order #${order.number} shipped` }),
|
|
54
|
-
mail: ({ order }, to) => ({
|
|
55
|
-
subject: `Order #${order.number} shipped`,
|
|
56
|
-
html: `<p>Hi ${to.get("name")}, on the way.</p>`,
|
|
57
|
-
}),
|
|
58
|
-
// telegram: ... (Phase 2 — bridges)
|
|
59
|
-
});
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## The renderer's 3rd arg — `RenderContext`
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
mail: ({ campaignId }, to, { locale = "en", meta }) => ({
|
|
66
|
-
subject: subjectsByLocale[locale],
|
|
67
|
-
html: htmlByLocale[locale],
|
|
68
|
-
}),
|
|
69
|
-
|
|
70
|
-
// usage:
|
|
71
|
-
await marketing.queue(user, payload, {
|
|
72
|
-
locale: user.get("locale"),
|
|
73
|
-
meta: { campaignId, source: "blast" },
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`ctx` carries `locale` + `meta` from `SendOptions`. `meta` flows through to every observability event too.
|
|
78
|
-
|
|
79
|
-
## Database channel — `type` is defaulted
|
|
80
|
-
|
|
81
|
-
The `database` renderer may OMIT `type` — the dispatcher injects `def.type` automatically. This keeps the notification type in ONE place.
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
defineNotification({
|
|
85
|
-
type: "order.shipped", // ← single source of truth
|
|
86
|
-
via: ["database"],
|
|
87
|
-
database: () => ({ title: "Shipped" }), // type omitted; dispatcher injects "order.shipped"
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Fan-out
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
await orderShipped.send([buyer, salesRep], { order });
|
|
95
|
-
// One render+dispatch per recipient. Database does N inserts (Phase 0);
|
|
96
|
-
// Phase 1 ships a single bulk insert via `Channel.sendMany?`.
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## `.only(...)` — restrict channels
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
await orderShipped.only("mail").send(user, { order });
|
|
103
|
-
// `via` is filtered to just "mail"; other channels are skipped.
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Queue (Phase 2)
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
await orderShipped.queue(user, { order }, { delay: "10m" });
|
|
110
|
-
// Throws NoQueueDispatcherError if no `queue` is configured in config/notifications.ts.
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## SendOptions
|
|
114
|
-
|
|
115
|
-
| Field | Effect |
|
|
116
|
-
|---|---|
|
|
117
|
-
| `delay` | Reserved — `"10m"` / `"3d"` / `600` (seconds). NOT honored yet: both `.send()` and the current `.queue()` worker dispatch immediately; delay-aware delivery is a follow-up. |
|
|
118
|
-
| `locale` | Passed to renderers via `RenderContext.locale`. |
|
|
119
|
-
| `meta` | Passed to renderers + included in every observability event. |
|
|
120
|
-
| `idempotencyKey` | Injected into the database payload (dedupe via unique index). |
|
|
121
|
-
| `force` | Bypass `PreferenceProvider`. Does NOT bypass `RateLimiter`. |
|
|
122
|
-
| `type` | Ignored by `defineNotification` — see `send-ad-hoc` for ad-hoc gating. |
|
|
123
|
-
|
|
124
|
-
## Failure isolation
|
|
125
|
-
|
|
126
|
-
Per-recipient: each recipient gets an independent `Promise.allSettled`. Per-channel: each channel within a recipient gets an independent `allSettled`. A throw becomes a `failed` event on the event bus AND re-throws within its own slot — never aborts siblings.
|
|
127
|
-
|
|
128
|
-
## See also
|
|
129
|
-
|
|
130
|
-
- [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — package front door.
|
|
131
|
-
- [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — `notify.<channel>` per-channel shorthand.
|
|
132
|
-
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — wire channels + preferences + rate-limit + queue.
|
|
133
|
-
- [`observe-notifications/SKILL.md`](../observe-notifications/SKILL.md) — `notifications.on("sending" | "sent" | "failed" | "skipped", …)`.
|
|
1
|
+
---
|
|
2
|
+
name: define-notification
|
|
3
|
+
description: 'Build a reusable multi-channel notification with `defineNotification<Data>({ type, via, ...renderers })`. `type` is the stable identifier preference/rate-limit gates use AND defaults the database channel''s `type`. `via` is `ChannelName[]` OR `(data, to) => ChannelName[]` for per-recipient channel selection. Renderers are `(data, to, ctx) => NotificationChannels[C]`; `ctx` carries `locale` + `meta` from `SendOptions`. The returned object exposes `.send(to|to[], data, options?)`, `.queue(to|to[], data, options?)`, and `.only(...channels)`. Per-recipient + per-channel `Promise.allSettled` isolates failures; the database channel''s `type` is auto-defaulted from `def.type`; `SendOptions.idempotencyKey` is injected into the database payload. Triggers: `defineNotification`, `via`, "reusable notification", "multi-channel notification", "send through several channels at once", "render per channel", `RenderContext`, `def.type`; "force on a notification", "queue with delay"; typical import `import { defineNotification } from "@warlock.js/notifications"`. Skip: single-channel ad-hoc sends — `@warlock.js/notifications/send-ad-hoc/SKILL.md`; adding a brand-new channel — `@warlock.js/notifications/define-channel/SKILL.md`; observing dispatch outcomes — `@warlock.js/notifications/observe-notifications/SKILL.md`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `defineNotification` — reusable multi-channel notification
|
|
7
|
+
|
|
8
|
+
The reusable pattern. Define a notification once with `type` + `via` + a renderer per channel; fire it anywhere with `.send` / `.queue` / `.only`.
|
|
9
|
+
|
|
10
|
+
## Shape
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
defineNotification<Data>({
|
|
14
|
+
type: string, // REQUIRED — gate key + database default
|
|
15
|
+
via: ChannelName[] | (data, to) => ChannelName[], // static array OR per-recipient callback
|
|
16
|
+
mail?: (data, to, ctx) => MailPayload,
|
|
17
|
+
database?: (data, to, ctx) => Omit<DatabasePayload, "type">, // `type` defaulted from `def.type`
|
|
18
|
+
// ...one renderer per channel listed in `via`
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Returns `{ send, queue, only }`:
|
|
23
|
+
|
|
24
|
+
| Method | Signature | What it does |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `.send` | `(to, data, options?)` | Render + dispatch synchronously. |
|
|
27
|
+
| `.queue` | `(to, data, options?)` | Render + enqueue. Throws if no queue dispatcher is configured. |
|
|
28
|
+
| `.only(...channels)` | returns `{send, queue, only}` | Restrict dispatch to a subset of channels. |
|
|
29
|
+
|
|
30
|
+
`to` accepts a `Notifiable` OR a `Notifiable[]`. Fan-out emits one render+dispatch per recipient with per-recipient + per-channel `Promise.allSettled` (one failure does not abort the others).
|
|
31
|
+
|
|
32
|
+
## Static `via`
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export const welcome = defineNotification<{ name: string }>({
|
|
36
|
+
type: "welcome",
|
|
37
|
+
via: ["mail"],
|
|
38
|
+
mail: ({ name }) => ({ subject: "Welcome", html: `<p>Hi ${name}</p>` }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await welcome.send(user, { name: "Hasan" });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Dynamic `via` per recipient
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export const orderShipped = defineNotification<{ order: Order }>({
|
|
48
|
+
type: "order.shipped",
|
|
49
|
+
via: (_data, to) =>
|
|
50
|
+
to.get("telegram_chat_id")
|
|
51
|
+
? ["database", "telegram"]
|
|
52
|
+
: ["database", "mail"],
|
|
53
|
+
database: ({ order }) => ({ title: `Order #${order.number} shipped` }),
|
|
54
|
+
mail: ({ order }, to) => ({
|
|
55
|
+
subject: `Order #${order.number} shipped`,
|
|
56
|
+
html: `<p>Hi ${to.get("name")}, on the way.</p>`,
|
|
57
|
+
}),
|
|
58
|
+
// telegram: ... (Phase 2 — bridges)
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The renderer's 3rd arg — `RenderContext`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
mail: ({ campaignId }, to, { locale = "en", meta }) => ({
|
|
66
|
+
subject: subjectsByLocale[locale],
|
|
67
|
+
html: htmlByLocale[locale],
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
// usage:
|
|
71
|
+
await marketing.queue(user, payload, {
|
|
72
|
+
locale: user.get("locale"),
|
|
73
|
+
meta: { campaignId, source: "blast" },
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`ctx` carries `locale` + `meta` from `SendOptions`. `meta` flows through to every observability event too.
|
|
78
|
+
|
|
79
|
+
## Database channel — `type` is defaulted
|
|
80
|
+
|
|
81
|
+
The `database` renderer may OMIT `type` — the dispatcher injects `def.type` automatically. This keeps the notification type in ONE place.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
defineNotification({
|
|
85
|
+
type: "order.shipped", // ← single source of truth
|
|
86
|
+
via: ["database"],
|
|
87
|
+
database: () => ({ title: "Shipped" }), // type omitted; dispatcher injects "order.shipped"
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Fan-out
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await orderShipped.send([buyer, salesRep], { order });
|
|
95
|
+
// One render+dispatch per recipient. Database does N inserts (Phase 0);
|
|
96
|
+
// Phase 1 ships a single bulk insert via `Channel.sendMany?`.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## `.only(...)` — restrict channels
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await orderShipped.only("mail").send(user, { order });
|
|
103
|
+
// `via` is filtered to just "mail"; other channels are skipped.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Queue (Phase 2)
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await orderShipped.queue(user, { order }, { delay: "10m" });
|
|
110
|
+
// Throws NoQueueDispatcherError if no `queue` is configured in config/notifications.ts.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## SendOptions
|
|
114
|
+
|
|
115
|
+
| Field | Effect |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `delay` | Reserved — `"10m"` / `"3d"` / `600` (seconds). NOT honored yet: both `.send()` and the current `.queue()` worker dispatch immediately; delay-aware delivery is a follow-up. |
|
|
118
|
+
| `locale` | Passed to renderers via `RenderContext.locale`. |
|
|
119
|
+
| `meta` | Passed to renderers + included in every observability event. |
|
|
120
|
+
| `idempotencyKey` | Injected into the database payload (dedupe via unique index). |
|
|
121
|
+
| `force` | Bypass `PreferenceProvider`. Does NOT bypass `RateLimiter`. |
|
|
122
|
+
| `type` | Ignored by `defineNotification` — see `send-ad-hoc` for ad-hoc gating. |
|
|
123
|
+
|
|
124
|
+
## Failure isolation
|
|
125
|
+
|
|
126
|
+
Per-recipient: each recipient gets an independent `Promise.allSettled`. Per-channel: each channel within a recipient gets an independent `allSettled`. A throw becomes a `failed` event on the event bus AND re-throws within its own slot — never aborts siblings.
|
|
127
|
+
|
|
128
|
+
## See also
|
|
129
|
+
|
|
130
|
+
- [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — package front door.
|
|
131
|
+
- [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — `notify.<channel>` per-channel shorthand.
|
|
132
|
+
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — wire channels + preferences + rate-limit + queue.
|
|
133
|
+
- [`observe-notifications/SKILL.md`](../observe-notifications/SKILL.md) — `notifications.on("sending" | "sent" | "failed" | "skipped", …)`.
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: observe-notifications
|
|
3
|
-
description: 'Wire metrics, logging, tracing, and audits via `notifications.on(event, handler)` — four events fire per (channel, recipient): `sending` (gates passed, about to hit the transport), `sent` (succeeded), `failed` (channel threw — carries the Error), `skipped` (a pre-send gate dropped it — carries `reason: "preference" | "rate-limit"`). Every event carries the recipient as `notifiable?` (undefined only for raw-target ad-hoc sends) and a `dispatchId` — `sending` and its terminal `sent`/`failed` share it, so observers can pair them (spans, latency, hung-send detection). `sent`/`failed` also carry `durationMs`. Handler exceptions are logged-and-swallowed so one bad listener can not break the dispatcher. `on(...)` returns an unsubscribe function; `off(event, handler)` exists for symmetry. Triggers: `notifications.on`, `notifications.off`, `"sending"` event, `"sent"` event, `"failed"` event, `"skipped"` event, `dispatchId`, `durationMs`, `NotificationEvents`, "notification metrics", "notification latency", "trace notifications", "notification audit log", "track notification drops"; typical import `import { notifications } from "@warlock.js/notifications"`. Skip: building notifications — `@warlock.js/notifications/define-notification/SKILL.md`; configuring the preference/rate-limit gates that produce `skipped` — `@warlock.js/notifications/configure-notifications/SKILL.md`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Observe notifications
|
|
7
|
-
|
|
8
|
-
Every dispatch emits a `sending` event then a terminal `sent` / `failed` — or a `skipped` if a gate dropped it — per (channel, recipient).
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { notifications } from "@warlock.js/notifications";
|
|
12
|
-
|
|
13
|
-
// latency + delivery, keyed by recipient
|
|
14
|
-
notifications.on("sent", ({ channel, notifiable, durationMs, options }) =>
|
|
15
|
-
metrics.timing(`notif.${channel}.sent`, durationMs, { userId: notifiable?.id, ...options.meta }));
|
|
16
|
-
|
|
17
|
-
notifications.on("failed", ({ channel, notifiable, error }) =>
|
|
18
|
-
log.error(`notif.${channel}`, error, { userId: notifiable?.id }));
|
|
19
|
-
|
|
20
|
-
notifications.on("skipped", ({ channel, reason }) =>
|
|
21
|
-
metrics.inc(`notif.${channel}.skipped.${reason}`));
|
|
22
|
-
|
|
23
|
-
// pair `sending` → terminal by dispatchId — open a span / arm a hung-send watchdog
|
|
24
|
-
notifications.on("sending", ({ dispatchId, channel, notifiable }) =>
|
|
25
|
-
tracer.open(dispatchId, { channel, userId: notifiable?.id }));
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Events
|
|
29
|
-
|
|
30
|
-
| Event | Fires when | Payload |
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| `sending` | Gates passed; about to hit the transport (sync) / enqueue (queue) | `{ dispatchId, channel, notifiable?, payload, options }` |
|
|
33
|
-
| `sent` | Channel dispatched OR job enqueued | `{ dispatchId, channel, notifiable?, payload, options, durationMs }` |
|
|
34
|
-
| `failed` | `channel.send` (or `queue.dispatch`) threw | `{ dispatchId, channel, notifiable?, payload, error, options, durationMs }` |
|
|
35
|
-
| `skipped` | A pre-send gate dropped this channel | `{ dispatchId, channel, notifiable?, reason, options }` — `reason: "preference" \| "rate-limit"` |
|
|
36
|
-
|
|
37
|
-
`notifiable` is the recipient **model** (undefined only for raw-target ad-hoc sends like `notify.mail("x@y.com", …)`) — read `notifiable?.id` / `notifiable?.get(...)` to key your metrics or logs.
|
|
38
|
-
|
|
39
|
-
`dispatchId` is unique per (channel, recipient) dispatch — `sending` and its terminal `sent`/`failed` share it, so you can pair them for a span, a latency measurement, or a watchdog that flags a `sending` with no terminal (a hung send). `durationMs` is transport time on a sync send, enqueue time on a queued one.
|
|
40
|
-
|
|
41
|
-
`options` is the `SendOptions` passed at the call site — including `meta`. Tagging a send with `meta: { campaignId }` lets downstream observers join the event back to the campaign.
|
|
42
|
-
|
|
43
|
-
## Unsubscribe
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
const off = notifications.on("sent", handler);
|
|
47
|
-
// later
|
|
48
|
-
off();
|
|
49
|
-
|
|
50
|
-
// or
|
|
51
|
-
notifications.off("sent", handler);
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Fan-out emits N events
|
|
55
|
-
|
|
56
|
-
`orderShipped.send([buyer, salesRep], { order })` with `via: ["mail", "database"]` fires up to **4** `sent` events (2 recipients × 2 channels). Aggregate by `meta` or by `channel + notifiable.id` in your observer.
|
|
57
|
-
|
|
58
|
-
## Handler isolation
|
|
59
|
-
|
|
60
|
-
A handler that throws is logged and swallowed — the other handlers for the same event still run, and the dispatcher never observes the throw. Observers can't **abort** a send (there's no veto here — use `preferences` / `rateLimit` for that). They can delay it, though: `sending` is awaited before the transport, so keep that handler fast.
|
|
61
|
-
|
|
62
|
-
## See also
|
|
63
|
-
|
|
64
|
-
- [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — front door + mental model.
|
|
65
|
-
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — `preferences` / `rateLimit` slots that emit `skipped`.
|
|
66
|
-
- [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — when `notifiable` is undefined in events.
|
|
1
|
+
---
|
|
2
|
+
name: observe-notifications
|
|
3
|
+
description: 'Wire metrics, logging, tracing, and audits via `notifications.on(event, handler)` — four events fire per (channel, recipient): `sending` (gates passed, about to hit the transport), `sent` (succeeded), `failed` (channel threw — carries the Error), `skipped` (a pre-send gate dropped it — carries `reason: "preference" | "rate-limit"`). Every event carries the recipient as `notifiable?` (undefined only for raw-target ad-hoc sends) and a `dispatchId` — `sending` and its terminal `sent`/`failed` share it, so observers can pair them (spans, latency, hung-send detection). `sent`/`failed` also carry `durationMs`. Handler exceptions are logged-and-swallowed so one bad listener can not break the dispatcher. `on(...)` returns an unsubscribe function; `off(event, handler)` exists for symmetry. Triggers: `notifications.on`, `notifications.off`, `"sending"` event, `"sent"` event, `"failed"` event, `"skipped"` event, `dispatchId`, `durationMs`, `NotificationEvents`, "notification metrics", "notification latency", "trace notifications", "notification audit log", "track notification drops"; typical import `import { notifications } from "@warlock.js/notifications"`. Skip: building notifications — `@warlock.js/notifications/define-notification/SKILL.md`; configuring the preference/rate-limit gates that produce `skipped` — `@warlock.js/notifications/configure-notifications/SKILL.md`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Observe notifications
|
|
7
|
+
|
|
8
|
+
Every dispatch emits a `sending` event then a terminal `sent` / `failed` — or a `skipped` if a gate dropped it — per (channel, recipient).
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { notifications } from "@warlock.js/notifications";
|
|
12
|
+
|
|
13
|
+
// latency + delivery, keyed by recipient
|
|
14
|
+
notifications.on("sent", ({ channel, notifiable, durationMs, options }) =>
|
|
15
|
+
metrics.timing(`notif.${channel}.sent`, durationMs, { userId: notifiable?.id, ...options.meta }));
|
|
16
|
+
|
|
17
|
+
notifications.on("failed", ({ channel, notifiable, error }) =>
|
|
18
|
+
log.error(`notif.${channel}`, error, { userId: notifiable?.id }));
|
|
19
|
+
|
|
20
|
+
notifications.on("skipped", ({ channel, reason }) =>
|
|
21
|
+
metrics.inc(`notif.${channel}.skipped.${reason}`));
|
|
22
|
+
|
|
23
|
+
// pair `sending` → terminal by dispatchId — open a span / arm a hung-send watchdog
|
|
24
|
+
notifications.on("sending", ({ dispatchId, channel, notifiable }) =>
|
|
25
|
+
tracer.open(dispatchId, { channel, userId: notifiable?.id }));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Events
|
|
29
|
+
|
|
30
|
+
| Event | Fires when | Payload |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `sending` | Gates passed; about to hit the transport (sync) / enqueue (queue) | `{ dispatchId, channel, notifiable?, payload, options }` |
|
|
33
|
+
| `sent` | Channel dispatched OR job enqueued | `{ dispatchId, channel, notifiable?, payload, options, durationMs }` |
|
|
34
|
+
| `failed` | `channel.send` (or `queue.dispatch`) threw | `{ dispatchId, channel, notifiable?, payload, error, options, durationMs }` |
|
|
35
|
+
| `skipped` | A pre-send gate dropped this channel | `{ dispatchId, channel, notifiable?, reason, options }` — `reason: "preference" \| "rate-limit"` |
|
|
36
|
+
|
|
37
|
+
`notifiable` is the recipient **model** (undefined only for raw-target ad-hoc sends like `notify.mail("x@y.com", …)`) — read `notifiable?.id` / `notifiable?.get(...)` to key your metrics or logs.
|
|
38
|
+
|
|
39
|
+
`dispatchId` is unique per (channel, recipient) dispatch — `sending` and its terminal `sent`/`failed` share it, so you can pair them for a span, a latency measurement, or a watchdog that flags a `sending` with no terminal (a hung send). `durationMs` is transport time on a sync send, enqueue time on a queued one.
|
|
40
|
+
|
|
41
|
+
`options` is the `SendOptions` passed at the call site — including `meta`. Tagging a send with `meta: { campaignId }` lets downstream observers join the event back to the campaign.
|
|
42
|
+
|
|
43
|
+
## Unsubscribe
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const off = notifications.on("sent", handler);
|
|
47
|
+
// later
|
|
48
|
+
off();
|
|
49
|
+
|
|
50
|
+
// or
|
|
51
|
+
notifications.off("sent", handler);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Fan-out emits N events
|
|
55
|
+
|
|
56
|
+
`orderShipped.send([buyer, salesRep], { order })` with `via: ["mail", "database"]` fires up to **4** `sent` events (2 recipients × 2 channels). Aggregate by `meta` or by `channel + notifiable.id` in your observer.
|
|
57
|
+
|
|
58
|
+
## Handler isolation
|
|
59
|
+
|
|
60
|
+
A handler that throws is logged and swallowed — the other handlers for the same event still run, and the dispatcher never observes the throw. Observers can't **abort** a send (there's no veto here — use `preferences` / `rateLimit` for that). They can delay it, though: `sending` is awaited before the transport, so keep that handler fast.
|
|
61
|
+
|
|
62
|
+
## See also
|
|
63
|
+
|
|
64
|
+
- [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — front door + mental model.
|
|
65
|
+
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — `preferences` / `rateLimit` slots that emit `skipped`.
|
|
66
|
+
- [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — when `notifiable` is undefined in events.
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: queue-notifications
|
|
3
|
-
description: "Send notifications asynchronously through the herald backend or the BullMQ driver. Configure a QueueDispatcher in config/notifications.ts, then use notif.queue(to, data, options?) to enqueue rendered jobs. Herald uses heraldQueue() and startNotificationsWorker(); bullmqQueue({ attempts, backoff }) from @warlock.js/notifications uses BullMQ (lazy-loading the optional @warlock.js/queue peer), supports retries, and honours SendOptions.delay. Without a queue configured, .queue() rejects NoQueueDispatcherError. Triggers: heraldQueue, bullmqQueue, startNotificationsWorker, QueueDispatcher, .queue(, async notifications, queue notifications, notification worker, background notifications, notifications.dispatch. Skip: synchronous sends - @warlock.js/notifications/define-notification/SKILL.md; herald itself - @warlock.js/herald/*."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Queue notifications (async)
|
|
7
|
-
|
|
8
|
-
Move slow channels (SMTP, HTTP) off the request path. `.queue()` renders the
|
|
9
|
-
payload and resolves the route before passing a serializable job to a dispatcher.
|
|
10
|
-
|
|
11
|
-
## Wire the herald dispatcher
|
|
12
|
-
|
|
13
|
-
```ts title="src/config/notifications.ts"
|
|
14
|
-
import {
|
|
15
|
-
type NotificationConfig,
|
|
16
|
-
heraldQueue,
|
|
17
|
-
inApp,
|
|
18
|
-
mailChannel,
|
|
19
|
-
} from "@warlock.js/notifications";
|
|
20
|
-
import { Notification } from "app/notifications/notification.model";
|
|
21
|
-
|
|
22
|
-
const config: NotificationConfig = {
|
|
23
|
-
channels: { mail: mailChannel(), database: inApp.configure({ model: Notification }) },
|
|
24
|
-
queue: heraldQueue(),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default config;
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
`heraldQueue({ channel?, broker? })` publishes to `"notifications.dispatch"`
|
|
31
|
-
on the default broker unless overridden. It lazy-imports `@warlock.js/herald`;
|
|
32
|
-
if that package is missing, the first `.queue()` throws an install message.
|
|
33
|
-
|
|
34
|
-
## Use BullMQ for delayed or retried delivery
|
|
35
|
-
|
|
36
|
-
Use the `bullmq` driver instead of the herald dispatcher — it ships inside
|
|
37
|
-
`@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an
|
|
38
|
-
OPTIONAL peer) the first time `.queue()` runs:
|
|
39
|
-
|
|
40
|
-
```ts title="src/config/notifications.ts"
|
|
41
|
-
import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
|
|
42
|
-
|
|
43
|
-
const config: NotificationConfig = {
|
|
44
|
-
channels: { mail: mailChannel() },
|
|
45
|
-
queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export default config;
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The BullMQ driver honours `SendOptions.delay`: numeric delays are seconds and
|
|
52
|
-
string delays use the normal duration syntax, such as `"10m"`. It also supports
|
|
53
|
-
BullMQ retries through `attempts` and `backoff`. If `@warlock.js/queue` isn't
|
|
54
|
-
installed, the first `.queue()` throws `QueuePackageNotInstalledError` naming
|
|
55
|
-
`warlock add queue`.
|
|
56
|
-
|
|
57
|
-
> **Removed (5.15.0):** `queueNotificationDispatcher` from
|
|
58
|
-
> `@warlock.js/queue/notifications` — deprecated in 5.14, removed this
|
|
59
|
-
> release, along with the `@warlock.js/notifications` peer dependency it was
|
|
60
|
-
> the only reason `@warlock.js/queue` carried. Use `bullmqQueue` above — same
|
|
61
|
-
> option names (`queue`, `attempts`, `backoff`).
|
|
62
|
-
|
|
63
|
-
## Run the herald worker
|
|
64
|
-
|
|
65
|
-
In a worker entrypoint (or the web process), after the config and herald broker
|
|
66
|
-
are up:
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
import { startNotificationsWorker } from "@warlock.js/notifications";
|
|
70
|
-
|
|
71
|
-
await startNotificationsWorker();
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The worker looks up the channel by name and runs
|
|
75
|
-
`channel.send({ payload, route, options })`. The worker needs no recipient model
|
|
76
|
-
because rendering happened at enqueue time.
|
|
77
|
-
|
|
78
|
-
## Send
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
await orderShipped.queue(user, { order });
|
|
82
|
-
await orderShipped.queue(buyers, { order }, { delay: "10m" });
|
|
83
|
-
await notify.mail(user, payload, {/* synchronous - notify.* does not queue */});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
`.queue()` renders per recipient and enqueues one job per recipient. Without a
|
|
87
|
-
configured dispatcher, it rejects `NoQueueDispatcherError`.
|
|
88
|
-
|
|
89
|
-
## Idempotency
|
|
90
|
-
|
|
91
|
-
Pass `idempotencyKey` so a redelivered or retried job does not create a duplicate
|
|
92
|
-
in-app row. `createFor` finds or creates the row by that key.
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
await orderShipped.queue(user, { order }, { idempotencyKey: `ship:${order.id}` });
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Current limits
|
|
99
|
-
|
|
100
|
-
- With the herald backend, `delay` is carried on the job but not honoured; the
|
|
101
|
-
worker delivers immediately. Use the BullMQ adapter for delayed delivery.
|
|
102
|
-
- Herald has no retry or dead-letter handling yet; failed `channel.send` calls
|
|
103
|
-
are logged and acknowledged.
|
|
104
|
-
- Rate-limit budget is consumed at enqueue time for queued sends.
|
|
105
|
-
|
|
106
|
-
## See also
|
|
107
|
-
|
|
108
|
-
- [`define-notification/SKILL.md`](../define-notification/SKILL.md) - `.queue()` on a defined notification.
|
|
109
|
-
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) - the `queue` config slot.
|
|
110
|
-
- [`@warlock.js/herald/herald-basics/SKILL.md`](../../../herald/skills/herald-basics/SKILL.md) - connecting the broker.
|
|
1
|
+
---
|
|
2
|
+
name: queue-notifications
|
|
3
|
+
description: "Send notifications asynchronously through the herald backend or the BullMQ driver. Configure a QueueDispatcher in config/notifications.ts, then use notif.queue(to, data, options?) to enqueue rendered jobs. Herald uses heraldQueue() and startNotificationsWorker(); bullmqQueue({ attempts, backoff }) from @warlock.js/notifications uses BullMQ (lazy-loading the optional @warlock.js/queue peer), supports retries, and honours SendOptions.delay. Without a queue configured, .queue() rejects NoQueueDispatcherError. Triggers: heraldQueue, bullmqQueue, startNotificationsWorker, QueueDispatcher, .queue(, async notifications, queue notifications, notification worker, background notifications, notifications.dispatch. Skip: synchronous sends - @warlock.js/notifications/define-notification/SKILL.md; herald itself - @warlock.js/herald/*."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Queue notifications (async)
|
|
7
|
+
|
|
8
|
+
Move slow channels (SMTP, HTTP) off the request path. `.queue()` renders the
|
|
9
|
+
payload and resolves the route before passing a serializable job to a dispatcher.
|
|
10
|
+
|
|
11
|
+
## Wire the herald dispatcher
|
|
12
|
+
|
|
13
|
+
```ts title="src/config/notifications.ts"
|
|
14
|
+
import {
|
|
15
|
+
type NotificationConfig,
|
|
16
|
+
heraldQueue,
|
|
17
|
+
inApp,
|
|
18
|
+
mailChannel,
|
|
19
|
+
} from "@warlock.js/notifications";
|
|
20
|
+
import { Notification } from "app/notifications/notification.model";
|
|
21
|
+
|
|
22
|
+
const config: NotificationConfig = {
|
|
23
|
+
channels: { mail: mailChannel(), database: inApp.configure({ model: Notification }) },
|
|
24
|
+
queue: heraldQueue(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default config;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`heraldQueue({ channel?, broker? })` publishes to `"notifications.dispatch"`
|
|
31
|
+
on the default broker unless overridden. It lazy-imports `@warlock.js/herald`;
|
|
32
|
+
if that package is missing, the first `.queue()` throws an install message.
|
|
33
|
+
|
|
34
|
+
## Use BullMQ for delayed or retried delivery
|
|
35
|
+
|
|
36
|
+
Use the `bullmq` driver instead of the herald dispatcher — it ships inside
|
|
37
|
+
`@warlock.js/notifications` itself and lazy-loads `@warlock.js/queue` (an
|
|
38
|
+
OPTIONAL peer) the first time `.queue()` runs:
|
|
39
|
+
|
|
40
|
+
```ts title="src/config/notifications.ts"
|
|
41
|
+
import { type NotificationConfig, bullmqQueue, mailChannel } from "@warlock.js/notifications";
|
|
42
|
+
|
|
43
|
+
const config: NotificationConfig = {
|
|
44
|
+
channels: { mail: mailChannel() },
|
|
45
|
+
queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default config;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The BullMQ driver honours `SendOptions.delay`: numeric delays are seconds and
|
|
52
|
+
string delays use the normal duration syntax, such as `"10m"`. It also supports
|
|
53
|
+
BullMQ retries through `attempts` and `backoff`. If `@warlock.js/queue` isn't
|
|
54
|
+
installed, the first `.queue()` throws `QueuePackageNotInstalledError` naming
|
|
55
|
+
`warlock add queue`.
|
|
56
|
+
|
|
57
|
+
> **Removed (5.15.0):** `queueNotificationDispatcher` from
|
|
58
|
+
> `@warlock.js/queue/notifications` — deprecated in 5.14, removed this
|
|
59
|
+
> release, along with the `@warlock.js/notifications` peer dependency it was
|
|
60
|
+
> the only reason `@warlock.js/queue` carried. Use `bullmqQueue` above — same
|
|
61
|
+
> option names (`queue`, `attempts`, `backoff`).
|
|
62
|
+
|
|
63
|
+
## Run the herald worker
|
|
64
|
+
|
|
65
|
+
In a worker entrypoint (or the web process), after the config and herald broker
|
|
66
|
+
are up:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { startNotificationsWorker } from "@warlock.js/notifications";
|
|
70
|
+
|
|
71
|
+
await startNotificationsWorker();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The worker looks up the channel by name and runs
|
|
75
|
+
`channel.send({ payload, route, options })`. The worker needs no recipient model
|
|
76
|
+
because rendering happened at enqueue time.
|
|
77
|
+
|
|
78
|
+
## Send
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
await orderShipped.queue(user, { order });
|
|
82
|
+
await orderShipped.queue(buyers, { order }, { delay: "10m" });
|
|
83
|
+
await notify.mail(user, payload, {/* synchronous - notify.* does not queue */});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`.queue()` renders per recipient and enqueues one job per recipient. Without a
|
|
87
|
+
configured dispatcher, it rejects `NoQueueDispatcherError`.
|
|
88
|
+
|
|
89
|
+
## Idempotency
|
|
90
|
+
|
|
91
|
+
Pass `idempotencyKey` so a redelivered or retried job does not create a duplicate
|
|
92
|
+
in-app row. `createFor` finds or creates the row by that key.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await orderShipped.queue(user, { order }, { idempotencyKey: `ship:${order.id}` });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Current limits
|
|
99
|
+
|
|
100
|
+
- With the herald backend, `delay` is carried on the job but not honoured; the
|
|
101
|
+
worker delivers immediately. Use the BullMQ adapter for delayed delivery.
|
|
102
|
+
- Herald has no retry or dead-letter handling yet; failed `channel.send` calls
|
|
103
|
+
are logged and acknowledged.
|
|
104
|
+
- Rate-limit budget is consumed at enqueue time for queued sends.
|
|
105
|
+
|
|
106
|
+
## See also
|
|
107
|
+
|
|
108
|
+
- [`define-notification/SKILL.md`](../define-notification/SKILL.md) - `.queue()` on a defined notification.
|
|
109
|
+
- [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) - the `queue` config slot.
|
|
110
|
+
- [`@warlock.js/herald/herald-basics/SKILL.md`](../../../herald/skills/herald-basics/SKILL.md) - connecting the broker.
|