@fluojs/email 1.0.1 → 2.0.0

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.ko.md CHANGED
@@ -10,9 +10,11 @@ fluo를 위한 transport-agnostic 이메일 코어 패키지입니다. Nest-like
10
10
  - [사용 시점](#사용-시점)
11
11
  - [빠른 시작](#빠른-시작)
12
12
  - [일반적인 패턴](#일반적인-패턴)
13
+ - [등록 범위와 async factory](#등록-범위와-async-factory)
13
14
  - [`@fluojs/email/node`를 이용한 Node 전용 SMTP](#fluojs-email-node를-이용한-node-전용-smtp)
14
15
  - [`EmailService`를 이용한 standalone 전달](#emailservice를-이용한-standalone-전달)
15
16
  - [`@fluojs/notifications`와의 통합](#fluojs-notifications와의-통합)
17
+ - [Template renderer 설정](#template-renderer-설정)
16
18
  - [큐 기반 대량 전달](#큐-기반-대량-전달)
17
19
  - [의도적인 제한 사항](#의도적인-제한-사항)
18
20
  - [공개 API 개요](#공개-api-개요)
@@ -104,6 +106,29 @@ export class WelcomeService {
104
106
 
105
107
  ## 일반적인 패턴
106
108
 
109
+ ### 등록 범위와 async factory
110
+
111
+ `EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global module을 반환합니다. 한 번 import하면 export된 `EmailService`, `EmailChannel`, `EMAIL`, `EMAIL_CHANNEL` provider가 애플리케이션 module graph에 표시됩니다. 이메일 provider를 반환된 module을 명시적으로 import한 module에만 보이게 해야 할 때만 `global: false`를 전달합니다.
112
+
113
+ Async 등록은 의도적으로 fluo의 명시적 factory 형태를 사용합니다:
114
+
115
+ ```typescript
116
+ EmailModule.forRootAsync({
117
+ global: false,
118
+ inject: [ConfigService],
119
+ useFactory: (config) => ({
120
+ defaultFrom: config.mail.from,
121
+ transport: {
122
+ kind: config.mail.transportKind,
123
+ create: () => config.mail.transport,
124
+ ownsResources: false,
125
+ },
126
+ }),
127
+ });
128
+ ```
129
+
130
+ `global`은 factory result가 아니라 `forRootAsync(...)` options object의 최상위에 둡니다. 지원되는 async 등록 형태는 `inject`와 `useFactory`뿐입니다. NestJS dynamic-module 형태인 `imports`, `useClass`, `useExisting`는 `@fluojs/email` 계약에 포함되지 않습니다. 필요한 의존성은 주변 애플리케이션 module graph에 먼저 등록한 뒤, factory가 필요로 하는 token을 `inject`에 나열하세요.
131
+
107
132
  ### `@fluojs/email/node`를 이용한 Node 전용 SMTP
108
133
 
109
134
  런타임 이식 가능한 루트 패키지 계약을 약화시키지 않으면서 1st-party Nodemailer/SMTP 전달이 필요하다면 전용 Node 서브패스를 사용합니다.
@@ -171,9 +196,11 @@ Behavioral contract 메모:
171
196
  - `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
172
197
  - 서비스는 모듈 bootstrap 시 transport를 초기화하며, `verifyOnModuleInit: true`인 경우 bootstrap 검증이 성공적으로 끝날 때까지 delivery가 transport handoff로 진행되지 않습니다.
173
198
  - 거부된 `forRootAsync(...)` 옵션 factory 결과는 영구 memoize되지 않으며, 다음 provider resolution에서 configuration lookup을 다시 시도할 수 있습니다.
174
- - shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다린 뒤 닫습니다.
199
+ - shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다리고, 활성 transport `verify()` / `send()` 호출을 drain한 소유 transport를 닫습니다. 동시에 또는 반복해서 shutdown을 호출하면 동일한 완료 Promise를 공유하므로 소유 transport는 최대 한 번만 닫힙니다.
175
200
  - transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
176
201
  - 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
202
+ - `EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global입니다. module-local visibility가 필요할 때만 `global: false`를 사용합니다.
203
+ - `EmailModule.forRootAsync(...)`는 `inject`와 `useFactory`만 지원합니다. NestJS `imports`, `useClass`, `useExisting` 등록 형태는 factory 호출 전에 애플리케이션 module boundary에서 해석해야 합니다.
177
204
  - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
178
205
 
179
206
  ### `@fluojs/notifications`와의 통합
@@ -209,15 +236,99 @@ export class AppModule {}
209
236
  지원하는 notification payload 필드:
210
237
 
211
238
  - `to`, `cc`, `bcc`, `from`, `replyTo`
212
- - `text`, `html`, `attachments`, `headers`
239
+ - `text`, `html`, `attachments`, `headers`, `metadata`
213
240
  - 모듈에 renderer가 구성된 경우 `templateData`
214
241
 
215
242
  Behavioral contract 메모:
216
243
 
217
- - `EmailChannel`은 `pending` 또는 `rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
244
+ - `EmailChannel`은 수락된 수신자가 0명인 경우(`accepted.length === 0`) 또는 `pending`/`rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
218
245
  - `EmailService.sendNotification(...)`은 렌더링된 template output을 payload 및 notification metadata와 병합합니다. payload 필드는 notification fallback보다 우선합니다.
219
246
  - Template rendering에는 notification `payload`, `metadata`, `locale`, `subject`, `template`이 전달되며, payload `text`, `html`과 notification `subject`가 렌더링된 fallback보다 우선합니다.
220
247
 
248
+ ### Template renderer 설정
249
+
250
+ `EmailModule`을 등록할 때 필수 transport와 함께 `EmailTemplateRenderer`를 전달합니다. 아래의 완전한 Node.js 예제는 1st-party SMTP factory를 사용합니다. 다른 런타임에서는 renderer를 그대로 두고 `transport`만 애플리케이션이 소유한 `EmailTransport` 또는 `EmailTransportFactory`로 교체하세요.
251
+
252
+ ```typescript
253
+ import { Module } from '@fluojs/core';
254
+ import { EmailModule, type EmailTemplateRenderer } from '@fluojs/email';
255
+ import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
256
+
257
+ const renderer: EmailTemplateRenderer = {
258
+ render({ payload, template }) {
259
+ const name = payload.templateData?.name;
260
+ const displayName = typeof name === 'string' ? name : 'customer';
261
+
262
+ return {
263
+ html: '<h1>Welcome</h1>',
264
+ subject: template === 'welcome' ? `Welcome, ${displayName}` : template,
265
+ text: `Hello, ${displayName}`,
266
+ };
267
+ },
268
+ };
269
+
270
+ @Module({
271
+ imports: [
272
+ EmailModule.forRoot({
273
+ defaultFrom: 'noreply@example.com',
274
+ renderer,
275
+ transport: createNodemailerEmailTransportFactory({
276
+ smtp: {
277
+ auth: {
278
+ pass: 'smtp-password',
279
+ user: 'smtp-user',
280
+ },
281
+ host: 'smtp.example.com',
282
+ port: 587,
283
+ secure: false,
284
+ },
285
+ }),
286
+ verifyOnModuleInit: true,
287
+ }),
288
+ ],
289
+ })
290
+ export class AppModule {}
291
+ ```
292
+
293
+ `EmailService.sendNotification(...)`을 호출할 때 template key를 전달하고 renderer 전용 값은 `payload.templateData` 아래에 둡니다. 위에서 설명한 대로 `EMAIL_CHANNEL`을 등록한 뒤에는 같은 request shape를 `NotificationsService.dispatch(...)`에서도 사용할 수 있습니다.
294
+
295
+ ```typescript
296
+ import { Inject } from '@fluojs/core';
297
+ import { EmailService } from '@fluojs/email';
298
+
299
+ @Inject(EmailService)
300
+ export class WelcomeEmailService {
301
+ constructor(private readonly email: EmailService) {}
302
+
303
+ async sendRenderedWelcome(address: string, name: string) {
304
+ await this.email.sendNotification({
305
+ channel: 'email',
306
+ recipients: [address],
307
+ template: 'welcome',
308
+ payload: {
309
+ templateData: { name },
310
+ },
311
+ });
312
+ }
313
+
314
+ async sendWelcomeWithOverrides(address: string, name: string) {
315
+ await this.email.sendNotification({
316
+ channel: 'email',
317
+ recipients: [address],
318
+ subject: 'Your account is ready',
319
+ template: 'welcome',
320
+ payload: {
321
+ html: '<p>Your account is ready.</p>',
322
+ templateData: { name },
323
+ text: 'Use this exact welcome message.',
324
+ },
325
+ });
326
+ }
327
+ }
328
+ ```
329
+
330
+ `template`과 `renderer`가 모두 있을 때만 renderer가 실행됩니다. Renderer가 반환하는 `subject`, `text`, `html`은 fallback입니다. 명시적인 notification `subject`는 렌더링된 subject보다 우선하고, 명시적인 `payload.text`와 `payload.html`은 렌더링된 body보다 우선합니다. `payload.to`도 notification `recipients` fallback보다 우선합니다. `templateData`는 opaque payload 안에 그대로 남아 renderer에서 `payload.templateData`로 사용할 수 있으며, email 패키지는 내부 key를 해석하지 않습니다.
331
+
221
332
  ### 큐 기반 대량 전달
222
333
 
223
334
  `@fluojs/notifications`가 대량 이메일 전달을 백그라운드로 넘겨야 한다면 `QueueModule`을 import하고, `QueueLifecycleService`를 주입해 `createEmailNotificationsQueueAdapter(queue)`를 만든 뒤, `EmailNotificationsQueueWorker`를 애플리케이션 provider로 등록합니다. 루트 `EmailModule`은 worker를 자동 등록하지 않으므로 `@fluojs/email/queue`를 import하지 않는 애플리케이션은 런타임에서 `@fluojs/queue`를 필요로 하지 않습니다.
@@ -273,6 +384,7 @@ Behavioral contract 메모:
273
384
 
274
385
  - Queue 지원은 opt-in입니다. 루트 `@fluojs/email` 엔트리포인트와 `EmailModule`은 `@fluojs/queue`를 import하거나 `EmailNotificationsQueueWorker`를 등록하거나 queue peer 설치를 요구하지 않습니다.
275
386
  - `EmailNotificationsQueueWorker`는 `@fluojs/email/queue`에서 export되며, queue 기반 전달을 활성화하는 애플리케이션이 직접 등록해야 합니다.
387
+ - worker는 transport handoff 전에 queued notification channel이 구성된 `EmailChannel.channel`과 정확히 일치하는지 확인합니다. 일치하지 않으면 `EmailMessageValidationError`로 실패하므로 non-email 작업이 email transport에 도달하지 않습니다.
276
388
  - worker는 `EmailChannel` 전달 semantics를 재사용하므로 transport가 수락된 수신자 0명 또는 `pending`/`rejected` 수신자를 보고하면 queued job이 실패합니다. 따라서 incomplete delivery는 성공한 job으로 승인되지 않고 `@fluojs/queue`의 retry/dead-letter 흐름으로 넘어갑니다.
277
389
 
278
390
  ### 의도적인 제한 사항
@@ -301,9 +413,10 @@ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
301
413
 
302
414
  ### 계약과 헬퍼
303
415
 
304
- - `Email`: `address`와 선택적 display `name`을 포함하는 정규화된 이메일 주소 값입니다.
416
+ - `Email`: `EMAIL` 호환성 토큰이 노출하는 애플리케이션용 전송 facade이며 address 값이 아닙니다. `EmailService`가 뒷받침하는 `send(...)`, `sendMany(...)`, `sendNotification(...)` 메서드를 제공합니다.
305
417
  - `EmailAddress` / `EmailAddressLike`: `EmailService`가 정규화하기 전에 허용하는 구조화 또는 축약 recipient 값입니다.
306
- - `EmailModuleOptions` / `EmailAsyncModuleOptions`: sender 기본값, renderer, lifecycle 검증, transport factory wiring을 포함하는 동기/비동기 모듈 등록 계약입니다.
418
+ - `EmailAttachment`: `EmailMessage.attachments`에서 허용되고 설정된 transport로 전달되는 file attachment payload입니다. `filename`, `content`, 선택적 `contentType` 필드를 포함합니다.
419
+ - `EmailModuleOptions` / `EmailAsyncModuleOptions`: sender 기본값, renderer, lifecycle 검증, transport factory wiring, 최상위 `global` visibility control, async `inject` + `useFactory` 형태를 포함하는 동기/비동기 모듈 등록 계약입니다.
307
420
  - `EmailMessage`
308
421
  - `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: `EmailChannel`이 소비하는 notification channel payload 계약입니다.
309
422
  - `EmailSendOptions` / `EmailSendManyOptions`: abort signal과 batch failure 수집 같은 per-send 제어 옵션입니다.
package/README.md CHANGED
@@ -10,9 +10,11 @@ Transport-agnostic email delivery core for fluo. It provides a Nest-like module
10
10
  - [When to Use](#when-to-use)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Common Patterns](#common-patterns)
13
+ - [Registration scope and async factories](#registration-scope-and-async-factories)
13
14
  - [Node-only SMTP with `@fluojs/email/node`](#node-only-smtp-with-fluojs-email-node)
14
15
  - [Standalone delivery with `EmailService`](#standalone-delivery-with-emailservice)
15
16
  - [Integration with `@fluojs/notifications`](#integration-with-fluojs-notifications)
17
+ - [Template renderer setup](#template-renderer-setup)
16
18
  - [Queue-backed bulk delivery](#queue-backed-bulk-delivery)
17
19
  - [Intentional limitations](#intentional-limitations)
18
20
  - [Public API Overview](#public-api-overview)
@@ -104,6 +106,29 @@ The root `@fluojs/email` surface is intentionally module-first. Register email d
104
106
 
105
107
  ## Common Patterns
106
108
 
109
+ ### Registration scope and async factories
110
+
111
+ `EmailModule.forRoot(...)` and `EmailModule.forRootAsync(...)` return a global module by default. After one import, the exported `EmailService`, `EmailChannel`, `EMAIL`, and `EMAIL_CHANNEL` providers are visible to the application module graph. Pass `global: false` only when email providers should stay visible to modules that explicitly import the returned module.
112
+
113
+ Async registration intentionally uses fluo's explicit factory shape:
114
+
115
+ ```typescript
116
+ EmailModule.forRootAsync({
117
+ global: false,
118
+ inject: [ConfigService],
119
+ useFactory: (config) => ({
120
+ defaultFrom: config.mail.from,
121
+ transport: {
122
+ kind: config.mail.transportKind,
123
+ create: () => config.mail.transport,
124
+ ownsResources: false,
125
+ },
126
+ }),
127
+ });
128
+ ```
129
+
130
+ `global` belongs on the top-level `forRootAsync(...)` options object, not in the factory result. The supported async registration shape is `inject` plus `useFactory`; NestJS dynamic-module forms such as `imports`, `useClass`, and `useExisting` are not part of the `@fluojs/email` contract. Register dependencies in the surrounding application module graph first, then list the tokens the factory needs in `inject`.
131
+
107
132
  ### Node-only SMTP with `@fluojs/email/node`
108
133
 
109
134
  Use the dedicated Node subpath when you want first-party Nodemailer/SMTP delivery without weakening the runtime-portable root package contract.
@@ -171,9 +196,11 @@ Behavioral contract notes:
171
196
  - `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
172
197
  - The service initializes the configured transport during module bootstrap and, when `verifyOnModuleInit: true`, delivery waits until bootstrap verification has completed successfully before transport handoff.
173
198
  - Rejected `forRootAsync(...)` option factories are not memoized permanently; the next provider resolution can retry configuration lookup.
174
- - Once shutdown starts, `EmailService.send(...)` and `EmailService.sendNotification(...)` fail with `EmailLifecycleError` instead of reusing or lazily creating transports; any in-flight factory-owned transport creation is awaited and closed by shutdown.
199
+ - Once shutdown starts, `EmailService.send(...)` and `EmailService.sendNotification(...)` fail with `EmailLifecycleError` instead of reusing or lazily creating transports; any in-flight factory-owned transport creation is awaited, active transport `verify()` / `send()` calls are drained, and then owned transports are closed by shutdown. Concurrent and repeated shutdown calls share the same completion promise, so an owned transport is closed at most once.
175
200
  - Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
176
201
  - Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
202
+ - `EmailModule.forRoot(...)` and `EmailModule.forRootAsync(...)` are global by default. Use `global: false` to opt into module-local visibility.
203
+ - `EmailModule.forRootAsync(...)` supports `inject` plus `useFactory` only; NestJS `imports`, `useClass`, and `useExisting` registration shapes must be resolved at the application module boundary before calling the factory.
177
204
  - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
178
205
 
179
206
  ### Integration with `@fluojs/notifications`
@@ -209,15 +236,99 @@ export class AppModule {}
209
236
  Supported notification payload fields:
210
237
 
211
238
  - `to`, `cc`, `bcc`, `from`, `replyTo`
212
- - `text`, `html`, `attachments`, `headers`
239
+ - `text`, `html`, `attachments`, `headers`, `metadata`
213
240
  - `templateData` when a renderer is configured on the module
214
241
 
215
242
  Behavioral contract notes:
216
243
 
217
- - `EmailChannel` treats any `pending` or `rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful.
244
+ - `EmailChannel` treats zero accepted recipients (`accepted.length === 0`) or any `pending`/`rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful.
218
245
  - `EmailService.sendNotification(...)` merges rendered template output with payload and notification metadata; payload fields override notification fallbacks.
219
246
  - Template rendering receives notification `payload`, `metadata`, `locale`, `subject`, and `template`; payload `text`, `html`, and notification `subject` override rendered fallbacks.
220
247
 
248
+ ### Template renderer setup
249
+
250
+ Pass an `EmailTemplateRenderer` together with the required transport when registering `EmailModule`. This complete Node.js example uses the first-party SMTP factory; on another runtime, keep the renderer and replace only `transport` with an application-owned `EmailTransport` or `EmailTransportFactory`.
251
+
252
+ ```typescript
253
+ import { Module } from '@fluojs/core';
254
+ import { EmailModule, type EmailTemplateRenderer } from '@fluojs/email';
255
+ import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
256
+
257
+ const renderer: EmailTemplateRenderer = {
258
+ render({ payload, template }) {
259
+ const name = payload.templateData?.name;
260
+ const displayName = typeof name === 'string' ? name : 'customer';
261
+
262
+ return {
263
+ html: '<h1>Welcome</h1>',
264
+ subject: template === 'welcome' ? `Welcome, ${displayName}` : template,
265
+ text: `Hello, ${displayName}`,
266
+ };
267
+ },
268
+ };
269
+
270
+ @Module({
271
+ imports: [
272
+ EmailModule.forRoot({
273
+ defaultFrom: 'noreply@example.com',
274
+ renderer,
275
+ transport: createNodemailerEmailTransportFactory({
276
+ smtp: {
277
+ auth: {
278
+ pass: 'smtp-password',
279
+ user: 'smtp-user',
280
+ },
281
+ host: 'smtp.example.com',
282
+ port: 587,
283
+ secure: false,
284
+ },
285
+ }),
286
+ verifyOnModuleInit: true,
287
+ }),
288
+ ],
289
+ })
290
+ export class AppModule {}
291
+ ```
292
+
293
+ Call `EmailService.sendNotification(...)` with a template key and put renderer-specific values under `payload.templateData`. The same request shape works through `NotificationsService.dispatch(...)` after registering `EMAIL_CHANNEL` as shown above.
294
+
295
+ ```typescript
296
+ import { Inject } from '@fluojs/core';
297
+ import { EmailService } from '@fluojs/email';
298
+
299
+ @Inject(EmailService)
300
+ export class WelcomeEmailService {
301
+ constructor(private readonly email: EmailService) {}
302
+
303
+ async sendRenderedWelcome(address: string, name: string) {
304
+ await this.email.sendNotification({
305
+ channel: 'email',
306
+ recipients: [address],
307
+ template: 'welcome',
308
+ payload: {
309
+ templateData: { name },
310
+ },
311
+ });
312
+ }
313
+
314
+ async sendWelcomeWithOverrides(address: string, name: string) {
315
+ await this.email.sendNotification({
316
+ channel: 'email',
317
+ recipients: [address],
318
+ subject: 'Your account is ready',
319
+ template: 'welcome',
320
+ payload: {
321
+ html: '<p>Your account is ready.</p>',
322
+ templateData: { name },
323
+ text: 'Use this exact welcome message.',
324
+ },
325
+ });
326
+ }
327
+ }
328
+ ```
329
+
330
+ The renderer runs only when both `template` and `renderer` are present. Its `subject`, `text`, and `html` are fallbacks: an explicit notification `subject` overrides the rendered subject, while explicit `payload.text` and `payload.html` override rendered bodies. `payload.to` also overrides the notification `recipients` fallback. `templateData` remains inside the opaque payload and is available to the renderer as `payload.templateData`; the email package does not interpret its keys.
331
+
221
332
  ### Queue-backed bulk delivery
222
333
 
223
334
  When `@fluojs/notifications` should offload bulk email delivery to the background, import `QueueModule`, inject `QueueLifecycleService`, call `createEmailNotificationsQueueAdapter(queue)`, and register `EmailNotificationsQueueWorker` as an application provider. The root `EmailModule` does not register the worker automatically, so applications that never import `@fluojs/email/queue` do not need `@fluojs/queue` at runtime.
@@ -273,6 +384,7 @@ Behavioral contract notes:
273
384
 
274
385
  - Queue support is opt-in. The root `@fluojs/email` entrypoint and `EmailModule` do not import `@fluojs/queue`, register `EmailNotificationsQueueWorker`, or require queue peer installation.
275
386
  - `EmailNotificationsQueueWorker` is exported from `@fluojs/email/queue` and must be registered by applications that enable queue-backed delivery.
387
+ - Before transport handoff, the worker requires the queued notification channel to exactly match the configured `EmailChannel.channel`. A mismatch fails with `EmailMessageValidationError`, so non-email work cannot reach the email transport.
276
388
  - The worker reuses `EmailChannel` delivery semantics, so a queued job fails when the underlying transport reports zero accepted recipients or any `pending`/`rejected` recipients. This lets `@fluojs/queue` retry and dead-letter incomplete deliveries instead of acknowledging them as successful jobs.
277
389
 
278
390
  ### Intentional limitations
@@ -301,9 +413,10 @@ These limitations are part of the package contract so transport selection, templ
301
413
 
302
414
  ### Contracts and helpers
303
415
 
304
- - `Email`: Normalized email address value with an `address` and optional display `name`.
416
+ - `Email`: Application-facing sending facade exposed by the `EMAIL` compatibility token, not an address value; it provides `send(...)`, `sendMany(...)`, and `sendNotification(...)` methods backed by `EmailService`.
305
417
  - `EmailAddress` / `EmailAddressLike`: Structured or shorthand recipient values accepted by `EmailService` before normalization.
306
- - `EmailModuleOptions` / `EmailAsyncModuleOptions`: Synchronous and async module registration contracts, including sender defaults, renderer, lifecycle verification, and transport factory wiring.
418
+ - `EmailAttachment`: File attachment payload accepted on `EmailMessage.attachments` and forwarded to the configured transport with `filename`, `content`, and optional `contentType` fields.
419
+ - `EmailModuleOptions` / `EmailAsyncModuleOptions`: Synchronous and async module registration contracts, including sender defaults, renderer, lifecycle verification, transport factory wiring, top-level `global` visibility control, and the async `inject` + `useFactory` shape.
307
420
  - `EmailMessage`
308
421
  - `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: Notification channel payload contracts consumed by `EmailChannel`.
309
422
  - `EmailSendOptions` / `EmailSendManyOptions`: Per-send controls such as abort signals and batch failure collection.
package/dist/queue.d.ts CHANGED
@@ -46,6 +46,7 @@ export declare class EmailNotificationsQueueWorker {
46
46
  *
47
47
  * @param job Queued notification payload created by `createEmailNotificationsQueueAdapter(...)`.
48
48
  * @returns A promise that resolves only when email delivery is accepted by the channel contract.
49
+ * @throws {EmailMessageValidationError} When the queued notification targets another configured channel.
49
50
  */
50
51
  handle(job: EmailNotificationQueueJob): Promise<void>;
51
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAyB,MAAM,uBAAuB,CAAC;AAC9F,OAAO,EAAe,KAAK,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGjG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAEzD,iFAAiF;AACjF,qBAAa,yBAAyB;aAQlB,YAAY,EAAE,gCAAgC;aAC9C,QAAQ,EAAE,MAAM;IARlC;;;;;OAKG;gBAEe,YAAY,EAAE,gCAAgC,EAC9C,QAAQ,EAAE,MAAM;CAEnC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,qBAAqB,GAAG,yBAAyB,CAW5G;AAED,oFAAoF;AACpF,qBAEa,6BAA6B;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY;IAElD;;;;;OAKG;IACG,MAAM,CAAC,GAAG,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;CAG5D"}
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAyB,MAAM,uBAAuB,CAAC;AAC9F,OAAO,EAAe,KAAK,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGjG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,CAAC;AAEzD,iFAAiF;AACjF,qBAAa,yBAAyB;aAQlB,YAAY,EAAE,gCAAgC;aAC9C,QAAQ,EAAE,MAAM;IARlC;;;;;OAKG;gBAEe,YAAY,EAAE,gCAAgC,EAC9C,QAAQ,EAAE,MAAM;CAEnC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,qBAAqB,GAAG,yBAAyB,CAS5G;AAED,oFAAoF;AACpF,qBAEa,6BAA6B;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY;IAElD;;;;;;OAMG;IACG,MAAM,CAAC,GAAG,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;CAW5D"}
package/dist/queue.js CHANGED
@@ -8,6 +8,7 @@ import { Inject } from '@fluojs/core';
8
8
  import { QueueWorker } from '@fluojs/queue';
9
9
  import { DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS } from './constants.js';
10
10
  import { EmailChannel } from './channel.js';
11
+ import { EmailMessageValidationError } from './errors.js';
11
12
 
12
13
  /** Queue worker execution defaults used by the built-in notifications queue integration. */
13
14
 
@@ -71,8 +72,13 @@ class EmailNotificationsQueueWorker {
71
72
  *
72
73
  * @param job Queued notification payload created by `createEmailNotificationsQueueAdapter(...)`.
73
74
  * @returns A promise that resolves only when email delivery is accepted by the channel contract.
75
+ * @throws {EmailMessageValidationError} When the queued notification targets another configured channel.
74
76
  */
75
77
  async handle(job) {
78
+ const expectedChannel = this.channel.channel;
79
+ if (job.notification.channel !== expectedChannel) {
80
+ throw new EmailMessageValidationError(`Queued notification channel "${job.notification.channel}" does not match configured email channel "${expectedChannel}".`);
81
+ }
76
82
  await this.channel.send(job.notification, {});
77
83
  }
78
84
  static {
package/dist/service.d.ts CHANGED
@@ -12,10 +12,13 @@ export declare class EmailService implements Email, OnModuleInit, OnApplicationS
12
12
  private readonly options;
13
13
  private lifecycleState;
14
14
  private bootstrapPromise;
15
+ private shutdownPromise;
16
+ private readonly inFlightOperations;
15
17
  private resolvedTransport;
16
18
  private transportPromise;
17
19
  constructor(options: NormalizedEmailModuleOptions);
18
20
  onApplicationShutdown(): Promise<void>;
21
+ private shutdown;
19
22
  onModuleInit(): Promise<void>;
20
23
  private startTransport;
21
24
  /**
@@ -78,6 +81,9 @@ export declare class EmailService implements Email, OnModuleInit, OnApplicationS
78
81
  sendNotification(notification: EmailNotificationDispatchRequest, options?: EmailSendOptions): Promise<EmailSendResult>;
79
82
  private ensureTransport;
80
83
  private clearResolvedTransport;
84
+ private handleTransportInitializationFailure;
85
+ private drainInFlightOperations;
86
+ private trackInFlightOperation;
81
87
  private ensureReadyForDelivery;
82
88
  private getLifecycleState;
83
89
  private assertCanCreateOrUseTransport;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAM3E,OAAO,KAAK,EACV,KAAK,EAGL,YAAY,EACZ,gCAAgC,EAChC,oBAAoB,EAEpB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAKf,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AA4EpB;;;;;;;GAOG;AACH,qBACa,YAAa,YAAW,KAAK,EAAE,YAAY,EAAE,qBAAqB;IAMjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAE5D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBtC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBrB,cAAc;IA+C5B;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6B3F;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA6BpH;;;;;;;;;;;;;;;;;OAiBG;IACG,gBAAgB,CACpB,YAAY,EAAE,gCAAgC,EAC9C,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC;YA+Bb,eAAe;IAiB7B,OAAO,CAAC,sBAAsB;YAKhB,sBAAsB;IAwBpC,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK3E,OAAO,KAAK,EACV,KAAK,EAGL,YAAY,EACZ,gCAAgC,EAChC,oBAAoB,EAEpB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAKf,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAgFpB;;;;;;;GAOG;AACH,qBACa,YAAa,YAAW,KAAK,EAAE,YAAY,EAAE,qBAAqB;IAQjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAClE,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAElE,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;YAKxB,QAAQ;IAyBhB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBrB,cAAc;IA4B5B;;;;OAIG;IACH,4BAA4B;IAW5B;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6B3F;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA6BpH;;;;;;;;;;;;;;;;;OAiBG;IACG,gBAAgB,CACpB,YAAY,EAAE,gCAAgC,EAC9C,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC;YA+Bb,eAAe;IAqB7B,OAAO,CAAC,sBAAsB;YAKhB,oCAAoC;YA8BpC,uBAAuB;YAMvB,sBAAsB;YAUtB,sBAAsB;IAwBpC,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"}
package/dist/service.js CHANGED
@@ -5,7 +5,6 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
7
  import { Inject } from '@fluojs/core';
8
- import { DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS } from './constants.js';
9
8
  import { EmailLifecycleError, EmailMessageValidationError } from './errors.js';
10
9
  import { createEmailPlatformStatusSnapshot } from './status.js';
11
10
  import { EMAIL_OPTIONS } from './tokens.js';
@@ -45,6 +44,9 @@ function createLifecycleError(message, cause) {
45
44
  cause
46
45
  });
47
46
  }
47
+ function isTransportInitializationLifecycleError(error) {
48
+ return error instanceof EmailLifecycleError && error.message === 'Email transport failed to initialize.';
49
+ }
48
50
  function createDeliveryLifecycleError(state) {
49
51
  return new EmailLifecycleError(`Email delivery cannot start while the service lifecycle is ${state}.`);
50
52
  }
@@ -84,15 +86,27 @@ class EmailService {
84
86
  }
85
87
  lifecycleState = 'created';
86
88
  bootstrapPromise;
89
+ shutdownPromise;
90
+ inFlightOperations = new Set();
87
91
  resolvedTransport;
88
92
  transportPromise;
89
93
  constructor(options) {
90
94
  this.options = options;
91
95
  }
92
- async onApplicationShutdown() {
96
+ onApplicationShutdown() {
97
+ this.shutdownPromise ??= this.shutdown();
98
+ return this.shutdownPromise;
99
+ }
100
+ async shutdown() {
93
101
  this.lifecycleState = 'stopping';
102
+ let transport;
94
103
  try {
95
- const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
104
+ transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
105
+ } catch (error) {
106
+ await this.handleTransportInitializationFailure(error);
107
+ }
108
+ try {
109
+ await this.drainInFlightOperations();
96
110
  if (transport && this.options.transport.ownsResources && transport.close) {
97
111
  await transport.close();
98
112
  }
@@ -126,29 +140,16 @@ class EmailService {
126
140
  return;
127
141
  }
128
142
  if (this.options.verifyOnModuleInit && transport.verify) {
129
- await transport.verify();
143
+ await this.trackInFlightOperation(Promise.resolve(transport.verify()));
130
144
  }
131
145
  if (this.lifecycleState !== 'starting') {
132
146
  return;
133
147
  }
134
148
  this.lifecycleState = 'ready';
135
149
  } catch (error) {
136
- if (isShutdownLifecycleState(this.lifecycleState)) {
137
- throw error;
138
- }
139
- this.lifecycleState = 'failed';
140
- let cause = error;
141
- const transport = this.resolvedTransport;
142
- if (transport && this.options.transport.ownsResources && transport.close) {
143
- try {
144
- await transport.close();
145
- } catch (cleanupError) {
146
- cause = createCleanupFailureCause(error, cleanupError);
147
- } finally {
148
- this.clearResolvedTransport();
149
- }
150
- }
151
- throw createLifecycleError('Email transport failed to initialize.', cause);
150
+ await this.handleTransportInitializationFailure(error, {
151
+ preserveShutdownState: true
152
+ });
152
153
  }
153
154
  }
154
155
 
@@ -163,7 +164,6 @@ class EmailService {
163
164
  defaultFromConfigured: this.options.defaultFrom !== undefined,
164
165
  lifecycleState: this.lifecycleState,
165
166
  ownsTransportResources: this.options.transport.ownsResources,
166
- queueWorkerJobName: DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS.jobName,
167
167
  transportKind: this.options.transport.kind,
168
168
  verifiedOnModuleInit: this.options.verifyOnModuleInit
169
169
  });
@@ -203,7 +203,7 @@ class EmailService {
203
203
  } else {
204
204
  this.assertCanDeliver();
205
205
  }
206
- const result = await transport.send(normalized, options);
206
+ const result = await this.trackInFlightOperation(Promise.resolve(transport.send(normalized, options)));
207
207
  return {
208
208
  accepted: result.accepted ?? [],
209
209
  messageId: result.messageId ?? '',
@@ -307,12 +307,49 @@ class EmailService {
307
307
  return transport;
308
308
  });
309
309
  }
310
- return this.transportPromise;
310
+ try {
311
+ return await this.transportPromise;
312
+ } catch (error) {
313
+ return await this.handleTransportInitializationFailure(error);
314
+ }
311
315
  }
312
316
  clearResolvedTransport() {
313
317
  this.resolvedTransport = undefined;
314
318
  this.transportPromise = undefined;
315
319
  }
320
+ async handleTransportInitializationFailure(error, options = {}) {
321
+ if (isTransportInitializationLifecycleError(error)) {
322
+ throw error;
323
+ }
324
+ if (options.preserveShutdownState && isShutdownLifecycleState(this.lifecycleState)) {
325
+ throw error;
326
+ }
327
+ this.lifecycleState = 'failed';
328
+ let cause = error;
329
+ const transport = this.resolvedTransport;
330
+ if (transport && this.options.transport.ownsResources && transport.close) {
331
+ try {
332
+ await transport.close();
333
+ } catch (cleanupError) {
334
+ cause = createCleanupFailureCause(error, cleanupError);
335
+ }
336
+ }
337
+ this.clearResolvedTransport();
338
+ throw createLifecycleError('Email transport failed to initialize.', cause);
339
+ }
340
+ async drainInFlightOperations() {
341
+ while (this.inFlightOperations.size > 0) {
342
+ await Promise.allSettled(Array.from(this.inFlightOperations));
343
+ }
344
+ }
345
+ async trackInFlightOperation(operation) {
346
+ this.inFlightOperations.add(operation);
347
+ try {
348
+ return await operation;
349
+ } finally {
350
+ this.inFlightOperations.delete(operation);
351
+ }
352
+ }
316
353
  async ensureReadyForDelivery() {
317
354
  this.assertCanDeliver();
318
355
  if (!this.options.verifyOnModuleInit) {
package/dist/status.d.ts CHANGED
@@ -7,7 +7,7 @@ export interface EmailStatusAdapterInput {
7
7
  defaultFromConfigured: boolean;
8
8
  lifecycleState: EmailLifecycleState;
9
9
  ownsTransportResources: boolean;
10
- queueWorkerJobName: string;
10
+ queueWorkerJobName?: string;
11
11
  transportKind: string;
12
12
  verifiedOnModuleInit: boolean;
13
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvG,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,cAAc,EAAE,mBAAmB,CAAC;IACpC,sBAAsB,EAAE,OAAO,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,iFAAiF;AACjF,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,SAAS,EAAE,uBAAuB,CAAC;CACpC;AA6DD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,uBAAuB,GAAG,2BAA2B,CAkB7G"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvG,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,cAAc,EAAE,mBAAmB,CAAC;IACpC,sBAAsB,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,iFAAiF;AACjF,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,SAAS,EAAE,uBAAuB,CAAC;CACpC;AA6DD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,uBAAuB,GAAG,2BAA2B,CAkB7G"}
package/dist/status.js CHANGED
@@ -69,7 +69,9 @@ export function createEmailPlatformStatusSnapshot(input) {
69
69
  defaultFromConfigured: input.defaultFromConfigured,
70
70
  dependencies: ['notifications.channel', 'email.transport'],
71
71
  lifecycleState: input.lifecycleState,
72
- queueWorkerJobName: input.queueWorkerJobName,
72
+ ...(input.queueWorkerJobName !== undefined ? {
73
+ queueWorkerJobName: input.queueWorkerJobName
74
+ } : {}),
73
75
  transportKind: input.transportKind,
74
76
  verifiedOnModuleInit: input.verifiedOnModuleInit
75
77
  },
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "queue",
11
11
  "mailer"
12
12
  ],
13
- "version": "1.0.1",
13
+ "version": "2.0.0",
14
14
  "private": false,
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -52,14 +52,14 @@
52
52
  "dist"
53
53
  ],
54
54
  "dependencies": {
55
- "@fluojs/core": "^1.0.3",
56
- "@fluojs/di": "^1.0.3",
57
- "@fluojs/notifications": "^1.0.1",
58
- "@fluojs/runtime": "^1.1.1"
55
+ "@fluojs/core": "^1.1.0",
56
+ "@fluojs/di": "^2.0.0",
57
+ "@fluojs/notifications": "^1.0.3",
58
+ "@fluojs/runtime": "^2.0.1"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "nodemailer": "^6.10.1",
62
- "@fluojs/queue": "^1.0.0"
62
+ "@fluojs/queue": "^2.0.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@fluojs/queue": {
@@ -72,7 +72,8 @@
72
72
  "devDependencies": {
73
73
  "@types/nodemailer": "^8.0.0",
74
74
  "vitest": "^3.2.4",
75
- "@fluojs/queue": "^1.0.0"
75
+ "@fluojs/queue": "^2.0.0",
76
+ "@fluojs/testing": "^2.0.0"
76
77
  },
77
78
  "scripts": {
78
79
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",