@vynelix/vynemit-core 1.0.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/README.md +400 -0
- package/dist/index.d.mts +335 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +829 -0
- package/dist/index.mjs +799 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
// src/notification_center.ts
|
|
2
|
+
var NotificationCenter = class {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
this.storage = config.storage;
|
|
6
|
+
this.queue = config.queue;
|
|
7
|
+
this.middleware = config.middleware || [];
|
|
8
|
+
this.transports = /* @__PURE__ */ new Map();
|
|
9
|
+
config.transports.forEach((transport) => {
|
|
10
|
+
this.transports.set(transport.name, transport);
|
|
11
|
+
});
|
|
12
|
+
this.templates = /* @__PURE__ */ new Map();
|
|
13
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
14
|
+
this.eventSubscribers = /* @__PURE__ */ new Map();
|
|
15
|
+
this.unreadSubscribers = /* @__PURE__ */ new Map();
|
|
16
|
+
this.isRunning = false;
|
|
17
|
+
}
|
|
18
|
+
// ========== DISPATCH ==========
|
|
19
|
+
async send(input) {
|
|
20
|
+
let notification = this.buildNotification(input);
|
|
21
|
+
notification = await this.applyBeforeSendMiddleware(notification);
|
|
22
|
+
if (!notification) {
|
|
23
|
+
throw new Error("Notification was filtered out by middleware");
|
|
24
|
+
}
|
|
25
|
+
await this.storage.save(notification);
|
|
26
|
+
try {
|
|
27
|
+
if (this.queue) {
|
|
28
|
+
console.log("Notification queued");
|
|
29
|
+
if (notification.scheduledFor) {
|
|
30
|
+
console.log("Notification scheduled for: " + notification.scheduledFor.toLocaleDateString());
|
|
31
|
+
const delay = notification.scheduledFor.getTime() - Date.now();
|
|
32
|
+
console.log("Notification delay: " + delay + "ms");
|
|
33
|
+
await this.queue.enqueueDelayed(notification, delay);
|
|
34
|
+
} else {
|
|
35
|
+
console.log("Notification sent directly after enqueue");
|
|
36
|
+
await this.queue.enqueue(notification);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
console.log("Notification sent directly in sendNow");
|
|
40
|
+
await this.sendNow(notification);
|
|
41
|
+
}
|
|
42
|
+
return notification;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
await this.applyErrorMiddleware(error, notification);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async sendBatch(inputs) {
|
|
49
|
+
const notifications = await Promise.all(
|
|
50
|
+
inputs.map((input) => this.send(input))
|
|
51
|
+
);
|
|
52
|
+
return notifications;
|
|
53
|
+
}
|
|
54
|
+
async sendMulticast(input) {
|
|
55
|
+
const notifications = await Promise.all(input.userIds.map(async (userId) => {
|
|
56
|
+
const notificationInput = { ...input, userId };
|
|
57
|
+
let notification = this.buildNotification(notificationInput);
|
|
58
|
+
notification = await this.applyBeforeSendMiddleware(notification);
|
|
59
|
+
return notification;
|
|
60
|
+
}));
|
|
61
|
+
const validNotifications = notifications.filter((n) => n !== null);
|
|
62
|
+
if (validNotifications.length === 0)
|
|
63
|
+
return [];
|
|
64
|
+
await this.storage.saveBatch(validNotifications);
|
|
65
|
+
const channels = input.channels !== void 0 ? input.channels : Array.from(this.transports.keys());
|
|
66
|
+
for (const channel of channels) {
|
|
67
|
+
const transport = this.transports.get(channel);
|
|
68
|
+
if (!transport)
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
if (transport.sendMulticast) {
|
|
72
|
+
if (channel === "push" || channel === "sms") {
|
|
73
|
+
await Promise.all(validNotifications.map(async (n) => {
|
|
74
|
+
const field = channel === "push" ? "deviceToken" : "phoneNumber";
|
|
75
|
+
if (!n.data?.[field]) {
|
|
76
|
+
const prefs = await this.storage.getPreferences(n.userId);
|
|
77
|
+
if (prefs.data?.[field]) {
|
|
78
|
+
n.data = { ...n.data || {}, [field]: prefs.data[field] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
const receipts = await transport.sendMulticast(validNotifications, {});
|
|
84
|
+
if (this.storage.saveReceipt) {
|
|
85
|
+
await Promise.all(receipts.map((r) => this.storage.saveReceipt(r)));
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
await Promise.all(validNotifications.map((n) => this.sendNow(n)));
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`Multicast failed for channel ${channel}:`, error);
|
|
92
|
+
await Promise.all(validNotifications.map((n) => this.applyErrorMiddleware(error, n)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return validNotifications;
|
|
96
|
+
}
|
|
97
|
+
async schedule(input, when) {
|
|
98
|
+
let notification = this.buildNotification({ ...input, scheduledFor: when });
|
|
99
|
+
let _notification = await this.applyBeforeSendMiddleware(notification);
|
|
100
|
+
if (!_notification) {
|
|
101
|
+
throw new Error("Notification was filtered out by middleware");
|
|
102
|
+
}
|
|
103
|
+
await this.storage.save(_notification);
|
|
104
|
+
if (this.queue) {
|
|
105
|
+
const delay = when.getTime() - Date.now();
|
|
106
|
+
if (delay > 0) {
|
|
107
|
+
await this.queue.enqueueDelayed(_notification, delay);
|
|
108
|
+
} else {
|
|
109
|
+
await this.queue.enqueue(_notification);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return _notification.id;
|
|
113
|
+
}
|
|
114
|
+
// ========== QUERYING ==========
|
|
115
|
+
async getForUser(userId, filters) {
|
|
116
|
+
return this.storage.findByUser(userId, filters);
|
|
117
|
+
}
|
|
118
|
+
async getUnreadCount(userId) {
|
|
119
|
+
return this.storage.countUnread(userId);
|
|
120
|
+
}
|
|
121
|
+
async getById(id) {
|
|
122
|
+
return this.storage.findById(id);
|
|
123
|
+
}
|
|
124
|
+
async getStats(userId) {
|
|
125
|
+
const all = await this.storage.findByUser(userId, {});
|
|
126
|
+
const unread = await this.storage.countUnread(userId);
|
|
127
|
+
const stats = {
|
|
128
|
+
total: all.length,
|
|
129
|
+
unread,
|
|
130
|
+
byStatus: {},
|
|
131
|
+
byChannel: {},
|
|
132
|
+
byPriority: {}
|
|
133
|
+
};
|
|
134
|
+
all.forEach((notif) => {
|
|
135
|
+
stats.byStatus[notif.status] = (stats.byStatus[notif.status] || 0) + 1;
|
|
136
|
+
stats.byPriority[notif.priority] = (stats.byPriority[notif.priority] || 0) + 1;
|
|
137
|
+
notif.channels.forEach((channel) => {
|
|
138
|
+
stats.byChannel[channel] = (stats.byChannel[channel] || 0) + 1;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
return stats;
|
|
142
|
+
}
|
|
143
|
+
// ========== STATE MANAGEMENT ==========
|
|
144
|
+
async markAsRead(notificationId) {
|
|
145
|
+
await this.storage.markAsRead(notificationId);
|
|
146
|
+
const notification = await this.storage.findById(notificationId);
|
|
147
|
+
if (notification) {
|
|
148
|
+
this.notifyEventSubscribers({
|
|
149
|
+
type: "read",
|
|
150
|
+
notification,
|
|
151
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
152
|
+
});
|
|
153
|
+
const count = await this.storage.countUnread(notification.userId);
|
|
154
|
+
this.notifyUnreadSubscribers(notification.userId, count);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async markAllAsRead(userId) {
|
|
158
|
+
await this.storage.markAllAsRead(userId);
|
|
159
|
+
this.notifyUnreadSubscribers(userId, 0);
|
|
160
|
+
}
|
|
161
|
+
async markAsUnread(notificationId) {
|
|
162
|
+
await this.storage.markAsUnread(notificationId);
|
|
163
|
+
const notification = await this.storage.findById(notificationId);
|
|
164
|
+
if (notification) {
|
|
165
|
+
this.notifyEventSubscribers({
|
|
166
|
+
type: "unread",
|
|
167
|
+
notification,
|
|
168
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
169
|
+
});
|
|
170
|
+
const count = await this.storage.countUnread(notification.userId);
|
|
171
|
+
this.notifyUnreadSubscribers(notification.userId, count);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async markAllAsUnread(userId) {
|
|
175
|
+
await this.storage.markAllAsUnread(userId);
|
|
176
|
+
this.notifyUnreadSubscribers(userId, 0);
|
|
177
|
+
}
|
|
178
|
+
async delete(notificationId) {
|
|
179
|
+
const notification = await this.storage.findById(notificationId);
|
|
180
|
+
await this.storage.delete(notificationId);
|
|
181
|
+
if (notification && notification.status !== "read") {
|
|
182
|
+
const count = await this.storage.countUnread(notification.userId);
|
|
183
|
+
this.notifyUnreadSubscribers(notification.userId, count);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async deleteAll(userId) {
|
|
187
|
+
const notifications = await this.storage.findByUser(userId, {});
|
|
188
|
+
await Promise.all(
|
|
189
|
+
notifications.map((notif) => this.storage.delete(notif.id))
|
|
190
|
+
);
|
|
191
|
+
this.notifyUnreadSubscribers(userId, 0);
|
|
192
|
+
}
|
|
193
|
+
// ========== PREFERENCES ==========
|
|
194
|
+
async getPreferences(userId) {
|
|
195
|
+
return this.storage.getPreferences(userId);
|
|
196
|
+
}
|
|
197
|
+
async updatePreferences(userId, prefs) {
|
|
198
|
+
const current = await this.storage.getPreferences(userId);
|
|
199
|
+
const updated = {
|
|
200
|
+
...current,
|
|
201
|
+
...prefs,
|
|
202
|
+
userId,
|
|
203
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
204
|
+
};
|
|
205
|
+
await this.storage.savePreferences(userId, updated);
|
|
206
|
+
}
|
|
207
|
+
// ========== TEMPLATES ==========
|
|
208
|
+
registerTemplate(template) {
|
|
209
|
+
this.templates.set(template.id, template);
|
|
210
|
+
}
|
|
211
|
+
getTemplate(id) {
|
|
212
|
+
return this.templates.get(id);
|
|
213
|
+
}
|
|
214
|
+
unregisterTemplate(id) {
|
|
215
|
+
this.templates.delete(id);
|
|
216
|
+
}
|
|
217
|
+
// ========== DIGEST ==========
|
|
218
|
+
async enableDigest(userId, config) {
|
|
219
|
+
const prefs = await this.getPreferences(userId);
|
|
220
|
+
await this.updatePreferences(userId, {
|
|
221
|
+
...prefs,
|
|
222
|
+
data: {
|
|
223
|
+
...prefs.data || {},
|
|
224
|
+
digestConfig: config
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async disableDigest(userId) {
|
|
229
|
+
const prefs = await this.getPreferences(userId);
|
|
230
|
+
const newData = { ...prefs.data || {} };
|
|
231
|
+
delete newData.digestConfig;
|
|
232
|
+
await this.updatePreferences(userId, {
|
|
233
|
+
...prefs,
|
|
234
|
+
data: newData
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async getDigestConfig(userId) {
|
|
238
|
+
const prefs = await this.getPreferences(userId);
|
|
239
|
+
return prefs.data?.digestConfig || null;
|
|
240
|
+
}
|
|
241
|
+
// ========== DELIVERY STATUS ==========
|
|
242
|
+
async getDeliveryStatus(notificationId) {
|
|
243
|
+
if (!this.storage.getReceipts) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
return this.storage.getReceipts(notificationId);
|
|
247
|
+
}
|
|
248
|
+
async retryFailed(notificationId, channel) {
|
|
249
|
+
const notification = await this.storage.findById(notificationId);
|
|
250
|
+
if (!notification) {
|
|
251
|
+
throw new Error(`Notification ${notificationId} not found`);
|
|
252
|
+
}
|
|
253
|
+
const receipts = await this.getDeliveryStatus(notificationId);
|
|
254
|
+
const failedReceipts = receipts.filter(
|
|
255
|
+
(r) => r.status === "failed" && (!channel || r.channel === channel)
|
|
256
|
+
);
|
|
257
|
+
for (const receipt of failedReceipts) {
|
|
258
|
+
const transport = this.transports.get(receipt.channel);
|
|
259
|
+
if (transport) {
|
|
260
|
+
const prefs = await this.getPreferences(notification.userId);
|
|
261
|
+
await transport.send(notification, prefs);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ========== SUBSCRIPTIONS (Reactive) ==========
|
|
266
|
+
subscribe(userId, callback) {
|
|
267
|
+
const sid = String(userId);
|
|
268
|
+
if (!this.subscribers.has(sid)) {
|
|
269
|
+
this.subscribers.set(sid, /* @__PURE__ */ new Set());
|
|
270
|
+
}
|
|
271
|
+
this.subscribers.get(sid).add(callback);
|
|
272
|
+
return () => {
|
|
273
|
+
const subs = this.subscribers.get(sid);
|
|
274
|
+
if (subs) {
|
|
275
|
+
subs.delete(callback);
|
|
276
|
+
if (subs.size === 0) {
|
|
277
|
+
this.subscribers.delete(sid);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
subscribeToEvents(userId, callback) {
|
|
283
|
+
const sid = String(userId);
|
|
284
|
+
if (!this.eventSubscribers.has(sid)) {
|
|
285
|
+
this.eventSubscribers.set(sid, /* @__PURE__ */ new Set());
|
|
286
|
+
}
|
|
287
|
+
this.eventSubscribers.get(sid).add(callback);
|
|
288
|
+
return () => {
|
|
289
|
+
const subs = this.eventSubscribers.get(sid);
|
|
290
|
+
if (subs) {
|
|
291
|
+
subs.delete(callback);
|
|
292
|
+
if (subs.size === 0) {
|
|
293
|
+
this.eventSubscribers.delete(sid);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
onUnreadCountChange(userId, callback) {
|
|
299
|
+
const sid = String(userId);
|
|
300
|
+
if (!this.unreadSubscribers.has(sid)) {
|
|
301
|
+
this.unreadSubscribers.set(sid, /* @__PURE__ */ new Set());
|
|
302
|
+
}
|
|
303
|
+
this.unreadSubscribers.get(sid).add(callback);
|
|
304
|
+
return () => {
|
|
305
|
+
const subs = this.unreadSubscribers.get(sid);
|
|
306
|
+
if (subs) {
|
|
307
|
+
subs.delete(callback);
|
|
308
|
+
if (subs.size === 0) {
|
|
309
|
+
this.unreadSubscribers.delete(sid);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// ========== MIDDLEWARE ==========
|
|
315
|
+
use(middleware) {
|
|
316
|
+
this.middleware.push(middleware);
|
|
317
|
+
}
|
|
318
|
+
removeMiddleware(name) {
|
|
319
|
+
this.middleware = this.middleware.filter((m) => m.name !== name);
|
|
320
|
+
}
|
|
321
|
+
// ========== LIFECYCLE ==========
|
|
322
|
+
async start() {
|
|
323
|
+
if (this.isRunning) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.isRunning = true;
|
|
327
|
+
if (this.storage.initialize) {
|
|
328
|
+
await this.storage.initialize();
|
|
329
|
+
}
|
|
330
|
+
if (this.queue) {
|
|
331
|
+
await this.queue.start();
|
|
332
|
+
if (this.config.workers?.enabled !== false) {
|
|
333
|
+
this.startWorker();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (this.config.cleanup?.enabled) {
|
|
337
|
+
this.startCleanup();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async stop() {
|
|
341
|
+
if (!this.isRunning) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.isRunning = false;
|
|
345
|
+
if (this.workerInterval) {
|
|
346
|
+
clearInterval(this.workerInterval);
|
|
347
|
+
this.workerInterval = void 0;
|
|
348
|
+
}
|
|
349
|
+
if (this.queue) {
|
|
350
|
+
await this.queue.stop();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async healthCheck() {
|
|
354
|
+
const results = {};
|
|
355
|
+
for (const [name, transport] of this.transports) {
|
|
356
|
+
if (transport.healthCheck) {
|
|
357
|
+
try {
|
|
358
|
+
results[name] = await transport.healthCheck();
|
|
359
|
+
} catch {
|
|
360
|
+
results[name] = false;
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
results[name] = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
// ========== PRIVATE METHODS ==========
|
|
369
|
+
buildNotification(input) {
|
|
370
|
+
const allChannels = Array.from(this.transports.keys());
|
|
371
|
+
if (input.template) {
|
|
372
|
+
const template = this.templates.get(input.template);
|
|
373
|
+
if (!template) {
|
|
374
|
+
throw new Error(`Template ${input.template} not found`);
|
|
375
|
+
}
|
|
376
|
+
const title = typeof template.defaults.title === "function" ? template.defaults.title(input.data) : template.defaults.title;
|
|
377
|
+
const body = typeof template.defaults.body === "function" ? template.defaults.body(input.data) : template.defaults.body;
|
|
378
|
+
const text = input.text || (typeof template.defaults.text === "function" ? template.defaults.text(input.data) : template.defaults.text);
|
|
379
|
+
const html = input.html || (typeof template.defaults.html === "function" ? template.defaults.html(input.data) : template.defaults.html);
|
|
380
|
+
return {
|
|
381
|
+
id: this.generateId(),
|
|
382
|
+
type: input.type || template.type,
|
|
383
|
+
title: input.title || title,
|
|
384
|
+
body: input.body || body,
|
|
385
|
+
text,
|
|
386
|
+
html,
|
|
387
|
+
data: input.data,
|
|
388
|
+
userId: input.userId,
|
|
389
|
+
groupId: input.groupId,
|
|
390
|
+
priority: input.priority || template.defaults.priority,
|
|
391
|
+
category: input.category || template.defaults.category,
|
|
392
|
+
status: "pending",
|
|
393
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
394
|
+
scheduledFor: input.scheduledFor,
|
|
395
|
+
expiresAt: input.expiresAt || (template.defaults.expiresIn ? new Date(Date.now() + template.defaults.expiresIn) : void 0),
|
|
396
|
+
// If channels are explicitly provided (even if empty), use them.
|
|
397
|
+
// Otherwise use template defaults.
|
|
398
|
+
channels: input.channels !== void 0 ? input.channels : template.defaults.channels,
|
|
399
|
+
actions: input.actions
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
id: this.generateId(),
|
|
404
|
+
type: input.type,
|
|
405
|
+
title: input.title,
|
|
406
|
+
body: input.body,
|
|
407
|
+
text: input.text,
|
|
408
|
+
html: input.html,
|
|
409
|
+
data: input.data,
|
|
410
|
+
userId: input.userId,
|
|
411
|
+
groupId: input.groupId,
|
|
412
|
+
priority: input.priority || "normal",
|
|
413
|
+
category: input.category,
|
|
414
|
+
status: "pending",
|
|
415
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
416
|
+
scheduledFor: input.scheduledFor,
|
|
417
|
+
expiresAt: input.expiresAt,
|
|
418
|
+
// If channels are explicitly provided (even if empty), use them.
|
|
419
|
+
// Otherwise default to all registered transports.
|
|
420
|
+
channels: input.channels !== void 0 ? input.channels : allChannels,
|
|
421
|
+
actions: input.actions
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async sendNow(notification) {
|
|
425
|
+
const prefs = await this.getPreferences(notification.userId);
|
|
426
|
+
const receipts = await Promise.all(
|
|
427
|
+
notification.channels.map(async (channel) => {
|
|
428
|
+
const transport = this.transports.get(channel);
|
|
429
|
+
if (!transport) {
|
|
430
|
+
console.warn(`[NotificationCenter] No transport found for channel: ${channel}`);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const channelNotification = this.castNotification(notification, channel);
|
|
434
|
+
if (!transport.canSend(channelNotification, prefs)) {
|
|
435
|
+
console.log(`[NotificationCenter] Delivery skipped for ${channel} due to preferences`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
console.log(`[NotificationCenter] Attempting delivery through transport: ${channel}`);
|
|
440
|
+
const receipt = await transport.send(channelNotification, prefs);
|
|
441
|
+
console.log(`[NotificationCenter] Delivery completed for transport: ${channel}`);
|
|
442
|
+
if (this.storage.saveReceipt) {
|
|
443
|
+
await this.storage.saveReceipt(receipt);
|
|
444
|
+
}
|
|
445
|
+
return receipt;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error(`[NotificationCenter] Delivery failed for channel ${channel}:`, error);
|
|
448
|
+
const receipt = {
|
|
449
|
+
notificationId: notification.id,
|
|
450
|
+
channel,
|
|
451
|
+
status: "failed",
|
|
452
|
+
attempts: 1,
|
|
453
|
+
lastAttempt: /* @__PURE__ */ new Date(),
|
|
454
|
+
error: error.message
|
|
455
|
+
};
|
|
456
|
+
if (this.storage.saveReceipt) {
|
|
457
|
+
await this.storage.saveReceipt(receipt);
|
|
458
|
+
}
|
|
459
|
+
return receipt;
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
);
|
|
463
|
+
const allFailed = receipts.every((r) => !r || r.status === "failed");
|
|
464
|
+
const anyDelivered = receipts.some((r) => r && r.status === "delivered");
|
|
465
|
+
if (receipts.length) {
|
|
466
|
+
notification.status = allFailed ? "failed" : anyDelivered ? "delivered" : "sent";
|
|
467
|
+
} else {
|
|
468
|
+
notification.status = "sent";
|
|
469
|
+
}
|
|
470
|
+
await this.applyAfterSendMiddleware(notification);
|
|
471
|
+
if (notification.channels.includes("inapp")) {
|
|
472
|
+
this.notifySubscribers(notification);
|
|
473
|
+
}
|
|
474
|
+
this.notifyEventSubscribers({
|
|
475
|
+
type: "sent",
|
|
476
|
+
notification,
|
|
477
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
478
|
+
});
|
|
479
|
+
await this.storage.save(notification);
|
|
480
|
+
}
|
|
481
|
+
castNotification(notification, channel) {
|
|
482
|
+
switch (channel) {
|
|
483
|
+
case "email":
|
|
484
|
+
return notification;
|
|
485
|
+
case "sms":
|
|
486
|
+
return notification;
|
|
487
|
+
case "push":
|
|
488
|
+
return notification;
|
|
489
|
+
case "inapp":
|
|
490
|
+
return notification;
|
|
491
|
+
default:
|
|
492
|
+
return notification;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
startWorker() {
|
|
496
|
+
const pollInterval = this.config.workers?.pollInterval || 1e3;
|
|
497
|
+
const concurrency = this.config.workers?.concurrency || 1;
|
|
498
|
+
this.workerInterval = setInterval(async () => {
|
|
499
|
+
if (!this.queue)
|
|
500
|
+
return;
|
|
501
|
+
const notifications = await this.queue.dequeueBatch(concurrency);
|
|
502
|
+
await Promise.all(
|
|
503
|
+
notifications.map((notif) => this.sendNow(notif))
|
|
504
|
+
);
|
|
505
|
+
}, pollInterval);
|
|
506
|
+
}
|
|
507
|
+
startCleanup() {
|
|
508
|
+
const interval = this.config.cleanup?.interval || 36e5;
|
|
509
|
+
setInterval(async () => {
|
|
510
|
+
await this.storage.deleteExpired();
|
|
511
|
+
}, interval);
|
|
512
|
+
}
|
|
513
|
+
async applyBeforeSendMiddleware(notification) {
|
|
514
|
+
let current = notification;
|
|
515
|
+
for (const middleware of this.middleware) {
|
|
516
|
+
if (middleware.beforeSend) {
|
|
517
|
+
current = await middleware.beforeSend(current);
|
|
518
|
+
if (!current)
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return current;
|
|
523
|
+
}
|
|
524
|
+
async applyAfterSendMiddleware(notification) {
|
|
525
|
+
for (const middleware of this.middleware) {
|
|
526
|
+
if (middleware.afterSend) {
|
|
527
|
+
await middleware.afterSend(notification);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async applyErrorMiddleware(error, notification) {
|
|
532
|
+
for (const middleware of this.middleware) {
|
|
533
|
+
if (middleware.onError) {
|
|
534
|
+
await middleware.onError(error, notification);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
notifySubscribers(notification) {
|
|
539
|
+
const subs = this.subscribers.get(String(notification.userId));
|
|
540
|
+
if (subs) {
|
|
541
|
+
subs.forEach((callback) => callback(notification));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
notifyEventSubscribers(event) {
|
|
545
|
+
const subs = this.eventSubscribers.get(String(event.notification.userId));
|
|
546
|
+
if (subs) {
|
|
547
|
+
subs.forEach((callback) => callback(event));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
notifyUnreadSubscribers(userId, count) {
|
|
551
|
+
const subs = this.unreadSubscribers.get(String(userId));
|
|
552
|
+
if (subs) {
|
|
553
|
+
subs.forEach((callback) => callback(count, userId));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
generateId() {
|
|
557
|
+
return `notif_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// src/adapters/console_transport_adapter.ts
|
|
562
|
+
var ConsoleTransportAdapter = class {
|
|
563
|
+
constructor(name = "inapp") {
|
|
564
|
+
this.name = "inapp";
|
|
565
|
+
this.name = name;
|
|
566
|
+
}
|
|
567
|
+
async send(notification) {
|
|
568
|
+
console.log(`[${this.name.toUpperCase()}] Notification sent:`, {
|
|
569
|
+
id: notification.id,
|
|
570
|
+
title: notification.title,
|
|
571
|
+
body: notification.body,
|
|
572
|
+
userId: notification.userId,
|
|
573
|
+
type: notification.type,
|
|
574
|
+
priority: notification.priority
|
|
575
|
+
});
|
|
576
|
+
return {
|
|
577
|
+
notificationId: notification.id,
|
|
578
|
+
channel: this.name,
|
|
579
|
+
status: "delivered",
|
|
580
|
+
attempts: 1,
|
|
581
|
+
lastAttempt: /* @__PURE__ */ new Date()
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
canSend(notification, preferences) {
|
|
585
|
+
if (preferences.globalMute) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
const channelPref = preferences.channels[this.name];
|
|
589
|
+
if (channelPref && !channelPref.enabled) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (channelPref?.categories && notification.category) {
|
|
593
|
+
if (!channelPref.categories.includes(notification.category)) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (channelPref?.quietHours) {
|
|
598
|
+
const now = /* @__PURE__ */ new Date();
|
|
599
|
+
const currentHour = now.getHours();
|
|
600
|
+
const [startHour] = channelPref.quietHours.start.split(":").map(Number);
|
|
601
|
+
const [endHour] = channelPref.quietHours.end.split(":").map(Number);
|
|
602
|
+
if (startHour > endHour) {
|
|
603
|
+
if (currentHour >= startHour || currentHour < endHour) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
if (currentHour >= startHour && currentHour < endHour) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
async healthCheck() {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/adapters/memory_queue_adapter.ts
|
|
620
|
+
var MemoryQueueAdapter = class {
|
|
621
|
+
constructor() {
|
|
622
|
+
this.queue = [];
|
|
623
|
+
this.delayedQueue = [];
|
|
624
|
+
this.isRunning = false;
|
|
625
|
+
}
|
|
626
|
+
async enqueue(notification) {
|
|
627
|
+
this.queue.push(notification);
|
|
628
|
+
}
|
|
629
|
+
async enqueueBatch(notifications) {
|
|
630
|
+
this.queue.push(...notifications);
|
|
631
|
+
}
|
|
632
|
+
async enqueueDelayed(notification, delay) {
|
|
633
|
+
this.delayedQueue.push({
|
|
634
|
+
notification,
|
|
635
|
+
executeAt: new Date(Date.now() + delay)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
async dequeue() {
|
|
639
|
+
return this.queue.shift() || null;
|
|
640
|
+
}
|
|
641
|
+
async dequeueBatch(count) {
|
|
642
|
+
return this.queue.splice(0, count);
|
|
643
|
+
}
|
|
644
|
+
async getQueueSize() {
|
|
645
|
+
return this.queue.length + this.delayedQueue.length;
|
|
646
|
+
}
|
|
647
|
+
async clear() {
|
|
648
|
+
this.queue = [];
|
|
649
|
+
this.delayedQueue = [];
|
|
650
|
+
}
|
|
651
|
+
async start() {
|
|
652
|
+
this.isRunning = true;
|
|
653
|
+
this.checkInterval = setInterval(() => {
|
|
654
|
+
this.processDelayed();
|
|
655
|
+
}, 1e3);
|
|
656
|
+
}
|
|
657
|
+
async stop() {
|
|
658
|
+
this.isRunning = false;
|
|
659
|
+
if (this.checkInterval) {
|
|
660
|
+
clearInterval(this.checkInterval);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
processDelayed() {
|
|
664
|
+
const now = /* @__PURE__ */ new Date();
|
|
665
|
+
const ready = this.delayedQueue.filter((item) => item.executeAt <= now);
|
|
666
|
+
ready.forEach((item) => {
|
|
667
|
+
this.queue.push(item.notification);
|
|
668
|
+
});
|
|
669
|
+
this.delayedQueue = this.delayedQueue.filter((item) => item.executeAt > now);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// src/adapters/memory_storage_adapter.ts
|
|
674
|
+
var MemoryStorageAdapter = class {
|
|
675
|
+
constructor() {
|
|
676
|
+
this.notifications = /* @__PURE__ */ new Map();
|
|
677
|
+
this.preferences = /* @__PURE__ */ new Map();
|
|
678
|
+
this.receipts = /* @__PURE__ */ new Map();
|
|
679
|
+
}
|
|
680
|
+
async save(notification) {
|
|
681
|
+
this.notifications.set(notification.id, notification);
|
|
682
|
+
}
|
|
683
|
+
async saveBatch(notifications) {
|
|
684
|
+
notifications.forEach((n) => this.notifications.set(n.id, n));
|
|
685
|
+
}
|
|
686
|
+
async findById(id) {
|
|
687
|
+
return this.notifications.get(id) || null;
|
|
688
|
+
}
|
|
689
|
+
async findByUser(userId, filters) {
|
|
690
|
+
let results = Array.from(this.notifications.values()).filter((n) => n.userId === userId);
|
|
691
|
+
if (filters) {
|
|
692
|
+
if (filters.status) {
|
|
693
|
+
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status];
|
|
694
|
+
results = results.filter((n) => statuses.includes(n.status));
|
|
695
|
+
}
|
|
696
|
+
if (filters.type) {
|
|
697
|
+
const types = Array.isArray(filters.type) ? filters.type : [filters.type];
|
|
698
|
+
results = results.filter((n) => types.includes(n.type));
|
|
699
|
+
}
|
|
700
|
+
if (filters.category) {
|
|
701
|
+
const categories = Array.isArray(filters.category) ? filters.category : [filters.category];
|
|
702
|
+
results = results.filter((n) => n.category && categories.includes(n.category));
|
|
703
|
+
}
|
|
704
|
+
if (filters.startDate) {
|
|
705
|
+
results = results.filter((n) => n.createdAt >= filters.startDate);
|
|
706
|
+
}
|
|
707
|
+
if (filters.endDate) {
|
|
708
|
+
results = results.filter((n) => n.createdAt <= filters.endDate);
|
|
709
|
+
}
|
|
710
|
+
const sortBy = filters.sortBy || "createdAt";
|
|
711
|
+
const sortOrder = filters.sortOrder || "desc";
|
|
712
|
+
results.sort((a, b) => {
|
|
713
|
+
const aVal = a[sortBy];
|
|
714
|
+
const bVal = b[sortBy];
|
|
715
|
+
if (!aVal || !bVal)
|
|
716
|
+
return 0;
|
|
717
|
+
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
718
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
719
|
+
});
|
|
720
|
+
if (filters.offset) {
|
|
721
|
+
results = results.slice(filters.offset);
|
|
722
|
+
}
|
|
723
|
+
if (filters.limit) {
|
|
724
|
+
results = results.slice(0, filters.limit);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return results;
|
|
728
|
+
}
|
|
729
|
+
async countUnread(userId) {
|
|
730
|
+
return Array.from(this.notifications.values()).filter((n) => n.userId === userId && n.status !== "read").length;
|
|
731
|
+
}
|
|
732
|
+
async markAsRead(id) {
|
|
733
|
+
const notification = this.notifications.get(id);
|
|
734
|
+
if (notification) {
|
|
735
|
+
notification.status = "read";
|
|
736
|
+
notification.readAt = /* @__PURE__ */ new Date();
|
|
737
|
+
this.notifications.set(id, notification);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async markAllAsRead(userId) {
|
|
741
|
+
Array.from(this.notifications.values()).filter((n) => n.userId === userId && n.status !== "read").forEach((n) => {
|
|
742
|
+
n.status = "read";
|
|
743
|
+
n.readAt = /* @__PURE__ */ new Date();
|
|
744
|
+
this.notifications.set(n.id, n);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async markAsUnread(id) {
|
|
748
|
+
const notification = this.notifications.get(id);
|
|
749
|
+
if (notification) {
|
|
750
|
+
notification.status = "delivered";
|
|
751
|
+
notification.readAt = void 0;
|
|
752
|
+
this.notifications.set(id, notification);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async markAllAsUnread(userId) {
|
|
756
|
+
Array.from(this.notifications.values()).filter((n) => n.userId === userId && n.status !== "delivered").forEach((n) => {
|
|
757
|
+
n.status = "delivered";
|
|
758
|
+
n.readAt = void 0;
|
|
759
|
+
this.notifications.set(n.id, n);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async delete(id) {
|
|
763
|
+
this.notifications.delete(id);
|
|
764
|
+
this.receipts.delete(id);
|
|
765
|
+
}
|
|
766
|
+
async getPreferences(userId) {
|
|
767
|
+
return this.preferences.get(userId) || {
|
|
768
|
+
userId,
|
|
769
|
+
channels: {},
|
|
770
|
+
globalMute: false
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
async savePreferences(userId, prefs) {
|
|
774
|
+
this.preferences.set(userId, prefs);
|
|
775
|
+
}
|
|
776
|
+
async deleteExpired() {
|
|
777
|
+
const now = /* @__PURE__ */ new Date();
|
|
778
|
+
let count = 0;
|
|
779
|
+
Array.from(this.notifications.values()).filter((n) => n.expiresAt && n.expiresAt < now).forEach((n) => {
|
|
780
|
+
this.notifications.delete(n.id);
|
|
781
|
+
count++;
|
|
782
|
+
});
|
|
783
|
+
return count;
|
|
784
|
+
}
|
|
785
|
+
async saveReceipt(receipt) {
|
|
786
|
+
const existing = this.receipts.get(receipt.notificationId) || [];
|
|
787
|
+
existing.push(receipt);
|
|
788
|
+
this.receipts.set(receipt.notificationId, existing);
|
|
789
|
+
}
|
|
790
|
+
async getReceipts(notificationId) {
|
|
791
|
+
return this.receipts.get(notificationId) || [];
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
export {
|
|
795
|
+
ConsoleTransportAdapter,
|
|
796
|
+
MemoryQueueAdapter,
|
|
797
|
+
MemoryStorageAdapter,
|
|
798
|
+
NotificationCenter
|
|
799
|
+
};
|