@gravito/flare 1.0.0-alpha.2

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 ADDED
@@ -0,0 +1,230 @@
1
+ # @gravito/flare
2
+
3
+ 輕量、高效的通知系統,支援多種通道(郵件、資料庫、廣播、Slack、SMS)。借鑑 Laravel 架構但保持 Gravito 的核心價值(高效能、低耗、輕量、AI 友善)。
4
+
5
+ > **狀態**:v0.1.0 - 核心功能已完成,支援多種通知通道
6
+
7
+ ## 特性
8
+
9
+ - **零運行時開銷**:純類型包裝,直接委派給驅動
10
+ - **多通道支援**:郵件、資料庫、廣播、Slack、SMS
11
+ - **完全模組化**:按需安裝通道,核心包極小
12
+ - **隊列化支援**:整合 `@gravito/stream`,支援異步發送
13
+ - **AI 友善**:完整的型別推導、清晰的 JSDoc、直觀的 API
14
+
15
+ ## 安裝
16
+
17
+ ```bash
18
+ bun add @gravito/flare
19
+ ```
20
+
21
+ ## 快速開始
22
+
23
+ ### 1. 建立通知類別
24
+
25
+ ```typescript
26
+ import { Notification } from '@gravito/flare'
27
+ import type { MailMessage, DatabaseNotification, Notifiable } from '@gravito/flare'
28
+
29
+ class InvoicePaid extends Notification {
30
+ constructor(private invoice: Invoice) {
31
+ super()
32
+ }
33
+
34
+ via(user: Notifiable): string[] {
35
+ return ['mail', 'database']
36
+ }
37
+
38
+ toMail(user: Notifiable): MailMessage {
39
+ return {
40
+ subject: 'Invoice Paid',
41
+ view: 'emails.invoice-paid',
42
+ data: { invoice: this.invoice },
43
+ to: user.email,
44
+ }
45
+ }
46
+
47
+ toDatabase(user: Notifiable): DatabaseNotification {
48
+ return {
49
+ type: 'invoice-paid',
50
+ data: {
51
+ invoice_id: this.invoice.id,
52
+ amount: this.invoice.amount,
53
+ },
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### 2. 配置 OrbitFlare
60
+
61
+ ```typescript
62
+ import { PlanetCore } from 'gravito-core'
63
+ import { OrbitFlare } from '@gravito/flare'
64
+ import { OrbitSignal } from '@gravito/signal'
65
+ import { OrbitStream } from '@gravito/stream'
66
+
67
+ const core = await PlanetCore.boot({
68
+ orbits: [
69
+ OrbitSignal.configure({ /* ... */ }),
70
+ OrbitStream.configure({ /* ... */ }),
71
+ OrbitFlare.configure({
72
+ enableMail: true,
73
+ enableDatabase: true,
74
+ enableBroadcast: true,
75
+ channels: {
76
+ slack: {
77
+ webhookUrl: 'https://hooks.slack.com/services/...',
78
+ },
79
+ },
80
+ }),
81
+ ],
82
+ })
83
+ ```
84
+
85
+ ### 3. 發送通知
86
+
87
+ ```typescript
88
+ // 在 Controller 中
89
+ const notifications = c.get('notifications') as NotificationManager
90
+
91
+ await notifications.send(user, new InvoicePaid(invoice))
92
+ ```
93
+
94
+ ### 4. 隊列化通知
95
+
96
+ ```typescript
97
+ import { Notification, ShouldQueue } from '@gravito/flare'
98
+
99
+ class SendEmailNotification extends Notification implements ShouldQueue {
100
+ queue = 'notifications'
101
+ delay = 60 // 延遲 60 秒
102
+
103
+ via(user: Notifiable): string[] {
104
+ return ['mail']
105
+ }
106
+
107
+ toMail(user: Notifiable): MailMessage {
108
+ return {
109
+ subject: 'Welcome!',
110
+ to: user.email,
111
+ view: 'emails.welcome',
112
+ }
113
+ }
114
+ }
115
+
116
+ // 自動推送到隊列
117
+ await notifications.send(user, new SendEmailNotification())
118
+ ```
119
+
120
+ ## 通道
121
+
122
+ ### 郵件通道
123
+
124
+ 需要安裝 `@gravito/signal`:
125
+
126
+ ```typescript
127
+ via(user: Notifiable): string[] {
128
+ return ['mail']
129
+ }
130
+
131
+ toMail(user: Notifiable): MailMessage {
132
+ return {
133
+ subject: 'Subject',
134
+ view: 'emails.template',
135
+ data: { /* ... */ },
136
+ to: user.email,
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### 資料庫通道
142
+
143
+ 需要資料庫服務支援:
144
+
145
+ ```typescript
146
+ via(user: Notifiable): string[] {
147
+ return ['database']
148
+ }
149
+
150
+ toDatabase(user: Notifiable): DatabaseNotification {
151
+ return {
152
+ type: 'notification-type',
153
+ data: { /* ... */ },
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### 廣播通道
159
+
160
+ 需要安裝 `@gravito/radiance`:
161
+
162
+ ```typescript
163
+ via(user: Notifiable): string[] {
164
+ return ['broadcast']
165
+ }
166
+
167
+ toBroadcast(user: Notifiable): BroadcastNotification {
168
+ return {
169
+ type: 'notification-type',
170
+ data: { /* ... */ },
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### Slack 通道
176
+
177
+ ```typescript
178
+ via(user: Notifiable): string[] {
179
+ return ['slack']
180
+ }
181
+
182
+ toSlack(user: Notifiable): SlackMessage {
183
+ return {
184
+ text: 'Notification message',
185
+ channel: '#notifications',
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### SMS 通道
191
+
192
+ ```typescript
193
+ via(user: Notifiable): string[] {
194
+ return ['sms']
195
+ }
196
+
197
+ toSms(user: Notifiable): SmsMessage {
198
+ return {
199
+ to: user.phone,
200
+ message: 'Notification message',
201
+ }
202
+ }
203
+ ```
204
+
205
+ ## API 參考
206
+
207
+ ### Notification
208
+
209
+ 所有通知都應該繼承 `Notification` 類別。
210
+
211
+ #### 方法
212
+
213
+ - `via(notifiable: Notifiable): string[]` - 指定通知通道(必須實作)
214
+ - `toMail(notifiable: Notifiable): MailMessage` - 郵件訊息(可選)
215
+ - `toDatabase(notifiable: Notifiable): DatabaseNotification` - 資料庫通知(可選)
216
+ - `toBroadcast(notifiable: Notifiable): BroadcastNotification` - 廣播通知(可選)
217
+ - `toSlack(notifiable: Notifiable): SlackMessage` - Slack 訊息(可選)
218
+ - `toSms(notifiable: Notifiable): SmsMessage` - SMS 訊息(可選)
219
+
220
+ ### NotificationManager
221
+
222
+ #### 方法
223
+
224
+ - `send(notifiable: Notifiable, notification: Notification): Promise<void>` - 發送通知
225
+ - `channel(name: string, channel: NotificationChannel): void` - 註冊自訂通道
226
+
227
+ ## 授權
228
+
229
+ MIT © Carl Lee
230
+
package/dist/index.cjs ADDED
@@ -0,0 +1,330 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/index.ts
30
+ var exports_src = {};
31
+ __export(exports_src, {
32
+ SmsChannel: () => SmsChannel,
33
+ SlackChannel: () => SlackChannel,
34
+ OrbitFlare: () => OrbitFlare,
35
+ NotificationManager: () => NotificationManager,
36
+ Notification: () => Notification,
37
+ MailChannel: () => MailChannel,
38
+ DatabaseChannel: () => DatabaseChannel,
39
+ BroadcastChannel: () => BroadcastChannel
40
+ });
41
+ module.exports = __toCommonJS(exports_src);
42
+
43
+ // src/channels/BroadcastChannel.ts
44
+ class BroadcastChannel {
45
+ broadcastService;
46
+ constructor(broadcastService) {
47
+ this.broadcastService = broadcastService;
48
+ }
49
+ async send(notification, notifiable) {
50
+ if (!notification.toBroadcast) {
51
+ throw new Error("Notification does not implement toBroadcast method");
52
+ }
53
+ const broadcastNotification = notification.toBroadcast(notifiable);
54
+ const notifiableId = notifiable.getNotifiableId();
55
+ const notifiableType = notifiable.getNotifiableType?.() || "user";
56
+ const channel = `private-${notifiableType}.${notifiableId}`;
57
+ await this.broadcastService.broadcast(channel, broadcastNotification.type, broadcastNotification.data);
58
+ }
59
+ }
60
+ // src/channels/DatabaseChannel.ts
61
+ class DatabaseChannel {
62
+ dbService;
63
+ constructor(dbService) {
64
+ this.dbService = dbService;
65
+ }
66
+ async send(notification, notifiable) {
67
+ if (!notification.toDatabase) {
68
+ throw new Error("Notification does not implement toDatabase method");
69
+ }
70
+ const dbNotification = notification.toDatabase(notifiable);
71
+ await this.dbService.insertNotification({
72
+ notifiableId: notifiable.getNotifiableId(),
73
+ notifiableType: notifiable.getNotifiableType?.() || "user",
74
+ type: dbNotification.type,
75
+ data: dbNotification.data
76
+ });
77
+ }
78
+ }
79
+ // src/channels/MailChannel.ts
80
+ class MailChannel {
81
+ mailService;
82
+ constructor(mailService) {
83
+ this.mailService = mailService;
84
+ }
85
+ async send(notification, notifiable) {
86
+ if (!notification.toMail) {
87
+ throw new Error("Notification does not implement toMail method");
88
+ }
89
+ const message = notification.toMail(notifiable);
90
+ await this.mailService.send(message);
91
+ }
92
+ }
93
+ // src/channels/SlackChannel.ts
94
+ class SlackChannel {
95
+ config;
96
+ constructor(config) {
97
+ this.config = config;
98
+ }
99
+ async send(notification, notifiable) {
100
+ if (!notification.toSlack) {
101
+ throw new Error("Notification does not implement toSlack method");
102
+ }
103
+ const slackMessage = notification.toSlack(notifiable);
104
+ const response = await fetch(this.config.webhookUrl, {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json"
108
+ },
109
+ body: JSON.stringify({
110
+ text: slackMessage.text,
111
+ channel: slackMessage.channel || this.config.defaultChannel,
112
+ username: slackMessage.username,
113
+ icon_emoji: slackMessage.iconEmoji,
114
+ attachments: slackMessage.attachments
115
+ })
116
+ });
117
+ if (!response.ok) {
118
+ throw new Error(`Failed to send Slack notification: ${response.statusText}`);
119
+ }
120
+ }
121
+ }
122
+ // src/channels/SmsChannel.ts
123
+ class SmsChannel {
124
+ config;
125
+ constructor(config) {
126
+ this.config = config;
127
+ }
128
+ async send(notification, notifiable) {
129
+ if (!notification.toSms) {
130
+ throw new Error("Notification does not implement toSms method");
131
+ }
132
+ const smsMessage = notification.toSms(notifiable);
133
+ switch (this.config.provider) {
134
+ case "twilio":
135
+ await this.sendViaTwilio(smsMessage);
136
+ break;
137
+ case "aws-sns":
138
+ await this.sendViaAwsSns(smsMessage);
139
+ break;
140
+ default:
141
+ throw new Error(`Unsupported SMS provider: ${this.config.provider}`);
142
+ }
143
+ }
144
+ async sendViaTwilio(message) {
145
+ if (!this.config.apiKey || !this.config.apiSecret) {
146
+ throw new Error("Twilio API key and secret are required");
147
+ }
148
+ const accountSid = this.config.apiKey;
149
+ const authToken = this.config.apiSecret;
150
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`, {
151
+ method: "POST",
152
+ headers: {
153
+ Authorization: `Basic ${btoa(`${accountSid}:${authToken}`)}`,
154
+ "Content-Type": "application/x-www-form-urlencoded"
155
+ },
156
+ body: new URLSearchParams({
157
+ From: this.config.from || "",
158
+ To: message.to,
159
+ Body: message.message
160
+ })
161
+ });
162
+ if (!response.ok) {
163
+ const error = await response.text();
164
+ throw new Error(`Failed to send SMS via Twilio: ${error}`);
165
+ }
166
+ }
167
+ async sendViaAwsSns(_message) {
168
+ throw new Error("AWS SNS SMS provider not yet implemented. Please install @aws-sdk/client-sns");
169
+ }
170
+ }
171
+ // src/Notification.ts
172
+ class Notification {
173
+ toMail(_notifiable) {
174
+ throw new Error("toMail method not implemented");
175
+ }
176
+ toDatabase(_notifiable) {
177
+ throw new Error("toDatabase method not implemented");
178
+ }
179
+ toBroadcast(_notifiable) {
180
+ throw new Error("toBroadcast method not implemented");
181
+ }
182
+ toSlack(_notifiable) {
183
+ throw new Error("toSlack method not implemented");
184
+ }
185
+ toSms(_notifiable) {
186
+ throw new Error("toSms method not implemented");
187
+ }
188
+ shouldQueue() {
189
+ return "queue" in this || "connection" in this || "delay" in this;
190
+ }
191
+ getQueueConfig() {
192
+ if (this.shouldQueue()) {
193
+ const queueable = this;
194
+ return {
195
+ queue: queueable.queue,
196
+ connection: queueable.connection,
197
+ delay: queueable.delay
198
+ };
199
+ }
200
+ return {};
201
+ }
202
+ }
203
+ // src/NotificationManager.ts
204
+ class NotificationManager {
205
+ core;
206
+ channels = new Map;
207
+ queueManager;
208
+ constructor(core) {
209
+ this.core = core;
210
+ }
211
+ channel(name, channel) {
212
+ this.channels.set(name, channel);
213
+ }
214
+ setQueueManager(manager) {
215
+ this.queueManager = manager;
216
+ }
217
+ async send(notifiable, notification) {
218
+ const channels = notification.via(notifiable);
219
+ if (notification.shouldQueue() && this.queueManager) {
220
+ const queueConfig = notification.getQueueConfig();
221
+ const queueJob = {
222
+ type: "notification",
223
+ notification: notification.constructor.name,
224
+ notifiableId: notifiable.getNotifiableId(),
225
+ notifiableType: notifiable.getNotifiableType?.() || "user",
226
+ channels,
227
+ notificationData: this.serializeNotification(notification),
228
+ handle: async () => {
229
+ await this.sendNow(notifiable, notification, channels);
230
+ }
231
+ };
232
+ await this.queueManager.push(queueJob, queueConfig.queue, queueConfig.connection, queueConfig.delay);
233
+ return;
234
+ }
235
+ await this.sendNow(notifiable, notification, channels);
236
+ }
237
+ async sendNow(notifiable, notification, channels) {
238
+ for (const channelName of channels) {
239
+ const channel = this.channels.get(channelName);
240
+ if (!channel) {
241
+ this.core.logger.warn(`[NotificationManager] Channel '${channelName}' not found, skipping`);
242
+ continue;
243
+ }
244
+ try {
245
+ await channel.send(notification, notifiable);
246
+ } catch (error) {
247
+ this.core.logger.error(`[NotificationManager] Failed to send notification via '${channelName}':`, error);
248
+ }
249
+ }
250
+ }
251
+ serializeNotification(notification) {
252
+ const data = {};
253
+ for (const [key, value] of Object.entries(notification)) {
254
+ if (!key.startsWith("_") && typeof value !== "function") {
255
+ data[key] = value;
256
+ }
257
+ }
258
+ return data;
259
+ }
260
+ }
261
+ // src/OrbitFlare.ts
262
+ class OrbitFlare {
263
+ options;
264
+ constructor(options = {}) {
265
+ this.options = {
266
+ enableMail: true,
267
+ enableDatabase: true,
268
+ enableBroadcast: true,
269
+ enableSlack: false,
270
+ enableSms: false,
271
+ ...options
272
+ };
273
+ }
274
+ static configure(options = {}) {
275
+ return new OrbitFlare(options);
276
+ }
277
+ async install(core) {
278
+ const manager = new NotificationManager(core);
279
+ if (this.options.enableMail) {
280
+ const mail = core.services.get("mail");
281
+ if (mail) {
282
+ manager.channel("mail", new MailChannel(mail));
283
+ } else {
284
+ core.logger.warn("[OrbitFlare] Mail service not found, mail channel disabled");
285
+ }
286
+ }
287
+ if (this.options.enableDatabase) {
288
+ const db = core.services.get("db");
289
+ if (db) {
290
+ manager.channel("database", new DatabaseChannel(db));
291
+ } else {
292
+ core.logger.warn("[OrbitFlare] Database service not found, database channel disabled");
293
+ }
294
+ }
295
+ if (this.options.enableBroadcast) {
296
+ const broadcast = core.services.get("broadcast");
297
+ if (broadcast) {
298
+ manager.channel("broadcast", new BroadcastChannel(broadcast));
299
+ } else {
300
+ core.logger.warn("[OrbitFlare] Broadcast service not found, broadcast channel disabled");
301
+ }
302
+ }
303
+ if (this.options.enableSlack) {
304
+ const slack = this.options.channels?.slack;
305
+ if (slack) {
306
+ manager.channel("slack", new SlackChannel(slack));
307
+ } else {
308
+ core.logger.warn("[OrbitFlare] Slack configuration not found, slack channel disabled");
309
+ }
310
+ }
311
+ if (this.options.enableSms) {
312
+ const sms = this.options.channels?.sms;
313
+ if (sms) {
314
+ manager.channel("sms", new SmsChannel(sms));
315
+ } else {
316
+ core.logger.warn("[OrbitFlare] SMS configuration not found, sms channel disabled");
317
+ }
318
+ }
319
+ core.services.set("notifications", manager);
320
+ const queue = core.services.get("queue");
321
+ if (queue) {
322
+ manager.setQueueManager({
323
+ push: async (job, queueName, connection, delay) => {
324
+ await queue.push(job, queueName, connection, delay);
325
+ }
326
+ });
327
+ }
328
+ core.logger.info("[OrbitFlare] Installed");
329
+ }
330
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,298 @@
1
+ // src/channels/BroadcastChannel.ts
2
+ class BroadcastChannel {
3
+ broadcastService;
4
+ constructor(broadcastService) {
5
+ this.broadcastService = broadcastService;
6
+ }
7
+ async send(notification, notifiable) {
8
+ if (!notification.toBroadcast) {
9
+ throw new Error("Notification does not implement toBroadcast method");
10
+ }
11
+ const broadcastNotification = notification.toBroadcast(notifiable);
12
+ const notifiableId = notifiable.getNotifiableId();
13
+ const notifiableType = notifiable.getNotifiableType?.() || "user";
14
+ const channel = `private-${notifiableType}.${notifiableId}`;
15
+ await this.broadcastService.broadcast(channel, broadcastNotification.type, broadcastNotification.data);
16
+ }
17
+ }
18
+ // src/channels/DatabaseChannel.ts
19
+ class DatabaseChannel {
20
+ dbService;
21
+ constructor(dbService) {
22
+ this.dbService = dbService;
23
+ }
24
+ async send(notification, notifiable) {
25
+ if (!notification.toDatabase) {
26
+ throw new Error("Notification does not implement toDatabase method");
27
+ }
28
+ const dbNotification = notification.toDatabase(notifiable);
29
+ await this.dbService.insertNotification({
30
+ notifiableId: notifiable.getNotifiableId(),
31
+ notifiableType: notifiable.getNotifiableType?.() || "user",
32
+ type: dbNotification.type,
33
+ data: dbNotification.data
34
+ });
35
+ }
36
+ }
37
+ // src/channels/MailChannel.ts
38
+ class MailChannel {
39
+ mailService;
40
+ constructor(mailService) {
41
+ this.mailService = mailService;
42
+ }
43
+ async send(notification, notifiable) {
44
+ if (!notification.toMail) {
45
+ throw new Error("Notification does not implement toMail method");
46
+ }
47
+ const message = notification.toMail(notifiable);
48
+ await this.mailService.send(message);
49
+ }
50
+ }
51
+ // src/channels/SlackChannel.ts
52
+ class SlackChannel {
53
+ config;
54
+ constructor(config) {
55
+ this.config = config;
56
+ }
57
+ async send(notification, notifiable) {
58
+ if (!notification.toSlack) {
59
+ throw new Error("Notification does not implement toSlack method");
60
+ }
61
+ const slackMessage = notification.toSlack(notifiable);
62
+ const response = await fetch(this.config.webhookUrl, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json"
66
+ },
67
+ body: JSON.stringify({
68
+ text: slackMessage.text,
69
+ channel: slackMessage.channel || this.config.defaultChannel,
70
+ username: slackMessage.username,
71
+ icon_emoji: slackMessage.iconEmoji,
72
+ attachments: slackMessage.attachments
73
+ })
74
+ });
75
+ if (!response.ok) {
76
+ throw new Error(`Failed to send Slack notification: ${response.statusText}`);
77
+ }
78
+ }
79
+ }
80
+ // src/channels/SmsChannel.ts
81
+ class SmsChannel {
82
+ config;
83
+ constructor(config) {
84
+ this.config = config;
85
+ }
86
+ async send(notification, notifiable) {
87
+ if (!notification.toSms) {
88
+ throw new Error("Notification does not implement toSms method");
89
+ }
90
+ const smsMessage = notification.toSms(notifiable);
91
+ switch (this.config.provider) {
92
+ case "twilio":
93
+ await this.sendViaTwilio(smsMessage);
94
+ break;
95
+ case "aws-sns":
96
+ await this.sendViaAwsSns(smsMessage);
97
+ break;
98
+ default:
99
+ throw new Error(`Unsupported SMS provider: ${this.config.provider}`);
100
+ }
101
+ }
102
+ async sendViaTwilio(message) {
103
+ if (!this.config.apiKey || !this.config.apiSecret) {
104
+ throw new Error("Twilio API key and secret are required");
105
+ }
106
+ const accountSid = this.config.apiKey;
107
+ const authToken = this.config.apiSecret;
108
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`, {
109
+ method: "POST",
110
+ headers: {
111
+ Authorization: `Basic ${btoa(`${accountSid}:${authToken}`)}`,
112
+ "Content-Type": "application/x-www-form-urlencoded"
113
+ },
114
+ body: new URLSearchParams({
115
+ From: this.config.from || "",
116
+ To: message.to,
117
+ Body: message.message
118
+ })
119
+ });
120
+ if (!response.ok) {
121
+ const error = await response.text();
122
+ throw new Error(`Failed to send SMS via Twilio: ${error}`);
123
+ }
124
+ }
125
+ async sendViaAwsSns(_message) {
126
+ throw new Error("AWS SNS SMS provider not yet implemented. Please install @aws-sdk/client-sns");
127
+ }
128
+ }
129
+ // src/Notification.ts
130
+ class Notification {
131
+ toMail(_notifiable) {
132
+ throw new Error("toMail method not implemented");
133
+ }
134
+ toDatabase(_notifiable) {
135
+ throw new Error("toDatabase method not implemented");
136
+ }
137
+ toBroadcast(_notifiable) {
138
+ throw new Error("toBroadcast method not implemented");
139
+ }
140
+ toSlack(_notifiable) {
141
+ throw new Error("toSlack method not implemented");
142
+ }
143
+ toSms(_notifiable) {
144
+ throw new Error("toSms method not implemented");
145
+ }
146
+ shouldQueue() {
147
+ return "queue" in this || "connection" in this || "delay" in this;
148
+ }
149
+ getQueueConfig() {
150
+ if (this.shouldQueue()) {
151
+ const queueable = this;
152
+ return {
153
+ queue: queueable.queue,
154
+ connection: queueable.connection,
155
+ delay: queueable.delay
156
+ };
157
+ }
158
+ return {};
159
+ }
160
+ }
161
+ // src/NotificationManager.ts
162
+ class NotificationManager {
163
+ core;
164
+ channels = new Map;
165
+ queueManager;
166
+ constructor(core) {
167
+ this.core = core;
168
+ }
169
+ channel(name, channel) {
170
+ this.channels.set(name, channel);
171
+ }
172
+ setQueueManager(manager) {
173
+ this.queueManager = manager;
174
+ }
175
+ async send(notifiable, notification) {
176
+ const channels = notification.via(notifiable);
177
+ if (notification.shouldQueue() && this.queueManager) {
178
+ const queueConfig = notification.getQueueConfig();
179
+ const queueJob = {
180
+ type: "notification",
181
+ notification: notification.constructor.name,
182
+ notifiableId: notifiable.getNotifiableId(),
183
+ notifiableType: notifiable.getNotifiableType?.() || "user",
184
+ channels,
185
+ notificationData: this.serializeNotification(notification),
186
+ handle: async () => {
187
+ await this.sendNow(notifiable, notification, channels);
188
+ }
189
+ };
190
+ await this.queueManager.push(queueJob, queueConfig.queue, queueConfig.connection, queueConfig.delay);
191
+ return;
192
+ }
193
+ await this.sendNow(notifiable, notification, channels);
194
+ }
195
+ async sendNow(notifiable, notification, channels) {
196
+ for (const channelName of channels) {
197
+ const channel = this.channels.get(channelName);
198
+ if (!channel) {
199
+ this.core.logger.warn(`[NotificationManager] Channel '${channelName}' not found, skipping`);
200
+ continue;
201
+ }
202
+ try {
203
+ await channel.send(notification, notifiable);
204
+ } catch (error) {
205
+ this.core.logger.error(`[NotificationManager] Failed to send notification via '${channelName}':`, error);
206
+ }
207
+ }
208
+ }
209
+ serializeNotification(notification) {
210
+ const data = {};
211
+ for (const [key, value] of Object.entries(notification)) {
212
+ if (!key.startsWith("_") && typeof value !== "function") {
213
+ data[key] = value;
214
+ }
215
+ }
216
+ return data;
217
+ }
218
+ }
219
+ // src/OrbitFlare.ts
220
+ class OrbitFlare {
221
+ options;
222
+ constructor(options = {}) {
223
+ this.options = {
224
+ enableMail: true,
225
+ enableDatabase: true,
226
+ enableBroadcast: true,
227
+ enableSlack: false,
228
+ enableSms: false,
229
+ ...options
230
+ };
231
+ }
232
+ static configure(options = {}) {
233
+ return new OrbitFlare(options);
234
+ }
235
+ async install(core) {
236
+ const manager = new NotificationManager(core);
237
+ if (this.options.enableMail) {
238
+ const mail = core.services.get("mail");
239
+ if (mail) {
240
+ manager.channel("mail", new MailChannel(mail));
241
+ } else {
242
+ core.logger.warn("[OrbitFlare] Mail service not found, mail channel disabled");
243
+ }
244
+ }
245
+ if (this.options.enableDatabase) {
246
+ const db = core.services.get("db");
247
+ if (db) {
248
+ manager.channel("database", new DatabaseChannel(db));
249
+ } else {
250
+ core.logger.warn("[OrbitFlare] Database service not found, database channel disabled");
251
+ }
252
+ }
253
+ if (this.options.enableBroadcast) {
254
+ const broadcast = core.services.get("broadcast");
255
+ if (broadcast) {
256
+ manager.channel("broadcast", new BroadcastChannel(broadcast));
257
+ } else {
258
+ core.logger.warn("[OrbitFlare] Broadcast service not found, broadcast channel disabled");
259
+ }
260
+ }
261
+ if (this.options.enableSlack) {
262
+ const slack = this.options.channels?.slack;
263
+ if (slack) {
264
+ manager.channel("slack", new SlackChannel(slack));
265
+ } else {
266
+ core.logger.warn("[OrbitFlare] Slack configuration not found, slack channel disabled");
267
+ }
268
+ }
269
+ if (this.options.enableSms) {
270
+ const sms = this.options.channels?.sms;
271
+ if (sms) {
272
+ manager.channel("sms", new SmsChannel(sms));
273
+ } else {
274
+ core.logger.warn("[OrbitFlare] SMS configuration not found, sms channel disabled");
275
+ }
276
+ }
277
+ core.services.set("notifications", manager);
278
+ const queue = core.services.get("queue");
279
+ if (queue) {
280
+ manager.setQueueManager({
281
+ push: async (job, queueName, connection, delay) => {
282
+ await queue.push(job, queueName, connection, delay);
283
+ }
284
+ });
285
+ }
286
+ core.logger.info("[OrbitFlare] Installed");
287
+ }
288
+ }
289
+ export {
290
+ SmsChannel,
291
+ SlackChannel,
292
+ OrbitFlare,
293
+ NotificationManager,
294
+ Notification,
295
+ MailChannel,
296
+ DatabaseChannel,
297
+ BroadcastChannel
298
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@gravito/flare",
3
+ "version": "1.0.0-alpha.2",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Lightweight, high-performance notification system for Gravito framework. Supports multiple channels (mail, database, broadcast, slack, sms) with zero runtime overhead.",
8
+ "module": "./dist/index.mjs",
9
+ "main": "./dist/index.cjs",
10
+ "type": "module",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "bun run build.ts",
26
+ "test": "bun test",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "keywords": [
30
+ "gravito",
31
+ "orbit",
32
+ "notifications",
33
+ "mail",
34
+ "sms",
35
+ "slack",
36
+ "database"
37
+ ],
38
+ "author": "Carl Lee <carllee0520@gmail.com>",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "gravito-core": "1.0.0-beta.2"
42
+ },
43
+ "peerDependencies": {
44
+ "@gravito/stream": "1.0.0-alpha.2",
45
+ "@gravito/signal": "1.0.0-alpha.2",
46
+ "@gravito/radiance": "1.0.0-alpha.2"
47
+ },
48
+ "devDependencies": {
49
+ "bun-types": "latest",
50
+ "typescript": "^5.9.3"
51
+ },
52
+ "homepage": "https://github.com/gravito-framework/gravito#readme",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/gravito-framework/gravito.git",
56
+ "directory": "packages/flare"
57
+ }
58
+ }