@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.
Files changed (189) hide show
  1. package/README.md +27 -0
  2. package/dist/api/handlers/{notifications.js → notifications.mjs} +39 -16
  3. package/dist/api/handlers/preferences.mjs +73 -0
  4. package/dist/api/handlers/utils.mjs +23 -0
  5. package/dist/api/types.d.mts +52 -0
  6. package/dist/api/types.mjs +1 -0
  7. package/dist/api.d.mts +22 -0
  8. package/dist/api.mjs +25 -0
  9. package/dist/channels/aws-sns/{channel.d.ts → channel.d.mts} +3 -3
  10. package/dist/channels/aws-sns/{channel.js → channel.mjs} +2 -3
  11. package/dist/channels/aws-sns/types.mjs +1 -0
  12. package/dist/channels/aws-sns.d.mts +3 -0
  13. package/dist/channels/aws-sns.mjs +4 -0
  14. package/dist/channels/discord/{channel.d.ts → channel.d.mts} +5 -5
  15. package/dist/channels/discord/{channel.js → channel.mjs} +1 -1
  16. package/dist/channels/discord/{message.d.ts → message.d.mts} +2 -2
  17. package/dist/channels/discord/{message.js → message.mjs} +1 -1
  18. package/dist/channels/discord/{types.d.ts → types.d.mts} +1 -1
  19. package/dist/channels/discord/types.mjs +1 -0
  20. package/dist/channels/discord.d.mts +3 -0
  21. package/dist/channels/discord.mjs +4 -0
  22. package/dist/channels/expo/{channel.d.ts → channel.d.mts} +5 -3
  23. package/dist/channels/expo/channel.mjs +69 -0
  24. package/dist/channels/expo/types.mjs +1 -0
  25. package/dist/channels/expo.d.mts +3 -0
  26. package/dist/channels/expo.mjs +4 -0
  27. package/dist/channels/fcm/{channel.d.ts → channel.d.mts} +5 -5
  28. package/dist/channels/fcm/channel.mjs +68 -0
  29. package/dist/channels/fcm/types.mjs +1 -0
  30. package/dist/channels/fcm.d.mts +3 -0
  31. package/dist/channels/fcm.mjs +4 -0
  32. package/dist/channels/slack/{channel.d.ts → channel.d.mts} +5 -5
  33. package/dist/channels/slack/{channel.js → channel.mjs} +2 -2
  34. package/dist/channels/slack/{message.d.ts → message.d.mts} +1 -1
  35. package/dist/channels/slack/{message.js → message.mjs} +1 -1
  36. package/dist/channels/slack/{types.d.ts → types.d.mts} +1 -1
  37. package/dist/channels/slack/types.mjs +1 -0
  38. package/dist/channels/slack.d.mts +3 -0
  39. package/dist/channels/slack.mjs +4 -0
  40. package/dist/channels/socketio/{channel.d.ts → channel.d.mts} +3 -3
  41. package/dist/channels/socketio/{channel.js → channel.mjs} +2 -3
  42. package/dist/channels/socketio/types.mjs +1 -0
  43. package/dist/channels/socketio.d.mts +3 -0
  44. package/dist/channels/socketio.mjs +4 -0
  45. package/dist/channels/transmit/{channel.d.ts → channel.d.mts} +3 -3
  46. package/dist/channels/transmit/{channel.js → channel.mjs} +2 -3
  47. package/dist/channels/transmit/types.mjs +1 -0
  48. package/dist/channels/transmit.d.mts +3 -0
  49. package/dist/channels/transmit.mjs +4 -0
  50. package/dist/channels/twilio/{channel.d.ts → channel.d.mts} +3 -3
  51. package/dist/channels/twilio/{channel.js → channel.mjs} +3 -6
  52. package/dist/channels/twilio/types.mjs +1 -0
  53. package/dist/channels/twilio.d.mts +4 -0
  54. package/dist/channels/twilio.mjs +4 -0
  55. package/dist/channels/webhook/{exceptions.d.ts → exceptions.d.mts} +1 -1
  56. package/dist/channels/webhook/{provider.d.ts → provider.d.mts} +3 -3
  57. package/dist/channels/webhook/{provider.js → provider.mjs} +6 -8
  58. package/dist/channels/webhook/types.mjs +1 -0
  59. package/dist/channels/webhook.d.mts +4 -0
  60. package/dist/channels/webhook.mjs +5 -0
  61. package/dist/channels/webpush/{channel.d.ts → channel.d.mts} +3 -3
  62. package/dist/channels/webpush/{channel.js → channel.mjs} +2 -3
  63. package/dist/channels/webpush/types.mjs +1 -0
  64. package/dist/channels/webpush.d.mts +3 -0
  65. package/dist/channels/webpush.mjs +4 -0
  66. package/dist/database/adapters/{knex.d.ts → knex.d.mts} +1 -1
  67. package/dist/database/adapters/{knex.js → knex.mjs} +19 -15
  68. package/dist/database/adapters/{kysely.d.ts → kysely.d.mts} +1 -1
  69. package/dist/database/adapters/{kysely.js → kysely.mjs} +2 -4
  70. package/dist/database/{channel.d.ts → channel.d.mts} +3 -3
  71. package/dist/database/{channel.js → channel.mjs} +4 -6
  72. package/dist/database/{database.d.ts → database.d.mts} +3 -3
  73. package/dist/database/{database.js → database.mjs} +15 -12
  74. package/dist/database/{message.d.ts → message.d.mts} +2 -2
  75. package/dist/database/{types.d.ts → types.d.mts} +3 -3
  76. package/dist/database/types.mjs +1 -0
  77. package/dist/database.d.mts +3 -0
  78. package/dist/database.mjs +4 -0
  79. package/dist/errors/{duplicate_notification_exception.js → duplicate_notification_exception.mjs} +1 -2
  80. package/dist/errors/{http_error.d.ts → http_error.d.mts} +0 -3
  81. package/dist/errors/{index.d.ts → index.d.mts} +1 -1
  82. package/dist/errors/{index.js → index.mjs} +2 -3
  83. package/dist/events/{events.d.ts → events.d.mts} +2 -2
  84. package/dist/facteur.d.mts +45 -0
  85. package/dist/facteur.mjs +167 -0
  86. package/dist/{fake.d.ts → fake.d.mts} +3 -3
  87. package/dist/{fake.js → fake.mjs} +2 -3
  88. package/dist/{index.d.ts → index.d.mts} +3 -3
  89. package/dist/{index.js → index.mjs} +2 -2
  90. package/dist/notifications/batching_sender.mjs +148 -0
  91. package/dist/notifications/{channel_resolver.js → channel_resolver.mjs} +40 -31
  92. package/dist/notifications/notification_builder.mjs +77 -0
  93. package/dist/notifications/{notification_discoverer.d.ts → notification_discoverer.d.mts} +1 -1
  94. package/dist/notifications/{notification_discoverer.js → notification_discoverer.mjs} +9 -14
  95. package/dist/notifications/notification_sender.mjs +356 -0
  96. package/dist/notifications/orchestration_sender.mjs +92 -0
  97. package/dist/{options.d.ts → options.d.mts} +7 -6
  98. package/dist/{options.js → options.mjs} +5 -4
  99. package/dist/types/builder.d.mts +122 -0
  100. package/dist/types/channel.d.mts +56 -0
  101. package/dist/types/{events.d.ts → events.d.mts} +1 -1
  102. package/dist/types/{extend.d.ts → extend.d.mts} +2 -2
  103. package/dist/types/{notifications.d.ts → notifications.d.mts} +3 -3
  104. package/dist/types/{options.d.ts → options.d.mts} +94 -11
  105. package/dist/types/{preferences.d.ts → preferences.d.mts} +1 -1
  106. package/dist/types.d.mts +9 -0
  107. package/dist/types.mjs +4 -0
  108. package/dist/utils/chunk.mjs +28 -0
  109. package/package.json +69 -54
  110. package/dist/api/handlers/preferences.js +0 -43
  111. package/dist/api/index.d.ts +0 -16
  112. package/dist/api/index.js +0 -21
  113. package/dist/api/types.d.ts +0 -22
  114. package/dist/api/types.js +0 -0
  115. package/dist/channels/aws-sns/index.d.ts +0 -3
  116. package/dist/channels/aws-sns/index.js +0 -4
  117. package/dist/channels/aws-sns/types.js +0 -0
  118. package/dist/channels/discord/index.d.ts +0 -3
  119. package/dist/channels/discord/index.js +0 -4
  120. package/dist/channels/discord/types.js +0 -0
  121. package/dist/channels/expo/channel.js +0 -38
  122. package/dist/channels/expo/index.d.ts +0 -3
  123. package/dist/channels/expo/index.js +0 -4
  124. package/dist/channels/expo/types.js +0 -0
  125. package/dist/channels/fcm/channel.js +0 -44
  126. package/dist/channels/fcm/index.d.ts +0 -3
  127. package/dist/channels/fcm/index.js +0 -4
  128. package/dist/channels/fcm/types.js +0 -0
  129. package/dist/channels/slack/index.d.ts +0 -3
  130. package/dist/channels/slack/index.js +0 -4
  131. package/dist/channels/slack/types.js +0 -0
  132. package/dist/channels/socketio/index.d.ts +0 -3
  133. package/dist/channels/socketio/index.js +0 -4
  134. package/dist/channels/socketio/types.js +0 -0
  135. package/dist/channels/transmit/index.d.ts +0 -3
  136. package/dist/channels/transmit/index.js +0 -4
  137. package/dist/channels/transmit/types.js +0 -0
  138. package/dist/channels/twilio/index.d.ts +0 -4
  139. package/dist/channels/twilio/index.js +0 -4
  140. package/dist/channels/twilio/types.js +0 -0
  141. package/dist/channels/webhook/index.d.ts +0 -4
  142. package/dist/channels/webhook/index.js +0 -5
  143. package/dist/channels/webhook/types.js +0 -0
  144. package/dist/channels/webpush/index.d.ts +0 -3
  145. package/dist/channels/webpush/index.js +0 -4
  146. package/dist/channels/webpush/types.js +0 -0
  147. package/dist/database/index.d.ts +0 -3
  148. package/dist/database/index.js +0 -4
  149. package/dist/database/types.js +0 -0
  150. package/dist/facteur.d.ts +0 -37
  151. package/dist/facteur.js +0 -100
  152. package/dist/notifications/notification_sender.js +0 -211
  153. package/dist/types/channel.d.ts +0 -18
  154. package/dist/types/index.d.ts +0 -8
  155. package/dist/types/index.js +0 -4
  156. /package/dist/channels/aws-sns/{message.d.ts → message.d.mts} +0 -0
  157. /package/dist/channels/aws-sns/{message.js → message.mjs} +0 -0
  158. /package/dist/channels/aws-sns/{types.d.ts → types.d.mts} +0 -0
  159. /package/dist/channels/expo/{message.d.ts → message.d.mts} +0 -0
  160. /package/dist/channels/expo/{message.js → message.mjs} +0 -0
  161. /package/dist/channels/expo/{types.d.ts → types.d.mts} +0 -0
  162. /package/dist/channels/fcm/{message.d.ts → message.d.mts} +0 -0
  163. /package/dist/channels/fcm/{message.js → message.mjs} +0 -0
  164. /package/dist/channels/fcm/{types.d.ts → types.d.mts} +0 -0
  165. /package/dist/channels/socketio/{message.d.ts → message.d.mts} +0 -0
  166. /package/dist/channels/socketio/{message.js → message.mjs} +0 -0
  167. /package/dist/channels/socketio/{types.d.ts → types.d.mts} +0 -0
  168. /package/dist/channels/transmit/{message.d.ts → message.d.mts} +0 -0
  169. /package/dist/channels/transmit/{message.js → message.mjs} +0 -0
  170. /package/dist/channels/transmit/{types.d.ts → types.d.mts} +0 -0
  171. /package/dist/channels/twilio/{message.d.ts → message.d.mts} +0 -0
  172. /package/dist/channels/twilio/{message.js → message.mjs} +0 -0
  173. /package/dist/channels/twilio/{types.d.ts → types.d.mts} +0 -0
  174. /package/dist/channels/webhook/{exceptions.js → exceptions.mjs} +0 -0
  175. /package/dist/channels/webhook/{message.d.ts → message.d.mts} +0 -0
  176. /package/dist/channels/webhook/{message.js → message.mjs} +0 -0
  177. /package/dist/channels/webhook/{types.d.ts → types.d.mts} +0 -0
  178. /package/dist/channels/webpush/{message.d.ts → message.d.mts} +0 -0
  179. /package/dist/channels/webpush/{message.js → message.mjs} +0 -0
  180. /package/dist/channels/webpush/{types.d.ts → types.d.mts} +0 -0
  181. /package/dist/database/{message.js → message.mjs} +0 -0
  182. /package/dist/{debug.js → debug.mjs} +0 -0
  183. /package/dist/errors/{duplicate_notification_exception.d.ts → duplicate_notification_exception.d.mts} +0 -0
  184. /package/dist/errors/{http_error.js → http_error.mjs} +0 -0
  185. /package/dist/events/{events.js → events.mjs} +0 -0
  186. /package/dist/{helpers.js → helpers.mjs} +0 -0
  187. /package/dist/types/{channel.js → channel.mjs} +0 -0
  188. /package/dist/types/{notifications.js → notifications.mjs} +0 -0
  189. /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 };
