@warlock.js/notifications 5.19.1 → 5.21.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 -1
- package/llms-full.txt +891 -891
- package/llms.txt +8 -8
- 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/index.md +21 -0
- 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 +70 -70
package/llms.txt
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [configure-notifications](@warlock.js/notifications/configure-notifications/SKILL.md):
|
|
10
|
-
- [define-channel](@warlock.js/notifications/define-channel/SKILL.md):
|
|
11
|
-
- [define-notification](@warlock.js/notifications/define-notification/SKILL.md):
|
|
9
|
+
- [configure-notifications](@warlock.js/notifications/configure-notifications/SKILL.md): (no description)
|
|
10
|
+
- [define-channel](@warlock.js/notifications/define-channel/SKILL.md): (no description)
|
|
11
|
+
- [define-notification](@warlock.js/notifications/define-notification/SKILL.md): (no description)
|
|
12
12
|
- [notifications-basics](@warlock.js/notifications/notifications-basics/SKILL.md): Start here for @warlock.js/notifications channels, notification definitions, delivery, and application setup.
|
|
13
|
-
- [observe-notifications](@warlock.js/notifications/observe-notifications/SKILL.md):
|
|
14
|
-
- [queue-notifications](@warlock.js/notifications/queue-notifications/SKILL.md):
|
|
15
|
-
- [send-ad-hoc](@warlock.js/notifications/send-ad-hoc/SKILL.md):
|
|
16
|
-
- [use-in-app](@warlock.js/notifications/use-in-app/SKILL.md):
|
|
17
|
-
- [write-notification-migration](@warlock.js/notifications/write-notification-migration/SKILL.md):
|
|
13
|
+
- [observe-notifications](@warlock.js/notifications/observe-notifications/SKILL.md): (no description)
|
|
14
|
+
- [queue-notifications](@warlock.js/notifications/queue-notifications/SKILL.md): (no description)
|
|
15
|
+
- [send-ad-hoc](@warlock.js/notifications/send-ad-hoc/SKILL.md): (no description)
|
|
16
|
+
- [use-in-app](@warlock.js/notifications/use-in-app/SKILL.md): (no description)
|
|
17
|
+
- [write-notification-migration](@warlock.js/notifications/write-notification-migration/SKILL.md): (no description)
|
package/package.json
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
"environment": "server"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@warlock.js/cache": "5.
|
|
9
|
-
"@warlock.js/cascade": "5.
|
|
10
|
-
"@warlock.js/core": "5.
|
|
11
|
-
"@warlock.js/logger": "5.
|
|
8
|
+
"@warlock.js/cache": "5.21.0",
|
|
9
|
+
"@warlock.js/cascade": "5.21.0",
|
|
10
|
+
"@warlock.js/core": "5.21.0",
|
|
11
|
+
"@warlock.js/logger": "5.21.0"
|
|
12
12
|
},
|
|
13
13
|
"optionalDependencies": {
|
|
14
|
-
"@warlock.js/herald": "5.
|
|
14
|
+
"@warlock.js/herald": "5.21.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@warlock.js/queue": "5.
|
|
17
|
+
"@warlock.js/queue": "5.21.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.
|
|
38
|
+
"version": "5.21.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: "
|
|
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:
|
|
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.
|