@mantiq/notify 0.5.22 → 0.5.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantiq/notify",
3
- "version": "0.5.22",
3
+ "version": "0.5.23",
4
4
  "description": "Multi-channel notifications — mail, database, broadcast, SMS, Slack, webhook",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,6 +4,7 @@ import type { NotifyConfig } from './contracts/NotifyConfig.ts'
4
4
  import { DEFAULT_CONFIG } from './contracts/NotifyConfig.ts'
5
5
  import { Notification } from './Notification.ts'
6
6
  import { NotifyError } from './errors/NotifyError.ts'
7
+ import { NotificationSent, NotificationFailed } from './events/NotificationEvents.ts'
7
8
  import { MailChannel } from './channels/MailChannel.ts'
8
9
  import { DatabaseChannel } from './channels/DatabaseChannel.ts'
9
10
  import { BroadcastChannel } from './channels/BroadcastChannel.ts'
@@ -27,10 +28,22 @@ import { FirebaseChannel } from './channels/FirebaseChannel.ts'
27
28
  * await notify().send(user, new OrderShipped(order))
28
29
  * await notify().send([user1, user2], new OrderShipped(order))
29
30
  */
31
+ export interface DeliveryLogEntry {
32
+ id: string
33
+ notification: string
34
+ channel: string
35
+ recipient: string
36
+ status: 'sent' | 'failed'
37
+ sentAt: Date
38
+ error?: string | undefined
39
+ }
40
+
30
41
  export class NotificationManager {
31
42
  private _channels = new Map<string, NotificationChannel>()
32
43
  private _factories = new Map<string, () => NotificationChannel>()
33
44
  private config: NotifyConfig
45
+ private _deliveryLog: DeliveryLogEntry[] = []
46
+ private _eventObservers: ((event: NotificationSent | NotificationFailed) => void)[] = []
34
47
 
35
48
  constructor(config?: Partial<NotifyConfig>) {
36
49
  this.config = { ...DEFAULT_CONFIG, ...config }
@@ -53,14 +66,14 @@ export class NotificationManager {
53
66
  }
54
67
  }
55
68
 
56
- /** Send immediately, bypassing queue */
69
+ /** Send immediately, bypassing queue. Uses retry for each channel. */
57
70
  async sendNow(notifiable: Notifiable, notification: Notification, channelNames?: string[]): Promise<void> {
58
71
  const channels = channelNames ?? notification.via(notifiable)
59
72
 
60
73
  for (const channelName of channels) {
61
74
  try {
62
75
  const channel = this.channel(channelName)
63
- await channel.send(notifiable, notification)
76
+ await this.sendWithRetry(notifiable, notification, channel, 3)
64
77
  } catch (err) {
65
78
  // Log but don't fail other channels
66
79
  console.error(`[Mantiq] Notification channel "${channelName}" failed:`, (err as Error).message)
@@ -68,6 +81,78 @@ export class NotificationManager {
68
81
  }
69
82
  }
70
83
 
84
+ /**
85
+ * Send a notification through a channel with retry logic.
86
+ * Records delivery status and emits events.
87
+ */
88
+ async sendWithRetry(
89
+ notifiable: Notifiable,
90
+ notification: Notification,
91
+ channel: NotificationChannel,
92
+ maxRetries: number = 1,
93
+ ): Promise<void> {
94
+ let lastError: Error | undefined
95
+
96
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
97
+ try {
98
+ await channel.send(notifiable, notification)
99
+
100
+ // Success — record and emit
101
+ this._deliveryLog.push({
102
+ id: notification.id,
103
+ notification: notification.constructor.name,
104
+ channel: channel.name,
105
+ recipient: String(notifiable.getKey()),
106
+ status: 'sent',
107
+ sentAt: new Date(),
108
+ })
109
+
110
+ this.emitEvent(new NotificationSent(notifiable, notification, channel.name))
111
+ return
112
+ } catch (err) {
113
+ lastError = err as Error
114
+ }
115
+ }
116
+
117
+ // All retries exhausted — record failure and emit
118
+ this._deliveryLog.push({
119
+ id: notification.id,
120
+ notification: notification.constructor.name,
121
+ channel: channel.name,
122
+ recipient: String(notifiable.getKey()),
123
+ status: 'failed',
124
+ sentAt: new Date(),
125
+ error: lastError?.message,
126
+ })
127
+
128
+ this.emitEvent(new NotificationFailed(notifiable, notification, channel.name, lastError!))
129
+ throw lastError!
130
+ }
131
+
132
+ // ── Delivery Log ──────────────────────────────────────────────────────
133
+
134
+ /** Get all delivery log entries. */
135
+ deliveryLog(): DeliveryLogEntry[] {
136
+ return [...this._deliveryLog]
137
+ }
138
+
139
+ /** Get deliveries for a specific notification ID. */
140
+ deliveriesFor(notificationId: string): DeliveryLogEntry[] {
141
+ return this._deliveryLog.filter((e) => e.id === notificationId)
142
+ }
143
+
144
+ /** Clear the delivery log. */
145
+ clearDeliveryLog(): void {
146
+ this._deliveryLog = []
147
+ }
148
+
149
+ // ── Delivery Events ───────────────────────────────────────────────────
150
+
151
+ /** Register an observer for delivery events. */
152
+ onDeliveryEvent(observer: (event: NotificationSent | NotificationFailed) => void): void {
153
+ this._eventObservers.push(observer)
154
+ }
155
+
71
156
  /** Get a channel by name */
72
157
  channel(name: string): NotificationChannel {
73
158
  if (this._channels.has(name)) return this._channels.get(name)!
@@ -125,6 +210,16 @@ export class NotificationManager {
125
210
  if (ch?.firebase) this._factories.set('firebase', () => new FirebaseChannel(ch.firebase!))
126
211
  }
127
212
 
213
+ private emitEvent(event: NotificationSent | NotificationFailed): void {
214
+ for (const observer of this._eventObservers) {
215
+ try {
216
+ observer(event)
217
+ } catch {
218
+ // Observer errors must not break delivery
219
+ }
220
+ }
221
+ }
222
+
128
223
  private async queueNotification(notifiable: Notifiable, notification: Notification): Promise<void> {
129
224
  try {
130
225
  const { dispatch } = await import('@mantiq/queue')
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export type { NotifyConfig, SmsConfig, SlackConfig, TelegramConfig, WhatsAppConf
7
7
  export { Notification } from './Notification.ts'
8
8
  export type { SlackMessage, BroadcastPayload, SmsPayload, WebhookPayload } from './Notification.ts'
9
9
  export { NotificationManager } from './NotificationManager.ts'
10
+ export type { DeliveryLogEntry } from './NotificationManager.ts'
10
11
 
11
12
  // ── Channels ─────────────────────────────────────────────────────────────────
12
13
  export { MailChannel } from './channels/MailChannel.ts'