@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,356 @@
|
|
|
1
|
+
import { errors } from "../errors/index.mjs";
|
|
2
|
+
import { chunk } from "../utils/chunk.mjs";
|
|
3
|
+
import "./channel_resolver.mjs";
|
|
4
|
+
import { capitalizeFirstLetter } from "../helpers.mjs";
|
|
5
|
+
import { facteurEvents } from "../events/events.mjs";
|
|
6
|
+
import debug_default from "../debug.mjs";
|
|
7
|
+
import { Tenace, backoff } from "@julr/tenace";
|
|
8
|
+
|
|
9
|
+
//#region src/notifications/notification_sender.ts
|
|
10
|
+
/**
|
|
11
|
+
* Responsible for sending notifications and messages
|
|
12
|
+
*/
|
|
13
|
+
var NotificationSender = class {
|
|
14
|
+
constructor(channels, channelResolver, emitter, retryConfig = {}) {
|
|
15
|
+
this.channels = channels;
|
|
16
|
+
this.channelResolver = channelResolver;
|
|
17
|
+
this.emitter = emitter;
|
|
18
|
+
this.retryConfig = retryConfig;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the message content by calling the notification's `as<ChannelName>Message` method
|
|
22
|
+
*/
|
|
23
|
+
#buildMessageContent(options) {
|
|
24
|
+
const { notification, channelName, sendOptions } = options;
|
|
25
|
+
const capitalizedChannelName = capitalizeFirstLetter(channelName);
|
|
26
|
+
const messageBuilder = notification[`as${capitalizedChannelName}Message`];
|
|
27
|
+
if (typeof messageBuilder !== "function") throw new errors.E_MISSING_MESSAGE_METHOD([capitalizedChannelName]);
|
|
28
|
+
const content = messageBuilder.call(notification, {
|
|
29
|
+
to: sendOptions.to,
|
|
30
|
+
params: sendOptions.params,
|
|
31
|
+
tenantId: sendOptions.tenantId
|
|
32
|
+
});
|
|
33
|
+
if (!content) return null;
|
|
34
|
+
return {
|
|
35
|
+
content,
|
|
36
|
+
capitalizedChannelName
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Execute a function with retry/timeout if configured
|
|
41
|
+
*/
|
|
42
|
+
async #executeWithRetry(fn, options) {
|
|
43
|
+
if (!(options.retries !== void 0 || options.timeout !== void 0)) return fn();
|
|
44
|
+
let builder = Tenace.call(fn);
|
|
45
|
+
if (options.timeout !== void 0) builder = builder.withTimeout(options.timeout, "aggressive");
|
|
46
|
+
if (options.retries !== void 0 && options.retries > 0) builder = builder.withRetry({
|
|
47
|
+
times: options.retries,
|
|
48
|
+
delay: backoff.exponentialWithJitter({
|
|
49
|
+
initial: 100,
|
|
50
|
+
max: 5e3
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
await builder.execute();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Emit notification sending event
|
|
57
|
+
*/
|
|
58
|
+
#emitNotificationSending(notification, resolvedChannels) {
|
|
59
|
+
const event = facteurEvents.notificationSending({
|
|
60
|
+
notification,
|
|
61
|
+
resolvedChannels
|
|
62
|
+
});
|
|
63
|
+
this.emitter.emit(event.name, event.data);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Emit notification sent event
|
|
67
|
+
*/
|
|
68
|
+
#emitNotificationSent(notification, results) {
|
|
69
|
+
const event = facteurEvents.notificationSent({
|
|
70
|
+
notification,
|
|
71
|
+
results
|
|
72
|
+
});
|
|
73
|
+
this.emitter.emit(event.name, event.data);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Emit notification failed event
|
|
77
|
+
*/
|
|
78
|
+
#emitNotificationFailed(notification, errors$1) {
|
|
79
|
+
const event = facteurEvents.notificationFailed({
|
|
80
|
+
notification,
|
|
81
|
+
errors: errors$1
|
|
82
|
+
});
|
|
83
|
+
this.emitter.emit(event.name, event.data);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Emit message sending event
|
|
87
|
+
*/
|
|
88
|
+
#emitMessageSending(notification, channelName, message) {
|
|
89
|
+
const event = facteurEvents.messageSending({
|
|
90
|
+
notification,
|
|
91
|
+
channelName,
|
|
92
|
+
message
|
|
93
|
+
});
|
|
94
|
+
this.emitter.emit(event.name, event.data);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Emit message sent event
|
|
98
|
+
*/
|
|
99
|
+
#emitMessageSent(notification, channelName, message) {
|
|
100
|
+
const event = facteurEvents.messageSent({
|
|
101
|
+
notification,
|
|
102
|
+
channelName,
|
|
103
|
+
message
|
|
104
|
+
});
|
|
105
|
+
this.emitter.emit(event.name, event.data);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emit message failed event
|
|
109
|
+
*/
|
|
110
|
+
#emitMessageFailed(notification, channelName, message, error) {
|
|
111
|
+
const event = facteurEvents.messageFailed({
|
|
112
|
+
notification,
|
|
113
|
+
channelName,
|
|
114
|
+
message,
|
|
115
|
+
error
|
|
116
|
+
});
|
|
117
|
+
this.emitter.emit(event.name, event.data);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get a channel by its name, throws if not registered
|
|
121
|
+
*/
|
|
122
|
+
#getChannel(channelName) {
|
|
123
|
+
const channel = this.channels[channelName];
|
|
124
|
+
if (!channel) throw new Error(`Channel '${channelName}' is not registered`);
|
|
125
|
+
return channel;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve retry options for a specific channel.
|
|
129
|
+
* Priority: send options > channel config > global config
|
|
130
|
+
*/
|
|
131
|
+
#resolveRetryOptions(channelName, sendOptions) {
|
|
132
|
+
const globalConfig = this.retryConfig;
|
|
133
|
+
const channelConfig = globalConfig.channels?.[channelName];
|
|
134
|
+
const retries = sendOptions.retries ?? channelConfig?.retries ?? globalConfig.retries;
|
|
135
|
+
const timeout = sendOptions.timeout ?? channelConfig?.timeout ?? globalConfig.timeout;
|
|
136
|
+
return {
|
|
137
|
+
...retries !== void 0 && { retries },
|
|
138
|
+
...timeout !== void 0 && { timeout }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Send a single message through a specific channel
|
|
143
|
+
*/
|
|
144
|
+
async #sendMessage(options) {
|
|
145
|
+
const { notification, channelName, options: sendOptions, channelConfig } = options;
|
|
146
|
+
const channel = this.#getChannel(channelName);
|
|
147
|
+
const messageResult = this.#buildMessageContent({
|
|
148
|
+
notification,
|
|
149
|
+
channelName,
|
|
150
|
+
sendOptions
|
|
151
|
+
});
|
|
152
|
+
if (!messageResult) return null;
|
|
153
|
+
const { content: messageContent } = messageResult;
|
|
154
|
+
debug_default(`Sending message via ${channelName}: %O`, messageContent);
|
|
155
|
+
this.#emitMessageSending(notification, channelName, messageContent);
|
|
156
|
+
const retryOptions = this.#resolveRetryOptions(channelName, sendOptions);
|
|
157
|
+
const doSend = async () => {
|
|
158
|
+
await channel.send({
|
|
159
|
+
tenantId: sendOptions.tenantId,
|
|
160
|
+
message: messageContent,
|
|
161
|
+
targets: channelConfig.target,
|
|
162
|
+
to: sendOptions.to
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
await this.#executeWithRetry(doSend, retryOptions);
|
|
167
|
+
debug_default(`Message sent via ${channelName}`);
|
|
168
|
+
this.#emitMessageSent(notification, channelName, messageContent);
|
|
169
|
+
return {
|
|
170
|
+
channel: channelName,
|
|
171
|
+
status: "success"
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.#emitMessageFailed(notification, channelName, messageContent, error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Process results from sending messages and emit appropriate events
|
|
180
|
+
*/
|
|
181
|
+
async #processResults(options) {
|
|
182
|
+
const { results, throwOnError, notification } = options;
|
|
183
|
+
const channelResults = results.filter((result) => result !== null);
|
|
184
|
+
const successes = channelResults.filter((r) => r.status === "success");
|
|
185
|
+
const failures = channelResults.filter((r) => r.status === "failed");
|
|
186
|
+
if (!failures.length) {
|
|
187
|
+
this.#emitNotificationSent(notification, channelResults);
|
|
188
|
+
await notification.afterSend();
|
|
189
|
+
return {
|
|
190
|
+
failed: failures.length,
|
|
191
|
+
success: successes.length,
|
|
192
|
+
results: channelResults
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const failureReasons = failures.map((r) => r.error);
|
|
196
|
+
this.#emitNotificationFailed(notification, failureReasons);
|
|
197
|
+
await notification.afterSend();
|
|
198
|
+
if (throwOnError !== false) throw new errors.E_SEND_NOTIFICATION_FAILED(failureReasons);
|
|
199
|
+
return {
|
|
200
|
+
failed: failures.length,
|
|
201
|
+
success: successes.length,
|
|
202
|
+
results: channelResults
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Send messages using the channel's batch API
|
|
207
|
+
*/
|
|
208
|
+
async #sendWithBatchApi(channelName, channel, messages) {
|
|
209
|
+
const batches = chunk(messages, channel.batchConfig?.maxSize ?? 100);
|
|
210
|
+
const allResults = [];
|
|
211
|
+
for (const batch of batches) {
|
|
212
|
+
debug_default(`Sending batch of ${batch.length} messages via ${channelName}`);
|
|
213
|
+
for (const msg of batch) this.#emitMessageSending(msg.notification, channelName, msg.messageContent);
|
|
214
|
+
try {
|
|
215
|
+
const batchResult = await channel.sendBatch(batch.map((m) => m.sendParams));
|
|
216
|
+
for (const result of batchResult.results) {
|
|
217
|
+
const msg = batch[result.index];
|
|
218
|
+
if (result.status === "success") this.#emitMessageSent(msg.notification, channelName, msg.messageContent);
|
|
219
|
+
else this.#emitMessageFailed(msg.notification, channelName, msg.messageContent, result.error || /* @__PURE__ */ new Error("Unknown batch error"));
|
|
220
|
+
allResults.push({
|
|
221
|
+
channel: channelName,
|
|
222
|
+
status: result.status,
|
|
223
|
+
error: result.error
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
for (const msg of batch) {
|
|
228
|
+
this.#emitMessageFailed(msg.notification, channelName, msg.messageContent, error);
|
|
229
|
+
allResults.push({
|
|
230
|
+
channel: channelName,
|
|
231
|
+
status: "failed",
|
|
232
|
+
error
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return allResults;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Send messages individually when batch API is not available
|
|
241
|
+
*/
|
|
242
|
+
async #sendIndividually(channelName, channel, messages, sendOptions) {
|
|
243
|
+
const retryOptions = this.#resolveRetryOptions(channelName, sendOptions);
|
|
244
|
+
const results = [];
|
|
245
|
+
for (const msg of messages) {
|
|
246
|
+
debug_default(`Sending message via ${channelName}: %O`, msg.messageContent);
|
|
247
|
+
this.#emitMessageSending(msg.notification, channelName, msg.messageContent);
|
|
248
|
+
try {
|
|
249
|
+
await this.#executeWithRetry(() => channel.send(msg.sendParams), retryOptions);
|
|
250
|
+
debug_default(`Message sent via ${channelName}`);
|
|
251
|
+
this.#emitMessageSent(msg.notification, channelName, msg.messageContent);
|
|
252
|
+
results.push({
|
|
253
|
+
channel: channelName,
|
|
254
|
+
status: "success"
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.#emitMessageFailed(msg.notification, channelName, msg.messageContent, error);
|
|
258
|
+
results.push({
|
|
259
|
+
channel: channelName,
|
|
260
|
+
status: "failed",
|
|
261
|
+
error
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Send a notification through all resolved channels
|
|
269
|
+
*/
|
|
270
|
+
async send(options, notification) {
|
|
271
|
+
const { via, params, tenantId, to } = options;
|
|
272
|
+
const resolvedChannels = await this.channelResolver.resolveChannels({
|
|
273
|
+
to,
|
|
274
|
+
params,
|
|
275
|
+
tenantId,
|
|
276
|
+
notification: options.notification,
|
|
277
|
+
...via ? { via } : {}
|
|
278
|
+
});
|
|
279
|
+
debug_default(`Resolved channels: %O`, resolvedChannels);
|
|
280
|
+
this.#emitNotificationSending(notification, resolvedChannels);
|
|
281
|
+
const promises = Object.entries(resolvedChannels).map(async ([name, config]) => {
|
|
282
|
+
if (!config.shouldSend || !config.target) return null;
|
|
283
|
+
return await this.#sendMessage({
|
|
284
|
+
notification,
|
|
285
|
+
channelConfig: config,
|
|
286
|
+
channelName: name,
|
|
287
|
+
options
|
|
288
|
+
}).catch((error) => {
|
|
289
|
+
debug_default(`Failed to send notification via ${name}: %O`, error);
|
|
290
|
+
return {
|
|
291
|
+
channel: name,
|
|
292
|
+
status: "failed",
|
|
293
|
+
error
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return await this.#processResults({
|
|
298
|
+
notification,
|
|
299
|
+
results: await Promise.all(promises),
|
|
300
|
+
throwOnError: options.throwOnError !== false
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Prepare a message for a specific channel without sending it.
|
|
305
|
+
* Returns null if the message should not be sent.
|
|
306
|
+
*/
|
|
307
|
+
prepareMessage(options) {
|
|
308
|
+
const { notification, channelName, sendOptions, channelConfig } = options;
|
|
309
|
+
this.#getChannel(channelName);
|
|
310
|
+
const messageResult = this.#buildMessageContent({
|
|
311
|
+
notification,
|
|
312
|
+
channelName,
|
|
313
|
+
sendOptions
|
|
314
|
+
});
|
|
315
|
+
if (!messageResult) return null;
|
|
316
|
+
const { content: messageContent } = messageResult;
|
|
317
|
+
return {
|
|
318
|
+
channelName,
|
|
319
|
+
notification,
|
|
320
|
+
messageContent,
|
|
321
|
+
sendParams: {
|
|
322
|
+
tenantId: sendOptions.tenantId,
|
|
323
|
+
message: messageContent,
|
|
324
|
+
targets: channelConfig.target,
|
|
325
|
+
to: sendOptions.to
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Send multiple messages through a channel using batch API if available.
|
|
331
|
+
* Falls back to individual sends if batch is not supported.
|
|
332
|
+
*/
|
|
333
|
+
async sendChannelBatch(options) {
|
|
334
|
+
const { channelName, messages, sendOptions } = options;
|
|
335
|
+
const channel = this.#getChannel(channelName);
|
|
336
|
+
const disableDriverBatch = sendOptions.disableDriverBatch === true;
|
|
337
|
+
if (channel.sendBatch && channel.batchConfig?.enabled !== false && !disableDriverBatch) return this.#sendWithBatchApi(channelName, channel, messages);
|
|
338
|
+
return this.#sendIndividually(channelName, channel, messages, sendOptions);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Check if a channel supports batch sending
|
|
342
|
+
*/
|
|
343
|
+
channelSupportsBatch(channelName) {
|
|
344
|
+
const ch = this.channels[channelName];
|
|
345
|
+
return !!(ch?.sendBatch && ch.batchConfig?.enabled !== false);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get the batch config for a channel
|
|
349
|
+
*/
|
|
350
|
+
getChannelBatchConfig(channelName) {
|
|
351
|
+
return this.channels[channelName]?.batchConfig;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
//#endregion
|
|
356
|
+
export { NotificationSender };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { chunk } from "../utils/chunk.mjs";
|
|
2
|
+
import { Tenace, backoff } from "@julr/tenace";
|
|
3
|
+
|
|
4
|
+
//#region src/notifications/orchestration_sender.ts
|
|
5
|
+
/**
|
|
6
|
+
* Handles bulk notification sending using orchestration mode.
|
|
7
|
+
*
|
|
8
|
+
* In orchestration mode, each recipient is processed individually with configurable
|
|
9
|
+
* concurrency, chunking, retries, and timeout. This provides fine-grained control
|
|
10
|
+
* over the sending process and per-recipient error handling.
|
|
11
|
+
*/
|
|
12
|
+
var OrchestrationSender = class {
|
|
13
|
+
#sender;
|
|
14
|
+
constructor(sender) {
|
|
15
|
+
this.#sender = sender;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sends notifications to multiple recipients with concurrency control
|
|
19
|
+
*/
|
|
20
|
+
async send(options) {
|
|
21
|
+
const { chunkSize = Infinity, concurrency = 10, continueOnError = false, retries = 0, timeout, onProgress } = options.builderOptions;
|
|
22
|
+
const chunks = chunk(options.recipients, chunkSize);
|
|
23
|
+
const allResults = [];
|
|
24
|
+
let completedCount = 0;
|
|
25
|
+
const totalCount = options.recipients.length;
|
|
26
|
+
const { retries: _retries, timeout: _timeout, ...perRecipientOptions } = options.builderOptions;
|
|
27
|
+
for (const recipientChunk of chunks) {
|
|
28
|
+
const chunkResults = await this.#processChunk({
|
|
29
|
+
recipientChunk,
|
|
30
|
+
perRecipientOptions,
|
|
31
|
+
prepareNotification: options.prepareNotification,
|
|
32
|
+
concurrency,
|
|
33
|
+
retries,
|
|
34
|
+
continueOnError,
|
|
35
|
+
onProgress: () => {
|
|
36
|
+
completedCount++;
|
|
37
|
+
onProgress?.(completedCount, totalCount);
|
|
38
|
+
},
|
|
39
|
+
...timeout && { timeout }
|
|
40
|
+
});
|
|
41
|
+
allResults.push(...chunkResults);
|
|
42
|
+
}
|
|
43
|
+
return this.#aggregateResults(allResults);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Processes a single chunk of recipients with Tenace for concurrency and retry
|
|
47
|
+
*/
|
|
48
|
+
async #processChunk(options) {
|
|
49
|
+
const builder = Tenace.map(options.recipientChunk, async (recipient) => {
|
|
50
|
+
const recipientOptions = {
|
|
51
|
+
...options.perRecipientOptions,
|
|
52
|
+
to: recipient
|
|
53
|
+
};
|
|
54
|
+
const { notification, shouldSkip } = await options.prepareNotification(recipient, recipientOptions);
|
|
55
|
+
if (shouldSkip) {
|
|
56
|
+
options.onProgress();
|
|
57
|
+
return {
|
|
58
|
+
success: 0,
|
|
59
|
+
failed: 0,
|
|
60
|
+
results: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const result = await this.#sender.send(recipientOptions, notification);
|
|
64
|
+
options.onProgress();
|
|
65
|
+
return result;
|
|
66
|
+
}).withConcurrency(options.concurrency);
|
|
67
|
+
if (options.retries > 0) builder.withRetryPerTask(options.retries, { delay: backoff.exponentialWithJitter({
|
|
68
|
+
initial: 100,
|
|
69
|
+
max: 5e3
|
|
70
|
+
}) });
|
|
71
|
+
if (options.timeout) builder.withTimeoutPerTask(options.timeout);
|
|
72
|
+
if (!options.continueOnError) return builder.execute();
|
|
73
|
+
return (await builder.settle()).map((result) => result.status === "fulfilled" ? result.value : {
|
|
74
|
+
success: 0,
|
|
75
|
+
failed: 1,
|
|
76
|
+
results: []
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Combines multiple send results into a single aggregated result
|
|
81
|
+
*/
|
|
82
|
+
#aggregateResults(results) {
|
|
83
|
+
return {
|
|
84
|
+
success: results.reduce((sum, r) => sum + r.success, 0),
|
|
85
|
+
failed: results.reduce((sum, r) => sum + r.failed, 0),
|
|
86
|
+
results: results.flatMap((r) => r.results)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { OrchestrationSender };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { Channel } from "./types/channel.mjs";
|
|
2
|
+
import { QueueAdapter } from "./types/queue.mjs";
|
|
3
|
+
import { ResolvedDefaultPreferences } from "./types/preferences.mjs";
|
|
4
|
+
import { Emitter } from "./types/events.mjs";
|
|
5
|
+
import { FacteurConfiguration, NotificationResolver, RetryConfig } from "./types/options.mjs";
|
|
6
|
+
import { DatabaseAdapter } from "./database/types.mjs";
|
|
7
7
|
import { Logger } from "@julr/utils/logger";
|
|
8
8
|
|
|
9
9
|
//#region src/options.d.ts
|
|
@@ -15,6 +15,7 @@ declare class FacteurOptions<KnownChannels extends Record<string, Channel>, DBAd
|
|
|
15
15
|
queueAdapter: QueueAdapter;
|
|
16
16
|
databaseAdapter: DBAdapter | null;
|
|
17
17
|
notificationResolver: NotificationResolver;
|
|
18
|
+
retry: RetryConfig<KnownChannels>;
|
|
18
19
|
readonly defaultPreferences: ResolvedDefaultPreferences<KnownChannels>;
|
|
19
20
|
constructor(config: FacteurConfiguration<KnownChannels, DBAdapter>);
|
|
20
21
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errors } from "./errors/index.
|
|
1
|
+
import { errors } from "./errors/index.mjs";
|
|
2
2
|
import EventEmitter from "node:events";
|
|
3
3
|
import { noopLogger } from "@julr/utils/logger";
|
|
4
4
|
import { invoke } from "@julr/utils/functions";
|
|
@@ -11,6 +11,7 @@ var FacteurOptions = class {
|
|
|
11
11
|
queueAdapter;
|
|
12
12
|
databaseAdapter = null;
|
|
13
13
|
notificationResolver;
|
|
14
|
+
retry = {};
|
|
14
15
|
defaultPreferences;
|
|
15
16
|
#resolveDefaultPreferences(preferences) {
|
|
16
17
|
preferences = preferences || {};
|
|
@@ -23,11 +24,10 @@ var FacteurOptions = class {
|
|
|
23
24
|
* Build categories preferences
|
|
24
25
|
*/
|
|
25
26
|
const categories = Object.entries(preferences.categories || {}).map(([category, config]) => {
|
|
26
|
-
|
|
27
|
+
return [category, { channels: invoke(() => {
|
|
27
28
|
if (typeof config === "boolean") return Object.fromEntries(allChannels.map((channel) => [channel, config]));
|
|
28
29
|
return Object.assign({}, globalChannelPreferences, config.channels || {});
|
|
29
|
-
});
|
|
30
|
-
return [category, { channels: value }];
|
|
30
|
+
}) }];
|
|
31
31
|
});
|
|
32
32
|
return {
|
|
33
33
|
enabled: preferences.enabled ?? true,
|
|
@@ -42,6 +42,7 @@ var FacteurOptions = class {
|
|
|
42
42
|
this.databaseAdapter = config.databaseAdapter ?? null;
|
|
43
43
|
this.defaultPreferences = this.#resolveDefaultPreferences(config.preferences);
|
|
44
44
|
this.notificationResolver = config.notificationResolver || ((notification, ctx) => new notification(ctx));
|
|
45
|
+
this.retry = config.retry || {};
|
|
45
46
|
const throwIfQueueNotSet = () => {
|
|
46
47
|
throw new errors.E_QUEUE_NOT_SET();
|
|
47
48
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ChannelName } from "./extend.mjs";
|
|
2
|
+
import { BulkSendOptions, ChannelSpecificConfig, ExtractNotifiable, ExtractParams, RetryOptions } from "./options.mjs";
|
|
3
|
+
import { NotificationClass, NotificationSendResult } from "./notifications.mjs";
|
|
4
|
+
import { Identifier } from "../database/types.mjs";
|
|
5
|
+
import { Duration } from "@julr/tenace/types";
|
|
6
|
+
|
|
7
|
+
//#region src/types/builder.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper type to check if params are required for a notification
|
|
11
|
+
*/
|
|
12
|
+
type HasRequiredParams<T extends NotificationClass<any, any>> = ExtractParams<T> extends Record<string, never> ? false : unknown extends ExtractParams<T> ? false : true;
|
|
13
|
+
/**
|
|
14
|
+
* Helper type to check if a notification is anonymous (no notifiable)
|
|
15
|
+
*/
|
|
16
|
+
type IsAnonymous<T extends NotificationClass<any, any>> = ExtractNotifiable<T> extends undefined ? true : false;
|
|
17
|
+
/**
|
|
18
|
+
* State tracking for the builder
|
|
19
|
+
*/
|
|
20
|
+
interface BuilderState {
|
|
21
|
+
hasParams: boolean;
|
|
22
|
+
hasTo: boolean;
|
|
23
|
+
hasVia: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Internal options accumulated by the builder. Extends BulkSendOptions for bulk operations.
|
|
27
|
+
*/
|
|
28
|
+
interface BuilderOptions<TNotification extends NotificationClass<any, any>> extends BulkSendOptions {
|
|
29
|
+
notification: TNotification;
|
|
30
|
+
params?: ExtractParams<TNotification>;
|
|
31
|
+
to?: ExtractNotifiable<TNotification> | ExtractNotifiable<TNotification>[] | unknown;
|
|
32
|
+
via?: ChannelSpecificConfig<any>;
|
|
33
|
+
tenantId?: Identifier;
|
|
34
|
+
throwOnError?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Minimal options needed by NotificationSender
|
|
38
|
+
*/
|
|
39
|
+
interface InternalSendOptions extends RetryOptions {
|
|
40
|
+
notification: NotificationClass<any, any>;
|
|
41
|
+
params?: Record<string, any>;
|
|
42
|
+
to?: unknown;
|
|
43
|
+
via?: ChannelSpecificConfig<any>;
|
|
44
|
+
tenantId?: Identifier;
|
|
45
|
+
throwOnError?: boolean;
|
|
46
|
+
disableDriverBatch?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Merges builder state updates. Used to track which methods have been called.
|
|
50
|
+
*/
|
|
51
|
+
type MergeState<TState extends BuilderState, TUpdate extends Partial<BuilderState>> = {
|
|
52
|
+
hasParams: TUpdate extends {
|
|
53
|
+
hasParams: infer P;
|
|
54
|
+
} ? P : TState['hasParams'];
|
|
55
|
+
hasTo: TUpdate extends {
|
|
56
|
+
hasTo: infer T;
|
|
57
|
+
} ? T : TState['hasTo'];
|
|
58
|
+
hasVia: TUpdate extends {
|
|
59
|
+
hasVia: infer V;
|
|
60
|
+
} ? V : TState['hasVia'];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Methods available on all builders: bulk options, retry, and tenant selection.
|
|
64
|
+
*/
|
|
65
|
+
interface CommonBuilderMethods<TNotification extends NotificationClass<any, any>, TState extends BuilderState> {
|
|
66
|
+
tenant(id: Identifier): NotificationBuilder<TNotification, TState>;
|
|
67
|
+
chunkSize(size: number): NotificationBuilder<TNotification, TState>;
|
|
68
|
+
concurrency(limit: number): NotificationBuilder<TNotification, TState>;
|
|
69
|
+
continueOnError(value?: boolean): NotificationBuilder<TNotification, TState>;
|
|
70
|
+
retries(count: number): NotificationBuilder<TNotification, TState>;
|
|
71
|
+
timeout(duration: Duration): NotificationBuilder<TNotification, TState>;
|
|
72
|
+
throwOnError(value?: boolean): NotificationBuilder<TNotification, TState>;
|
|
73
|
+
useDriverBatching(value?: boolean): NotificationBuilder<TNotification, TState>;
|
|
74
|
+
disableDriverBatch(value?: boolean): NotificationBuilder<TNotification, TState>;
|
|
75
|
+
onProgress(callback: (completed: number, total: number) => void): NotificationBuilder<TNotification, TState>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Override channel targets directly (e.g., send to a specific email address).
|
|
79
|
+
*/
|
|
80
|
+
interface ViaMethod<TNotification extends NotificationClass<any, any>, TState extends BuilderState> {
|
|
81
|
+
via(config: { [K in ChannelName]?: boolean | any }): NotificationBuilder<TNotification, MergeState<TState, {
|
|
82
|
+
hasVia: true;
|
|
83
|
+
}>>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* params() method type - always available but changes state
|
|
87
|
+
*/
|
|
88
|
+
interface ParamsMethod<TNotification extends NotificationClass<any, any>, TState extends BuilderState> {
|
|
89
|
+
params(params: ExtractParams<TNotification>): NotificationBuilder<TNotification, MergeState<TState, {
|
|
90
|
+
hasParams: true;
|
|
91
|
+
}>>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Set recipients. Accepts single, array, or async iterable. Not available for anonymous notifications.
|
|
95
|
+
*/
|
|
96
|
+
interface ToMethod<TNotification extends NotificationClass<any, any>, TState extends BuilderState> {
|
|
97
|
+
to(recipients: NonNullable<ExtractNotifiable<TNotification>> | NonNullable<ExtractNotifiable<TNotification>>[] | AsyncIterable<NonNullable<ExtractNotifiable<TNotification>>>): NotificationBuilder<TNotification, MergeState<TState, {
|
|
98
|
+
hasTo: true;
|
|
99
|
+
}>>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* send() method type
|
|
103
|
+
*/
|
|
104
|
+
interface SendMethod {
|
|
105
|
+
send(): Promise<NotificationSendResult>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns true if send() should be available. Requires:
|
|
109
|
+
* - params() called if notification has required params
|
|
110
|
+
* - to() called for non-anonymous, or via() called for anonymous
|
|
111
|
+
*/
|
|
112
|
+
type CanSend<TNotification extends NotificationClass<any, any>, TState extends BuilderState> = HasRequiredParams<TNotification> extends true ? TState['hasParams'] extends true ? CheckToOrVia<TNotification, TState> : false : CheckToOrVia<TNotification, TState>;
|
|
113
|
+
/**
|
|
114
|
+
* Anonymous notifications require via(), non-anonymous require to().
|
|
115
|
+
*/
|
|
116
|
+
type CheckToOrVia<TNotification extends NotificationClass<any, any>, TState extends BuilderState> = IsAnonymous<TNotification> extends true ? TState['hasVia'] extends true ? true : false : TState['hasTo'] extends true ? true : false;
|
|
117
|
+
/**
|
|
118
|
+
* Fluent builder for sending notifications. Methods appear/disappear based on state.
|
|
119
|
+
*/
|
|
120
|
+
type NotificationBuilder<TNotification extends NotificationClass<any, any>, TState extends BuilderState> = CommonBuilderMethods<TNotification, TState> & ViaMethod<TNotification, TState> & (TState['hasParams'] extends true ? {} : ParamsMethod<TNotification, TState>) & (IsAnonymous<TNotification> extends true ? {} : TState['hasTo'] extends true ? {} : ToMethod<TNotification, TState>) & (CanSend<TNotification, TState> extends true ? SendMethod : {});
|
|
121
|
+
//#endregion
|
|
122
|
+
export { BuilderOptions, BuilderState, HasRequiredParams, InternalSendOptions, IsAnonymous, NotificationBuilder };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Identifier } from "../database/types.mjs";
|
|
2
|
+
import { Awaitable } from "@julr/utils/types";
|
|
3
|
+
|
|
4
|
+
//#region src/types/channel.d.ts
|
|
5
|
+
type ChannelSendParams<Message, Targets> = {
|
|
6
|
+
to?: any;
|
|
7
|
+
message: Message;
|
|
8
|
+
targets?: Targets;
|
|
9
|
+
tenantId?: Identifier | undefined;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Result of a batch send operation
|
|
13
|
+
*/
|
|
14
|
+
interface BatchSendResult {
|
|
15
|
+
success: number;
|
|
16
|
+
failed: number;
|
|
17
|
+
results: Array<{
|
|
18
|
+
index: number;
|
|
19
|
+
status: 'success' | 'failed';
|
|
20
|
+
error?: Error;
|
|
21
|
+
response?: any;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for batch sending capabilities
|
|
26
|
+
*/
|
|
27
|
+
interface BatchConfig {
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of messages per batch API call.
|
|
30
|
+
* E.g. 500 for FCM, 100 for Expo
|
|
31
|
+
*/
|
|
32
|
+
maxSize: number;
|
|
33
|
+
/**
|
|
34
|
+
* Whether batching is enabled for this channel.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
declare const kTargetSymbol: unique symbol;
|
|
40
|
+
interface Channel<_Options = any, Message = any, Response = any, Targets = any> {
|
|
41
|
+
[kTargetSymbol]: Targets;
|
|
42
|
+
name: string;
|
|
43
|
+
send: (options: ChannelSendParams<Message, Targets>) => Awaitable<Response>;
|
|
44
|
+
/**
|
|
45
|
+
* Optional batch send method for providers that support sending multiple messages in one API call.
|
|
46
|
+
* When implemented, the orchestration layer will group messages and use this method.
|
|
47
|
+
*/
|
|
48
|
+
sendBatch?: (messages: ChannelSendParams<Message, Targets>[]) => Awaitable<BatchSendResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for batch sending.
|
|
51
|
+
* Required when sendBatch is implemented.
|
|
52
|
+
*/
|
|
53
|
+
batchConfig?: BatchConfig;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { BatchConfig, BatchSendResult, Channel, ChannelSendParams, kTargetSymbol };
|