@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +89 -60
  3. package/README.zh-TW.md +140 -9
  4. package/dist/MjmlRenderer-IUH663FT.mjs +8 -0
  5. package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +8 -0
  6. package/dist/ReactRenderer-2JFLRVST.mjs +45 -0
  7. package/dist/{ReactRenderer-L5INVYKT.mjs → ReactRenderer-LYEOSYFS.mjs} +9 -8
  8. package/dist/ReactRenderer-V54CUUEI.mjs +45 -0
  9. package/dist/VueMjmlRenderer-4F4CXHDB.mjs +8 -0
  10. package/dist/VueMjmlRenderer-5WZR4CQG.mjs +8 -0
  11. package/dist/VueMjmlRenderer-U5YMWI44.mjs +8 -0
  12. package/dist/VueRenderer-3YBRQXME.mjs +48 -0
  13. package/dist/VueRenderer-46JGXTJ2.mjs +48 -0
  14. package/dist/VueRenderer-5KWD4R3C.mjs +48 -0
  15. package/dist/VueRenderer-C23U4O5E.mjs +48 -0
  16. package/dist/VueRenderer-LEVDFLHP.mjs +31 -0
  17. package/dist/VueRenderer-RNHSCCRI.mjs +48 -0
  18. package/dist/chunk-3WOR3XSL.mjs +82 -0
  19. package/dist/chunk-DBFIVHHG.mjs +79 -0
  20. package/dist/{chunk-6DZX6EAA.mjs → chunk-HEBXNMVQ.mjs} +12 -1
  21. package/dist/chunk-KB7IDDBT.mjs +82 -0
  22. package/dist/chunk-LZL5UUPC.mjs +82 -0
  23. package/dist/chunk-W6LXIJKK.mjs +57 -0
  24. package/dist/chunk-XBIVBJS2.mjs +8 -0
  25. package/dist/index.d.mts +1680 -209
  26. package/dist/index.d.ts +1680 -209
  27. package/dist/index.js +69405 -542
  28. package/dist/index.mjs +993 -110
  29. package/dist/lib-HJTRWKU5.mjs +67788 -0
  30. package/dist/{VueRenderer-Z5PRVBNH.mjs → server-renderer-4IM3P5XZ.mjs} +308 -423
  31. package/dist/server-renderer-7KWFSTPV.mjs +37193 -0
  32. package/dist/{VueRenderer-S65ZARRI.mjs → server-renderer-S5FPSTJ2.mjs} +931 -877
  33. package/dist/server-renderer-X5LUFVWT.mjs +37193 -0
  34. package/doc/OPTIMIZATION_PLAN.md +496 -0
  35. package/package.json +14 -12
  36. package/scripts/check-coverage.ts +64 -0
  37. package/src/Mailable.ts +340 -44
  38. package/src/OrbitSignal.ts +350 -50
  39. package/src/TypedMailable.ts +96 -0
  40. package/src/dev/DevMailbox.ts +89 -33
  41. package/src/dev/DevServer.ts +14 -14
  42. package/src/dev/storage/FileMailboxStorage.ts +66 -0
  43. package/src/dev/storage/MailboxStorage.ts +15 -0
  44. package/src/dev/storage/MemoryMailboxStorage.ts +36 -0
  45. package/src/dev/ui/mailbox.ts +1 -1
  46. package/src/dev/ui/preview.ts +4 -4
  47. package/src/errors.ts +69 -0
  48. package/src/events.ts +72 -0
  49. package/src/index.ts +20 -1
  50. package/src/renderers/HtmlRenderer.ts +20 -18
  51. package/src/renderers/MjmlRenderer.ts +73 -0
  52. package/src/renderers/ReactMjmlRenderer.ts +94 -0
  53. package/src/renderers/ReactRenderer.ts +26 -21
  54. package/src/renderers/Renderer.ts +43 -3
  55. package/src/renderers/TemplateRenderer.ts +48 -15
  56. package/src/renderers/VueMjmlRenderer.ts +99 -0
  57. package/src/renderers/VueRenderer.ts +26 -21
  58. package/src/renderers/mjml-templates.ts +50 -0
  59. package/src/transports/BaseTransport.ts +148 -0
  60. package/src/transports/LogTransport.ts +28 -6
  61. package/src/transports/MemoryTransport.ts +34 -6
  62. package/src/transports/SesTransport.ts +62 -17
  63. package/src/transports/SmtpTransport.ts +123 -27
  64. package/src/transports/Transport.ts +33 -4
  65. package/src/types.ts +172 -3
  66. package/src/utils/html.ts +43 -0
  67. package/src/webhooks/SendGridWebhookDriver.ts +80 -0
  68. package/src/webhooks/SesWebhookDriver.ts +44 -0
  69. package/tests/DevMailbox.test.ts +54 -0
  70. package/tests/FileMailboxStorage.test.ts +56 -0
  71. package/tests/MjmlLayout.test.ts +28 -0
  72. package/tests/MjmlRenderer.test.ts +53 -0
  73. package/tests/OrbitSignalWebhook.test.ts +56 -0
  74. package/tests/ReactMjmlRenderer.test.ts +33 -0
  75. package/tests/SendGridWebhookDriver.test.ts +69 -0
  76. package/tests/SesWebhookDriver.test.ts +46 -0
  77. package/tests/VueMjmlRenderer.test.ts +35 -0
  78. package/tests/dev-server.test.ts +1 -1
  79. package/tests/transports.test.ts +3 -3
  80. package/tsconfig.json +12 -24
  81. package/dist/OrbitMail-2Z7ZTKYA.mjs +0 -7
  82. package/dist/OrbitMail-BGV32HWN.mjs +0 -7
  83. package/dist/OrbitMail-FUYZQSAV.mjs +0 -7
  84. package/dist/OrbitMail-NAPCRK7B.mjs +0 -7
  85. package/dist/OrbitMail-REGJ276B.mjs +0 -7
  86. package/dist/OrbitMail-TCFBJWDT.mjs +0 -7
  87. package/dist/OrbitMail-XZZW6U4N.mjs +0 -7
  88. package/dist/OrbitSignal-IPSA2CDO.mjs +0 -7
  89. package/dist/OrbitSignal-MABW4DDW.mjs +0 -7
  90. package/dist/OrbitSignal-QSW5VQ5M.mjs +0 -7
  91. package/dist/OrbitSignal-R22QHWAA.mjs +0 -7
  92. package/dist/OrbitSignal-ZKKMEC27.mjs +0 -7
  93. package/dist/chunk-3U2CYJO5.mjs +0 -367
  94. package/dist/chunk-3XFC4T6M.mjs +0 -392
  95. package/dist/chunk-456QRYFW.mjs +0 -401
  96. package/dist/chunk-DT3R2TNV.mjs +0 -367
  97. package/dist/chunk-F6MVTUCT.mjs +0 -421
  98. package/dist/chunk-GADWIVC4.mjs +0 -400
  99. package/dist/chunk-HHKFAMSE.mjs +0 -380
  100. package/dist/chunk-NEQCQSZI.mjs +0 -406
  101. package/dist/chunk-OKRNL6PN.mjs +0 -400
  102. package/dist/chunk-ULN3GMY2.mjs +0 -367
  103. package/dist/chunk-XAWO7RSP.mjs +0 -398
  104. package/dist/chunk-YLVDJSED.mjs +0 -431
