@gravito/signal 3.0.3 → 3.0.4
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/CHANGELOG.md +16 -0
- package/README.md +89 -60
- package/README.zh-TW.md +140 -9
- package/dist/MjmlRenderer-IUH663FT.mjs +8 -0
- package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +8 -0
- package/dist/ReactRenderer-2JFLRVST.mjs +45 -0
- package/dist/{ReactRenderer-L5INVYKT.mjs → ReactRenderer-LYEOSYFS.mjs} +9 -8
- package/dist/ReactRenderer-V54CUUEI.mjs +45 -0
- package/dist/VueMjmlRenderer-4F4CXHDB.mjs +8 -0
- package/dist/VueMjmlRenderer-5WZR4CQG.mjs +8 -0
- package/dist/VueMjmlRenderer-U5YMWI44.mjs +8 -0
- package/dist/VueRenderer-3YBRQXME.mjs +48 -0
- package/dist/VueRenderer-46JGXTJ2.mjs +48 -0
- package/dist/VueRenderer-5KWD4R3C.mjs +48 -0
- package/dist/VueRenderer-C23U4O5E.mjs +48 -0
- package/dist/VueRenderer-LEVDFLHP.mjs +31 -0
- package/dist/VueRenderer-RNHSCCRI.mjs +48 -0
- package/dist/chunk-3WOR3XSL.mjs +82 -0
- package/dist/chunk-DBFIVHHG.mjs +79 -0
- package/dist/{chunk-6DZX6EAA.mjs → chunk-HEBXNMVQ.mjs} +12 -1
- package/dist/chunk-KB7IDDBT.mjs +82 -0
- package/dist/chunk-LZL5UUPC.mjs +82 -0
- package/dist/chunk-W6LXIJKK.mjs +57 -0
- package/dist/chunk-XBIVBJS2.mjs +8 -0
- package/dist/index.d.mts +1680 -209
- package/dist/index.d.ts +1680 -209
- package/dist/index.js +69405 -542
- package/dist/index.mjs +993 -110
- package/dist/lib-HJTRWKU5.mjs +67788 -0
- package/dist/{VueRenderer-Z5PRVBNH.mjs → server-renderer-4IM3P5XZ.mjs} +308 -423
- package/dist/server-renderer-7KWFSTPV.mjs +37193 -0
- package/dist/{VueRenderer-S65ZARRI.mjs → server-renderer-S5FPSTJ2.mjs} +931 -877
- package/dist/server-renderer-X5LUFVWT.mjs +37193 -0
- package/doc/OPTIMIZATION_PLAN.md +496 -0
- package/package.json +14 -12
- package/scripts/check-coverage.ts +64 -0
- package/src/Mailable.ts +340 -44
- package/src/OrbitSignal.ts +350 -50
- package/src/TypedMailable.ts +96 -0
- package/src/dev/DevMailbox.ts +89 -33
- package/src/dev/DevServer.ts +14 -14
- package/src/dev/storage/FileMailboxStorage.ts +66 -0
- package/src/dev/storage/MailboxStorage.ts +15 -0
- package/src/dev/storage/MemoryMailboxStorage.ts +36 -0
- package/src/dev/ui/mailbox.ts +1 -1
- package/src/dev/ui/preview.ts +4 -4
- package/src/errors.ts +69 -0
- package/src/events.ts +72 -0
- package/src/index.ts +20 -1
- package/src/renderers/HtmlRenderer.ts +20 -18
- package/src/renderers/MjmlRenderer.ts +73 -0
- package/src/renderers/ReactMjmlRenderer.ts +94 -0
- package/src/renderers/ReactRenderer.ts +26 -21
- package/src/renderers/Renderer.ts +43 -3
- package/src/renderers/TemplateRenderer.ts +48 -15
- package/src/renderers/VueMjmlRenderer.ts +99 -0
- package/src/renderers/VueRenderer.ts +26 -21
- package/src/renderers/mjml-templates.ts +50 -0
- package/src/transports/BaseTransport.ts +148 -0
- package/src/transports/LogTransport.ts +28 -6
- package/src/transports/MemoryTransport.ts +34 -6
- package/src/transports/SesTransport.ts +62 -17
- package/src/transports/SmtpTransport.ts +123 -27
- package/src/transports/Transport.ts +33 -4
- package/src/types.ts +172 -3
- package/src/utils/html.ts +43 -0
- package/src/webhooks/SendGridWebhookDriver.ts +80 -0
- package/src/webhooks/SesWebhookDriver.ts +44 -0
- package/tests/DevMailbox.test.ts +54 -0
- package/tests/FileMailboxStorage.test.ts +56 -0
- package/tests/MjmlLayout.test.ts +28 -0
- package/tests/MjmlRenderer.test.ts +53 -0
- package/tests/OrbitSignalWebhook.test.ts +56 -0
- package/tests/ReactMjmlRenderer.test.ts +33 -0
- package/tests/SendGridWebhookDriver.test.ts +69 -0
- package/tests/SesWebhookDriver.test.ts +46 -0
- package/tests/VueMjmlRenderer.test.ts +35 -0
- package/tests/dev-server.test.ts +1 -1
- package/tests/transports.test.ts +3 -3
- package/tsconfig.json +12 -24
- package/dist/OrbitMail-2Z7ZTKYA.mjs +0 -7
- package/dist/OrbitMail-BGV32HWN.mjs +0 -7
- package/dist/OrbitMail-FUYZQSAV.mjs +0 -7
- package/dist/OrbitMail-NAPCRK7B.mjs +0 -7
- package/dist/OrbitMail-REGJ276B.mjs +0 -7
- package/dist/OrbitMail-TCFBJWDT.mjs +0 -7
- package/dist/OrbitMail-XZZW6U4N.mjs +0 -7
- package/dist/OrbitSignal-IPSA2CDO.mjs +0 -7
- package/dist/OrbitSignal-MABW4DDW.mjs +0 -7
- package/dist/OrbitSignal-QSW5VQ5M.mjs +0 -7
- package/dist/OrbitSignal-R22QHWAA.mjs +0 -7
- package/dist/OrbitSignal-ZKKMEC27.mjs +0 -7
- package/dist/chunk-3U2CYJO5.mjs +0 -367
- package/dist/chunk-3XFC4T6M.mjs +0 -392
- package/dist/chunk-456QRYFW.mjs +0 -401
- package/dist/chunk-DT3R2TNV.mjs +0 -367
- package/dist/chunk-F6MVTUCT.mjs +0 -421
- package/dist/chunk-GADWIVC4.mjs +0 -400
- package/dist/chunk-HHKFAMSE.mjs +0 -380
- package/dist/chunk-NEQCQSZI.mjs +0 -406
- package/dist/chunk-OKRNL6PN.mjs +0 -400
- package/dist/chunk-ULN3GMY2.mjs +0 -367
- package/dist/chunk-XAWO7RSP.mjs +0 -398
- package/dist/chunk-YLVDJSED.mjs +0 -431
package/src/OrbitSignal.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GravitoContext, GravitoNext, GravitoOrbit, PlanetCore } from '@gravito/core'
|
|
2
2
|
import { DevMailbox } from './dev/DevMailbox'
|
|
3
3
|
import { DevServer } from './dev/DevServer'
|
|
4
|
+
import type { MailEvent, MailEventHandler, MailEventType } from './events'
|
|
4
5
|
import type { Mailable } from './Mailable'
|
|
5
6
|
import { LogTransport } from './transports/LogTransport'
|
|
6
7
|
import { MemoryTransport } from './transports/MemoryTransport'
|
|
@@ -9,31 +10,176 @@ import type { MailConfig, Message } from './types'
|
|
|
9
10
|
/**
|
|
10
11
|
* OrbitSignal - Mail service orbit for Gravito framework.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* @description
|
|
14
|
+
* A production-ready email service providing multi-transport support, automatic retry,
|
|
15
|
+
* event-driven lifecycle hooks, and development tooling. Integrates seamlessly with
|
|
16
|
+
* Gravito's orbit system and queue infrastructure.
|
|
17
|
+
*
|
|
18
|
+
* @architecture
|
|
19
|
+
* ```
|
|
20
|
+
* OrbitSignal
|
|
21
|
+
* ├── Configuration (MailConfig)
|
|
22
|
+
* │ ├── transport: Transport (SMTP, SES, Log, Memory)
|
|
23
|
+
* │ ├── from: Default sender
|
|
24
|
+
* │ ├── devMode: Development interception
|
|
25
|
+
* │ └── translator: i18n support
|
|
26
|
+
* ├── Lifecycle Events
|
|
27
|
+
* │ ├── beforeRender → afterRender
|
|
28
|
+
* │ ├── beforeSend → afterSend
|
|
29
|
+
* │ └── sendFailed (on error)
|
|
30
|
+
* ├── Transport Layer
|
|
31
|
+
* │ ├── BaseTransport (retry, backoff)
|
|
32
|
+
* │ ├── SmtpTransport (connection pooling)
|
|
33
|
+
* │ ├── SesTransport (AWS SES)
|
|
34
|
+
* │ ├── LogTransport (console output)
|
|
35
|
+
* │ └── MemoryTransport (dev mode)
|
|
36
|
+
* └── Dev Tools
|
|
37
|
+
* ├── DevMailbox (in-memory storage)
|
|
38
|
+
* └── DevServer (preview UI at /__mail)
|
|
39
|
+
* ```
|
|
14
40
|
*
|
|
15
41
|
* @example
|
|
42
|
+
* **Basic SMTP Configuration**
|
|
16
43
|
* ```typescript
|
|
17
|
-
* import { OrbitSignal } from '@gravito/signal'
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* auth: { user: 'user', pass: 'pass' }
|
|
27
|
-
* }),
|
|
28
|
-
* from: { name: 'App', email: 'noreply@example.com' }
|
|
29
|
-
* })
|
|
30
|
-
* ]
|
|
44
|
+
* import { OrbitSignal, SmtpTransport } from '@gravito/signal'
|
|
45
|
+
*
|
|
46
|
+
* const mail = new OrbitSignal({
|
|
47
|
+
* transport: new SmtpTransport({
|
|
48
|
+
* host: 'smtp.mailtrap.io',
|
|
49
|
+
* port: 2525,
|
|
50
|
+
* auth: { user: 'username', pass: 'password' }
|
|
51
|
+
* }),
|
|
52
|
+
* from: { name: 'My App', address: 'noreply@myapp.com' }
|
|
31
53
|
* })
|
|
32
54
|
*
|
|
33
|
-
*
|
|
34
|
-
* await c.get('mail').send(new WelcomeEmail(user))
|
|
55
|
+
* mail.install(core)
|
|
35
56
|
* ```
|
|
36
57
|
*
|
|
58
|
+
* @example
|
|
59
|
+
* **AWS SES with Retry Configuration**
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { OrbitSignal, SesTransport } from '@gravito/signal'
|
|
62
|
+
*
|
|
63
|
+
* const mail = new OrbitSignal({
|
|
64
|
+
* transport: new SesTransport({
|
|
65
|
+
* region: 'us-east-1',
|
|
66
|
+
* retries: 3,
|
|
67
|
+
* retryDelay: 1000,
|
|
68
|
+
* retryMultiplier: 2
|
|
69
|
+
* }),
|
|
70
|
+
* from: { name: 'Production App', address: 'noreply@example.com' }
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* **Development Mode with Preview UI**
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const mail = new OrbitSignal({
|
|
78
|
+
* devMode: process.env.NODE_ENV === 'development',
|
|
79
|
+
* devUiPrefix: '/__mail',
|
|
80
|
+
* from: { name: 'Dev App', address: 'dev@localhost' }
|
|
81
|
+
* })
|
|
82
|
+
*
|
|
83
|
+
* // All emails intercepted to memory, view at http://localhost:3000/__mail
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* **Event-Driven Analytics & Error Handling**
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const mail = new OrbitSignal({ ... })
|
|
90
|
+
*
|
|
91
|
+
* // Track successful sends
|
|
92
|
+
* mail.on('afterSend', async (event) => {
|
|
93
|
+
* await analytics.track('email_sent', {
|
|
94
|
+
* to: event.message?.to,
|
|
95
|
+
* subject: event.message?.subject,
|
|
96
|
+
* timestamp: event.timestamp
|
|
97
|
+
* })
|
|
98
|
+
* })
|
|
99
|
+
*
|
|
100
|
+
* // Log failures for monitoring
|
|
101
|
+
* mail.on('sendFailed', async (event) => {
|
|
102
|
+
* logger.error('Email send failed', {
|
|
103
|
+
* error: event.error?.message,
|
|
104
|
+
* mailable: event.mailable.constructor.name
|
|
105
|
+
* })
|
|
106
|
+
* await sentry.captureException(event.error)
|
|
107
|
+
* })
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* **SMTP with Connection Pooling**
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const mail = new OrbitSignal({
|
|
114
|
+
* transport: new SmtpTransport({
|
|
115
|
+
* host: 'smtp.gmail.com',
|
|
116
|
+
* port: 465,
|
|
117
|
+
* secure: true,
|
|
118
|
+
* auth: { user: 'user@gmail.com', pass: 'app-password' },
|
|
119
|
+
* poolSize: 5,
|
|
120
|
+
* maxIdleTime: 30000
|
|
121
|
+
* })
|
|
122
|
+
* })
|
|
123
|
+
*
|
|
124
|
+
* // Graceful shutdown
|
|
125
|
+
* process.on('SIGTERM', async () => {
|
|
126
|
+
* await mail.config.transport?.close?.()
|
|
127
|
+
* })
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* **Usage in Route Handlers**
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // Injected automatically into GravitoContext
|
|
134
|
+
* app.post('/register', async (c) => {
|
|
135
|
+
* const user = await createUser(c.req.json())
|
|
136
|
+
*
|
|
137
|
+
* await c.get('mail').send(new WelcomeEmail(user))
|
|
138
|
+
*
|
|
139
|
+
* return c.json({ success: true })
|
|
140
|
+
* })
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* **Queue Integration for Background Processing**
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // Requires @gravito/stream
|
|
147
|
+
* const email = new WelcomeEmail(user)
|
|
148
|
+
* .onQueue('emails')
|
|
149
|
+
* .delay(60)
|
|
150
|
+
*
|
|
151
|
+
* await email.queue()
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* **Error Handling Best Practices**
|
|
156
|
+
* ```typescript
|
|
157
|
+
* try {
|
|
158
|
+
* await mail.send(new InvoiceEmail(order))
|
|
159
|
+
* } catch (error) {
|
|
160
|
+
* if (error instanceof MailTransportError) {
|
|
161
|
+
* switch (error.code) {
|
|
162
|
+
* case MailErrorCode.RATE_LIMIT:
|
|
163
|
+
* await queue.pushDelayed(email, 300)
|
|
164
|
+
* break
|
|
165
|
+
* case MailErrorCode.RECIPIENT_REJECTED:
|
|
166
|
+
* await markUserEmailInvalid(user.id)
|
|
167
|
+
* break
|
|
168
|
+
* default:
|
|
169
|
+
* throw error
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @see {@link Mailable} Base class for email definitions
|
|
176
|
+
* @see {@link TypedMailable} Strongly-typed mailable with generic data
|
|
177
|
+
* @see {@link Transport} Transport interface
|
|
178
|
+
* @see {@link BaseTransport} Retry-enabled base transport
|
|
179
|
+
* @see {@link MailConfig} Configuration interface
|
|
180
|
+
* @see {@link MailEvent} Event types
|
|
181
|
+
* @see {@link MailTransportError} Error handling
|
|
182
|
+
*
|
|
37
183
|
* @since 3.0.0
|
|
38
184
|
* @public
|
|
39
185
|
*/
|
|
@@ -41,15 +187,26 @@ export class OrbitSignal implements GravitoOrbit {
|
|
|
41
187
|
private config: MailConfig
|
|
42
188
|
private devMailbox?: DevMailbox
|
|
43
189
|
private core?: PlanetCore
|
|
190
|
+
private eventHandlers = new Map<MailEventType, MailEventHandler[]>()
|
|
44
191
|
|
|
45
192
|
constructor(config: MailConfig = {}) {
|
|
46
193
|
this.config = config
|
|
47
194
|
}
|
|
48
195
|
|
|
49
196
|
/**
|
|
50
|
-
* Install the orbit into PlanetCore
|
|
197
|
+
* Install the orbit into PlanetCore.
|
|
198
|
+
*
|
|
199
|
+
* Registers the mail service in the IoC container and sets up development
|
|
200
|
+
* tools if enabled. It also injects the service into the GravitoContext
|
|
201
|
+
* for easy access in route handlers.
|
|
51
202
|
*
|
|
52
|
-
* @param core - The PlanetCore instance
|
|
203
|
+
* @param core - The PlanetCore instance to install into
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const mail = new OrbitSignal(config);
|
|
208
|
+
* mail.install(core);
|
|
209
|
+
* ```
|
|
53
210
|
*/
|
|
54
211
|
install(core: PlanetCore): void {
|
|
55
212
|
this.core = core
|
|
@@ -81,49 +238,155 @@ export class OrbitSignal implements GravitoOrbit {
|
|
|
81
238
|
c.set('mail', this)
|
|
82
239
|
return await next()
|
|
83
240
|
})
|
|
241
|
+
|
|
242
|
+
// 5. Register Webhook Endpoint
|
|
243
|
+
if (this.config.webhookPrefix) {
|
|
244
|
+
// @ts-expect-error: Accessing internal adapter methods
|
|
245
|
+
core.adapter.post(`${this.config.webhookPrefix}/:driver`, async (c: GravitoContext) => {
|
|
246
|
+
const driverName = c.req.param('driver')
|
|
247
|
+
const driver = this.config.webhookDrivers?.[driverName as string]
|
|
248
|
+
|
|
249
|
+
if (!driver) {
|
|
250
|
+
return c.json({ error: `Webhook driver "${driverName}" not found` }, 404)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const results = await driver.handle(c)
|
|
255
|
+
if (results && Array.isArray(results)) {
|
|
256
|
+
for (const result of results) {
|
|
257
|
+
await this.handleWebhook(driverName as string, result.event, result.payload)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return c.json({ success: true })
|
|
261
|
+
} catch (error) {
|
|
262
|
+
core.logger.error(`[OrbitSignal] Webhook error (${driverName}):`, error)
|
|
263
|
+
return c.json({ error: 'Webhook processing failed' }, 500)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
}
|
|
84
267
|
}
|
|
85
268
|
|
|
86
269
|
/**
|
|
87
|
-
*
|
|
270
|
+
* Internal: Handle processed webhook.
|
|
271
|
+
*/
|
|
272
|
+
private async handleWebhook(driver: string, event: string, payload: any): Promise<void> {
|
|
273
|
+
// We don't have a specific mailable for generic webhooks, so we create a dummy or pass null
|
|
274
|
+
// But since MailEvent requires a mailable, we might need to adjust the interface or pass a Proxy
|
|
275
|
+
// For now, let's assume it's acceptable to emit with a partial event if types allow,
|
|
276
|
+
// or we might need to add a specialized emitWebhook method.
|
|
277
|
+
|
|
278
|
+
await this.emit({
|
|
279
|
+
type: 'webhookReceived',
|
|
280
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dummy mailable for webhook events
|
|
281
|
+
mailable: {} as any,
|
|
282
|
+
timestamp: new Date(),
|
|
283
|
+
webhook: { driver, event, payload },
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Send a mailable instance immediately.
|
|
289
|
+
*
|
|
290
|
+
* Orchestrates the full email sending lifecycle: building the envelope,
|
|
291
|
+
* rendering content, emitting events, and delivering via the configured transport.
|
|
292
|
+
*
|
|
293
|
+
* @param mailable - The email definition to send
|
|
294
|
+
* @throws {Error} If mandatory fields (from, to) are missing or transport fails
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* await mail.send(new WelcomeEmail(user));
|
|
299
|
+
* ```
|
|
88
300
|
*/
|
|
89
301
|
async send(mailable: Mailable): Promise<void> {
|
|
90
|
-
|
|
91
|
-
|
|
302
|
+
try {
|
|
303
|
+
// 1. Build envelope and get configuration
|
|
304
|
+
const envelope = await mailable.buildEnvelope(this.config)
|
|
92
305
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
306
|
+
// Validate required fields
|
|
307
|
+
if (!envelope.from) {
|
|
308
|
+
throw new Error('Message is missing "from" address')
|
|
309
|
+
}
|
|
310
|
+
if (!envelope.to || envelope.to.length === 0) {
|
|
311
|
+
throw new Error('Message is missing "to" address')
|
|
312
|
+
}
|
|
100
313
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
from: envelope.from!,
|
|
108
|
-
to: envelope.to!,
|
|
109
|
-
subject: envelope.subject || '(No Subject)',
|
|
110
|
-
priority: envelope.priority || 'normal',
|
|
111
|
-
html: content.html,
|
|
112
|
-
}
|
|
314
|
+
// 2. Emit beforeRender event
|
|
315
|
+
await this.emit({
|
|
316
|
+
type: 'beforeRender',
|
|
317
|
+
mailable,
|
|
318
|
+
timestamp: new Date(),
|
|
319
|
+
})
|
|
113
320
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
321
|
+
// 3. Render content
|
|
322
|
+
const content = await mailable.renderContent()
|
|
323
|
+
|
|
324
|
+
// 4. Emit afterRender event
|
|
325
|
+
await this.emit({
|
|
326
|
+
type: 'afterRender',
|
|
327
|
+
mailable,
|
|
328
|
+
timestamp: new Date(),
|
|
329
|
+
})
|
|
117
330
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
331
|
+
// 5. Construct full message
|
|
332
|
+
const message: Message = {
|
|
333
|
+
...envelope,
|
|
334
|
+
from: envelope.from!,
|
|
335
|
+
to: envelope.to!,
|
|
336
|
+
subject: envelope.subject || '(No Subject)',
|
|
337
|
+
priority: envelope.priority || 'normal',
|
|
338
|
+
html: content.html,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (content.text) {
|
|
342
|
+
message.text = content.text
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 6. Emit beforeSend event
|
|
346
|
+
await this.emit({
|
|
347
|
+
type: 'beforeSend',
|
|
348
|
+
mailable,
|
|
349
|
+
message,
|
|
350
|
+
timestamp: new Date(),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// 7. Send via transport
|
|
354
|
+
if (!this.config.transport) {
|
|
355
|
+
throw new Error('[OrbitSignal] No transport configured. Did you call register the orbit?')
|
|
356
|
+
}
|
|
357
|
+
await this.config.transport.send(message)
|
|
358
|
+
|
|
359
|
+
// 8. Emit afterSend event
|
|
360
|
+
await this.emit({
|
|
361
|
+
type: 'afterSend',
|
|
362
|
+
mailable,
|
|
363
|
+
message,
|
|
364
|
+
timestamp: new Date(),
|
|
365
|
+
})
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// Emit sendFailed event
|
|
368
|
+
await this.emit({
|
|
369
|
+
type: 'sendFailed',
|
|
370
|
+
mailable,
|
|
371
|
+
error: error as Error,
|
|
372
|
+
timestamp: new Date(),
|
|
373
|
+
})
|
|
374
|
+
throw error
|
|
121
375
|
}
|
|
122
|
-
await this.config.transport.send(message)
|
|
123
376
|
}
|
|
124
377
|
|
|
125
378
|
/**
|
|
126
|
-
* Queue a mailable instance
|
|
379
|
+
* Queue a mailable instance for background processing.
|
|
380
|
+
*
|
|
381
|
+
* Attempts to use the 'queue' service (OrbitStream) if available in the
|
|
382
|
+
* container. Falls back to immediate sending if no queue service is found.
|
|
383
|
+
*
|
|
384
|
+
* @param mailable - The email definition to queue
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* await mail.queue(new WelcomeEmail(user));
|
|
389
|
+
* ```
|
|
127
390
|
*/
|
|
128
391
|
async queue(mailable: Mailable): Promise<void> {
|
|
129
392
|
try {
|
|
@@ -140,6 +403,43 @@ export class OrbitSignal implements GravitoOrbit {
|
|
|
140
403
|
// Fallback: 直接發送
|
|
141
404
|
await this.send(mailable)
|
|
142
405
|
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Register an event handler.
|
|
409
|
+
*
|
|
410
|
+
* @description
|
|
411
|
+
* Subscribe to mail lifecycle events for logging, analytics, or custom processing.
|
|
412
|
+
*
|
|
413
|
+
* @param event - The event type to listen for
|
|
414
|
+
* @param handler - The handler function to execute
|
|
415
|
+
* @returns This instance for method chaining
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```typescript
|
|
419
|
+
* mail.on('afterSend', async (event) => {
|
|
420
|
+
* await analytics.track('email_sent', {
|
|
421
|
+
* to: event.message?.to,
|
|
422
|
+
* subject: event.message?.subject
|
|
423
|
+
* })
|
|
424
|
+
* })
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @public
|
|
428
|
+
* @since 3.1.0
|
|
429
|
+
*/
|
|
430
|
+
on(event: MailEventType, handler: MailEventHandler): this {
|
|
431
|
+
const handlers = this.eventHandlers.get(event) || []
|
|
432
|
+
handlers.push(handler)
|
|
433
|
+
this.eventHandlers.set(event, handlers)
|
|
434
|
+
return this
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private async emit(event: MailEvent): Promise<void> {
|
|
438
|
+
const handlers = this.eventHandlers.get(event.type) || []
|
|
439
|
+
for (const handler of handlers) {
|
|
440
|
+
await handler(event)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
143
443
|
}
|
|
144
444
|
|
|
145
445
|
// Module augmentation for GravitoVariables
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Mailable } from './Mailable'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for strongly-typed Mailable messages.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* TypedMailable extends the base Mailable class to provide compile-time type safety
|
|
8
|
+
* for email data props. This ensures that the data passed to templates, React, or Vue
|
|
9
|
+
* components is correctly typed and validated at build time.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam TData - The shape of data required by this mailable's template/component.
|
|
12
|
+
* Must extend Record<string, unknown>.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { TypedMailable } from '@gravito/signal'
|
|
17
|
+
*
|
|
18
|
+
* // Define the data interface
|
|
19
|
+
* interface WelcomeData {
|
|
20
|
+
* name: string
|
|
21
|
+
* email: string
|
|
22
|
+
* activationUrl: string
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* // Create strongly-typed mailable
|
|
26
|
+
* class WelcomeEmail extends TypedMailable<WelcomeData> {
|
|
27
|
+
* protected data: WelcomeData
|
|
28
|
+
*
|
|
29
|
+
* constructor(data: WelcomeData) {
|
|
30
|
+
* super()
|
|
31
|
+
* this.data = data
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* build() {
|
|
35
|
+
* return this
|
|
36
|
+
* .to(this.data.email)
|
|
37
|
+
* .subject('Welcome to Gravito!')
|
|
38
|
+
* .view('emails/welcome', this.data) // Type-safe: compiler ensures WelcomeData matches template
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* // Usage - compiler enforces correct data shape
|
|
43
|
+
* const email = new WelcomeEmail({
|
|
44
|
+
* name: 'Alice',
|
|
45
|
+
* email: 'alice@example.com',
|
|
46
|
+
* activationUrl: 'https://app.com/activate?token=abc123'
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* await mail.send(email)
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* // With React components
|
|
55
|
+
* interface InvoiceData {
|
|
56
|
+
* invoiceNumber: string
|
|
57
|
+
* amount: number
|
|
58
|
+
* dueDate: Date
|
|
59
|
+
* items: Array<{ name: string; price: number }>
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* class InvoiceEmail extends TypedMailable<InvoiceData> {
|
|
63
|
+
* protected data: InvoiceData
|
|
64
|
+
*
|
|
65
|
+
* constructor(data: InvoiceData) {
|
|
66
|
+
* super()
|
|
67
|
+
* this.data = data
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* build() {
|
|
71
|
+
* return this
|
|
72
|
+
* .to('billing@example.com')
|
|
73
|
+
* .subject(`Invoice ${this.data.invoiceNumber}`)
|
|
74
|
+
* .react(InvoiceComponent, this.data) // Type-safe props
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @see {@link Mailable} Base mailable class
|
|
80
|
+
* @see {@link OrbitSignal} Mail service orchestrator
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
* @since 3.0.0
|
|
84
|
+
*/
|
|
85
|
+
export abstract class TypedMailable<TData extends Record<string, unknown>> extends Mailable {
|
|
86
|
+
/**
|
|
87
|
+
* The strongly-typed data for this mailable.
|
|
88
|
+
*
|
|
89
|
+
* This property holds the data that will be passed to the template or component
|
|
90
|
+
* during rendering. By defining it as an abstract property with the generic
|
|
91
|
+
* type TData, we force subclasses to provide a concrete, type-safe implementation.
|
|
92
|
+
*
|
|
93
|
+
* @protected
|
|
94
|
+
*/
|
|
95
|
+
protected abstract data: TData
|
|
96
|
+
}
|