@fluojs/email
Advanced tools
@@ -15,2 +15,3 @@ import type { OnApplicationShutdown, OnModuleInit } from '@fluojs/runtime'; | ||
| private bootstrapPromise; | ||
| private readonly inFlightOperations; | ||
| private resolvedTransport; | ||
@@ -82,2 +83,4 @@ private transportPromise; | ||
| private clearResolvedTransport; | ||
| private drainInFlightOperations; | ||
| private trackInFlightOperation; | ||
| private ensureReadyForDelivery; | ||
@@ -84,0 +87,0 @@ private getLifecycleState; |
@@ -1,1 +0,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"} | ||
| {"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;IAOjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAClE,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAE5D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBtC,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,uBAAuB;YAMvB,sBAAsB;YAUtB,sBAAsB;IAwBpC,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"} |
+17
-2
@@ -87,2 +87,3 @@ let _initClass; | ||
| bootstrapPromise; | ||
| inFlightOperations = new Set(); | ||
| resolvedTransport; | ||
@@ -97,2 +98,3 @@ transportPromise; | ||
| const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined); | ||
| await this.drainInFlightOperations(); | ||
| if (transport && this.options.transport.ownsResources && transport.close) { | ||
@@ -131,3 +133,3 @@ await transport.close(); | ||
| if (this.options.verifyOnModuleInit && transport.verify) { | ||
| await transport.verify(); | ||
| await this.trackInFlightOperation(Promise.resolve(transport.verify())); | ||
| } | ||
@@ -209,3 +211,3 @@ if (this.lifecycleState !== 'starting') { | ||
| } | ||
| const result = await transport.send(normalized, options); | ||
| const result = await this.trackInFlightOperation(Promise.resolve(transport.send(normalized, options))); | ||
| return { | ||
@@ -320,2 +322,15 @@ accepted: result.accepted ?? [], | ||
| } | ||
| async drainInFlightOperations() { | ||
| while (this.inFlightOperations.size > 0) { | ||
| await Promise.allSettled(Array.from(this.inFlightOperations)); | ||
| } | ||
| } | ||
| async trackInFlightOperation(operation) { | ||
| this.inFlightOperations.add(operation); | ||
| try { | ||
| return await operation; | ||
| } finally { | ||
| this.inFlightOperations.delete(operation); | ||
| } | ||
| } | ||
| async ensureReadyForDelivery() { | ||
@@ -322,0 +337,0 @@ this.assertCanDeliver(); |
+6
-6
@@ -13,3 +13,3 @@ { | ||
| ], | ||
| "version": "1.0.1", | ||
| "version": "1.0.2", | ||
| "private": false, | ||
@@ -57,9 +57,9 @@ "license": "MIT", | ||
| "@fluojs/core": "^1.0.3", | ||
| "@fluojs/di": "^1.0.3", | ||
| "@fluojs/notifications": "^1.0.1", | ||
| "@fluojs/runtime": "^1.1.1" | ||
| "@fluojs/di": "^1.1.0", | ||
| "@fluojs/notifications": "^1.0.2", | ||
| "@fluojs/runtime": "^1.1.8" | ||
| }, | ||
| "peerDependencies": { | ||
| "nodemailer": "^6.10.1", | ||
| "@fluojs/queue": "^1.0.0" | ||
| "@fluojs/queue": "^1.0.2" | ||
| }, | ||
@@ -77,3 +77,3 @@ "peerDependenciesMeta": { | ||
| "vitest": "^3.2.4", | ||
| "@fluojs/queue": "^1.0.0" | ||
| "@fluojs/queue": "^1.0.2" | ||
| }, | ||
@@ -80,0 +80,0 @@ "scripts": { |
+31
-4
@@ -13,2 +13,3 @@ # @fluojs/email | ||
| - [일반적인 패턴](#일반적인-패턴) | ||
| - [등록 범위와 async factory](#등록-범위와-async-factory) | ||
| - [`@fluojs/email/node`를 이용한 Node 전용 SMTP](#fluojs-email-node를-이용한-node-전용-smtp) | ||
@@ -108,2 +109,25 @@ - [`EmailService`를 이용한 standalone 전달](#emailservice를-이용한-standalone-전달) | ||
| ### 등록 범위와 async factory | ||
| `EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global module을 반환합니다. 한 번 import하면 export된 `EmailService`, `EmailChannel`, `EMAIL`, `EMAIL_CHANNEL` provider가 애플리케이션 module graph에 표시됩니다. 이메일 provider를 반환된 module을 명시적으로 import한 module에만 보이게 해야 할 때만 `global: false`를 전달합니다. | ||
| Async 등록은 의도적으로 fluo의 명시적 factory 형태를 사용합니다: | ||
| ```typescript | ||
| EmailModule.forRootAsync({ | ||
| global: false, | ||
| inject: [ConfigService], | ||
| useFactory: (config) => ({ | ||
| defaultFrom: config.mail.from, | ||
| transport: { | ||
| kind: config.mail.transportKind, | ||
| create: () => config.mail.transport, | ||
| ownsResources: false, | ||
| }, | ||
| }), | ||
| }); | ||
| ``` | ||
| `global`은 factory result가 아니라 `forRootAsync(...)` options object의 최상위에 둡니다. 지원되는 async 등록 형태는 `inject`와 `useFactory`뿐입니다. NestJS dynamic-module 형태인 `imports`, `useClass`, `useExisting`는 `@fluojs/email` 계약에 포함되지 않습니다. 필요한 의존성은 주변 애플리케이션 module graph에 먼저 등록한 뒤, factory가 필요로 하는 token을 `inject`에 나열하세요. | ||
| ### `@fluojs/email/node`를 이용한 Node 전용 SMTP | ||
@@ -176,5 +200,7 @@ | ||
| - 거부된 `forRootAsync(...)` 옵션 factory 결과는 영구 memoize되지 않으며, 다음 provider resolution에서 configuration lookup을 다시 시도할 수 있습니다. | ||
| - shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다린 뒤 닫습니다. | ||
| - shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다리고, 활성 transport `verify()` / `send()` 호출을 drain한 뒤 소유 transport를 닫습니다. | ||
| - transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다. | ||
| - 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다. | ||
| - `EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global입니다. module-local visibility가 필요할 때만 `global: false`를 사용합니다. | ||
| - `EmailModule.forRootAsync(...)`는 `inject`와 `useFactory`만 지원합니다. NestJS `imports`, `useClass`, `useExisting` 등록 형태는 factory 호출 전에 애플리케이션 module boundary에서 해석해야 합니다. | ||
| - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다. | ||
@@ -220,3 +246,3 @@ | ||
| - `EmailChannel`은 `pending` 또는 `rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다. | ||
| - `EmailChannel`은 수락된 수신자가 0명인 경우(`accepted.length === 0`) 또는 `pending`/`rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다. | ||
| - `EmailService.sendNotification(...)`은 렌더링된 template output을 payload 및 notification metadata와 병합합니다. payload 필드는 notification fallback보다 우선합니다. | ||
@@ -308,5 +334,6 @@ - Template rendering에는 notification `payload`, `metadata`, `locale`, `subject`, `template`이 전달되며, payload `text`, `html`과 notification `subject`가 렌더링된 fallback보다 우선합니다. | ||
| - `Email`: `address`와 선택적 display `name`을 포함하는 정규화된 이메일 주소 값입니다. | ||
| - `Email`: `EMAIL` 호환성 토큰이 노출하는 애플리케이션용 전송 facade이며 address 값이 아닙니다. `EmailService`가 뒷받침하는 `send(...)`, `sendMany(...)`, `sendNotification(...)` 메서드를 제공합니다. | ||
| - `EmailAddress` / `EmailAddressLike`: `EmailService`가 정규화하기 전에 허용하는 구조화 또는 축약 recipient 값입니다. | ||
| - `EmailModuleOptions` / `EmailAsyncModuleOptions`: sender 기본값, renderer, lifecycle 검증, transport factory wiring을 포함하는 동기/비동기 모듈 등록 계약입니다. | ||
| - `EmailAttachment`: `EmailMessage.attachments`에서 허용되고 설정된 transport로 전달되는 file attachment payload입니다. `filename`, `content`, 선택적 `contentType` 필드를 포함합니다. | ||
| - `EmailModuleOptions` / `EmailAsyncModuleOptions`: sender 기본값, renderer, lifecycle 검증, transport factory wiring, 최상위 `global` visibility control, async `inject` + `useFactory` 형태를 포함하는 동기/비동기 모듈 등록 계약입니다. | ||
| - `EmailMessage` | ||
@@ -313,0 +340,0 @@ - `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: `EmailChannel`이 소비하는 notification channel payload 계약입니다. |
+31
-4
@@ -13,2 +13,3 @@ # @fluojs/email | ||
| - [Common Patterns](#common-patterns) | ||
| - [Registration scope and async factories](#registration-scope-and-async-factories) | ||
| - [Node-only SMTP with `@fluojs/email/node`](#node-only-smtp-with-fluojs-email-node) | ||
@@ -108,2 +109,25 @@ - [Standalone delivery with `EmailService`](#standalone-delivery-with-emailservice) | ||
| ### Registration scope and async factories | ||
| `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. | ||
| Async registration intentionally uses fluo's explicit factory shape: | ||
| ```typescript | ||
| EmailModule.forRootAsync({ | ||
| global: false, | ||
| inject: [ConfigService], | ||
| useFactory: (config) => ({ | ||
| defaultFrom: config.mail.from, | ||
| transport: { | ||
| kind: config.mail.transportKind, | ||
| create: () => config.mail.transport, | ||
| ownsResources: false, | ||
| }, | ||
| }), | ||
| }); | ||
| ``` | ||
| `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`. | ||
| ### Node-only SMTP with `@fluojs/email/node` | ||
@@ -176,5 +200,7 @@ | ||
| - Rejected `forRootAsync(...)` option factories are not memoized permanently; the next provider resolution can retry configuration lookup. | ||
| - 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. | ||
| - 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. | ||
| - Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics. | ||
| - Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership. | ||
| - `EmailModule.forRoot(...)` and `EmailModule.forRootAsync(...)` are global by default. Use `global: false` to opt into module-local visibility. | ||
| - `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. | ||
| - The package never reads `process.env` directly. All configuration must enter through explicit options or DI. | ||
@@ -220,3 +246,3 @@ | ||
| - `EmailChannel` treats any `pending` or `rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful. | ||
| - `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. | ||
| - `EmailService.sendNotification(...)` merges rendered template output with payload and notification metadata; payload fields override notification fallbacks. | ||
@@ -308,5 +334,6 @@ - Template rendering receives notification `payload`, `metadata`, `locale`, `subject`, and `template`; payload `text`, `html`, and notification `subject` override rendered fallbacks. | ||
| - `Email`: Normalized email address value with an `address` and optional display `name`. | ||
| - `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`. | ||
| - `EmailAddress` / `EmailAddressLike`: Structured or shorthand recipient values accepted by `EmailService` before normalization. | ||
| - `EmailModuleOptions` / `EmailAsyncModuleOptions`: Synchronous and async module registration contracts, including sender defaults, renderer, lifecycle verification, and transport factory wiring. | ||
| - `EmailAttachment`: File attachment payload accepted on `EmailMessage.attachments` and forwarded to the configured transport with `filename`, `content`, and optional `contentType` fields. | ||
| - `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. | ||
| - `EmailMessage` | ||
@@ -313,0 +340,0 @@ - `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: Notification channel payload contracts consumed by `EmailChannel`. |
128793
4.23%1638
1.11%393
7.38%Updated
Updated
Updated