@facteurjs/core 1.0.0-beta.3 → 2.0.0-beta.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 +27 -0
- package/dist/api/handlers/{notifications.js → notifications.mjs} +39 -16
- package/dist/api/handlers/preferences.mjs +73 -0
- package/dist/api/handlers/utils.mjs +23 -0
- package/dist/api/types.d.mts +52 -0
- package/dist/api/types.mjs +1 -0
- package/dist/api.d.mts +22 -0
- package/dist/api.mjs +25 -0
- package/dist/channels/aws-sns/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/channels/aws-sns/{channel.js → channel.mjs} +2 -3
- package/dist/channels/aws-sns/types.mjs +1 -0
- package/dist/channels/aws-sns.d.mts +3 -0
- package/dist/channels/aws-sns.mjs +4 -0
- package/dist/channels/discord/{channel.d.ts → channel.d.mts} +5 -5
- package/dist/channels/discord/{channel.js → channel.mjs} +1 -1
- package/dist/channels/discord/{message.d.ts → message.d.mts} +2 -2
- package/dist/channels/discord/{message.js → message.mjs} +1 -1
- package/dist/channels/discord/{types.d.ts → types.d.mts} +1 -1
- package/dist/channels/discord/types.mjs +1 -0
- package/dist/channels/discord.d.mts +3 -0
- package/dist/channels/discord.mjs +4 -0
- package/dist/channels/expo/{channel.d.ts → channel.d.mts} +5 -3
- package/dist/channels/expo/channel.mjs +69 -0
- package/dist/channels/expo/types.mjs +1 -0
- package/dist/channels/expo.d.mts +3 -0
- package/dist/channels/expo.mjs +4 -0
- package/dist/channels/fcm/{channel.d.ts → channel.d.mts} +5 -5
- package/dist/channels/fcm/channel.mjs +68 -0
- package/dist/channels/fcm/types.mjs +1 -0
- package/dist/channels/fcm.d.mts +3 -0
- package/dist/channels/fcm.mjs +4 -0
- package/dist/channels/slack/{channel.d.ts → channel.d.mts} +5 -5
- package/dist/channels/slack/{channel.js → channel.mjs} +2 -2
- package/dist/channels/slack/{message.d.ts → message.d.mts} +1 -1
- package/dist/channels/slack/{message.js → message.mjs} +1 -1
- package/dist/channels/slack/{types.d.ts → types.d.mts} +1 -1
- package/dist/channels/slack/types.mjs +1 -0
- package/dist/channels/slack.d.mts +3 -0
- package/dist/channels/slack.mjs +4 -0
- package/dist/channels/socketio/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/channels/socketio/{channel.js → channel.mjs} +2 -3
- package/dist/channels/socketio/types.mjs +1 -0
- package/dist/channels/socketio.d.mts +3 -0
- package/dist/channels/socketio.mjs +4 -0
- package/dist/channels/transmit/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/channels/transmit/{channel.js → channel.mjs} +2 -3
- package/dist/channels/transmit/types.mjs +1 -0
- package/dist/channels/transmit.d.mts +3 -0
- package/dist/channels/transmit.mjs +4 -0
- package/dist/channels/twilio/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/channels/twilio/{channel.js → channel.mjs} +3 -6
- package/dist/channels/twilio/types.mjs +1 -0
- package/dist/channels/twilio.d.mts +4 -0
- package/dist/channels/twilio.mjs +4 -0
- package/dist/channels/webhook/{exceptions.d.ts → exceptions.d.mts} +1 -1
- package/dist/channels/webhook/{provider.d.ts → provider.d.mts} +3 -3
- package/dist/channels/webhook/{provider.js → provider.mjs} +6 -8
- package/dist/channels/webhook/types.mjs +1 -0
- package/dist/channels/webhook.d.mts +4 -0
- package/dist/channels/webhook.mjs +5 -0
- package/dist/channels/webpush/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/channels/webpush/{channel.js → channel.mjs} +2 -3
- package/dist/channels/webpush/types.mjs +1 -0
- package/dist/channels/webpush.d.mts +3 -0
- package/dist/channels/webpush.mjs +4 -0
- package/dist/database/adapters/{knex.d.ts → knex.d.mts} +1 -1
- package/dist/database/adapters/{knex.js → knex.mjs} +19 -15
- package/dist/database/adapters/{kysely.d.ts → kysely.d.mts} +1 -1
- package/dist/database/adapters/{kysely.js → kysely.mjs} +2 -4
- package/dist/database/{channel.d.ts → channel.d.mts} +3 -3
- package/dist/database/{channel.js → channel.mjs} +4 -6
- package/dist/database/{database.d.ts → database.d.mts} +3 -3
- package/dist/database/{database.js → database.mjs} +15 -12
- package/dist/database/{message.d.ts → message.d.mts} +2 -2
- package/dist/database/{types.d.ts → types.d.mts} +3 -3
- package/dist/database/types.mjs +1 -0
- package/dist/database.d.mts +3 -0
- package/dist/database.mjs +4 -0
- package/dist/errors/{duplicate_notification_exception.js → duplicate_notification_exception.mjs} +1 -2
- package/dist/errors/{http_error.d.ts → http_error.d.mts} +0 -3
- package/dist/errors/{index.d.ts → index.d.mts} +1 -1
- package/dist/errors/{index.js → index.mjs} +2 -3
- package/dist/events/{events.d.ts → events.d.mts} +2 -2
- package/dist/facteur.d.mts +45 -0
- package/dist/facteur.mjs +167 -0
- package/dist/{fake.d.ts → fake.d.mts} +3 -3
- package/dist/{fake.js → fake.mjs} +2 -3
- package/dist/{index.d.ts → index.d.mts} +3 -3
- package/dist/{index.js → index.mjs} +2 -2
- package/dist/notifications/batching_sender.mjs +148 -0
- package/dist/notifications/{channel_resolver.js → channel_resolver.mjs} +40 -31
- package/dist/notifications/notification_builder.mjs +77 -0
- package/dist/notifications/{notification_discoverer.d.ts → notification_discoverer.d.mts} +1 -1
- package/dist/notifications/{notification_discoverer.js → notification_discoverer.mjs} +9 -14
- package/dist/notifications/notification_sender.mjs +356 -0
- package/dist/notifications/orchestration_sender.mjs +92 -0
- package/dist/{options.d.ts → options.d.mts} +7 -6
- package/dist/{options.js → options.mjs} +5 -4
- package/dist/types/builder.d.mts +122 -0
- package/dist/types/channel.d.mts +56 -0
- package/dist/types/{events.d.ts → events.d.mts} +1 -1
- package/dist/types/{extend.d.ts → extend.d.mts} +2 -2
- package/dist/types/{notifications.d.ts → notifications.d.mts} +3 -3
- package/dist/types/{options.d.ts → options.d.mts} +94 -11
- package/dist/types/{preferences.d.ts → preferences.d.mts} +1 -1
- package/dist/types.d.mts +9 -0
- package/dist/types.mjs +4 -0
- package/dist/utils/chunk.mjs +28 -0
- package/package.json +69 -54
- package/dist/api/handlers/preferences.js +0 -43
- package/dist/api/index.d.ts +0 -16
- package/dist/api/index.js +0 -21
- package/dist/api/types.d.ts +0 -22
- package/dist/api/types.js +0 -0
- package/dist/channels/aws-sns/index.d.ts +0 -3
- package/dist/channels/aws-sns/index.js +0 -4
- package/dist/channels/aws-sns/types.js +0 -0
- package/dist/channels/discord/index.d.ts +0 -3
- package/dist/channels/discord/index.js +0 -4
- package/dist/channels/discord/types.js +0 -0
- package/dist/channels/expo/channel.js +0 -38
- package/dist/channels/expo/index.d.ts +0 -3
- package/dist/channels/expo/index.js +0 -4
- package/dist/channels/expo/types.js +0 -0
- package/dist/channels/fcm/channel.js +0 -44
- package/dist/channels/fcm/index.d.ts +0 -3
- package/dist/channels/fcm/index.js +0 -4
- package/dist/channels/fcm/types.js +0 -0
- package/dist/channels/slack/index.d.ts +0 -3
- package/dist/channels/slack/index.js +0 -4
- package/dist/channels/slack/types.js +0 -0
- package/dist/channels/socketio/index.d.ts +0 -3
- package/dist/channels/socketio/index.js +0 -4
- package/dist/channels/socketio/types.js +0 -0
- package/dist/channels/transmit/index.d.ts +0 -3
- package/dist/channels/transmit/index.js +0 -4
- package/dist/channels/transmit/types.js +0 -0
- package/dist/channels/twilio/index.d.ts +0 -4
- package/dist/channels/twilio/index.js +0 -4
- package/dist/channels/twilio/types.js +0 -0
- package/dist/channels/webhook/index.d.ts +0 -4
- package/dist/channels/webhook/index.js +0 -5
- package/dist/channels/webhook/types.js +0 -0
- package/dist/channels/webpush/index.d.ts +0 -3
- package/dist/channels/webpush/index.js +0 -4
- package/dist/channels/webpush/types.js +0 -0
- package/dist/database/index.d.ts +0 -3
- package/dist/database/index.js +0 -4
- package/dist/database/types.js +0 -0
- package/dist/facteur.d.ts +0 -37
- package/dist/facteur.js +0 -100
- package/dist/notifications/notification_sender.js +0 -211
- package/dist/types/channel.d.ts +0 -18
- package/dist/types/index.d.ts +0 -8
- package/dist/types/index.js +0 -4
- /package/dist/channels/aws-sns/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/aws-sns/{message.js → message.mjs} +0 -0
- /package/dist/channels/aws-sns/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/expo/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/expo/{message.js → message.mjs} +0 -0
- /package/dist/channels/expo/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/fcm/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/fcm/{message.js → message.mjs} +0 -0
- /package/dist/channels/fcm/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/socketio/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/socketio/{message.js → message.mjs} +0 -0
- /package/dist/channels/socketio/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/transmit/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/transmit/{message.js → message.mjs} +0 -0
- /package/dist/channels/transmit/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/twilio/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/twilio/{message.js → message.mjs} +0 -0
- /package/dist/channels/twilio/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/webhook/{exceptions.js → exceptions.mjs} +0 -0
- /package/dist/channels/webhook/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/webhook/{message.js → message.mjs} +0 -0
- /package/dist/channels/webhook/{types.d.ts → types.d.mts} +0 -0
- /package/dist/channels/webpush/{message.d.ts → message.d.mts} +0 -0
- /package/dist/channels/webpush/{message.js → message.mjs} +0 -0
- /package/dist/channels/webpush/{types.d.ts → types.d.mts} +0 -0
- /package/dist/database/{message.js → message.mjs} +0 -0
- /package/dist/{debug.js → debug.mjs} +0 -0
- /package/dist/errors/{duplicate_notification_exception.d.ts → duplicate_notification_exception.d.mts} +0 -0
- /package/dist/errors/{http_error.js → http_error.mjs} +0 -0
- /package/dist/events/{events.js → events.mjs} +0 -0
- /package/dist/{helpers.js → helpers.mjs} +0 -0
- /package/dist/types/{channel.js → channel.mjs} +0 -0
- /package/dist/types/{notifications.js → notifications.mjs} +0 -0
- /package/dist/types/{queue.d.ts → queue.d.mts} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Channel } from "./types/channel.mjs";
|
|
2
|
+
import { NotificationBuilder } from "./types/builder.mjs";
|
|
3
|
+
import { FacteurFake } from "./fake.mjs";
|
|
4
|
+
import { FacteurDatabase } from "./database/database.mjs";
|
|
5
|
+
import { FacteurConfiguration } from "./types/options.mjs";
|
|
6
|
+
import { Notification, NotificationClass } from "./types/notifications.mjs";
|
|
7
|
+
import { DatabaseAdapter } from "./database/types.mjs";
|
|
8
|
+
|
|
9
|
+
//#region src/facteur.d.ts
|
|
10
|
+
declare function createFacteur<T extends Record<string, Channel>>(config: FacteurConfiguration<T>): Facteur<T, null>;
|
|
11
|
+
declare class Facteur<KnownChannels extends Record<string, Channel>, DBAdapter extends DatabaseAdapter | null = null> {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(config: FacteurConfiguration<KnownChannels, DBAdapter>);
|
|
14
|
+
/**
|
|
15
|
+
* Access the database layer for in-app notifications and preferences
|
|
16
|
+
*/
|
|
17
|
+
get db(): DBAdapter extends DatabaseAdapter ? FacteurDatabase : never;
|
|
18
|
+
/**
|
|
19
|
+
* Access notification discovery utilities for finding and managing notifications
|
|
20
|
+
*/
|
|
21
|
+
get discoverer(): {
|
|
22
|
+
discoverNotifications: () => Promise<(new (...args: any[]) => Notification)[]>;
|
|
23
|
+
getNotifications: () => Promise<(new (...args: any[]) => Notification)[]>;
|
|
24
|
+
getNotificationTags: () => Promise<string[]>;
|
|
25
|
+
clearCache: () => void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Enable fake mode for testing - captures sent notifications instead of sending
|
|
29
|
+
*/
|
|
30
|
+
fake(): FacteurFake;
|
|
31
|
+
/**
|
|
32
|
+
* Restore normal sending behavior after fake mode
|
|
33
|
+
*/
|
|
34
|
+
restore(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Create a notification builder for fluent API
|
|
37
|
+
*/
|
|
38
|
+
notification<TNotification extends NotificationClass<any, any>>(notificationClass: TNotification): NotificationBuilder<TNotification, {
|
|
39
|
+
hasParams: false;
|
|
40
|
+
hasTo: false;
|
|
41
|
+
hasVia: false;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { Facteur, createFacteur };
|
package/dist/facteur.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { collect, isAsyncIterable } from "./utils/chunk.mjs";
|
|
2
|
+
import { FacteurOptions } from "./options.mjs";
|
|
3
|
+
import { OrchestrationSender } from "./notifications/orchestration_sender.mjs";
|
|
4
|
+
import { ChannelResolver } from "./notifications/channel_resolver.mjs";
|
|
5
|
+
import { NotificationSender } from "./notifications/notification_sender.mjs";
|
|
6
|
+
import { NotificationDiscoverer } from "./notifications/notification_discoverer.mjs";
|
|
7
|
+
import { createNotificationBuilder } from "./notifications/notification_builder.mjs";
|
|
8
|
+
import { BatchingSender } from "./notifications/batching_sender.mjs";
|
|
9
|
+
import { FacteurFake } from "./fake.mjs";
|
|
10
|
+
import { FacteurDatabase } from "./database/database.mjs";
|
|
11
|
+
|
|
12
|
+
//#region src/facteur.ts
|
|
13
|
+
function createFacteur(config) {
|
|
14
|
+
return new Facteur(config);
|
|
15
|
+
}
|
|
16
|
+
var Facteur = class {
|
|
17
|
+
#sender;
|
|
18
|
+
#orchestrationSender;
|
|
19
|
+
#batchingSender;
|
|
20
|
+
#fake = null;
|
|
21
|
+
#db = null;
|
|
22
|
+
#discoverer;
|
|
23
|
+
#options;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.#options = new FacteurOptions(config);
|
|
26
|
+
this.#discoverer = new NotificationDiscoverer({
|
|
27
|
+
searchDirectory: config.discoverer.searchDirectory,
|
|
28
|
+
fileSuffix: config.discoverer.fileSuffix
|
|
29
|
+
});
|
|
30
|
+
let channelResolver;
|
|
31
|
+
if (this.#options.databaseAdapter) {
|
|
32
|
+
const options = this.#options;
|
|
33
|
+
this.#db = new FacteurDatabase(options, this.#discoverer);
|
|
34
|
+
channelResolver = new ChannelResolver(this.#db, this.#options.defaultPreferences);
|
|
35
|
+
} else channelResolver = new ChannelResolver(void 0, this.#options.defaultPreferences);
|
|
36
|
+
this.#sender = new NotificationSender(this.#options.channels, channelResolver, this.#options.emitter, this.#options.retry);
|
|
37
|
+
this.#orchestrationSender = new OrchestrationSender(this.#sender);
|
|
38
|
+
this.#batchingSender = new BatchingSender(this.#sender, channelResolver);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Converts recipients input to an array, handling single values, arrays, and async iterables
|
|
42
|
+
*/
|
|
43
|
+
async #resolveRecipients(to) {
|
|
44
|
+
if (!to) return [];
|
|
45
|
+
if (Array.isArray(to)) return to;
|
|
46
|
+
if (isAsyncIterable(to)) return collect(to);
|
|
47
|
+
return [to];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves notification instance and runs lifecycle hooks
|
|
51
|
+
*/
|
|
52
|
+
async #prepareNotification(recipient, options) {
|
|
53
|
+
const notification = await this.#options.notificationResolver(options.notification, {
|
|
54
|
+
to: recipient,
|
|
55
|
+
params: options.params,
|
|
56
|
+
tenantId: options.tenantId
|
|
57
|
+
});
|
|
58
|
+
await notification.beforeSend();
|
|
59
|
+
return {
|
|
60
|
+
notification,
|
|
61
|
+
shouldSkip: await notification.shouldSend() === false
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handles anonymous sends (via without to) - e.g., sending to a fixed webhook
|
|
66
|
+
*/
|
|
67
|
+
async #sendAnonymous(options) {
|
|
68
|
+
const { notification, shouldSkip } = await this.#prepareNotification(void 0, options);
|
|
69
|
+
if (shouldSkip) return {
|
|
70
|
+
success: 0,
|
|
71
|
+
failed: 0,
|
|
72
|
+
results: []
|
|
73
|
+
};
|
|
74
|
+
if (this.#fake) return this.#fake.recordSent(options);
|
|
75
|
+
return this.#sender.send(options, notification);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Main send entry point - unified handling for all recipient types
|
|
79
|
+
*/
|
|
80
|
+
async #send(options) {
|
|
81
|
+
if (options.via && !options.to) return this.#sendAnonymous(options);
|
|
82
|
+
const recipients = await this.#resolveRecipients(options.to);
|
|
83
|
+
if (recipients.length === 0) return {
|
|
84
|
+
success: 0,
|
|
85
|
+
failed: 0,
|
|
86
|
+
results: []
|
|
87
|
+
};
|
|
88
|
+
if (this.#fake) {
|
|
89
|
+
for (const recipient of recipients) this.#fake.recordSent({
|
|
90
|
+
...options,
|
|
91
|
+
to: recipient
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
success: recipients.length,
|
|
95
|
+
failed: 0,
|
|
96
|
+
results: []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (recipients.length === 1) return this.#sendSingle({
|
|
100
|
+
...options,
|
|
101
|
+
to: recipients[0]
|
|
102
|
+
});
|
|
103
|
+
const prepareNotification = this.#prepareNotification.bind(this);
|
|
104
|
+
if (options.useDriverBatching) return this.#batchingSender.send({
|
|
105
|
+
recipients,
|
|
106
|
+
builderOptions: options,
|
|
107
|
+
prepareNotification
|
|
108
|
+
});
|
|
109
|
+
return this.#orchestrationSender.send({
|
|
110
|
+
recipients,
|
|
111
|
+
builderOptions: options,
|
|
112
|
+
prepareNotification
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fast path for single recipient - passes options directly to sender
|
|
117
|
+
*/
|
|
118
|
+
async #sendSingle(options) {
|
|
119
|
+
const { notification, shouldSkip } = await this.#prepareNotification(options.to, options);
|
|
120
|
+
if (shouldSkip) return {
|
|
121
|
+
success: 0,
|
|
122
|
+
failed: 0,
|
|
123
|
+
results: []
|
|
124
|
+
};
|
|
125
|
+
return this.#sender.send(options, notification);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Access the database layer for in-app notifications and preferences
|
|
129
|
+
*/
|
|
130
|
+
get db() {
|
|
131
|
+
if (!this.#options.databaseAdapter) throw new Error("No database adapter configured");
|
|
132
|
+
return this.#db;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Access notification discovery utilities for finding and managing notifications
|
|
136
|
+
*/
|
|
137
|
+
get discoverer() {
|
|
138
|
+
return {
|
|
139
|
+
discoverNotifications: () => this.#discoverer.discoverNotifications(),
|
|
140
|
+
getNotifications: () => this.#discoverer.getNotifications(),
|
|
141
|
+
getNotificationTags: () => this.#discoverer.getAllNotificationTags(),
|
|
142
|
+
clearCache: () => this.#discoverer.clearCache()
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Enable fake mode for testing - captures sent notifications instead of sending
|
|
147
|
+
*/
|
|
148
|
+
fake() {
|
|
149
|
+
this.#fake = new FacteurFake();
|
|
150
|
+
return this.#fake;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Restore normal sending behavior after fake mode
|
|
154
|
+
*/
|
|
155
|
+
restore() {
|
|
156
|
+
this.#fake = null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a notification builder for fluent API
|
|
160
|
+
*/
|
|
161
|
+
notification(notificationClass) {
|
|
162
|
+
return createNotificationBuilder((options) => this.#send(options), notificationClass);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { Facteur, createFacteur };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Notification, NotificationSendResult } from "./types/notifications.
|
|
1
|
+
import { InternalSendOptions } from "./types/builder.mjs";
|
|
2
|
+
import { Notification, NotificationSendResult } from "./types/notifications.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/fake.d.ts
|
|
5
5
|
interface SentNotification<N extends Notification = Notification> {
|
|
@@ -13,7 +13,7 @@ declare class FacteurFake {
|
|
|
13
13
|
/**
|
|
14
14
|
* Record a notification as sent during fake mode
|
|
15
15
|
*/
|
|
16
|
-
recordSent(options:
|
|
16
|
+
recordSent(options: InternalSendOptions): NotificationSendResult;
|
|
17
17
|
/**
|
|
18
18
|
* Assert a total of expected number of notifications were sent
|
|
19
19
|
*/
|
|
@@ -66,15 +66,14 @@ var FacteurFake = class {
|
|
|
66
66
|
const sentNotifications = this.sent(notificationClass);
|
|
67
67
|
if (sentNotifications.length === 0) throw new AssertionError({ message: `Expected notification "${notificationClass.name}" was not sent` });
|
|
68
68
|
if (callback) {
|
|
69
|
-
|
|
69
|
+
if (!sentNotifications.some((sent) => {
|
|
70
70
|
try {
|
|
71
71
|
callback(sent);
|
|
72
72
|
return true;
|
|
73
73
|
} catch {
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
|
-
});
|
|
77
|
-
if (!found) throw new AssertionError({ message: `No notifications of type ${notificationClass.name} matched the given callback` });
|
|
76
|
+
})) throw new AssertionError({ message: `No notifications of type ${notificationClass.name} matched the given callback` });
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Channel } from "./types/channel.
|
|
2
|
-
import { Facteur, createFacteur } from "./facteur.
|
|
3
|
-
import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.
|
|
1
|
+
import { Channel } from "./types/channel.mjs";
|
|
2
|
+
import { Facteur, createFacteur } from "./facteur.mjs";
|
|
3
|
+
import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.
|
|
2
|
-
import { Facteur, createFacteur } from "./facteur.
|
|
1
|
+
import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.mjs";
|
|
2
|
+
import { Facteur, createFacteur } from "./facteur.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
/**
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { chunk } from "../utils/chunk.mjs";
|
|
2
|
+
import { Tenace, backoff } from "@julr/tenace";
|
|
3
|
+
|
|
4
|
+
//#region src/notifications/batching_sender.ts
|
|
5
|
+
/**
|
|
6
|
+
* Handles bulk notification sending using driver batching mode.
|
|
7
|
+
*
|
|
8
|
+
* In driver batching mode, messages are grouped by channel and sent using
|
|
9
|
+
* the channel's batch API when available. This is more efficient for channels
|
|
10
|
+
* that support batch operations (e.g., FCM, Expo push notifications).
|
|
11
|
+
*/
|
|
12
|
+
var BatchingSender = class {
|
|
13
|
+
#sender;
|
|
14
|
+
#channelResolver;
|
|
15
|
+
constructor(sender, channelResolver) {
|
|
16
|
+
this.#sender = sender;
|
|
17
|
+
this.#channelResolver = channelResolver;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sends notifications to multiple recipients using channel batch APIs
|
|
21
|
+
*/
|
|
22
|
+
async send(options) {
|
|
23
|
+
const { builderOptions, recipients, prepareNotification } = options;
|
|
24
|
+
const { chunkSize = Infinity, concurrency = 10, continueOnError = false, retries = 0, timeout, onProgress } = builderOptions;
|
|
25
|
+
const allResults = [];
|
|
26
|
+
let completedCount = 0;
|
|
27
|
+
for (const recipientChunk of chunk(recipients, chunkSize)) {
|
|
28
|
+
const chunkResults = await this.#processChunk({
|
|
29
|
+
recipientChunk,
|
|
30
|
+
builderOptions,
|
|
31
|
+
prepareNotification,
|
|
32
|
+
concurrency,
|
|
33
|
+
continueOnError,
|
|
34
|
+
retries,
|
|
35
|
+
...timeout && { timeout }
|
|
36
|
+
});
|
|
37
|
+
allResults.push(...chunkResults);
|
|
38
|
+
completedCount += recipientChunk.length;
|
|
39
|
+
onProgress?.(completedCount, recipients.length);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
success: allResults.filter((r) => r.status === "success").length,
|
|
43
|
+
failed: allResults.filter((r) => r.status === "failed").length,
|
|
44
|
+
results: allResults
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Processes a single chunk: prepare recipients, group by channel, send batches
|
|
49
|
+
*/
|
|
50
|
+
async #processChunk(options) {
|
|
51
|
+
const preparedRecipients = await this.#prepareRecipients({
|
|
52
|
+
recipients: options.recipientChunk,
|
|
53
|
+
builderOptions: options.builderOptions,
|
|
54
|
+
prepareNotification: options.prepareNotification,
|
|
55
|
+
concurrency: options.concurrency,
|
|
56
|
+
continueOnError: options.continueOnError,
|
|
57
|
+
...options.timeout && { timeout: options.timeout }
|
|
58
|
+
});
|
|
59
|
+
const messagesByChannel = this.#groupByChannel(preparedRecipients);
|
|
60
|
+
return this.#sendBatches({
|
|
61
|
+
messagesByChannel,
|
|
62
|
+
builderOptions: options.builderOptions,
|
|
63
|
+
retries: options.retries,
|
|
64
|
+
...options.timeout && { timeout: options.timeout }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Prepares all recipients for batching by resolving notifications and channels in parallel
|
|
69
|
+
*/
|
|
70
|
+
async #prepareRecipients(options) {
|
|
71
|
+
const { recipients, builderOptions, prepareNotification, concurrency, continueOnError, timeout } = options;
|
|
72
|
+
const builder = Tenace.map(recipients, (recipient) => this.#prepareRecipient(recipient, builderOptions, prepareNotification)).withConcurrency(concurrency);
|
|
73
|
+
if (timeout) builder.withTimeoutPerTask(timeout);
|
|
74
|
+
return (continueOnError ? (await builder.settle()).map((r) => r.status === "fulfilled" ? r.value : null) : await builder.execute()).filter((p) => p !== null);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Prepares a single recipient by resolving notification and channel targets
|
|
78
|
+
*/
|
|
79
|
+
async #prepareRecipient(recipient, builderOptions, prepareNotification) {
|
|
80
|
+
const { notification, shouldSkip } = await prepareNotification(recipient, builderOptions);
|
|
81
|
+
if (shouldSkip) return null;
|
|
82
|
+
return {
|
|
83
|
+
recipient,
|
|
84
|
+
notification,
|
|
85
|
+
resolvedChannels: await this.#channelResolver.resolveChannels({
|
|
86
|
+
to: recipient,
|
|
87
|
+
params: builderOptions.params,
|
|
88
|
+
tenantId: builderOptions.tenantId,
|
|
89
|
+
notification: builderOptions.notification,
|
|
90
|
+
...builderOptions.via ? { via: builderOptions.via } : {}
|
|
91
|
+
}),
|
|
92
|
+
options: {
|
|
93
|
+
...builderOptions,
|
|
94
|
+
to: recipient
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Groups prepared messages by channel name for batch sending
|
|
100
|
+
*/
|
|
101
|
+
#groupByChannel(preparedRecipients) {
|
|
102
|
+
const messagesByChannel = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const prepared of preparedRecipients) for (const [channelName, channelConfig] of Object.entries(prepared.resolvedChannels)) {
|
|
104
|
+
if (!channelConfig.shouldSend || !channelConfig.target) continue;
|
|
105
|
+
const message = this.#sender.prepareMessage({
|
|
106
|
+
notification: prepared.notification,
|
|
107
|
+
channelName,
|
|
108
|
+
sendOptions: prepared.options,
|
|
109
|
+
channelConfig
|
|
110
|
+
});
|
|
111
|
+
if (!message) continue;
|
|
112
|
+
const messages = messagesByChannel.get(channelName) ?? [];
|
|
113
|
+
messages.push(message);
|
|
114
|
+
messagesByChannel.set(channelName, messages);
|
|
115
|
+
}
|
|
116
|
+
return messagesByChannel;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Sends batched messages per channel with optional retries and timeout
|
|
120
|
+
*/
|
|
121
|
+
async #sendBatches(options) {
|
|
122
|
+
const { messagesByChannel, builderOptions, retries, timeout } = options;
|
|
123
|
+
const { retries: _r, timeout: _t, ...sendOptions } = builderOptions;
|
|
124
|
+
const sendChannel = async (channelName, messages) => {
|
|
125
|
+
const sendBatch = () => this.#sender.sendChannelBatch({
|
|
126
|
+
channelName,
|
|
127
|
+
messages,
|
|
128
|
+
sendOptions
|
|
129
|
+
});
|
|
130
|
+
if (!retries && !timeout) return sendBatch();
|
|
131
|
+
let builder = Tenace.call(sendBatch);
|
|
132
|
+
if (timeout) builder = builder.withTimeout(timeout, "aggressive");
|
|
133
|
+
if (retries > 0) builder = builder.withRetry({
|
|
134
|
+
times: retries,
|
|
135
|
+
delay: backoff.exponentialWithJitter({
|
|
136
|
+
initial: 100,
|
|
137
|
+
max: 5e3
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
return builder.execute();
|
|
141
|
+
};
|
|
142
|
+
const promises = Array.from(messagesByChannel.entries()).map(([channel, msgs]) => sendChannel(channel, msgs));
|
|
143
|
+
return (await Promise.all(promises)).flat();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { BatchingSender };
|
|
@@ -5,8 +5,10 @@ import { is } from "@julr/utils/is";
|
|
|
5
5
|
//#region src/notifications/channel_resolver.ts
|
|
6
6
|
var ChannelResolver = class {
|
|
7
7
|
#database = null;
|
|
8
|
-
|
|
8
|
+
#defaultPreferences = null;
|
|
9
|
+
constructor(database, defaultPreferences) {
|
|
9
10
|
this.#database = database || null;
|
|
11
|
+
this.#defaultPreferences = defaultPreferences || null;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Resolve channels and targets provided by via
|
|
@@ -14,14 +16,12 @@ var ChannelResolver = class {
|
|
|
14
16
|
#resolveVia(options) {
|
|
15
17
|
const notifiableTargets = options.to?.notificationTargets?.();
|
|
16
18
|
return mapEntries(options.via, (channelName, channelConfig) => {
|
|
17
|
-
const shouldSend = typeof channelConfig === "boolean" ? channelConfig : true;
|
|
18
|
-
const target = invoke(() => {
|
|
19
|
-
if (typeof channelConfig === "object") return channelConfig;
|
|
20
|
-
return notifiableTargets?.[channelName] || null;
|
|
21
|
-
});
|
|
22
19
|
return [channelName, {
|
|
23
|
-
shouldSend,
|
|
24
|
-
target
|
|
20
|
+
shouldSend: typeof channelConfig === "boolean" ? channelConfig : true,
|
|
21
|
+
target: invoke(() => {
|
|
22
|
+
if (typeof channelConfig === "object") return channelConfig;
|
|
23
|
+
return notifiableTargets?.[channelName] || null;
|
|
24
|
+
})
|
|
25
25
|
}];
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -35,32 +35,41 @@ var ChannelResolver = class {
|
|
|
35
35
|
*/
|
|
36
36
|
if (via) return this.#resolveVia(options);
|
|
37
37
|
/**
|
|
38
|
-
* Get preferences for the notifiable and tenant
|
|
39
|
-
*/
|
|
40
|
-
const preferences = await this.#database?.getPreferences({
|
|
41
|
-
notifiableId: to.id,
|
|
42
|
-
tenantId
|
|
43
|
-
});
|
|
44
|
-
/**
|
|
45
38
|
* Resolve channels based on deliverBy options
|
|
46
39
|
*/
|
|
47
40
|
const fromDeliverBy = mapEntries(notificationOptions.deliverBy, (channelName, deliverBy) => {
|
|
48
|
-
const shouldSend = invoke(() => {
|
|
49
|
-
if (typeof deliverBy === "boolean") return deliverBy;
|
|
50
|
-
return deliverBy.if({
|
|
51
|
-
to,
|
|
52
|
-
params,
|
|
53
|
-
preferences
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
const target = notifiableTargets?.[channelName] || null;
|
|
57
41
|
return [channelName, {
|
|
58
|
-
shouldSend
|
|
59
|
-
|
|
42
|
+
shouldSend: invoke(() => {
|
|
43
|
+
if (typeof deliverBy === "boolean") return deliverBy;
|
|
44
|
+
if (!to) return true;
|
|
45
|
+
return deliverBy.if({
|
|
46
|
+
to,
|
|
47
|
+
params
|
|
48
|
+
});
|
|
49
|
+
}),
|
|
50
|
+
target: notifiableTargets?.[channelName] || null
|
|
60
51
|
}];
|
|
61
52
|
});
|
|
62
53
|
/**
|
|
63
|
-
*
|
|
54
|
+
* If the notification is critical, bypass all user preferences
|
|
55
|
+
*/
|
|
56
|
+
if (notificationOptions.critical) return fromDeliverBy;
|
|
57
|
+
/**
|
|
58
|
+
* Get preferences for the notifiable and tenant.
|
|
59
|
+
* Skip if no notifiable (anonymous notification) or no database.
|
|
60
|
+
*/
|
|
61
|
+
const notifiableId = to?.id;
|
|
62
|
+
const preferences = notifiableId ? await this.#database?.getPreferences({
|
|
63
|
+
notifiableId,
|
|
64
|
+
tenantId
|
|
65
|
+
}) : void 0;
|
|
66
|
+
/**
|
|
67
|
+
* Get category preferences from default config
|
|
68
|
+
*/
|
|
69
|
+
const category = notificationOptions.category;
|
|
70
|
+
const categoryPreferences = category ? this.#defaultPreferences?.categories[category]?.channels : void 0;
|
|
71
|
+
/**
|
|
72
|
+
* Apply user preferences with priority order
|
|
64
73
|
*/
|
|
65
74
|
const currentTenant = preferences?.tenants?.[tenantId || -1];
|
|
66
75
|
const tenantPreferences = currentTenant?.global;
|
|
@@ -72,13 +81,13 @@ var ChannelResolver = class {
|
|
|
72
81
|
shouldSend: false,
|
|
73
82
|
target
|
|
74
83
|
}];
|
|
75
|
-
|
|
84
|
+
shouldSend = [
|
|
76
85
|
notificationTenantPreference?.channels[channelName],
|
|
77
86
|
tenantPreferences?.channels[channelName],
|
|
78
87
|
notificationGlobalPreference?.channels[channelName],
|
|
79
|
-
globalPreferences?.channels[channelName]
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
globalPreferences?.channels[channelName],
|
|
89
|
+
categoryPreferences?.[channelName]
|
|
90
|
+
].find((preference) => !is.undefined(preference)) ?? shouldSend;
|
|
82
91
|
return [channelName, {
|
|
83
92
|
shouldSend,
|
|
84
93
|
target
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/notifications/notification_builder.ts
|
|
2
|
+
/**
|
|
3
|
+
* Fluent builder for sending notifications.
|
|
4
|
+
* Provides a chainable API with full type-safety.
|
|
5
|
+
*/
|
|
6
|
+
var NotificationBuilderImpl = class {
|
|
7
|
+
#options;
|
|
8
|
+
#sendFn;
|
|
9
|
+
constructor(sendFn, notification) {
|
|
10
|
+
this.#sendFn = sendFn;
|
|
11
|
+
this.#options = { notification };
|
|
12
|
+
}
|
|
13
|
+
params(params) {
|
|
14
|
+
this.#options.params = params;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
to(recipients) {
|
|
18
|
+
this.#options.to = recipients;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
via(config) {
|
|
22
|
+
this.#options.via = config;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
tenant(id) {
|
|
26
|
+
this.#options.tenantId = id;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
chunkSize(size) {
|
|
30
|
+
this.#options.chunkSize = size;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
concurrency(limit) {
|
|
34
|
+
this.#options.concurrency = limit;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
continueOnError(value = true) {
|
|
38
|
+
this.#options.continueOnError = value;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
retries(count) {
|
|
42
|
+
this.#options.retries = count;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
timeout(duration) {
|
|
46
|
+
this.#options.timeout = duration;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
throwOnError(value = true) {
|
|
50
|
+
this.#options.throwOnError = value;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
useDriverBatching(value = true) {
|
|
54
|
+
this.#options.useDriverBatching = value;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
disableDriverBatch(value = true) {
|
|
58
|
+
this.#options.disableDriverBatch = value;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
onProgress(callback) {
|
|
62
|
+
this.#options.onProgress = callback;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
async send() {
|
|
66
|
+
return this.#sendFn(this.#options);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Create a new notification builder with proper typing
|
|
71
|
+
*/
|
|
72
|
+
function createNotificationBuilder(sendFn, notification) {
|
|
73
|
+
return new NotificationBuilderImpl(sendFn, notification);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createNotificationBuilder };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { errors } from "../errors/index.
|
|
2
|
-
import { Notification } from "../types/notifications.
|
|
3
|
-
import "../types/index.js";
|
|
1
|
+
import { errors } from "../errors/index.mjs";
|
|
2
|
+
import { Notification } from "../types/notifications.mjs";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { fsReadAll
|
|
4
|
+
import { fsReadAll } from "@poppinss/utils/fs";
|
|
5
|
+
import { isScriptFile } from "@poppinss/utils";
|
|
6
6
|
|
|
7
7
|
//#region src/notifications/notification_discoverer.ts
|
|
8
8
|
/**
|
|
@@ -27,10 +27,8 @@ var NotificationDiscoverer = class {
|
|
|
27
27
|
pathType: "url",
|
|
28
28
|
ignoreMissingRoot: true,
|
|
29
29
|
filter: (file) => {
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
const isNodeModule = fileURLToPath(file).includes("/node_modules/");
|
|
33
|
-
if (isNodeModule) return false;
|
|
30
|
+
if (!isScriptFile(file)) return false;
|
|
31
|
+
if (fileURLToPath(file).includes("/node_modules/")) return false;
|
|
34
32
|
const fileName = file.toString().split("/").pop() || "";
|
|
35
33
|
return fileName.endsWith(`${this.#config.fileSuffix}.ts`) || fileName.endsWith(`${this.#config.fileSuffix}.js`);
|
|
36
34
|
}
|
|
@@ -71,8 +69,7 @@ var NotificationDiscoverer = class {
|
|
|
71
69
|
}
|
|
72
70
|
async discoverNotifications() {
|
|
73
71
|
if (this.#cachedNotifications !== null) return this.#cachedNotifications;
|
|
74
|
-
const
|
|
75
|
-
const validNotifications = notifications.filter((i) => i.notification && typeof i.notification === "function").filter((i) => i.notification.prototype instanceof Notification);
|
|
72
|
+
const validNotifications = (await this.#importNotifications()).filter((i) => i.notification && typeof i.notification === "function").filter((i) => i.notification.prototype instanceof Notification);
|
|
76
73
|
this.#validateUniqueNotificationNames(validNotifications);
|
|
77
74
|
this.#cachedNotifications = validNotifications.map(({ notification }) => notification);
|
|
78
75
|
return this.#cachedNotifications;
|
|
@@ -83,15 +80,13 @@ var NotificationDiscoverer = class {
|
|
|
83
80
|
}
|
|
84
81
|
async getAllNotificationTags() {
|
|
85
82
|
await this.#ensureNotificationsDiscovered();
|
|
86
|
-
|
|
87
|
-
return notifications.map((notification) => notification.name);
|
|
83
|
+
return this.#cachedNotifications.map((notification) => notification.name);
|
|
88
84
|
}
|
|
89
85
|
/**
|
|
90
86
|
* Get notification identities with both display name and class identifier
|
|
91
87
|
*/
|
|
92
88
|
async getNotificationIdentities() {
|
|
93
|
-
|
|
94
|
-
return notifications.map((NotificationClass) => {
|
|
89
|
+
return (await this.getNotifications()).map((NotificationClass) => {
|
|
95
90
|
const options = NotificationClass.options || {};
|
|
96
91
|
return {
|
|
97
92
|
name: options.name,
|