@@ -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 { SendOptions } from "./types/options.js";
2
- import { Notification, NotificationSendResult } from "./types/notifications.js";
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: SendOptions<any>): NotificationSendResult;
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
- const found = sentNotifications.some((sent) => {
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.js";
2
- import { Facteur, createFacteur } from "./facteur.js";
3
- import { E_MISSING_MESSAGE_METHOD, E_QUEUE_NOT_SET, E_SEND_NOTIFICATION_FAILED, E_UNAVAILABLE_TARGETS, errors } from "./errors/index.js";
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.js";
2
- import { Facteur, createFacteur } from "./facteur.js";
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
- constructor(database) {
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
- target
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
- * And then we can apply user preferences
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
- const preferencesSources = [
84
+ shouldSend = [
76
85
  notificationTenantPreference?.channels[channelName],
77
86
  tenantPreferences?.channels[channelName],
78
87
  notificationGlobalPreference?.channels[channelName],
79
- globalPreferences?.channels[channelName]
80
- ];
81
- shouldSend = preferencesSources.find((preference) => !is.undefined(preference)) ?? shouldSend;
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,4 +1,4 @@
1
- import { Notification } from "../types/notifications.js";
1
+ import { Notification } from "../types/notifications.mjs";
2
2
 
3
3
  //#region src/notifications/notification_discoverer.d.ts
4
4
  interface NotificationDiscovererConfig {
@@ -1,8 +1,8 @@
1
- import { errors } from "../errors/index.js";
2
- import { Notification } from "../types/notifications.js";
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, isScriptFile } from "@poppinss/utils";
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
- const isScript = isScriptFile(file);
31
- if (!isScript) return false;
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 notifications = await this.#importNotifications();
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
- const notifications = this.#cachedNotifications;
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
- const notifications = await this.getNotifications();
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,