@@ -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
- * Provides email sending capabilities with support for multiple transports,
13
- * development mode with email preview UI, and queue integration.
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
- * import { SmtpTransport } from '@gravito/signal'
19
- *
20
- * const app = new Application({
21
- * orbits: [
22
- * new OrbitSignal({
23
- * transport: new SmtpTransport({
24
- * host: 'smtp.example.com',
25
- * port: 587,
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
- * // In route handler
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
- * Send a mailable instance
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
- // 1. Build envelope and get configuration
91
- const envelope = await mailable.buildEnvelope(this.config)
302
+ try {
303
+ // 1. Build envelope and get configuration
304
+ const envelope = await mailable.buildEnvelope(this.config)
92
305
 
93
- // Validate required fields
94
- if (!envelope.from) {
95
- throw new Error('Message is missing "from" address')
96
- }
97
- if (!envelope.to || envelope.to.length === 0) {
98
- throw new Error('Message is missing "to" address')
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
- // 2. Render content
102
- const content = await mailable.renderContent()
103
-
104
- // 3. Construct full message
105
- const message: Message = {
106
- ...envelope,
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
- if (content.text) {
115
- message.text = content.text
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
- // 4. Send via transport
119
- if (!this.config.transport) {
120
- throw new Error('[OrbitSignal] No transport configured. Did you call register the orbit?')
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
+ }