@warlock.js/notifications 5.23.2 → 5.25.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cjs/index.cjs +1 -1
  3. package/cjs/rabbitmq-driver-DvWaCPES.cjs +3 -0
  4. package/cjs/{src-DbAWktJm.cjs → src-BVlqtCR2.cjs} +22 -6
  5. package/cjs/src-BVlqtCR2.cjs.map +1 -0
  6. package/esm/herald/src/communicators/broker-registry.mjs.map +1 -1
  7. package/esm/herald/src/communicators/broker.mjs.map +1 -1
  8. package/esm/herald/src/drivers/rabbitmq/rabbitmq-channel.mjs.map +1 -1
  9. package/esm/herald/src/drivers/rabbitmq/rabbitmq-driver.mjs.map +1 -1
  10. package/esm/herald/src/message-managers/prepare-consumer-subscription.mjs.map +1 -1
  11. package/esm/herald/src/utils/connect-to-broker.mjs.map +1 -1
  12. package/esm/seal/src/config.mjs.map +1 -1
  13. package/esm/seal/src/factory/validate.mjs.map +1 -1
  14. package/esm/seal/src/factory/validators.mjs +1 -1
  15. package/esm/seal/src/factory/validators.mjs.map +1 -1
  16. package/esm/seal/src/helpers/date-helpers.mjs.map +1 -1
  17. package/esm/seal/src/helpers/get-field-value.mjs.map +1 -1
  18. package/esm/seal/src/helpers/path-helpers.mjs.map +1 -1
  19. package/esm/seal/src/mutators/array-mutators.mjs.map +1 -1
  20. package/esm/seal/src/mutators/date-mutators.mjs.map +1 -1
  21. package/esm/seal/src/mutators/number-mutators.mjs.map +1 -1
  22. package/esm/seal/src/mutators/object-mutators.mjs.map +1 -1
  23. package/esm/seal/src/mutators/scalar-mutators.mjs.map +1 -1
  24. package/esm/seal/src/mutators/string-mutators.mjs.map +1 -1
  25. package/esm/seal/src/rules/array/array-rules.mjs.map +1 -1
  26. package/esm/seal/src/rules/color/color-rules.mjs.map +1 -1
  27. package/esm/seal/src/rules/common/enum.mjs.map +1 -1
  28. package/esm/seal/src/rules/common/equals-field-rules.mjs.map +1 -1
  29. package/esm/seal/src/rules/common/type-rules.mjs.map +1 -1
  30. package/esm/seal/src/rules/conditional/forbidden-if-rules.mjs.map +1 -1
  31. package/esm/seal/src/rules/conditional/present-if-rules.mjs.map +1 -1
  32. package/esm/seal/src/rules/conditional/present-unless-rules.mjs.map +1 -1
  33. package/esm/seal/src/rules/conditional/present-with-rules.mjs.map +1 -1
  34. package/esm/seal/src/rules/conditional/present-without-rules.mjs.map +1 -1
  35. package/esm/seal/src/rules/conditional/required-if-rules.mjs.map +1 -1
  36. package/esm/seal/src/rules/conditional/required-unless-rules.mjs.map +1 -1
  37. package/esm/seal/src/rules/conditional/required-with-rules.mjs.map +1 -1
  38. package/esm/seal/src/rules/conditional/required-without-rules.mjs.map +1 -1
  39. package/esm/seal/src/rules/core/equal.mjs.map +1 -1
  40. package/esm/seal/src/rules/core/forbidden.mjs.map +1 -1
  41. package/esm/seal/src/rules/core/required.mjs.map +1 -1
  42. package/esm/seal/src/rules/core/union.mjs.map +1 -1
  43. package/esm/seal/src/rules/core/when.mjs.map +1 -1
  44. package/esm/seal/src/rules/date/date-comparison-rules.mjs.map +1 -1
  45. package/esm/seal/src/rules/date/date-day-rules.mjs.map +1 -1
  46. package/esm/seal/src/rules/date/date-field-comparison-rules.mjs.map +1 -1
  47. package/esm/seal/src/rules/date/date-period-rules.mjs.map +1 -1
  48. package/esm/seal/src/rules/date/date-relative-rules.mjs.map +1 -1
  49. package/esm/seal/src/rules/date/date-special-rules.mjs.map +1 -1
  50. package/esm/seal/src/rules/date/date.mjs.map +1 -1
  51. package/esm/seal/src/rules/length/length-rules.mjs.map +1 -1
  52. package/esm/seal/src/rules/number/number-rules.mjs.map +1 -1
  53. package/esm/seal/src/rules/scalar/accepted-rule.mjs.map +1 -1
  54. package/esm/seal/src/rules/scalar/declined-rule.mjs.map +1 -1
  55. package/esm/seal/src/rules/string/alpha.mjs.map +1 -1
  56. package/esm/seal/src/rules/string/credit-card.mjs.map +1 -1
  57. package/esm/seal/src/rules/string/email.mjs.map +1 -1
  58. package/esm/seal/src/rules/string/pattern.mjs.map +1 -1
  59. package/esm/seal/src/rules/string/string-comparison.mjs.map +1 -1
  60. package/esm/seal/src/rules/string/strong-password-rule.mjs.map +1 -1
  61. package/esm/seal/src/rules/string/url.mjs.map +1 -1
  62. package/esm/seal/src/rules/string/without-whitespace.mjs.map +1 -1
  63. package/esm/seal/src/types/date-types.mjs.map +1 -1
  64. package/esm/seal/src/validators/any-validator.mjs.map +1 -1
  65. package/esm/seal/src/validators/array-validator.mjs +1 -0
  66. package/esm/seal/src/validators/array-validator.mjs.map +1 -1
  67. package/esm/seal/src/validators/base-validator.mjs +12 -1
  68. package/esm/seal/src/validators/base-validator.mjs.map +1 -1
  69. package/esm/seal/src/validators/boolean-validator.mjs +1 -0
  70. package/esm/seal/src/validators/boolean-validator.mjs.map +1 -1
  71. package/esm/seal/src/validators/date-validator.mjs +1 -0
  72. package/esm/seal/src/validators/date-validator.mjs.map +1 -1
  73. package/esm/seal/src/validators/float-validator.mjs.map +1 -1
  74. package/esm/seal/src/validators/int-validator.mjs.map +1 -1
  75. package/esm/seal/src/validators/literal-validator.mjs +1 -3
  76. package/esm/seal/src/validators/literal-validator.mjs.map +1 -1
  77. package/esm/seal/src/validators/methods/equality-conditional-methods.mjs.map +1 -1
  78. package/esm/seal/src/validators/methods/forbidden-methods.mjs.map +1 -1
  79. package/esm/seal/src/validators/methods/present-methods.mjs.map +1 -1
  80. package/esm/seal/src/validators/methods/required-methods.mjs.map +1 -1
  81. package/esm/seal/src/validators/number-validator.mjs +1 -0
  82. package/esm/seal/src/validators/number-validator.mjs.map +1 -1
  83. package/esm/seal/src/validators/numeric-validator.mjs.map +1 -1
  84. package/esm/seal/src/validators/object-validator.mjs +1 -1
  85. package/esm/seal/src/validators/object-validator.mjs.map +1 -1
  86. package/esm/seal/src/validators/record-validator.mjs.map +1 -1
  87. package/esm/seal/src/validators/scalar-validator.mjs +1 -0
  88. package/esm/seal/src/validators/scalar-validator.mjs.map +1 -1
  89. package/esm/seal/src/validators/string-validator.mjs.map +1 -1
  90. package/esm/seal/src/validators/tuple-validator.mjs.map +1 -1
  91. package/esm/seal/src/validators/union-validator.mjs.map +1 -1
  92. package/llms-full.txt +891 -891
  93. package/package.json +7 -7
  94. package/skills/configure-notifications/SKILL.md +138 -138
  95. package/skills/define-channel/SKILL.md +107 -107
  96. package/skills/define-notification/SKILL.md +133 -133
  97. package/skills/observe-notifications/SKILL.md +66 -66
  98. package/skills/queue-notifications/SKILL.md +110 -110
  99. package/skills/send-ad-hoc/SKILL.md +95 -95
  100. package/skills/use-in-app/SKILL.md +172 -172
  101. package/skills/write-notification-migration/SKILL.md +66 -66
  102. package/cjs/rabbitmq-driver-C0o3Pz15.cjs +0 -3
  103. package/cjs/src-DbAWktJm.cjs.map +0 -1
