@strav/notification 1.0.0-alpha.25
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/README.md +36 -0
- package/package.json +48 -0
- package/src/drivers/broadcast/broadcast_config.ts +21 -0
- package/src/drivers/broadcast/broadcast_notification_driver.ts +105 -0
- package/src/drivers/broadcast/broadcast_notification_provider.ts +18 -0
- package/src/drivers/broadcast/index.ts +7 -0
- package/src/drivers/database/apply_notification_migration.ts +44 -0
- package/src/drivers/database/database_config.ts +7 -0
- package/src/drivers/database/database_notification_driver.ts +72 -0
- package/src/drivers/database/database_notification_provider.ts +44 -0
- package/src/drivers/database/index.ts +16 -0
- package/src/drivers/database/notification_record.ts +22 -0
- package/src/drivers/database/notification_repository.ts +67 -0
- package/src/drivers/database/schemas/notification_schema.ts +44 -0
- package/src/drivers/database/tenanted/apply_tenanted_notification_migration.ts +19 -0
- package/src/drivers/database/tenanted/index.ts +10 -0
- package/src/drivers/database/tenanted/schemas/tenanted_notification_schema.ts +28 -0
- package/src/drivers/database/tenanted/tenanted_notification_record.ts +15 -0
- package/src/drivers/database/tenanted/tenanted_notification_repository.ts +63 -0
- package/src/drivers/log/index.ts +6 -0
- package/src/drivers/log/log_config.ts +12 -0
- package/src/drivers/log/log_notification_driver.ts +72 -0
- package/src/drivers/log/log_notification_provider.ts +29 -0
- package/src/drivers/mail/index.ts +6 -0
- package/src/drivers/mail/mail_config.ts +7 -0
- package/src/drivers/mail/mail_notification_driver.ts +66 -0
- package/src/drivers/mail/mail_notification_provider.ts +18 -0
- package/src/drivers/mock.ts +48 -0
- package/src/drivers/unsupported.ts +15 -0
- package/src/drivers/webhook/index.ts +10 -0
- package/src/drivers/webhook/sign.ts +47 -0
- package/src/drivers/webhook/webhook_config.ts +43 -0
- package/src/drivers/webhook/webhook_notification_driver.ts +172 -0
- package/src/drivers/webhook/webhook_notification_provider.ts +44 -0
- package/src/index.ts +38 -0
- package/src/notifiable.ts +22 -0
- package/src/notification.ts +26 -0
- package/src/notification_config.ts +26 -0
- package/src/notification_driver.ts +40 -0
- package/src/notification_error.ts +62 -0
- package/src/notification_manager.ts +135 -0
- package/src/notification_provider.ts +42 -0
- package/src/types.ts +34 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Public API of @strav/notification.
|
|
2
|
+
//
|
|
3
|
+
// V1: NotificationManager facade + NotificationDriver interface +
|
|
4
|
+
// BaseNotification abstract class + Notifiable interface + four
|
|
5
|
+
// channel drivers under subpaths (./mail, ./database, ./log, ./webhook).
|
|
6
|
+
// Broadcast / SSE / Discord / SMS channels deferred to future slices.
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
MockNotificationDriver,
|
|
10
|
+
type MockNotificationRecord,
|
|
11
|
+
mockNotificationDriverFactory,
|
|
12
|
+
} from './drivers/mock.ts'
|
|
13
|
+
export type { Notifiable } from './notifiable.ts'
|
|
14
|
+
export { BaseNotification } from './notification.ts'
|
|
15
|
+
export type {
|
|
16
|
+
ChannelConfig,
|
|
17
|
+
NotificationConfig,
|
|
18
|
+
} from './notification_config.ts'
|
|
19
|
+
export type {
|
|
20
|
+
NotificationDriver,
|
|
21
|
+
NotificationDriverFactory,
|
|
22
|
+
} from './notification_driver.ts'
|
|
23
|
+
export {
|
|
24
|
+
NotificationConfigError,
|
|
25
|
+
NotificationDeliveryError,
|
|
26
|
+
NotificationError,
|
|
27
|
+
UnknownChannelError,
|
|
28
|
+
} from './notification_error.ts'
|
|
29
|
+
export {
|
|
30
|
+
NotificationManager,
|
|
31
|
+
type NotificationManagerOptions,
|
|
32
|
+
} from './notification_manager.ts'
|
|
33
|
+
export { NotificationProvider } from './notification_provider.ts'
|
|
34
|
+
export type {
|
|
35
|
+
NotificationContext,
|
|
36
|
+
NotificationDeliveryResult,
|
|
37
|
+
NotificationDispatchResult,
|
|
38
|
+
} from './types.ts'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Notifiable` — the minimum a notification recipient must expose.
|
|
3
|
+
*
|
|
4
|
+
* Apps' domain models implement this interface (or extend it via mixin
|
|
5
|
+
* / Repository pattern). Channel-specific data lives on the domain
|
|
6
|
+
* shape: a `User` notifiable might have `email: string` for the mail
|
|
7
|
+
* channel, `phone: string` for an SMS channel, and any per-channel
|
|
8
|
+
* routing-preference fields the app cares about.
|
|
9
|
+
*
|
|
10
|
+
* The framework stays out of the routing decision — each notification's
|
|
11
|
+
* `via(notifiable)` returns the channels to dispatch through, and each
|
|
12
|
+
* channel reads what it needs off the notifiable directly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Notifiable {
|
|
16
|
+
/** Identity. Channels persist this on delivery rows so apps can resolve back. */
|
|
17
|
+
readonly id: string | number
|
|
18
|
+
/** Class / type name. Channels record this alongside the id for polymorphic resolution. */
|
|
19
|
+
readonly notifiableType?: string
|
|
20
|
+
/** Apps add channel-specific fields directly: `email`, `phone`, `preferences`, …. */
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BaseNotification` — apps subclass this to define a notification.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities the subclass owns:
|
|
5
|
+
*
|
|
6
|
+
* 1. `via(notifiable)` — return the channel names the manager
|
|
7
|
+
* fan-outs to. Apps that pre-load a user's channel preferences
|
|
8
|
+
* branch here (e.g. `notifiable.preferences.email === true`).
|
|
9
|
+
*
|
|
10
|
+
* 2. One `to<Channel>(notifiable)` hook per channel the notification
|
|
11
|
+
* supports. The hook returns the channel's input shape (a `Message`
|
|
12
|
+
* for mail, a `NotificationPayload` for database, etc.). Channels
|
|
13
|
+
* whose hook isn't implemented get skipped — no runtime error.
|
|
14
|
+
*
|
|
15
|
+
* Hooks aren't declared on the base class because each channel knows
|
|
16
|
+
* its own input type and reaches for the named method at dispatch time.
|
|
17
|
+
* Apps that want compile-time hook enforcement extend a per-channel
|
|
18
|
+
* mixin (out of scope for v1 — bring your own discipline for now).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Notifiable } from './notifiable.ts'
|
|
22
|
+
|
|
23
|
+
export abstract class BaseNotification {
|
|
24
|
+
/** Channel names the manager fan-outs to. Apps override. */
|
|
25
|
+
abstract via(notifiable: Notifiable): readonly string[]
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification configuration shape — what `config.notification` looks
|
|
3
|
+
* like. Mirrors the manager-pattern config used by `@strav/payment`
|
|
4
|
+
* and `@strav/social`: a `default` channel key + a `channels` map
|
|
5
|
+
* keyed by name. Each channel entry carries its driver + driver-
|
|
6
|
+
* specific options.
|
|
7
|
+
*
|
|
8
|
+
* `default` is OPTIONAL on this manager: notifications route per-call
|
|
9
|
+
* via `via(notifiable)`, not via a single configured default. Apps
|
|
10
|
+
* set `default` only when they want `manager.use()` (no arg) to
|
|
11
|
+
* resolve to a specific channel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface NotificationConfig {
|
|
15
|
+
/** Optional default channel name — must exist in `channels` when set. */
|
|
16
|
+
default?: string
|
|
17
|
+
/** Channel registry. Each entry is one configured backend. */
|
|
18
|
+
channels: Record<string, ChannelConfig>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChannelConfig {
|
|
22
|
+
/** Driver identifier — matches a registered factory (`mail`, `database`, `log`, or custom). */
|
|
23
|
+
driver: string
|
|
24
|
+
/** Free-form driver-specific fields. */
|
|
25
|
+
[key: string]: unknown
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NotificationDriver` — the contract every channel implements.
|
|
3
|
+
*
|
|
4
|
+
* One driver instance per configured channel. The manager calls
|
|
5
|
+
* `send(notifiable, notification, context)` once per channel that
|
|
6
|
+
* `notification.via(notifiable)` named; drivers either deliver or
|
|
7
|
+
* return `{ delivered: false, error }`.
|
|
8
|
+
*
|
|
9
|
+
* Drivers MAY ignore notifications they can't service (no
|
|
10
|
+
* `to<Channel>` hook, recipient missing channel-required fields) by
|
|
11
|
+
* returning `{ delivered: false }` with no `error` — the manager
|
|
12
|
+
* surfaces this in the dispatch result without treating it as a
|
|
13
|
+
* failure.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Notifiable } from './notifiable.ts'
|
|
17
|
+
import type { BaseNotification } from './notification.ts'
|
|
18
|
+
import type { NotificationContext, NotificationDeliveryResult } from './types.ts'
|
|
19
|
+
|
|
20
|
+
export interface NotificationDriver {
|
|
21
|
+
/** Identifier — matches `config.notification.channels` key. */
|
|
22
|
+
readonly name: string
|
|
23
|
+
|
|
24
|
+
send(
|
|
25
|
+
notifiable: Notifiable,
|
|
26
|
+
notification: BaseNotification,
|
|
27
|
+
context: NotificationContext,
|
|
28
|
+
): Promise<NotificationDeliveryResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Channel factory — apps register custom channels via
|
|
33
|
+
* `manager.extend(name, factory)`. The factory receives the channel's
|
|
34
|
+
* config sub-tree plus an `instanceName` (the key under
|
|
35
|
+
* `config.notification.channels`).
|
|
36
|
+
*/
|
|
37
|
+
export type NotificationDriverFactory = (args: {
|
|
38
|
+
instanceName: string
|
|
39
|
+
config: { driver: string; [key: string]: unknown }
|
|
40
|
+
}) => NotificationDriver
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy. Mirrors `@strav/payment`'s `PaymentError`
|
|
3
|
+
* shape: a base `NotificationError` extending `StravError`, plus
|
|
4
|
+
* subclasses with specific codes apps can branch on.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StravError } from '@strav/kernel'
|
|
8
|
+
|
|
9
|
+
export class NotificationError extends StravError {
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
options: {
|
|
13
|
+
code?: string
|
|
14
|
+
status?: number
|
|
15
|
+
context?: Record<string, unknown>
|
|
16
|
+
cause?: unknown
|
|
17
|
+
} = {},
|
|
18
|
+
) {
|
|
19
|
+
super(
|
|
20
|
+
message,
|
|
21
|
+
{ code: options.code ?? 'notification.error', status: options.status ?? 500 },
|
|
22
|
+
{
|
|
23
|
+
...(options.context ? { context: options.context } : {}),
|
|
24
|
+
...(options.cause !== undefined ? { cause: options.cause } : {}),
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class NotificationConfigError extends NotificationError {
|
|
31
|
+
constructor(message: string, options: { context?: Record<string, unknown> } = {}) {
|
|
32
|
+
super(message, {
|
|
33
|
+
code: 'notification.config',
|
|
34
|
+
status: 500,
|
|
35
|
+
...(options.context ? { context: options.context } : {}),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UnknownChannelError extends NotificationError {
|
|
41
|
+
constructor(message: string, options: { context?: Record<string, unknown> } = {}) {
|
|
42
|
+
super(message, {
|
|
43
|
+
code: 'notification.unknown_channel',
|
|
44
|
+
status: 400,
|
|
45
|
+
...(options.context ? { context: options.context } : {}),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class NotificationDeliveryError extends NotificationError {
|
|
51
|
+
constructor(
|
|
52
|
+
message: string,
|
|
53
|
+
options: { context?: Record<string, unknown>; cause?: unknown } = {},
|
|
54
|
+
) {
|
|
55
|
+
super(message, {
|
|
56
|
+
code: 'notification.delivery',
|
|
57
|
+
status: 502,
|
|
58
|
+
...(options.context ? { context: options.context } : {}),
|
|
59
|
+
...(options.cause !== undefined ? { cause: options.cause } : {}),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NotificationManager` — the facade apps inject for sending
|
|
3
|
+
* notifications.
|
|
4
|
+
*
|
|
5
|
+
* Two concept clusters:
|
|
6
|
+
*
|
|
7
|
+
* - **Channels.** Apps declare configured channels in
|
|
8
|
+
* `config.notification.channels`. The manager constructs each
|
|
9
|
+
* channel driver lazily on first `use(name)` + memoizes. Custom
|
|
10
|
+
* channels register via `manager.extend(name, factory)`. Tests
|
|
11
|
+
* hand-wire via `manager.useDriver(name, driver)`.
|
|
12
|
+
*
|
|
13
|
+
* - **Fan-out.** `send(notifiable, notification)` calls
|
|
14
|
+
* `notification.via(notifiable)` to get the channel list, then
|
|
15
|
+
* dispatches each channel in order, collecting per-channel
|
|
16
|
+
* `NotificationDeliveryResult`s into a single
|
|
17
|
+
* `NotificationDispatchResult`. Channels that throw are
|
|
18
|
+
* captured into the result (`delivered: false`, `error: ...`)
|
|
19
|
+
* — the manager never rethrows; apps inspect the result.
|
|
20
|
+
*
|
|
21
|
+
* One ULID per send shared across channels (for correlation in
|
|
22
|
+
* downstream logs / persistence). The manager constructs the
|
|
23
|
+
* `NotificationContext` once and threads it through.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { ulid } from '@strav/kernel'
|
|
27
|
+
import type { Notifiable } from './notifiable.ts'
|
|
28
|
+
import type { BaseNotification } from './notification.ts'
|
|
29
|
+
import type { NotificationConfig } from './notification_config.ts'
|
|
30
|
+
import type { NotificationDriver, NotificationDriverFactory } from './notification_driver.ts'
|
|
31
|
+
import { NotificationConfigError, UnknownChannelError } from './notification_error.ts'
|
|
32
|
+
import type {
|
|
33
|
+
NotificationContext,
|
|
34
|
+
NotificationDeliveryResult,
|
|
35
|
+
NotificationDispatchResult,
|
|
36
|
+
} from './types.ts'
|
|
37
|
+
|
|
38
|
+
export interface NotificationManagerOptions {
|
|
39
|
+
config: NotificationConfig
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class NotificationManager {
|
|
43
|
+
readonly config: NotificationConfig
|
|
44
|
+
|
|
45
|
+
private readonly drivers = new Map<string, NotificationDriver>()
|
|
46
|
+
private readonly extensions = new Map<string, NotificationDriverFactory>()
|
|
47
|
+
|
|
48
|
+
constructor(options: NotificationManagerOptions) {
|
|
49
|
+
const { config } = options
|
|
50
|
+
if (config.default !== undefined && !config.channels[config.default]) {
|
|
51
|
+
throw new NotificationConfigError(
|
|
52
|
+
`NotificationManager: default channel "${config.default}" is not configured.`,
|
|
53
|
+
{
|
|
54
|
+
context: {
|
|
55
|
+
default: config.default,
|
|
56
|
+
available: Object.keys(config.channels),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
this.config = config
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Channel routing ──────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/** Resolve a channel by name (or the default when omitted). */
|
|
67
|
+
use(name?: string): NotificationDriver {
|
|
68
|
+
const key = name ?? this.config.default
|
|
69
|
+
if (key === undefined) {
|
|
70
|
+
throw new NotificationConfigError(
|
|
71
|
+
'NotificationManager.use(): no name given and no default channel is configured.',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const cached = this.drivers.get(key)
|
|
75
|
+
if (cached) return cached
|
|
76
|
+
|
|
77
|
+
const cfg = this.config.channels[key]
|
|
78
|
+
if (!cfg) {
|
|
79
|
+
throw new UnknownChannelError(`NotificationManager: channel "${key}" is not configured.`, {
|
|
80
|
+
context: { requested: key, available: Object.keys(this.config.channels) },
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const factory = this.extensions.get(cfg.driver)
|
|
85
|
+
if (!factory) {
|
|
86
|
+
throw new UnknownChannelError(
|
|
87
|
+
`NotificationManager: unknown driver "${cfg.driver}" for channel "${key}". Register it via \`manager.extend("${cfg.driver}", factory)\`.`,
|
|
88
|
+
{ context: { driver: cfg.driver, available: [...this.extensions.keys()] } },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
const driver = factory({ instanceName: key, config: cfg })
|
|
92
|
+
this.drivers.set(key, driver)
|
|
93
|
+
return driver
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Register a channel driver factory. Adapter packages call this from their ServiceProvider. */
|
|
97
|
+
extend(driverName: string, factory: NotificationDriverFactory): void {
|
|
98
|
+
this.extensions.set(driverName, factory)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Hand-wire a channel instance under an app-chosen name (tests / one-offs). */
|
|
102
|
+
useDriver(instanceName: string, driver: NotificationDriver): void {
|
|
103
|
+
this.drivers.set(instanceName, driver)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Fan-out ──────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async send(
|
|
109
|
+
notifiable: Notifiable,
|
|
110
|
+
notification: BaseNotification,
|
|
111
|
+
): Promise<NotificationDispatchResult> {
|
|
112
|
+
const context: NotificationContext = {
|
|
113
|
+
id: ulid(),
|
|
114
|
+
dispatchedAt: new Date(),
|
|
115
|
+
}
|
|
116
|
+
const channels = notification.via(notifiable)
|
|
117
|
+
const deliveries: NotificationDeliveryResult[] = []
|
|
118
|
+
|
|
119
|
+
for (const channelName of channels) {
|
|
120
|
+
try {
|
|
121
|
+
const driver = this.use(channelName)
|
|
122
|
+
const result = await driver.send(notifiable, notification, context)
|
|
123
|
+
deliveries.push(result)
|
|
124
|
+
} catch (err) {
|
|
125
|
+
deliveries.push({
|
|
126
|
+
channel: channelName,
|
|
127
|
+
delivered: false,
|
|
128
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { id: context.id, deliveries }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NotificationProvider` — ServiceProvider that wires
|
|
3
|
+
* `NotificationManager` into the container.
|
|
4
|
+
*
|
|
5
|
+
* Eager construction at boot — a malformed config (missing default
|
|
6
|
+
* channel, unknown channel driver) surfaces at startup, not on first
|
|
7
|
+
* `send()` call.
|
|
8
|
+
*
|
|
9
|
+
* Channel adapter packages register themselves AFTER this provider:
|
|
10
|
+
* each `<Channel>NotificationProvider` declares `dependencies =
|
|
11
|
+
* ['notification']` and calls `manager.extend(name, factory)` from
|
|
12
|
+
* its `boot()` so the channel becomes available without the manager
|
|
13
|
+
* needing to import it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type Application, ConfigError, ConfigRepository, ServiceProvider } from '@strav/kernel'
|
|
17
|
+
import type { NotificationConfig } from './notification_config.ts'
|
|
18
|
+
import { NotificationManager } from './notification_manager.ts'
|
|
19
|
+
|
|
20
|
+
export class NotificationProvider extends ServiceProvider {
|
|
21
|
+
override readonly name = 'notification'
|
|
22
|
+
override readonly dependencies = ['config']
|
|
23
|
+
|
|
24
|
+
override register(app: Application): void {
|
|
25
|
+
app.singleton(NotificationManager, (c) => {
|
|
26
|
+
const config = c.resolve(ConfigRepository).get('notification') as
|
|
27
|
+
| NotificationConfig
|
|
28
|
+
| undefined
|
|
29
|
+
if (!config) {
|
|
30
|
+
throw new ConfigError(
|
|
31
|
+
'NotificationProvider: `config.notification` is missing. Add `config/notification.ts` with at least one channel.',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return new NotificationManager({ config })
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override async boot(app: Application): Promise<void> {
|
|
39
|
+
// Force-resolve so config errors surface at boot, not on first call.
|
|
40
|
+
app.resolve(NotificationManager)
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-send metadata. The manager constructs one on every `send()` call
|
|
3
|
+
* and threads it through each channel driver. Apps can use the
|
|
4
|
+
* `idempotencyKey` to deduplicate downstream (e.g. preventing the
|
|
5
|
+
* database channel from inserting two rows for the same retry).
|
|
6
|
+
*/
|
|
7
|
+
export interface NotificationContext {
|
|
8
|
+
/** ULID — stable per send, shared across channels. */
|
|
9
|
+
id: string
|
|
10
|
+
dispatchedAt: Date
|
|
11
|
+
/** App-supplied idempotency key. Optional. */
|
|
12
|
+
idempotencyKey?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Outcome a channel driver reports back. The manager aggregates these
|
|
17
|
+
* into a `NotificationDispatchResult` so apps can branch on per-channel
|
|
18
|
+
* success / failure without each driver having to throw.
|
|
19
|
+
*/
|
|
20
|
+
export interface NotificationDeliveryResult {
|
|
21
|
+
channel: string
|
|
22
|
+
delivered: boolean
|
|
23
|
+
/** Driver-specific reference (mail message id, database row id, etc.). */
|
|
24
|
+
reference?: string
|
|
25
|
+
/** Set when `delivered === false`. */
|
|
26
|
+
error?: Error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NotificationDispatchResult {
|
|
30
|
+
/** Notification ULID — matches `NotificationContext.id`. */
|
|
31
|
+
id: string
|
|
32
|
+
/** One entry per channel attempted, in the order `via()` returned them. */
|
|
33
|
+
deliveries: readonly NotificationDeliveryResult[]
|
|
34
|
+
}
|