@gravito/flare 3.3.0 → 4.0.1

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/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,131 +30,445 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/types/middleware.ts
34
+ var MiddlewarePriority;
35
+ var init_middleware = __esm({
36
+ "src/types/middleware.ts"() {
37
+ "use strict";
38
+ MiddlewarePriority = {
39
+ /** 最高優先級:安全檢查 (100) */
40
+ SECURITY: 100,
41
+ /** 高優先級:限流 (80) */
42
+ RATE_LIMIT: 80,
43
+ /** 中等優先級:驗證 (50) */
44
+ VALIDATION: 50,
45
+ /** 預設優先級 (0) */
46
+ DEFAULT: 0,
47
+ /** 低優先級:日誌記錄 (-50) */
48
+ LOGGING: -50,
49
+ /** 最低優先級:監控 (-100) */
50
+ MONITORING: -100
51
+ };
52
+ }
53
+ });
54
+
55
+ // src/middleware/PreferenceMiddleware.ts
56
+ var PreferenceMiddleware_exports = {};
57
+ __export(PreferenceMiddleware_exports, {
58
+ PreferenceMiddleware: () => PreferenceMiddleware
59
+ });
60
+ var PreferenceMiddleware;
61
+ var init_PreferenceMiddleware = __esm({
62
+ "src/middleware/PreferenceMiddleware.ts"() {
63
+ "use strict";
64
+ init_middleware();
65
+ PreferenceMiddleware = class {
66
+ /**
67
+ * Create a new PreferenceMiddleware instance.
68
+ *
69
+ * @param preferenceProvider - Optional preference provider; uses Notifiable method if not provided.
70
+ * @param logger - Optional logger instance for recording errors.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // Without provider (reads from Notifiable.getNotificationPreferences)
75
+ * const middleware = new PreferenceMiddleware();
76
+ *
77
+ * // Using database provider and logger
78
+ * const middleware = new PreferenceMiddleware(new DatabasePreferenceProvider(), logger);
79
+ * ```
80
+ */
81
+ constructor(preferenceProvider, logger) {
82
+ this.preferenceProvider = preferenceProvider;
83
+ this.logger = logger;
84
+ }
85
+ /**
86
+ * Middleware name.
87
+ */
88
+ name = "preference";
89
+ /**
90
+ * Middleware priority (medium priority for validation).
91
+ */
92
+ priority = MiddlewarePriority.VALIDATION;
93
+ /**
94
+ * Handle the notification and apply user preference filtering.
95
+ *
96
+ * Processes the notification and filters based on user preferences:
97
+ * 1. If notification type is in disabledNotifications, it is skipped.
98
+ * 2. If channel is in disabledChannels, it is skipped.
99
+ * 3. If enabledChannels is set, only channels in that list are allowed.
100
+ * 4. If preference loading fails, the notification is allowed as a fallback.
101
+ *
102
+ * @param notification - The notification to send.
103
+ * @param notifiable - The recipient.
104
+ * @param channel - The channel name.
105
+ * @param next - Callback to proceed to the next middleware or send operation.
106
+ * @returns A promise that resolves when processing is complete.
107
+ */
108
+ async handle(notification, notifiable, channel, next) {
109
+ try {
110
+ const preferences = await this.getPreferences(notifiable);
111
+ if (!preferences) {
112
+ await next();
113
+ return;
114
+ }
115
+ if (this.isNotificationDisabled(notification, preferences)) {
116
+ return;
117
+ }
118
+ if (!this.isChannelAllowed(channel, preferences)) {
119
+ return;
120
+ }
121
+ await next();
122
+ } catch (error) {
123
+ const errorMessage = `[PreferenceMiddleware] Failed to load preferences for ${notifiable.getNotifiableId()}, allowing notification to proceed:`;
124
+ if (this.logger) {
125
+ this.logger.error(errorMessage, error);
126
+ } else {
127
+ console.error(errorMessage, error);
128
+ }
129
+ await next();
130
+ }
131
+ }
132
+ /**
133
+ * Get user preferences from Notifiable or custom provider.
134
+ *
135
+ * Priority: Notifiable.getNotificationPreferences > preferenceProvider.
136
+ *
137
+ * @param notifiable - The recipient.
138
+ * @returns The user preferences or null if not found.
139
+ */
140
+ async getPreferences(notifiable) {
141
+ if (notifiable.getNotificationPreferences) {
142
+ const prefs = await notifiable.getNotificationPreferences();
143
+ return prefs || null;
144
+ }
145
+ if (this.preferenceProvider) {
146
+ const prefs = await this.preferenceProvider.getUserPreferences(notifiable);
147
+ return prefs || null;
148
+ }
149
+ return null;
150
+ }
151
+ /**
152
+ * Check if notification type is disabled by user.
153
+ *
154
+ * @param notification - The notification instance.
155
+ * @param preferences - User preferences.
156
+ * @returns True if the notification is disabled.
157
+ */
158
+ isNotificationDisabled(notification, preferences) {
159
+ const { disabledNotifications } = preferences;
160
+ if (!disabledNotifications || disabledNotifications.length === 0) {
161
+ return false;
162
+ }
163
+ const notificationName = notification.constructor.name;
164
+ return disabledNotifications.includes(notificationName);
165
+ }
166
+ /**
167
+ * Check if channel is allowed by user preferences.
168
+ *
169
+ * Priority:
170
+ * 1. disabledChannels (if listed, it is denied)
171
+ * 2. enabledChannels (if set, only listed are allowed)
172
+ * 3. Allow all if neither are set.
173
+ *
174
+ * @param channel - The channel name.
175
+ * @param preferences - User preferences.
176
+ * @returns True if the channel is allowed.
177
+ */
178
+ isChannelAllowed(channel, preferences) {
179
+ const { enabledChannels, disabledChannels } = preferences;
180
+ if (disabledChannels && disabledChannels.length > 0) {
181
+ if (disabledChannels.includes(channel)) {
182
+ return false;
183
+ }
184
+ }
185
+ if (enabledChannels && enabledChannels.length > 0) {
186
+ return enabledChannels.includes(channel);
187
+ }
188
+ if (enabledChannels !== void 0 && enabledChannels.length === 0) {
189
+ return false;
190
+ }
191
+ return true;
192
+ }
193
+ };
194
+ }
195
+ });
196
+
30
197
  // src/index.ts
31
198
  var index_exports = {};