package/package.json CHANGED
@@ -5,16 +5,16 @@
5
5
  "environment": "server"
6
6
  },
7
7
  "dependencies": {
8
- "@warlock.js/cache": "5.23.2",
9
- "@warlock.js/cascade": "5.23.2",
10
- "@warlock.js/core": "5.23.2",
11
- "@warlock.js/logger": "5.23.2"
8
+ "@warlock.js/cache": "5.25.0",
9
+ "@warlock.js/cascade": "5.25.0",
10
+ "@warlock.js/core": "5.25.0",
11
+ "@warlock.js/logger": "5.25.0"
12
12
  },
13
13
  "optionalDependencies": {
14
- "@warlock.js/herald": "5.23.2"
14
+ "@warlock.js/herald": "5.25.0"
15
15
  },
16
16
  "peerDependencies": {
17
- "@warlock.js/queue": "5.23.2"
17
+ "@warlock.js/queue": "5.25.0"
18
18
  },
19
19
  "peerDependenciesMeta": {
20
20
  "@warlock.js/queue": {
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "author": "hassanzohdy",
37
37
  "license": "MIT",
38
- "version": "5.23.2",
38
+ "version": "5.25.0",
39
39
  "main": "./cjs/index.cjs",
40
40
  "module": "./esm/index.mjs",
41
41
  "types": "./esm/index.d.mts",
@@ -1,138 +1,138 @@
1
- ---
2
- name: configure-notifications
3
- description: 'Wire `@warlock.js/notifications` via a declarative `src/config/notifications.ts` that exports a `NotificationConfig` default — the notifications connector registers it at boot, so app code never calls `setNotificationConfig`. `channels`: registry mapping name → factory result (`mailChannel`, `inApp.configure`, custom `defineChannel`); `queue`: optional `QueueDispatcher` (`.queue()` throws without it); `preferences`: optional `PreferenceProvider` (drops channels the recipient muted); `rateLimit`: optional `RateLimiter` (drops channels exceeding per-recipient budgets). Triggers: `config/notifications.ts`, `NotificationConfig`, `setNotificationConfig`, `getNotificationConfig`, `mailChannel`, `inApp.configure`, "configure notifications", "wire notifications at boot", "set preferences provider", "set rate limit"; typical import `import { type NotificationConfig, mailChannel, inApp } from "@warlock.js/notifications"`. Skip: building a reusable notification — `@warlock.js/notifications/define-notification/SKILL.md`; the in-app read API — `@warlock.js/notifications/use-in-app/SKILL.md`; adding a custom channel — `@warlock.js/notifications/define-channel/SKILL.md`.'
4
- ---
5
-
6
- # Configure notifications
7
-
8
- `src/config/notifications.ts` exports a `NotificationConfig` as its default; the notifications connector registers it at boot and the dispatcher reads from it on every send. The file is **declarative** — you never call `setNotificationConfig` yourself (the connector does, the same way the rest of `src/config/*.ts` is wired).
9
-
10
- > `npx warlock add notifications` ejects this file pre-wired (mail + in-app) alongside the `Notification` model + migration and the recipient-scoped read/dismiss routes + controllers. Edit the ejected config to taste — the sections below are the full surface.
11
-
12
- ## Minimal
13
-
14
- ```ts title="src/config/notifications.ts"
15
- import { type NotificationConfig, inApp, mailChannel } from "@warlock.js/notifications";
16
- import { Notification } from "app/notifications/notification.model";
17
-
18
- const config: NotificationConfig = {
19
- channels: {
20
- mail: mailChannel({ from: "no-reply@store.com" }),
21
- database: inApp.configure({ model: Notification }),
22
- },
23
- };
24
-
25
- export default config;
26
- ```
27
-
28
- The `database` channel is returned by `inApp.configure({ model | repository })` — it binds the in-app store AND returns the channel, so the read side + the write side share ONE repository instance.
29
-
30
- ## Full surface
31
-
32
- ```ts
33
- const config: NotificationConfig = {
34
- channels: {
35
- mail: mailChannel({ from }),
36
- database: inApp.configure({ model: Notification }),
37
-
38
- // custom channels
39
- discord: discordChannel(), // defineChannel + declare module
40
- },
41
-
42
- // optional gates — both app-owned; package ships no defaults
43
- preferences: userPreferences, // drops channels recipient muted
44
- rateLimit, // drops channels over budget
45
- queue: heraldQueue(), // backs `.queue()` (needs @warlock.js/herald)
46
- };
47
-
48
- export default config;
49
- ```
50
-
51
- ## Channel keys must match the registry
52
-
53
- `channels: { mail, database, discord }` — keys correspond to `NotificationChannels` entries. Built-ins (`mail`, `database`) ship in the registry; custom channels extend it with `declare module`.
54
-
55
- ```ts
56
- // in your custom channel file
57
- declare module "@warlock.js/notifications" {
58
- interface NotificationChannels {
59
- discord: { content: string };
60
- }
61
- }
62
- ```
63
-
64
- After that, `notify.discord(...)`, `via: ["discord"]`, and the `discord:` key in `defineNotification` all autocomplete + type-check.
65
-
66
- ## `inApp.configure({ model | repository })`
67
-
68
- Two paths; mutually exclusive at the type level.
69
-
70
- ```ts
71
- // 90% case — pass the model class; default repo built internally.
72
- database: inApp.configure({ model: Notification }),
73
-
74
- // 10% case — pass a custom repo (only for EXTRA query methods; column names
75
- // come from the model's columnMap, not a repo override).
76
- database: inApp.configure({ repository: notificationsRepository }),
77
- ```
78
-
79
- There is NO zero-arg form — the package ships no concrete table/model.
80
-
81
- ## `preferences` — pre-send opt-in gate (optional)
82
-
83
- ```ts
84
- import type { PreferenceProvider } from "@warlock.js/notifications";
85
-
86
- export const userPreferences: PreferenceProvider = {
87
- resolveChannels(user, type, requested) {
88
- const muted: Record<string, string[]> = user.get("preferences.muted") ?? {};
89
- return requested.filter((c) => !(muted[c] ?? []).includes(type));
90
- },
91
- };
92
- ```
93
-
94
- Dropped channels fire a `skipped` event with reason `"preference"`. `SendOptions.force === true` bypasses this gate.
95
-
96
- ## `rateLimit` — pre-send safety valve (optional)
97
-
98
- ```ts
99
- import { cache } from "@warlock.js/cache";
100
- import type { RateLimiter } from "@warlock.js/notifications";
101
-
102
- export const rateLimit: RateLimiter = {
103
- async allow(user, channel, type) {
104
- const key = `notif.rl.${user.id}.${channel}.${type}`;
105
- const count = await cache.increment(key, 1);
106
- if (count === 1) await cache.set(key, 1, { ttl: 3600 });
107
- return count <= 5;
108
- },
109
- };
110
- ```
111
-
112
- Dropped channels fire a `skipped` event with reason `"rate-limit"`. `force` does NOT bypass this — rate limits are a safety valve, not a UX preference.
113
-
114
- ## Reading the config back
115
-
116
- ```ts
117
- import { getNotificationConfig } from "@warlock.js/notifications";
118
-
119
- const cfg = getNotificationConfig(); // throws NotificationsNotConfiguredError if unset
120
- ```
121
-
122
- ## Tests
123
-
124
- Tests call `setNotificationConfig` directly — bypassing the connector — to set state, then `resetNotificationConfig` to tear down:
125
-
126
- ```ts
127
- import { resetNotificationConfig, setNotificationConfig } from "@warlock.js/notifications";
128
-
129
- beforeEach(() => setNotificationConfig({ channels: { ... } }));
130
- afterEach(() => resetNotificationConfig());
131
- ```
132
-
133
- ## See also
134
-
135
- - [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — package front door.
136
- - [`define-notification/SKILL.md`](../define-notification/SKILL.md) — reusable multi-channel definitions.
137
- - [`use-in-app/SKILL.md`](../use-in-app/SKILL.md) — in-app read API.
138
- - [`define-channel/SKILL.md`](../define-channel/SKILL.md) — custom channels.
1
+ ---
2
+ name: configure-notifications
3
+ description: 'Wire `@warlock.js/notifications` via a declarative `src/config/notifications.ts` that exports a `NotificationConfig` default — the notifications connector registers it at boot, so app code never calls `setNotificationConfig`. `channels`: registry mapping name → factory result (`mailChannel`, `inApp.configure`, custom `defineChannel`); `queue`: optional `QueueDispatcher` (`.queue()` throws without it); `preferences`: optional `PreferenceProvider` (drops channels the recipient muted); `rateLimit`: optional `RateLimiter` (drops channels exceeding per-recipient budgets). Triggers: `config/notifications.ts`, `NotificationConfig`, `setNotificationConfig`, `getNotificationConfig`, `mailChannel`, `inApp.configure`, "configure notifications", "wire notifications at boot", "set preferences provider", "set rate limit"; typical import `import { type NotificationConfig, mailChannel, inApp } from "@warlock.js/notifications"`. Skip: building a reusable notification — `@warlock.js/notifications/define-notification/SKILL.md`; the in-app read API — `@warlock.js/notifications/use-in-app/SKILL.md`; adding a custom channel — `@warlock.js/notifications/define-channel/SKILL.md`.'
4
+ ---
5
+
6
+ # Configure notifications
7
+
8
+ `src/config/notifications.ts` exports a `NotificationConfig` as its default; the notifications connector registers it at boot and the dispatcher reads from it on every send. The file is **declarative** — you never call `setNotificationConfig` yourself (the connector does, the same way the rest of `src/config/*.ts` is wired).
9
+
10
+ > `npx warlock add notifications` ejects this file pre-wired (mail + in-app) alongside the `Notification` model + migration and the recipient-scoped read/dismiss routes + controllers. Edit the ejected config to taste — the sections below are the full surface.
11
+
12
+ ## Minimal
13
+
14
+ ```ts title="src/config/notifications.ts"
15
+ import { type NotificationConfig, inApp, mailChannel } from "@warlock.js/notifications";
16
+ import { Notification } from "app/notifications/notification.model";
17
+
18
+ const config: NotificationConfig = {
19
+ channels: {
20
+ mail: mailChannel({ from: "no-reply@store.com" }),
21
+ database: inApp.configure({ model: Notification }),
22
+ },
23
+ };
24
+
25
+ export default config;
26
+ ```
27
+
28
+ The `database` channel is returned by `inApp.configure({ model | repository })` — it binds the in-app store AND returns the channel, so the read side + the write side share ONE repository instance.
29
+
30
+ ## Full surface
31
+
32
+ ```ts
33
+ const config: NotificationConfig = {
34
+ channels: {
35
+ mail: mailChannel({ from }),
36
+ database: inApp.configure({ model: Notification }),
37
+
38
+ // custom channels
39
+ discord: discordChannel(), // defineChannel + declare module
40
+ },
41
+
42
+ // optional gates — both app-owned; package ships no defaults
43
+ preferences: userPreferences, // drops channels recipient muted
44
+ rateLimit, // drops channels over budget
45
+ queue: heraldQueue(), // backs `.queue()` (needs @warlock.js/herald)
46
+ };
47
+
48
+ export default config;
49
+ ```
50
+
51
+ ## Channel keys must match the registry
52
+
53
+ `channels: { mail, database, discord }` — keys correspond to `NotificationChannels` entries. Built-ins (`mail`, `database`) ship in the registry; custom channels extend it with `declare module`.
54
+
55
+ ```ts
56
+ // in your custom channel file
57
+ declare module "@warlock.js/notifications" {
58
+ interface NotificationChannels {
59
+ discord: { content: string };
60
+ }
61
+ }
62
+ ```
63
+
64
+ After that, `notify.discord(...)`, `via: ["discord"]`, and the `discord:` key in `defineNotification` all autocomplete + type-check.
65
+
66
+ ## `inApp.configure({ model | repository })`
67
+
68
+ Two paths; mutually exclusive at the type level.
69
+
70
+ ```ts
71
+ // 90% case — pass the model class; default repo built internally.
72
+ database: inApp.configure({ model: Notification }),
73
+
74
+ // 10% case — pass a custom repo (only for EXTRA query methods; column names
75
+ // come from the model's columnMap, not a repo override).
76
+ database: inApp.configure({ repository: notificationsRepository }),
77
+ ```
78
+
79
+ There is NO zero-arg form — the package ships no concrete table/model.
80
+
81
+ ## `preferences` — pre-send opt-in gate (optional)
82
+
83
+ ```ts
84
+ import type { PreferenceProvider } from "@warlock.js/notifications";
85
+
86
+ export const userPreferences: PreferenceProvider = {
87
+ resolveChannels(user, type, requested) {
88
+ const muted: Record<string, string[]> = user.get("preferences.muted") ?? {};
89
+ return requested.filter((c) => !(muted[c] ?? []).includes(type));
90
+ },
91
+ };
92
+ ```
93
+
94
+ Dropped channels fire a `skipped` event with reason `"preference"`. `SendOptions.force === true` bypasses this gate.
95
+
96
+ ## `rateLimit` — pre-send safety valve (optional)
97
+
98
+ ```ts
99
+ import { cache } from "@warlock.js/cache";
100
+ import type { RateLimiter } from "@warlock.js/notifications";
101
+
102
+ export const rateLimit: RateLimiter = {
103
+ async allow(user, channel, type) {
104
+ const key = `notif.rl.${user.id}.${channel}.${type}`;
105
+ const count = await cache.increment(key, 1);
106
+ if (count === 1) await cache.set(key, 1, { ttl: 3600 });
107
+ return count <= 5;
108
+ },
109
+ };
110
+ ```
111
+
112
+ Dropped channels fire a `skipped` event with reason `"rate-limit"`. `force` does NOT bypass this — rate limits are a safety valve, not a UX preference.
113
+
114
+ ## Reading the config back
115
+
116
+ ```ts
117
+ import { getNotificationConfig } from "@warlock.js/notifications";
118
+
119
+ const cfg = getNotificationConfig(); // throws NotificationsNotConfiguredError if unset
120
+ ```
121
+
122
+ ## Tests
123
+
124
+ Tests call `setNotificationConfig` directly — bypassing the connector — to set state, then `resetNotificationConfig` to tear down:
125
+
126
+ ```ts
127
+ import { resetNotificationConfig, setNotificationConfig } from "@warlock.js/notifications";
128
+
129
+ beforeEach(() => setNotificationConfig({ channels: { ... } }));
130
+ afterEach(() => resetNotificationConfig());
131
+ ```
132
+
133
+ ## See also
134
+
135
+ - [`notifications-basics/SKILL.md`](../notifications-basics/SKILL.md) — package front door.
136
+ - [`define-notification/SKILL.md`](../define-notification/SKILL.md) — reusable multi-channel definitions.
137
+ - [`use-in-app/SKILL.md`](../use-in-app/SKILL.md) — in-app read API.
138
+ - [`define-channel/SKILL.md`](../define-channel/SKILL.md) — custom channels.
@@ -1,107 +1,107 @@
1
- ---
2
- name: define-channel
3
- description: 'Add a custom notification channel — `defineChannel<P>({ name, route?, send })` returns a `Channel<P>` you register in the `channels` map of `setNotificationConfig`. `name` matches the key in `NotificationChannels` (extend via `declare module` for typing); `route(notifiable)` resolves the recipient''s address (defaults to `{ id }` when omitted); `send({ payload, route, notifiable, options })` does the actual transport. Use for Slack/Discord/internal-webhook/anything else — `fetch`-based with NO SDK is the simplest variant. Channels that load a heavy SDK should follow the lazy-import pattern from the develop-feature playbook. Triggers: `defineChannel`, `Channel<P>`, "custom notification channel", "add discord channel", "slack channel", "webhook channel", "new channel type", `declare module "@warlock.js/notifications"`; typical import `import { defineChannel } from "@warlock.js/notifications"`. Skip: lazy-loading an optional SDK behind the channel — `D:/xampp/htdocs/mongez/node/.claude/skills/develop-warlock.js-feature/SKILL.md`; using built-in channels — `@warlock.js/notifications/configure-notifications/SKILL.md`.'
4
- ---
5
-
6
- # `defineChannel` — custom channels
7
-
8
- The escape hatch for channel types the package doesn't ship.
9
-
10
- ## Minimal — `fetch`-based webhook (no SDK)
11
-
12
- ```ts title="src/app/notifications/channels/discord.channel.ts"
13
- import { defineChannel } from "@warlock.js/notifications";
14
-
15
- type DiscordPayload = { content: string };
16
-
17
- export const discordChannel = () =>
18
- defineChannel<DiscordPayload>({
19
- name: "discord",
20
- route: (notifiable) => notifiable.get("discord_webhook") as string,
21
- async send({ payload, route }) {
22
- const response = await fetch(route as string, {
23
- method: "POST",
24
- headers: { "content-type": "application/json" },
25
- body: JSON.stringify(payload),
26
- });
27
- if (!response.ok) throw new Error(`Discord send failed: ${response.status}`);
28
- },
29
- });
30
-
31
- // Teach TypeScript about the new channel — picks up notify.discord and
32
- // `defineNotification` typing.
33
- declare module "@warlock.js/notifications" {
34
- interface NotificationChannels {
35
- discord: DiscordPayload;
36
- }
37
- }
38
- ```
39
-
40
- Register in `config/notifications.ts`:
41
-
42
- ```ts
43
- channels: {
44
- // ...
45
- discord: discordChannel(),
46
- }
47
- ```
48
-
49
- Now `notify.discord(user, { content: "🎉" })` works, and `discord:` is a valid renderer key in `defineNotification`.
50
-
51
- ## `route` resolution
52
-
53
- | What `route` returns | Effect |
54
- |---|---|
55
- | `string` | Used directly as the address (`"user@example.com"`, webhook URL). |
56
- | `{ id: Id }` | The recipient's storage id — for database/internal channels. |
57
- | `undefined` | Dispatcher falls back to `{ id: notifiable.id }`. |
58
- | `route` omitted | Same as returning `undefined` — `{ id }` fallback. |
59
-
60
- For raw-target ad-hoc sends (`notify.discord("https://hooks.../foo", payload)`), the raw string wins over `route()`.
61
-
62
- ## `send` — the contract
63
-
64
- ```ts
65
- async send({
66
- payload, // P — the channel's payload type
67
- route, // string | { id: Id } — resolved by `route()` or raw target
68
- notifiable, // Notifiable | undefined — undefined for raw-target ad-hoc
69
- options, // SendOptions — delay/locale/meta/idempotencyKey/force/type
70
- }): Promise<void>
71
- ```
72
-
73
- Throw to fail the send. The dispatcher catches per channel, fires a `failed` event, and continues with siblings (per-channel isolation).
74
-
75
- ## SDK-backed channels — lazy-import the SDK
76
-
77
- Channels that wrap a heavy SDK (Twilio, Slack SDK, FCM SDK) should follow the lazy-import pattern so apps that don't use them don't pay the install/load cost. The recipe + race-safe template lives in [`develop-warlock.js-feature`](../../../../.claude/skills/develop-warlock.js-feature/SKILL.md) — copy from there; do not invent a variant.
78
-
79
- Sketch:
80
-
81
- ```ts
82
- import type { WebClient } from "@slack/web-api";
83
- let Slack: typeof import("@slack/web-api");
84
- let isModuleExists: boolean | null = null;
85
-
86
- async function loadSlack() { /* try/catch await import; set flag */ }
87
- loadSlack();
88
-
89
- export const slackChannel = (config: { token: string }) =>
90
- defineChannel<{ text: string }>({
91
- name: "slack",
92
- route: (n) => n.get("slack_channel"),
93
- async send({ payload, route }) {
94
- await loadSlack();
95
- if (!isModuleExists) throw new Error(INSTALL_INSTRUCTIONS);
96
- const client = new Slack.WebClient(config.token);
97
- await client.chat.postMessage({ channel: route as string, text: payload.text });
98
- },
99
- });
100
- ```
101
-
102
- ## See also
103
-
104
- - [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — register channels in the config.
105
- - [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — `notify.<custom-channel>` works after registration.
106
- - [`define-notification/SKILL.md`](../define-notification/SKILL.md) — multi-channel renderers with the custom channel key.
107
- - `D:/xampp/htdocs/mongez/node/.claude/skills/develop-warlock.js-feature/SKILL.md` — lazy-import pattern for SDK-backed channels.
1
+ ---
2
+ name: define-channel
3
+ description: 'Add a custom notification channel — `defineChannel<P>({ name, route?, send })` returns a `Channel<P>` you register in the `channels` map of `setNotificationConfig`. `name` matches the key in `NotificationChannels` (extend via `declare module` for typing); `route(notifiable)` resolves the recipient''s address (defaults to `{ id }` when omitted); `send({ payload, route, notifiable, options })` does the actual transport. Use for Slack/Discord/internal-webhook/anything else — `fetch`-based with NO SDK is the simplest variant. Channels that load a heavy SDK should follow the lazy-import pattern from the develop-feature playbook. Triggers: `defineChannel`, `Channel<P>`, "custom notification channel", "add discord channel", "slack channel", "webhook channel", "new channel type", `declare module "@warlock.js/notifications"`; typical import `import { defineChannel } from "@warlock.js/notifications"`. Skip: lazy-loading an optional SDK behind the channel — `D:/xampp/htdocs/mongez/node/.claude/skills/develop-warlock.js-feature/SKILL.md`; using built-in channels — `@warlock.js/notifications/configure-notifications/SKILL.md`.'
4
+ ---
5
+
6
+ # `defineChannel` — custom channels
7
+
8
+ The escape hatch for channel types the package doesn't ship.
9
+
10
+ ## Minimal — `fetch`-based webhook (no SDK)
11
+
12
+ ```ts title="src/app/notifications/channels/discord.channel.ts"
13
+ import { defineChannel } from "@warlock.js/notifications";
14
+
15
+ type DiscordPayload = { content: string };
16
+
17
+ export const discordChannel = () =>
18
+ defineChannel<DiscordPayload>({
19
+ name: "discord",
20
+ route: (notifiable) => notifiable.get("discord_webhook") as string,
21
+ async send({ payload, route }) {
22
+ const response = await fetch(route as string, {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify(payload),
26
+ });
27
+ if (!response.ok) throw new Error(`Discord send failed: ${response.status}`);
28
+ },
29
+ });
30
+
31
+ // Teach TypeScript about the new channel — picks up notify.discord and
32
+ // `defineNotification` typing.
33
+ declare module "@warlock.js/notifications" {
34
+ interface NotificationChannels {
35
+ discord: DiscordPayload;
36
+ }
37
+ }
38
+ ```
39
+
40
+ Register in `config/notifications.ts`:
41
+
42
+ ```ts
43
+ channels: {
44
+ // ...
45
+ discord: discordChannel(),
46
+ }
47
+ ```
48
+
49
+ Now `notify.discord(user, { content: "🎉" })` works, and `discord:` is a valid renderer key in `defineNotification`.
50
+
51
+ ## `route` resolution
52
+
53
+ | What `route` returns | Effect |
54
+ |---|---|
55
+ | `string` | Used directly as the address (`"user@example.com"`, webhook URL). |
56
+ | `{ id: Id }` | The recipient's storage id — for database/internal channels. |
57
+ | `undefined` | Dispatcher falls back to `{ id: notifiable.id }`. |
58
+ | `route` omitted | Same as returning `undefined` — `{ id }` fallback. |
59
+
60
+ For raw-target ad-hoc sends (`notify.discord("https://hooks.../foo", payload)`), the raw string wins over `route()`.
61
+
62
+ ## `send` — the contract
63
+
64
+ ```ts
65
+ async send({
66
+ payload, // P — the channel's payload type
67
+ route, // string | { id: Id } — resolved by `route()` or raw target
68
+ notifiable, // Notifiable | undefined — undefined for raw-target ad-hoc
69
+ options, // SendOptions — delay/locale/meta/idempotencyKey/force/type
70
+ }): Promise<void>
71
+ ```
72
+
73
+ Throw to fail the send. The dispatcher catches per channel, fires a `failed` event, and continues with siblings (per-channel isolation).
74
+
75
+ ## SDK-backed channels — lazy-import the SDK
76
+
77
+ Channels that wrap a heavy SDK (Twilio, Slack SDK, FCM SDK) should follow the lazy-import pattern so apps that don't use them don't pay the install/load cost. The recipe + race-safe template lives in [`develop-warlock.js-feature`](../../../../.claude/skills/develop-warlock.js-feature/SKILL.md) — copy from there; do not invent a variant.
78
+
79
+ Sketch:
80
+
81
+ ```ts
82
+ import type { WebClient } from "@slack/web-api";
83
+ let Slack: typeof import("@slack/web-api");
84
+ let isModuleExists: boolean | null = null;
85
+
86
+ async function loadSlack() { /* try/catch await import; set flag */ }
87
+ loadSlack();
88
+
89
+ export const slackChannel = (config: { token: string }) =>
90
+ defineChannel<{ text: string }>({
91
+ name: "slack",
92
+ route: (n) => n.get("slack_channel"),
93
+ async send({ payload, route }) {
94
+ await loadSlack();
95
+ if (!isModuleExists) throw new Error(INSTALL_INSTRUCTIONS);
96
+ const client = new Slack.WebClient(config.token);
97
+ await client.chat.postMessage({ channel: route as string, text: payload.text });
98
+ },
99
+ });
100
+ ```
101
+
102
+ ## See also
103
+
104
+ - [`configure-notifications/SKILL.md`](../configure-notifications/SKILL.md) — register channels in the config.
105
+ - [`send-ad-hoc/SKILL.md`](../send-ad-hoc/SKILL.md) — `notify.<custom-channel>` works after registration.
106
+ - [`define-notification/SKILL.md`](../define-notification/SKILL.md) — multi-channel renderers with the custom channel key.
107
+ - `D:/xampp/htdocs/mongez/node/.claude/skills/develop-warlock.js-feature/SKILL.md` — lazy-import pattern for SDK-backed channels.