@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 +230 -0
- package/dist/index.cjs +330 -0
- package/dist/index.mjs +298 -0
- package/package.json +58 -0
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
|
+
}
|