32
199
  __export(index_exports, {
200
+ AbortError: () => AbortError,
33
201
  BroadcastChannel: () => BroadcastChannel,
34
202
  DatabaseChannel: () => DatabaseChannel,
203
+ LazyNotification: () => LazyNotification,
35
204
  MailChannel: () => MailChannel,
205
+ MemoryStore: () => MemoryStore,
206
+ MiddlewarePriority: () => MiddlewarePriority,
36
207
  Notification: () => Notification,
37
208
  NotificationManager: () => NotificationManager,
38
209
  NotificationMetricsCollector: () => NotificationMetricsCollector,
39
210
  OrbitFlare: () => OrbitFlare,
211
+ PreferenceMiddleware: () => PreferenceMiddleware,
212
+ RateLimitMiddleware: () => RateLimitMiddleware,
40
213
  SlackChannel: () => SlackChannel,
41
214
  SmsChannel: () => SmsChannel,
42
215
  TemplatedNotification: () => TemplatedNotification,
216
+ TimeoutChannel: () => TimeoutChannel,
217
+ TimeoutError: () => TimeoutError,
218
+ TokenBucket: () => TokenBucket,
219
+ assertSerializable: () => assertSerializable,
220
+ checkSerializable: () => checkSerializable,
221
+ createHookEmitter: () => createHookEmitter,
222
+ deepDeserialize: () => deepDeserialize,
223
+ deepSerialize: () => deepSerialize,
43
224
  toPrometheusFormat: () => toPrometheusFormat
44
225
  });
45
226
  module.exports = __toCommonJS(index_exports);
46
227
 
228
+ // src/channels/TimeoutChannel.ts
229
+ var TimeoutError = class extends Error {
230
+ constructor(message) {
231
+ super(message);
232
+ this.name = "TimeoutError";
233
+ }
234
+ };
235
+ var AbortError = class extends Error {
236
+ constructor(message) {
237
+ super(message);
238
+ this.name = "AbortError";
239
+ }
240
+ };
241
+ var TimeoutChannel = class {
242
+ constructor(inner, config) {
243
+ this.inner = inner;
244
+ this.config = config;
245
+ }
246
+ /**
247
+ * Sends a notification through the inner channel with a timeout guard.
248
+ *
249
+ * @param notification - The notification to send.
250
+ * @param notifiable - The recipient of the notification.
251
+ * @param options - Send options including an optional AbortSignal.
252
+ * @returns A promise that resolves when the notification is sent.
253
+ * @throws {TimeoutError} Thrown if the operation exceeds the configured timeout.
254
+ * @throws {AbortError} Thrown if the operation is aborted via the provided signal.
255
+ */
256
+ async send(notification, notifiable, options) {
257
+ if (this.config.timeout <= 0) {
258
+ if (this.config.onTimeout) {
259
+ this.config.onTimeout(this.inner.constructor.name, notification);
260
+ }
261
+ throw new TimeoutError(`Notification send timeout after ${this.config.timeout}ms`);
262
+ }
263
+ const controller = new AbortController();
264
+ const { signal } = controller;
265
+ if (options?.signal) {
266
+ if (options.signal.aborted) {
267
+ throw new AbortError("Request was aborted before sending");
268
+ }
269
+ options.signal.addEventListener("abort", () => {
270
+ controller.abort();
271
+ });
272
+ }
273
+ const timeoutPromise = new Promise((_, reject) => {
274
+ setTimeout(() => {
275
+ if (this.config.onTimeout) {
276
+ this.config.onTimeout(this.inner.constructor.name, notification);
277
+ }
278
+ controller.abort();
279
+ if (options?.signal?.aborted) {
280
+ reject(new AbortError("Request was aborted externally"));
281
+ } else {
282
+ reject(new TimeoutError(`Notification send timeout after ${this.config.timeout}ms`));
283
+ }
284
+ }, this.config.timeout);
285
+ });
286
+ const sendPromise = this.inner.send(notification, notifiable, { signal }).catch((error) => {
287
+ if (options?.signal?.aborted) {
288
+ throw new AbortError("Request was aborted externally");
289
+ }
290
+ if (signal.aborted) {
291
+ throw new TimeoutError(`Notification send timeout after ${this.config.timeout}ms`);
292
+ }
293
+ throw error;
294
+ });
295
+ return Promise.race([sendPromise, timeoutPromise]);
296
+ }
297
+ };
298
+
47
299
  // src/channels/BroadcastChannel.ts
300
+ var DEFAULT_TIMEOUT_MS = 1e4;
48
301
  var BroadcastChannel = class {
49
- constructor(broadcastService) {
302
+ constructor(broadcastService, config) {
50
303
  this.broadcastService = broadcastService;
304
+ this.config = config;
305
+ const innerChannel = {
306
+ send: async (notification, notifiable, _options) => {
307
+ if (!notification.toBroadcast) {
308
+ throw new Error("Notification does not implement toBroadcast method");
309
+ }
310
+ const broadcastNotification = notification.toBroadcast(notifiable);
311
+ const notifiableId = notifiable.getNotifiableId();
312
+ const notifiableType = notifiable.getNotifiableType?.() || "user";
313
+ const channel = `private-${notifiableType}.${notifiableId}`;
314
+ await this.broadcastService.broadcast(
315
+ channel,
316
+ broadcastNotification.type,
317
+ broadcastNotification.data
318
+ );
319
+ }
320
+ };
321
+ const timeout = this.config?.timeout ?? DEFAULT_TIMEOUT_MS;
322
+ this.timeoutChannel = new TimeoutChannel(innerChannel, {
323
+ timeout,
324
+ onTimeout: this.config?.onTimeout
325
+ });
51
326
  }
52
- async send(notification, notifiable) {
53
- if (!notification.toBroadcast) {
54
- throw new Error("Notification does not implement toBroadcast method");
55
- }
56
- const broadcastNotification = notification.toBroadcast(notifiable);
57
- const notifiableId = notifiable.getNotifiableId();
58
- const notifiableType = notifiable.getNotifiableType?.() || "user";
59
- const channel = `private-${notifiableType}.${notifiableId}`;
60
- await this.broadcastService.broadcast(
61
- channel,
62
- broadcastNotification.type,
63
- broadcastNotification.data
64
- );
327
+ timeoutChannel;
328
+ async send(notification, notifiable, options) {
329
+ return this.timeoutChannel.send(notification, notifiable, options);
65
330
  }
66
331
  };
67
332
 
68
333
  // src/channels/DatabaseChannel.ts
334
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
69
335
  var DatabaseChannel = class {
70
- constructor(dbService) {
336
+ constructor(dbService, config) {
71
337
  this.dbService = dbService;
72
- }
73
- async send(notification, notifiable) {
74
- if (!notification.toDatabase) {
75
- throw new Error("Notification does not implement toDatabase method");
76
- }
77
- const dbNotification = notification.toDatabase(notifiable);
78
- await this.dbService.insertNotification({
79
- notifiableId: notifiable.getNotifiableId(),
80
- notifiableType: notifiable.getNotifiableType?.() || "user",
81
- type: dbNotification.type,
82
- data: dbNotification.data
338
+ this.config = config;
339
+ const innerChannel = {
340
+ send: async (notification, notifiable, _options) => {
341
+ if (!notification.toDatabase) {
342
+ throw new Error("Notification does not implement toDatabase method");
343
+ }
344
+ const dbNotification = notification.toDatabase(notifiable);
345
+ await this.dbService.insertNotification({
346
+ notifiableId: notifiable.getNotifiableId(),
347
+ notifiableType: notifiable.getNotifiableType?.() || "user",
348
+ type: dbNotification.type,
349
+ data: dbNotification.data
350
+ });
351
+ }
352
+ };
353
+ const timeout = this.config?.timeout ?? DEFAULT_TIMEOUT_MS2;
354
+ this.timeoutChannel = new TimeoutChannel(innerChannel, {
355
+ timeout,
356
+ onTimeout: this.config?.onTimeout
83
357
  });
84
358
  }
359
+ timeoutChannel;
360
+ async send(notification, notifiable, options) {
361
+ return this.timeoutChannel.send(notification, notifiable, options);
362
+ }
85
363
  };
86
364
 
87
365
  // src/channels/MailChannel.ts
366
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
88
367
  var MailChannel = class {
89
- constructor(mailService) {
368
+ constructor(mailService, config) {
90
369
  this.mailService = mailService;
370
+ this.config = config;
371
+ const innerChannel = {
372
+ send: async (notification, notifiable, _options) => {
373
+ if (!notification.toMail) {
374
+ throw new Error("Notification does not implement toMail method");
375
+ }
376
+ const message = notification.toMail(notifiable);
377
+ await this.mailService.send(message);
378
+ }
379
+ };
380
+ const timeout = this.config?.timeout ?? DEFAULT_TIMEOUT_MS3;
381
+ this.timeoutChannel = new TimeoutChannel(innerChannel, {
382
+ timeout,
383
+ onTimeout: this.config?.onTimeout
384
+ });
91
385
  }
92
- async send(notification, notifiable) {
93
- if (!notification.toMail) {
94
- throw new Error("Notification does not implement toMail method");
95
- }
96
- const message = notification.toMail(notifiable);
97
- await this.mailService.send(message);
386
+ timeoutChannel;
387
+ async send(notification, notifiable, options) {
388
+ return this.timeoutChannel.send(notification, notifiable, options);
98
389
  }
99
390
  };
100
391
 
101
392
  // src/channels/SlackChannel.ts
393
+ var DEFAULT_TIMEOUT_MS4 = 3e4;
102
394
  var SlackChannel = class {
103
395
  constructor(config) {
104
396
  this.config = config;
105
- }
106
- async send(notification, notifiable) {
107
- if (!notification.toSlack) {
108
- throw new Error("Notification does not implement toSlack method");
109
- }
110
- const slackMessage = notification.toSlack(notifiable);
111
- const response = await fetch(this.config.webhookUrl, {
112
- method: "POST",
113
- headers: {
114
- "Content-Type": "application/json"
115
- },
116
- body: JSON.stringify({
117
- text: slackMessage.text,
118
- channel: slackMessage.channel || this.config.defaultChannel,
119
- username: slackMessage.username,
120
- icon_emoji: slackMessage.iconEmoji,
121
- attachments: slackMessage.attachments
122
- })
397
+ const innerChannel = {
398
+ send: async (notification, notifiable, options) => {
399
+ if (!notification.toSlack) {
400
+ throw new Error("Notification does not implement toSlack method");
401
+ }
402
+ const slackMessage = notification.toSlack(notifiable);
403
+ const response = await fetch(this.config.webhookUrl, {
404
+ method: "POST",
405
+ headers: {
406
+ "Content-Type": "application/json"
407
+ },
408
+ body: JSON.stringify({
409
+ text: slackMessage.text,
410
+ channel: slackMessage.channel || this.config.defaultChannel,
411
+ username: slackMessage.username,
412
+ icon_emoji: slackMessage.iconEmoji,
413
+ attachments: slackMessage.attachments
414
+ }),
415
+ signal: options?.signal
416
+ // Pass AbortSignal to fetch
417
+ });
418
+ if (!response.ok) {
419
+ throw new Error(`Failed to send Slack notification: ${response.statusText}`);
420
+ }
421
+ }
422
+ };
423
+ const timeout = this.config.timeout ?? DEFAULT_TIMEOUT_MS4;
424
+ this.timeoutChannel = new TimeoutChannel(innerChannel, {
425
+ timeout,
426
+ onTimeout: this.config.onTimeout
123
427
  });
124
- if (!response.ok) {
125
- throw new Error(`Failed to send Slack notification: ${response.statusText}`);
126
- }
428
+ }
429
+ timeoutChannel;
430
+ async send(notification, notifiable, options) {
431
+ return this.timeoutChannel.send(notification, notifiable, options);
127
432
  }
128
433
  };
129
434
 
130
435
  // src/channels/SmsChannel.ts
436
+ var DEFAULT_TIMEOUT_MS5 = 3e4;
131
437
  var SmsChannel = class {
132
438
  constructor(config) {
133
439
  this.config = config;
440
+ const innerChannel = {
441
+ send: async (notification, notifiable, options) => {
442
+ if (!notification.toSms) {
443
+ throw new Error("Notification does not implement toSms method");
444
+ }
445
+ const smsMessage = notification.toSms(notifiable);
446
+ switch (this.config.provider) {
447
+ case "twilio":
448
+ await this.sendViaTwilio(smsMessage, options?.signal);
449
+ break;
450
+ case "aws-sns":
451
+ await this.sendViaAwsSns(smsMessage, options?.signal);
452
+ break;
453
+ default:
454
+ throw new Error(`Unsupported SMS provider: ${this.config.provider}`);
455
+ }
456
+ }
457
+ };
458
+ const timeout = this.config.timeout ?? DEFAULT_TIMEOUT_MS5;
459
+ this.timeoutChannel = new TimeoutChannel(innerChannel, {
460
+ timeout,
461
+ onTimeout: this.config.onTimeout
462
+ });
134
463
  }
135
- async send(notification, notifiable) {
136
- if (!notification.toSms) {
137
- throw new Error("Notification does not implement toSms method");
138
- }
139
- const smsMessage = notification.toSms(notifiable);
140
- switch (this.config.provider) {
141
- case "twilio":
142
- await this.sendViaTwilio(smsMessage);
143
- break;
144
- case "aws-sns":
145
- await this.sendViaAwsSns(smsMessage);
146
- break;
147
- default:
148
- throw new Error(`Unsupported SMS provider: ${this.config.provider}`);
149
- }
464
+ timeoutChannel;
465
+ async send(notification, notifiable, options) {
466
+ return this.timeoutChannel.send(notification, notifiable, options);
150
467
  }
151
468
  /**
152
- * Send SMS via Twilio.
469
+ * Send SMS via Twilio with AbortSignal support.
153
470
  */
154
- async sendViaTwilio(message) {
471
+ async sendViaTwilio(message, signal) {
155
472
  if (!this.config.apiKey || !this.config.apiSecret) {
156
473
  throw new Error("Twilio API key and secret are required");
157
474
  }
@@ -166,10 +483,12 @@ var SmsChannel = class {
166
483
  "Content-Type": "application/x-www-form-urlencoded"
167
484
  },
168
485
  body: new URLSearchParams({
169
- From: this.config.from || "",
486
+ From: message.from || this.config.from || "",
170
487
  To: message.to,
171
488
  Body: message.message
172
- })
489
+ }),
490
+ signal
491
+ // Pass AbortSignal to fetch
173
492
  }
174
493
  );
175
494
  if (!response.ok) {
@@ -178,9 +497,9 @@ var SmsChannel = class {
178
497
  }
179
498
  }
180
499
  /**
181
- * Send SMS via AWS SNS.
500
+ * Send SMS via AWS SNS with AbortSignal support.
182
501
  */
183
- async sendViaAwsSns(message) {
502
+ async sendViaAwsSns(message, signal) {
184
503
  let SNSClient;
185
504
  let PublishCommand;
186
505
  try {
@@ -216,7 +535,7 @@ var SmsChannel = class {
216
535
  }
217
536
  });
218
537
  try {
219
- await client.send(command);
538
+ await client.send(command, { abortSignal: signal });
220
539
  } catch (error) {
221
540
  const err = error instanceof Error ? error : new Error(String(error));
222
541
  throw new Error(`Failed to send SMS via AWS SNS: ${err.message}`);
@@ -317,6 +636,338 @@ var NotificationMetricsCollector = class {
317
636
  }
318
637
  };
319
638
 
639
+ // src/index.ts
640
+ init_PreferenceMiddleware();
641
+
642
+ // src/middleware/RateLimitMiddleware.ts
643
+ init_middleware();
644
+
645
+ // src/utils/TokenBucket.ts
646
+ var TokenBucket = class {
647
+ /**
648
+ * 創建一個新的 TokenBucket 實例
649
+ *
650
+ * @param capacity - 桶的最大容量(tokens 上限)
651
+ * @param refillRate - 每秒補充的 tokens 數量
652
+ *
653
+ * @example
654
+ * ```typescript
655
+ * // 每秒最多 100 個請求
656
+ * const bucket = new TokenBucket(100, 100)
657
+ * ```
658
+ */
659
+ constructor(capacity, refillRate) {
660
+ this.capacity = capacity;
661
+ this.refillRate = refillRate;
662
+ this.tokens = capacity;
663
+ this.lastRefill = Date.now();
664
+ }
665
+ /**
666
+ * 當前可用的 tokens 數量
667
+ */
668
+ tokens;
669
+ /**
670
+ * 上次補充 tokens 的時間戳(毫秒)
671
+ */
672
+ lastRefill;
673
+ /**
674
+ * 嘗試從桶中消耗指定數量的 tokens
675
+ *
676
+ * 此方法會先執行 token 補充,然後檢查是否有足夠的 tokens。
677
+ * 如果有足夠的 tokens,則消耗並返回 true;否則返回 false 且不消耗。
678
+ *
679
+ * @param tokens - 要消耗的 tokens 數量(預設為 1)
680
+ * @returns 如果成功消耗則返回 true,否則返回 false
681
+ *
682
+ * @example
683
+ * ```typescript
684
+ * const bucket = new TokenBucket(10, 1)
685
+ *
686
+ * // 嘗試消耗 1 個 token
687
+ * if (bucket.tryConsume()) {
688
+ * console.log('請求被允許')
689
+ * }
690
+ *
691
+ * // 嘗試消耗 3 個 tokens
692
+ * if (bucket.tryConsume(3)) {
693
+ * console.log('批次請求被允許')
694
+ * }
695
+ * ```
696
+ */
697
+ tryConsume(tokens = 1) {
698
+ if (tokens <= 0) {
699
+ return true;
700
+ }
701
+ this.refill();
702
+ if (this.tokens >= tokens) {
703
+ this.tokens -= tokens;
704
+ return true;
705
+ }
706
+ return false;
707
+ }
708
+ /**
709
+ * 獲取當前可用的 tokens 數量
710
+ *
711
+ * 此方法會先執行補充操作,然後返回當前的 tokens 數量。
712
+ *
713
+ * @returns 當前可用的 tokens 數量
714
+ *
715
+ * @example
716
+ * ```typescript
717
+ * const bucket = new TokenBucket(10, 1)
718
+ * console.log(`剩餘 ${bucket.getTokens()} 個 tokens`)
719
+ * ```
720
+ */
721
+ getTokens() {
722
+ this.refill();
723
+ return this.tokens;
724
+ }
725
+ /**
726
+ * 根據經過的時間補充 tokens
727
+ *
728
+ * 此方法計算自上次補充以來經過的時間,並根據 refillRate 補充相應數量的 tokens。
729
+ * tokens 數量不會超過容量上限。
730
+ *
731
+ * @private
732
+ */
733
+ refill() {
734
+ const now = Date.now();
735
+ const elapsed = now - this.lastRefill;
736
+ if (elapsed <= 0 || this.refillRate <= 0) {
737
+ return;
738
+ }
739
+ const tokensToAdd = elapsed / 1e3 * this.refillRate;
740
+ if (tokensToAdd > 0) {
741
+ this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
742
+ this.lastRefill = now;
743
+ }
744
+ }
745
+ };
746
+
747
+ // src/middleware/RateLimitMiddleware.ts
748
+ var SECONDS_PER_MINUTE = 60;
749
+ var SECONDS_PER_HOUR = 3600;
750
+ var DEFAULT_CLEANUP_INTERVAL_MS = 6e4;
751
+ var MemoryStore = class {
752
+ cache = /* @__PURE__ */ new Map();
753
+ cleanupInterval;
754
+ constructor(cleanupIntervalMs = DEFAULT_CLEANUP_INTERVAL_MS) {
755
+ this.cleanupInterval = setInterval(() => {
756
+ const now = Date.now();
757
+ for (const [key, item] of this.cache.entries()) {
758
+ if (now > item.expiry) {
759
+ this.cache.delete(key);
760
+ }
761
+ }
762
+ }, cleanupIntervalMs);
763
+ }
764
+ async get(key) {
765
+ const item = this.cache.get(key);
766
+ if (!item) {
767
+ return null;
768
+ }
769
+ if (Date.now() > item.expiry) {
770
+ this.cache.delete(key);
771
+ return null;
772
+ }
773
+ return item.value;
774
+ }
775
+ async put(key, value, ttl) {
776
+ this.cache.set(key, {
777
+ value,
778
+ expiry: Date.now() + ttl * 1e3
779
+ });
780
+ }
781
+ async forget(key) {
782
+ this.cache.delete(key);
783
+ }
784
+ /**
785
+ * 清理所有資源,停止清理計時器
786
+ */
787
+ destroy() {
788
+ if (this.cleanupInterval) {
789
+ clearInterval(this.cleanupInterval);
790
+ this.cleanupInterval = void 0;
791
+ }
792
+ this.cache.clear();
793
+ }
794
+ };
795
+ var RateLimitMiddleware = class {
796
+ /**
797
+ * Create a new RateLimitMiddleware instance.
798
+ *
799
+ * @param config - Rate limit configuration for each channel
800
+ * @param store - Optional cache store for distributed rate limiting
801
+ *
802
+ * @example
803
+ * ```typescript
804
+ * // 使用預設記憶體儲存
805
+ * const middleware = new RateLimitMiddleware({
806
+ * email: { maxPerSecond: 10 }
807
+ * })
808
+ *
809
+ * // 使用 Redis 儲存(分散式環境)
810
+ * const middleware = new RateLimitMiddleware({
811
+ * email: { maxPerSecond: 10 }
812
+ * }, redisStore)
813
+ * ```
814
+ */
815
+ constructor(config, store) {
816
+ this.config = config;
817
+ this.store = store ?? new MemoryStore();
818
+ this.initializeBuckets();
819
+ }
820
+ /**
821
+ * Middleware name.
822
+ */
823
+ name = "rate-limit";
824
+ /**
825
+ * Middleware priority (high priority, executes early in the chain).
826
+ * 中介層優先級(高優先級,在鏈中較早執行)
827
+ */
828
+ priority = MiddlewarePriority.RATE_LIMIT;
829
+ /**
830
+ * Token buckets for each channel and time window.
831
+ * Key format: `{channel}:{window}` (e.g., 'email:second', 'sms:minute')
832
+ */
833
+ buckets = /* @__PURE__ */ new Map();
834
+ /**
835
+ * Cache store for distributed rate limiting.
836
+ * 分散式限流使用的快取儲存
837
+ */
838
+ store;
839
+ /**
840
+ * Initialize token buckets for all configured channels.
841
+ *
842
+ * 為所有配置的通道初始化 Token Bucket。
843
+ *
844
+ * @private
845
+ */
846
+ initializeBuckets() {
847
+ for (const [channel, limits] of Object.entries(this.config)) {
848
+ if (limits.maxPerSecond) {
849
+ const key = `${channel}:second`;
850
+ this.buckets.set(key, new TokenBucket(limits.maxPerSecond, limits.maxPerSecond));
851
+ }
852
+ if (limits.maxPerMinute) {
853
+ const key = `${channel}:minute`;
854
+ this.buckets.set(
855
+ key,
856
+ new TokenBucket(limits.maxPerMinute, limits.maxPerMinute / SECONDS_PER_MINUTE)
857
+ );
858
+ }
859
+ if (limits.maxPerHour) {
860
+ const key = `${channel}:hour`;
861
+ this.buckets.set(
862
+ key,
863
+ new TokenBucket(limits.maxPerHour, limits.maxPerHour / SECONDS_PER_HOUR)
864
+ );
865
+ }
866
+ }
867
+ }
868
+ /**
869
+ * Handle the notification and apply rate limiting.
870
+ *
871
+ * 處理通知並應用限流規則。如果超過任一時間窗口的限制,
872
+ * 將拋出錯誤阻止通知發送。
873
+ *
874
+ * @param notification - The notification being sent
875
+ * @param notifiable - The recipient
876
+ * @param channel - The channel name
877
+ * @param next - Continue to the next middleware or send the notification
878
+ *
879
+ * @throws {Error} 當超過限流時拋出錯誤
880
+ */
881
+ async handle(_notification, _notifiable, channel, next) {
882
+ const channelConfig = this.config[channel];
883
+ if (!channelConfig) {
884
+ await next();
885
+ return;
886
+ }
887
+ const windows = ["second", "minute", "hour"];
888
+ for (const window of windows) {
889
+ const key = `${channel}:${window}`;
890
+ const bucket = this.buckets.get(key);
891
+ if (bucket) {
892
+ const allowed = bucket.tryConsume();
893
+ if (!allowed) {
894
+ throw new Error(
895
+ `Rate limit exceeded for channel '${channel}' (${window}ly limit). Please try again later.`
896
+ );
897
+ }
898
+ }
899
+ }
900
+ await next();
901
+ }
902
+ /**
903
+ * Get current rate limit status for a channel.
904
+ *
905
+ * 獲取指定通道的當前限流狀態(用於除錯和監控)。
906
+ *
907
+ * @param channel - The channel name
908
+ * @returns Current token counts for each time window
909
+ *
910
+ * @example
911
+ * ```typescript
912
+ * const status = middleware.getStatus('email')
913
+ * console.log(`Email remaining: ${status.second}/${config.email.maxPerSecond}`)
914
+ * ```
915
+ */
916
+ getStatus(channel) {
917
+ const status = {};
918
+ const secondBucket = this.buckets.get(`${channel}:second`);
919
+ if (secondBucket) {
920
+ status.second = secondBucket.getTokens();
921
+ }
922
+ const minuteBucket = this.buckets.get(`${channel}:minute`);
923
+ if (minuteBucket) {
924
+ status.minute = minuteBucket.getTokens();
925
+ }
926
+ const hourBucket = this.buckets.get(`${channel}:hour`);
927
+ if (hourBucket) {
928
+ status.hour = hourBucket.getTokens();
929
+ }
930
+ return status;
931
+ }
932
+ /**
933
+ * Reset rate limit for a specific channel.
934
+ *
935
+ * 重置指定通道的限流計數(用於測試或手動重置)。
936
+ *
937
+ * @param channel - The channel name to reset
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * // 在測試中重置限流
942
+ * middleware.reset('email')
943
+ * ```
944
+ */
945
+ reset(channel) {
946
+ const channelConfig = this.config[channel];
947
+ if (!channelConfig) {
948
+ return;
949
+ }
950
+ if (channelConfig.maxPerSecond) {
951
+ const key = `${channel}:second`;
952
+ this.buckets.set(key, new TokenBucket(channelConfig.maxPerSecond, channelConfig.maxPerSecond));
953
+ }
954
+ if (channelConfig.maxPerMinute) {
955
+ const key = `${channel}:minute`;
956
+ this.buckets.set(
957
+ key,
958
+ new TokenBucket(channelConfig.maxPerMinute, channelConfig.maxPerMinute / SECONDS_PER_MINUTE)
959
+ );
960
+ }
961
+ if (channelConfig.maxPerHour) {
962
+ const key = `${channel}:hour`;
963
+ this.buckets.set(
964
+ key,
965
+ new TokenBucket(channelConfig.maxPerHour, channelConfig.maxPerHour / SECONDS_PER_HOUR)
966
+ );
967
+ }
968
+ }
969
+ };
970
+
320
971
  // src/Notification.ts
321
972
  var Notification = class {
322
973
  /**
@@ -341,6 +992,15 @@ var Notification = class {
341
992
  }
342
993
  };
343
994
 
995
+ // src/utils/hookEmitter.ts
996
+ function createHookEmitter(core) {
997
+ return {
998
+ emit: async (event, payload) => {
999
+ await core.hooks.emit(event, payload);
1000
+ }
1001
+ };
1002
+ }
1003
+
344
1004
  // src/utils/retry.ts
345
1005
  var DEFAULT_RETRY_OPTIONS = {
346
1006
  maxAttempts: 3,
@@ -435,26 +1095,140 @@ function deepSerialize(obj, seen = /* @__PURE__ */ new WeakSet()) {
435
1095
  }
436
1096
  const result = {};
437
1097
  for (const [key, value] of Object.entries(obj)) {
438
- if (!key.startsWith("_") && typeof value !== "function") {
1098
+ const isSpecialProperty = key.startsWith("__");
1099
+ const isPrivateProperty = key.startsWith("_") && !isSpecialProperty;
1100
+ if (!isPrivateProperty && typeof value !== "function") {
439
1101
  result[key] = deepSerialize(value, seen);
440
1102
  }
441
1103
  }
442
1104
  return result;
443
1105
  }
1106
+ function deepDeserialize(obj) {
1107
+ if (obj === null || typeof obj !== "object") {
1108
+ return obj;
1109
+ }
1110
+ if ("__type" in obj) {
1111
+ const typed = obj;
1112
+ switch (typed.__type) {
1113
+ case "Date":
1114
+ return new Date(typed.value);
1115
+ case "Map":
1116
+ return new Map(
1117
+ typed.value.map(([k, v]) => [
1118
+ deepDeserialize(k),
1119
+ deepDeserialize(v)
1120
+ ])
1121
+ );
1122
+ case "Set":
1123
+ return new Set(typed.value.map((v) => deepDeserialize(v)));
1124
+ }
1125
+ }
1126
+ if (Array.isArray(obj)) {
1127
+ return obj.map((item) => deepDeserialize(item));
1128
+ }
1129
+ const result = {};
1130
+ for (const [key, value] of Object.entries(obj)) {
1131
+ result[key] = deepDeserialize(value);
1132
+ }
1133
+ return result;
1134
+ }
1135
+
1136
+ // src/utils/serializationGuard.ts
1137
+ function checkSerializable(obj, path = "") {
1138
+ const problematicPaths = [];
1139
+ const warnings = [];
1140
+ const seen = /* @__PURE__ */ new WeakSet();
1141
+ function check(value, currentPath) {
1142
+ if (value === null || value === void 0) {
1143
+ return;
1144
+ }
1145
+ if (typeof value !== "object" && typeof value !== "function" && typeof value !== "symbol") {
1146
+ return;
1147
+ }
1148
+ if (typeof value === "symbol") {
1149
+ problematicPaths.push(currentPath);
1150
+ warnings.push(`\u767C\u73FE\u4E0D\u53EF\u5E8F\u5217\u5316\u7684 Symbol \u65BC\u8DEF\u5F91: ${currentPath}`);
1151
+ return;
1152
+ }
1153
+ if (typeof value === "function") {
1154
+ problematicPaths.push(currentPath);
1155
+ warnings.push(`\u767C\u73FE\u4E0D\u53EF\u5E8F\u5217\u5316\u7684 Function \u65BC\u8DEF\u5F91: ${currentPath}`);
1156
+ return;
1157
+ }
1158
+ if (seen.has(value)) {
1159
+ problematicPaths.push(currentPath);
1160
+ warnings.push(`\u767C\u73FE\u5FAA\u74B0\u5F15\u7528\u65BC\u8DEF\u5F91: ${currentPath}`);
1161
+ return;
1162
+ }
1163
+ seen.add(value);
1164
+ if (value instanceof Date || value instanceof Map || value instanceof Set || value instanceof RegExp || value instanceof Error) {
1165
+ return;
1166
+ }
1167
+ if (value instanceof Promise || value instanceof WeakMap || value instanceof WeakSet || value instanceof ArrayBuffer || value instanceof DataView || typeof Buffer !== "undefined" && value instanceof Buffer) {
1168
+ problematicPaths.push(currentPath);
1169
+ warnings.push(`\u767C\u73FE\u4E0D\u53EF\u5E8F\u5217\u5316\u7684 ${value.constructor.name} \u65BC\u8DEF\u5F91: ${currentPath}`);
1170
+ return;
1171
+ }
1172
+ if (Array.isArray(value)) {
1173
+ value.forEach((item, index) => {
1174
+ const itemPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
1175
+ check(item, itemPath);
1176
+ });
1177
+ return;
1178
+ }
1179
+ for (const [key, val] of Object.entries(value)) {
1180
+ if (key.startsWith("_")) {
1181
+ continue;
1182
+ }
1183
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
1184
+ check(val, newPath);
1185
+ }
1186
+ }
1187
+ check(obj, path);
1188
+ return {
1189
+ serializable: problematicPaths.length === 0,
1190
+ problematicPaths,
1191
+ warnings
1192
+ };
1193
+ }
1194
+ function assertSerializable(obj) {
1195
+ const result = checkSerializable(obj);
1196
+ if (!result.serializable) {
1197
+ throw new Error(
1198
+ `\u7269\u4EF6\u5305\u542B\u4E0D\u53EF\u5E8F\u5217\u5316\u7684\u5C6C\u6027:
1199
+ \u554F\u984C\u8DEF\u5F91: ${result.problematicPaths.join(", ")}
1200
+ \u8A73\u7D30\u8CC7\u8A0A:
1201
+ ${result.warnings.join("\n")}`
1202
+ );
1203
+ }
1204
+ }
444
1205
 
445
1206
  // src/NotificationManager.ts
446
1207
  var NotificationManager = class {
447
1208
  constructor(core) {
448
1209
  this.core = core;
1210
+ this.hookEmitter = createHookEmitter(core);
449
1211
  }
450
1212
  /**
451
1213
  * Channel registry.
452
1214
  */
453
1215
  channels = /* @__PURE__ */ new Map();
1216
+ /**
1217
+ * Middleware stack for intercepting channel sends.
1218
+ */
1219
+ middlewares = [];
1220
+ /**
1221
+ * Indicates whether the middleware stack needs re-sorting.
1222
+ */
1223
+ middlewaresDirty = false;
454
1224
  /**
455
1225
  * Queue manager (optional, injected by `orbit-queue`).
456
1226
  */
457
1227
  queueManager;
1228
+ /**
1229
+ * Type-safe hook emitter for notification events.
1230
+ */
1231
+ hookEmitter;
458
1232
  metrics;
459
1233
  /**
460
1234
  * Enable metrics collection.
@@ -483,6 +1257,30 @@ var NotificationManager = class {
483
1257
  channel(name, channel) {
484
1258
  this.channels.set(name, channel);
485
1259
  }
1260
+ /**
1261
+ * Register a middleware for intercepting channel sends.
1262
+ *
1263
+ * Middleware will be executed in the order they are registered.
1264
+ * Each middleware can modify, block, or monitor the notification flow.
1265
+ *
1266
+ * @param middleware - The middleware instance to register.
1267
+ *
1268
+ * @example
1269
+ * ```typescript
1270
+ * import { RateLimitMiddleware } from '@gravito/flare'
1271
+ *
1272
+ * const rateLimiter = new RateLimitMiddleware({
1273
+ * email: { maxPerSecond: 10 },
1274
+ * sms: { maxPerSecond: 5 }
1275
+ * })
1276
+ *
1277
+ * manager.use(rateLimiter)
1278
+ * ```
1279
+ */
1280
+ use(middleware) {
1281
+ this.middlewares.push(middleware);
1282
+ this.middlewaresDirty = true;
1283
+ }
486
1284
  /**
487
1285
  * Register the queue manager (called by `orbit-queue`).
488
1286
  *
@@ -508,13 +1306,13 @@ var NotificationManager = class {
508
1306
  async send(notifiable, notification, options = {}) {
509
1307
  const channels = notification.via(notifiable);
510
1308
  const startTime = Date.now();
511
- await this.core.hooks.emit("notification:sending", {
1309
+ await this.hookEmitter.emit("notification:sending", {
512
1310
  notification,
513
1311
  notifiable,
514
1312
  channels
515
1313
  });
516
1314
  if (notification.shouldQueue() && this.queueManager) {
517
- await this.core.hooks.emit("notification:queued", {
1315
+ await this.hookEmitter.emit("notification:queued", {
518
1316
  notification,
519
1317
  notifiable,
520
1318
  channels
@@ -526,7 +1324,15 @@ var NotificationManager = class {
526
1324
  notifiableId: notifiable.getNotifiableId(),
527
1325
  notifiableType: notifiable.getNotifiableType?.() || "user",
528
1326
  channels,
529
- notificationData: this.serializeNotification(notification),
1327
+ notificationData: (() => {
1328
+ const checkResult = checkSerializable(notification);
1329
+ if (!checkResult.serializable) {
1330
+ this.core.logger.warn(
1331
+ `[NotificationManager] Notification '${notification.constructor.name}' contains non-serializable properties. These will be filtered out during queuing. Problematic paths: ${checkResult.problematicPaths.join(", ")}. Advice: Use LazyNotification or keep only IDs/pure data in the constructor.`
1332
+ );
1333
+ }
1334
+ return this.serializeNotification(notification);
1335
+ })(),
530
1336
  handle: async () => {
531
1337
  await this.sendNow(notifiable, notification, channels);
532
1338
  }
@@ -547,7 +1353,7 @@ var NotificationManager = class {
547
1353
  }
548
1354
  const results = await this.sendNow(notifiable, notification, channels, options);
549
1355
  const totalDuration = Date.now() - startTime;
550
- await this.core.hooks.emit("notification:sent", {
1356
+ await this.hookEmitter.emit("notification:sent", {
551
1357
  notification,
552
1358
  notifiable,
553
1359
  results,
@@ -580,7 +1386,7 @@ var NotificationManager = class {
580
1386
  const { batchConcurrency = 10 } = options;
581
1387
  const startTime = Date.now();
582
1388
  const results = [];
583
- await this.core.hooks.emit("notification:batch:start", {
1389
+ await this.hookEmitter.emit("notification:batch:start", {
584
1390
  notification,
585
1391
  count: notifiables.length
586
1392
  });
@@ -592,7 +1398,7 @@ var NotificationManager = class {
592
1398
  }
593
1399
  const duration = Date.now() - startTime;
594
1400
  const successCount = results.filter((r) => r.allSuccess).length;
595
- await this.core.hooks.emit("notification:batch:complete", {
1401
+ await this.hookEmitter.emit("notification:batch:complete", {
596
1402
  notification,
597
1403
  total: notifiables.length,
598
1404
  success: successCount,
@@ -710,7 +1516,7 @@ var NotificationManager = class {
710
1516
  `[NotificationManager] Channel '${channelName}' failed, retrying (${attempt}/${retry.maxAttempts}) in ${delay}ms`,
711
1517
  error
712
1518
  );
713
- this.core.hooks.emit("notification:channel:retry", {
1519
+ this.hookEmitter.emit("notification:channel:retry", {
714
1520
  notification,
715
1521
  notifiable,
716
1522
  channel: channelName,
@@ -742,7 +1548,7 @@ var NotificationManager = class {
742
1548
  } catch (error) {
743
1549
  const duration = Date.now() - startTime;
744
1550
  const err = error instanceof Error ? error : new Error(String(error));
745
- await this.core.hooks.emit("notification:channel:failed", {
1551
+ await this.hookEmitter.emit("notification:channel:failed", {
746
1552
  notification,
747
1553
  notifiable,
748
1554
  channel: channelName,
@@ -772,20 +1578,76 @@ var NotificationManager = class {
772
1578
  }
773
1579
  }
774
1580
  async executeChannelSend(channel, notification, notifiable, channelName) {
775
- await this.core.hooks.emit("notification:channel:sending", {
1581
+ await this.hookEmitter.emit("notification:channel:sending", {
776
1582
  notification,
777
1583
  notifiable,
778
1584
  channel: channelName
779
1585
  });
780
- await channel.send(notification, notifiable);
1586
+ const executeWithMiddleware = async () => {
1587
+ await this.executeMiddlewareChain(0, notification, notifiable, channelName, async () => {
1588
+ await channel.send(notification, notifiable);
1589
+ });
1590
+ };
1591
+ await executeWithMiddleware();
781
1592
  const duration = 0;
782
- await this.core.hooks.emit("notification:channel:sent", {
1593
+ await this.hookEmitter.emit("notification:channel:sent", {
783
1594
  notification,
784
1595
  notifiable,
785
1596
  channel: channelName,
786
1597
  duration
787
1598
  });
788
1599
  }
1600
+ /**
1601
+ * Retrieves the sorted list of middleware (Lazy sorting).
1602
+ *
1603
+ * Uses a lazy evaluation strategy: sorting only happens when needed to avoid
1604
+ * overhead on every `use()` call.
1605
+ * Sorting rules:
1606
+ * 1. Higher priority (larger number) executes first.
1607
+ * 2. Stable sort is maintained for identical priorities (registration order).
1608
+ *
1609
+ * @returns The sorted list of middleware.
1610
+ * @private
1611
+ */
1612
+ getSortedMiddlewares() {
1613
+ if (!this.middlewaresDirty) {
1614
+ return this.middlewares;
1615
+ }
1616
+ this.middlewares.sort((a, b) => {
1617
+ const priorityA = a.priority ?? 0;
1618
+ const priorityB = b.priority ?? 0;
1619
+ return priorityB - priorityA;
1620
+ });
1621
+ this.middlewaresDirty = false;
1622
+ return this.middlewares;
1623
+ }
1624
+ /**
1625
+ * Execute middleware chain recursively.
1626
+ *
1627
+ * @param index - Current middleware index
1628
+ * @param notification - The notification being sent
1629
+ * @param notifiable - The recipient
1630
+ * @param channelName - The channel name
1631
+ * @param finalHandler - The final handler to execute (actual channel.send)
1632
+ * @private
1633
+ */
1634
+ async executeMiddlewareChain(index, notification, notifiable, channelName, finalHandler) {
1635
+ const sortedMiddlewares = this.getSortedMiddlewares();
1636
+ if (index >= sortedMiddlewares.length) {
1637
+ await finalHandler();
1638
+ return;
1639
+ }
1640
+ const middleware = sortedMiddlewares[index];
1641
+ await middleware.handle(notification, notifiable, channelName, async () => {
1642
+ await this.executeMiddlewareChain(
1643
+ index + 1,
1644
+ notification,
1645
+ notifiable,
1646
+ channelName,
1647
+ finalHandler
1648
+ );
1649
+ });
1650
+ }
789
1651
  getRetryConfig(notification, options) {
790
1652
  const notificationRetry = notification.retry;
791
1653
  if (options === false || notificationRetry === void 0) {
@@ -904,6 +1766,7 @@ var OrbitFlare = class _OrbitFlare {
904
1766
  if (this.options.enableSms) {
905
1767
  this.setupSmsChannel(core, manager);
906
1768
  }
1769
+ this.setupMiddleware(core, manager);
907
1770
  core.container.instance("notifications", manager);
908
1771
  this.setupQueueIntegration(core, manager);
909
1772
  core.logger.info("[OrbitFlare] Installed");
@@ -918,8 +1781,9 @@ var OrbitFlare = class _OrbitFlare {
918
1781
  }
919
1782
  setupDatabaseChannel(core, manager) {
920
1783
  const db = core.container.make("db");
1784
+ const config = this.options.channels?.database;
921
1785
  if (db && this.isDatabaseService(db)) {
922
- manager.channel("database", new DatabaseChannel(db));
1786
+ manager.channel("database", new DatabaseChannel(db, config));
923
1787
  } else {
924
1788
  core.logger.warn(
925
1789
  "[OrbitFlare] Database service not found or invalid, database channel disabled"
@@ -928,25 +1792,20 @@ var OrbitFlare = class _OrbitFlare {
928
1792
  }
929
1793
  setupBroadcastChannel(core, manager) {
930
1794
  const broadcast = core.container.make("broadcast");
1795
+ const config = this.options.channels?.broadcast;
931
1796
  if (broadcast && this.isBroadcastService(broadcast)) {
932
- manager.channel("broadcast", new BroadcastChannel(broadcast));
1797
+ manager.channel("broadcast", new BroadcastChannel(broadcast, config));
933
1798
  } else {
934
1799
  core.logger.warn(
935
1800
  "[OrbitFlare] Broadcast service not found or invalid, broadcast channel disabled"
936
1801
  );
937
1802
  }
938
1803
  }
939
- setupSlackChannel(core, manager) {
1804
+ setupSlackChannel(_core, manager) {
940
1805
  const slack = this.options.channels?.slack;
941
1806
  if (slack) {
942
1807
  manager.channel("slack", new SlackChannel(slack));
943
1808
  }
944
- if (this.options.enableSms) {
945
- const sms = this.options.channels?.sms;
946
- if (sms) {
947
- manager.channel("sms", new SmsChannel(sms));
948
- }
949
- }
950
1809
  }
951
1810
  setupSmsChannel(core, manager) {
952
1811
  const sms = this.options.channels?.sms;
@@ -966,6 +1825,19 @@ var OrbitFlare = class _OrbitFlare {
966
1825
  });
967
1826
  }
968
1827
  }
1828
+ setupMiddleware(core, manager) {
1829
+ if (this.options.middleware) {
1830
+ for (const middleware of this.options.middleware) {
1831
+ manager.use(middleware);
1832
+ }
1833
+ }
1834
+ if (this.options.enablePreference) {
1835
+ const { PreferenceMiddleware: PreferenceMiddleware2 } = (init_PreferenceMiddleware(), __toCommonJS(PreferenceMiddleware_exports));
1836
+ const preferenceMiddleware = new PreferenceMiddleware2(this.options.preferenceProvider);
1837
+ manager.use(preferenceMiddleware);
1838
+ core.logger.info("[OrbitFlare] Preference middleware enabled");
1839
+ }
1840
+ }
969
1841
  isMailService(service) {
970
1842
  return typeof service === "object" && service !== null && "send" in service && typeof service.send === "function";
971
1843
  }
@@ -1019,17 +1891,84 @@ var TemplatedNotification = class extends Notification {
1019
1891
  throw new Error("Notifiable does not have an email property");
1020
1892
  }
1021
1893
  };
1894
+
1895
+ // src/index.ts
1896
+ init_middleware();
1897
+
1898
+ // src/utils/LazyNotification.ts
1899
+ var LazyNotification = class extends Notification {
1900
+ /**
1901
+ * 快取的資料
1902
+ */
1903
+ _cachedData;
1904
+ /**
1905
+ * 取得快取的資料
1906
+ *
1907
+ * @returns 快取的資料,如果尚未載入則回傳 undefined
1908
+ */
1909
+ getCached() {
1910
+ return this._cachedData;
1911
+ }
1912
+ /**
1913
+ * 設定快取資料
1914
+ *
1915
+ * @param data - 要快取的資料
1916
+ */
1917
+ setCached(data) {
1918
+ this._cachedData = data;
1919
+ }
1920
+ /**
1921
+ * 檢查資料是否已載入
1922
+ *
1923
+ * @returns 如果資料已快取則回傳 true
1924
+ */
1925
+ isLoaded() {
1926
+ return this._cachedData !== void 0;
1927
+ }
1928
+ /**
1929
+ * 清除快取
1930
+ */
1931
+ clearCache() {
1932
+ this._cachedData = void 0;
1933
+ }
1934
+ /**
1935
+ * 載入資料並快取(如果尚未載入)
1936
+ *
1937
+ * @param notifiable - 通知接收者
1938
+ * @returns 載入的資料
1939
+ */
1940
+ async ensureLoaded(notifiable) {
1941
+ if (this._cachedData === void 0) {
1942
+ this._cachedData = await this.loadData(notifiable);
1943
+ }
1944
+ return this._cachedData;
1945
+ }
1946
+ };
1022
1947
  // Annotate the CommonJS export names for ESM import in node:
1023
1948
  0 && (module.exports = {
1949
+ AbortError,
1024
1950
  BroadcastChannel,
1025
1951
  DatabaseChannel,
1952
+ LazyNotification,
1026
1953
  MailChannel,
1954
+ MemoryStore,
1955
+ MiddlewarePriority,
1027
1956
  Notification,
1028
1957
  NotificationManager,
1029
1958
  NotificationMetricsCollector,
1030
1959
  OrbitFlare,
1960
+ PreferenceMiddleware,
1961
+ RateLimitMiddleware,
1031
1962
  SlackChannel,
1032
1963
  SmsChannel,
1033
1964
  TemplatedNotification,
1965
+ TimeoutChannel,
1966
+ TimeoutError,
1967
+ TokenBucket,
1968
+ assertSerializable,
1969
+ checkSerializable,
1970
+ createHookEmitter,
1971
+ deepDeserialize,
1972
+ deepSerialize,
1034
1973
  toPrometheusFormat
1035
1974
  });