@fluojs/email 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +43 -2
- package/README.md +43 -2
- package/dist/service.d.ts +4 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +36 -3
- package/package.json +7 -7
package/README.ko.md
CHANGED
|
@@ -10,6 +10,7 @@ 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와의-통합)
|
|
@@ -104,6 +105,29 @@ export class WelcomeService {
|
|
|
104
105
|
|
|
105
106
|
## 일반적인 패턴
|
|
106
107
|
|
|
108
|
+
### 등록 범위와 async factory
|
|
109
|
+
|
|
110
|
+
`EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global module을 반환합니다. 한 번 import하면 export된 `EmailService`, `EmailChannel`, `EMAIL`, `EMAIL_CHANNEL` provider가 애플리케이션 module graph에 표시됩니다. 이메일 provider를 반환된 module을 명시적으로 import한 module에만 보이게 해야 할 때만 `global: false`를 전달합니다.
|
|
111
|
+
|
|
112
|
+
Async 등록은 의도적으로 fluo의 명시적 factory 형태를 사용합니다:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
EmailModule.forRootAsync({
|
|
116
|
+
global: false,
|
|
117
|
+
inject: [ConfigService],
|
|
118
|
+
useFactory: (config) => ({
|
|
119
|
+
defaultFrom: config.mail.from,
|
|
120
|
+
transport: {
|
|
121
|
+
kind: config.mail.transportKind,
|
|
122
|
+
create: () => config.mail.transport,
|
|
123
|
+
ownsResources: false,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`global`은 factory result가 아니라 `forRootAsync(...)` options object의 최상위에 둡니다. 지원되는 async 등록 형태는 `inject`와 `useFactory`뿐입니다. NestJS dynamic-module 형태인 `imports`, `useClass`, `useExisting`는 `@fluojs/email` 계약에 포함되지 않습니다. 필요한 의존성은 주변 애플리케이션 module graph에 먼저 등록한 뒤, factory가 필요로 하는 token을 `inject`에 나열하세요.
|
|
130
|
+
|
|
107
131
|
### `@fluojs/email/node`를 이용한 Node 전용 SMTP
|
|
108
132
|
|
|
109
133
|
런타임 이식 가능한 루트 패키지 계약을 약화시키지 않으면서 1st-party Nodemailer/SMTP 전달이 필요하다면 전용 Node 서브패스를 사용합니다.
|
|
@@ -171,9 +195,11 @@ Behavioral contract 메모:
|
|
|
171
195
|
- `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
|
|
172
196
|
- 서비스는 모듈 bootstrap 시 transport를 초기화하며, `verifyOnModuleInit: true`인 경우 bootstrap 검증이 성공적으로 끝날 때까지 delivery가 transport handoff로 진행되지 않습니다.
|
|
173
197
|
- 거부된 `forRootAsync(...)` 옵션 factory 결과는 영구 memoize되지 않으며, 다음 provider resolution에서 configuration lookup을 다시 시도할 수 있습니다.
|
|
174
|
-
- shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이
|
|
198
|
+
- shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다리고, 활성 transport `verify()` / `send()` 호출을 drain한 뒤 소유 transport를 닫습니다.
|
|
175
199
|
- transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
|
|
176
200
|
- 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
|
|
201
|
+
- `EmailModule.forRoot(...)`와 `EmailModule.forRootAsync(...)`는 기본적으로 global입니다. module-local visibility가 필요할 때만 `global: false`를 사용합니다.
|
|
202
|
+
- `EmailModule.forRootAsync(...)`는 `inject`와 `useFactory`만 지원합니다. NestJS `imports`, `useClass`, `useExisting` 등록 형태는 factory 호출 전에 애플리케이션 module boundary에서 해석해야 합니다.
|
|
177
203
|
- 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
|
|
178
204
|
|
|
179
205
|
### `@fluojs/notifications`와의 통합
|
|
@@ -214,7 +240,7 @@ export class AppModule {}
|
|
|
214
240
|
|
|
215
241
|
Behavioral contract 메모:
|
|
216
242
|
|
|
217
|
-
- `EmailChannel`은 `
|
|
243
|
+
- `EmailChannel`은 수락된 수신자가 0명인 경우(`accepted.length === 0`) 또는 `pending`/`rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
|
|
218
244
|
- `EmailService.sendNotification(...)`은 렌더링된 template output을 payload 및 notification metadata와 병합합니다. payload 필드는 notification fallback보다 우선합니다.
|
|
219
245
|
- Template rendering에는 notification `payload`, `metadata`, `locale`, `subject`, `template`이 전달되며, payload `text`, `html`과 notification `subject`가 렌더링된 fallback보다 우선합니다.
|
|
220
246
|
|
|
@@ -301,10 +327,22 @@ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
|
|
|
301
327
|
|
|
302
328
|
### 계약과 헬퍼
|
|
303
329
|
|
|
330
|
+
- `Email`: `EMAIL` 호환성 토큰이 노출하는 애플리케이션용 전송 facade이며 address 값이 아닙니다. `EmailService`가 뒷받침하는 `send(...)`, `sendMany(...)`, `sendNotification(...)` 메서드를 제공합니다.
|
|
331
|
+
- `EmailAddress` / `EmailAddressLike`: `EmailService`가 정규화하기 전에 허용하는 구조화 또는 축약 recipient 값입니다.
|
|
332
|
+
- `EmailAttachment`: `EmailMessage.attachments`에서 허용되고 설정된 transport로 전달되는 file attachment payload입니다. `filename`, `content`, 선택적 `contentType` 필드를 포함합니다.
|
|
333
|
+
- `EmailModuleOptions` / `EmailAsyncModuleOptions`: sender 기본값, renderer, lifecycle 검증, transport factory wiring, 최상위 `global` visibility control, async `inject` + `useFactory` 형태를 포함하는 동기/비동기 모듈 등록 계약입니다.
|
|
304
334
|
- `EmailMessage`
|
|
335
|
+
- `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: `EmailChannel`이 소비하는 notification channel payload 계약입니다.
|
|
336
|
+
- `EmailSendOptions` / `EmailSendManyOptions`: abort signal과 batch failure 수집 같은 per-send 제어 옵션입니다.
|
|
337
|
+
- `EmailSendResult` / `EmailSendBatchResult` / `EmailSendFailure`: accepted, pending, rejected, failed message를 보존하는 직접/배치 전달 결과 계약입니다.
|
|
338
|
+
- `EmailTransportReceipt`: `EmailSendResult`에 보존되는 transport-level provider receipt입니다.
|
|
305
339
|
- `EmailTransport`
|
|
340
|
+
- `EmailTransportContext`
|
|
306
341
|
- `EmailTransportFactory`
|
|
342
|
+
- `EmailTemplateRenderInput`
|
|
307
343
|
- `EmailTemplateRenderer`
|
|
344
|
+
- `EmailTemplateRenderResult`
|
|
345
|
+
- `NormalizedEmailAddressList` / `NormalizedEmailMessage`: typed integration과 테스트를 위해 노출되는 내부 정규화 message shape입니다.
|
|
308
346
|
|
|
309
347
|
### 통합 서브패스
|
|
310
348
|
|
|
@@ -313,6 +351,9 @@ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
|
|
|
313
351
|
### 상태 및 에러
|
|
314
352
|
|
|
315
353
|
- `createEmailPlatformStatusSnapshot(...)`
|
|
354
|
+
- `EmailLifecycleState`
|
|
355
|
+
- `EmailPlatformStatusSnapshot`
|
|
356
|
+
- `EmailStatusAdapterInput`
|
|
316
357
|
- `EmailConfigurationError`
|
|
317
358
|
- `EmailLifecycleError`: lifecycle로 차단된 전달, transport 초기화 또는 검증, 소유 리소스 shutdown 실패에서 발생합니다. 애플리케이션 teardown과 전송이 경합할 수 있다면 이 에러를 catch하세요.
|
|
318
359
|
- `EmailMessageValidationError`
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ 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)
|
|
@@ -104,6 +105,29 @@ The root `@fluojs/email` surface is intentionally module-first. Register email d
|
|
|
104
105
|
|
|
105
106
|
## Common Patterns
|
|
106
107
|
|
|
108
|
+
### Registration scope and async factories
|
|
109
|
+
|
|
110
|
+
`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.
|
|
111
|
+
|
|
112
|
+
Async registration intentionally uses fluo's explicit factory shape:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
EmailModule.forRootAsync({
|
|
116
|
+
global: false,
|
|
117
|
+
inject: [ConfigService],
|
|
118
|
+
useFactory: (config) => ({
|
|
119
|
+
defaultFrom: config.mail.from,
|
|
120
|
+
transport: {
|
|
121
|
+
kind: config.mail.transportKind,
|
|
122
|
+
create: () => config.mail.transport,
|
|
123
|
+
ownsResources: false,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`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`.
|
|
130
|
+
|
|
107
131
|
### Node-only SMTP with `@fluojs/email/node`
|
|
108
132
|
|
|
109
133
|
Use the dedicated Node subpath when you want first-party Nodemailer/SMTP delivery without weakening the runtime-portable root package contract.
|
|
@@ -171,9 +195,11 @@ Behavioral contract notes:
|
|
|
171
195
|
- `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
|
|
172
196
|
- 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
197
|
- 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.
|
|
198
|
+
- 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.
|
|
175
199
|
- Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
|
|
176
200
|
- Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
|
|
201
|
+
- `EmailModule.forRoot(...)` and `EmailModule.forRootAsync(...)` are global by default. Use `global: false` to opt into module-local visibility.
|
|
202
|
+
- `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
203
|
- The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
|
|
178
204
|
|
|
179
205
|
### Integration with `@fluojs/notifications`
|
|
@@ -214,7 +240,7 @@ Supported notification payload fields:
|
|
|
214
240
|
|
|
215
241
|
Behavioral contract notes:
|
|
216
242
|
|
|
217
|
-
- `EmailChannel` treats
|
|
243
|
+
- `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
244
|
- `EmailService.sendNotification(...)` merges rendered template output with payload and notification metadata; payload fields override notification fallbacks.
|
|
219
245
|
- Template rendering receives notification `payload`, `metadata`, `locale`, `subject`, and `template`; payload `text`, `html`, and notification `subject` override rendered fallbacks.
|
|
220
246
|
|
|
@@ -301,10 +327,22 @@ These limitations are part of the package contract so transport selection, templ
|
|
|
301
327
|
|
|
302
328
|
### Contracts and helpers
|
|
303
329
|
|
|
330
|
+
- `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`.
|
|
331
|
+
- `EmailAddress` / `EmailAddressLike`: Structured or shorthand recipient values accepted by `EmailService` before normalization.
|
|
332
|
+
- `EmailAttachment`: File attachment payload accepted on `EmailMessage.attachments` and forwarded to the configured transport with `filename`, `content`, and optional `contentType` fields.
|
|
333
|
+
- `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.
|
|
304
334
|
- `EmailMessage`
|
|
335
|
+
- `EmailNotificationDispatchRequest` / `EmailNotificationPayload`: Notification channel payload contracts consumed by `EmailChannel`.
|
|
336
|
+
- `EmailSendOptions` / `EmailSendManyOptions`: Per-send controls such as abort signals and batch failure collection.
|
|
337
|
+
- `EmailSendResult` / `EmailSendBatchResult` / `EmailSendFailure`: Direct and batch delivery result contracts that preserve accepted, pending, rejected, and failed messages.
|
|
338
|
+
- `EmailTransportReceipt`: Transport-level provider receipt preserved by `EmailSendResult`.
|
|
305
339
|
- `EmailTransport`
|
|
340
|
+
- `EmailTransportContext`
|
|
306
341
|
- `EmailTransportFactory`
|
|
342
|
+
- `EmailTemplateRenderInput`
|
|
307
343
|
- `EmailTemplateRenderer`
|
|
344
|
+
- `EmailTemplateRenderResult`
|
|
345
|
+
- `NormalizedEmailAddressList` / `NormalizedEmailMessage`: Internal-normalized message shapes exposed for typed integrations and tests.
|
|
308
346
|
|
|
309
347
|
### Integration subpaths
|
|
310
348
|
|
|
@@ -313,6 +351,9 @@ These limitations are part of the package contract so transport selection, templ
|
|
|
313
351
|
### Status and errors
|
|
314
352
|
|
|
315
353
|
- `createEmailPlatformStatusSnapshot(...)`
|
|
354
|
+
- `EmailLifecycleState`
|
|
355
|
+
- `EmailPlatformStatusSnapshot`
|
|
356
|
+
- `EmailStatusAdapterInput`
|
|
316
357
|
- `EmailConfigurationError`
|
|
317
358
|
- `EmailLifecycleError`: thrown by lifecycle-gated delivery, transport initialization or verification, and owned-resource shutdown failures. Catch this error when sends can race with application teardown.
|
|
318
359
|
- `EmailMessageValidationError`
|
package/dist/service.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class EmailService implements Email, OnModuleInit, OnApplicationS
|
|
|
12
12
|
private readonly options;
|
|
13
13
|
private lifecycleState;
|
|
14
14
|
private bootstrapPromise;
|
|
15
|
+
private readonly inFlightOperations;
|
|
15
16
|
private resolvedTransport;
|
|
16
17
|
private transportPromise;
|
|
17
18
|
constructor(options: NormalizedEmailModuleOptions);
|
|
@@ -77,6 +78,9 @@ export declare class EmailService implements Email, OnModuleInit, OnApplicationS
|
|
|
77
78
|
*/
|
|
78
79
|
sendNotification(notification: EmailNotificationDispatchRequest, options?: EmailSendOptions): Promise<EmailSendResult>;
|
|
79
80
|
private ensureTransport;
|
|
81
|
+
private clearResolvedTransport;
|
|
82
|
+
private drainInFlightOperations;
|
|
83
|
+
private trackInFlightOperation;
|
|
80
84
|
private ensureReadyForDelivery;
|
|
81
85
|
private getLifecycleState;
|
|
82
86
|
private assertCanCreateOrUseTransport;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -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;
|
|
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;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"}
|
package/dist/service.js
CHANGED
|
@@ -48,6 +48,9 @@ function createLifecycleError(message, cause) {
|
|
|
48
48
|
function createDeliveryLifecycleError(state) {
|
|
49
49
|
return new EmailLifecycleError(`Email delivery cannot start while the service lifecycle is ${state}.`);
|
|
50
50
|
}
|
|
51
|
+
function createCleanupFailureCause(originalError, cleanupError) {
|
|
52
|
+
return new AggregateError([originalError, cleanupError], 'Email transport verification failed and the owned transport failed to close.');
|
|
53
|
+
}
|
|
51
54
|
function isShutdownLifecycleState(state) {
|
|
52
55
|
return state === 'stopping' || state === 'stopped';
|
|
53
56
|
}
|
|
@@ -81,6 +84,7 @@ class EmailService {
|
|
|
81
84
|
}
|
|
82
85
|
lifecycleState = 'created';
|
|
83
86
|
bootstrapPromise;
|
|
87
|
+
inFlightOperations = new Set();
|
|
84
88
|
resolvedTransport;
|
|
85
89
|
transportPromise;
|
|
86
90
|
constructor(options) {
|
|
@@ -90,6 +94,7 @@ class EmailService {
|
|
|
90
94
|
this.lifecycleState = 'stopping';
|
|
91
95
|
try {
|
|
92
96
|
const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
|
|
97
|
+
await this.drainInFlightOperations();
|
|
93
98
|
if (transport && this.options.transport.ownsResources && transport.close) {
|
|
94
99
|
await transport.close();
|
|
95
100
|
}
|
|
@@ -123,7 +128,7 @@ class EmailService {
|
|
|
123
128
|
return;
|
|
124
129
|
}
|
|
125
130
|
if (this.options.verifyOnModuleInit && transport.verify) {
|
|
126
|
-
await transport.verify();
|
|
131
|
+
await this.trackInFlightOperation(Promise.resolve(transport.verify()));
|
|
127
132
|
}
|
|
128
133
|
if (this.lifecycleState !== 'starting') {
|
|
129
134
|
return;
|
|
@@ -134,7 +139,18 @@ class EmailService {
|
|
|
134
139
|
throw error;
|
|
135
140
|
}
|
|
136
141
|
this.lifecycleState = 'failed';
|
|
137
|
-
|
|
142
|
+
let cause = error;
|
|
143
|
+
const transport = this.resolvedTransport;
|
|
144
|
+
if (transport && this.options.transport.ownsResources && transport.close) {
|
|
145
|
+
try {
|
|
146
|
+
await transport.close();
|
|
147
|
+
} catch (cleanupError) {
|
|
148
|
+
cause = createCleanupFailureCause(error, cleanupError);
|
|
149
|
+
} finally {
|
|
150
|
+
this.clearResolvedTransport();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw createLifecycleError('Email transport failed to initialize.', cause);
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -189,7 +205,7 @@ class EmailService {
|
|
|
189
205
|
} else {
|
|
190
206
|
this.assertCanDeliver();
|
|
191
207
|
}
|
|
192
|
-
const result = await transport.send(normalized, options);
|
|
208
|
+
const result = await this.trackInFlightOperation(Promise.resolve(transport.send(normalized, options)));
|
|
193
209
|
return {
|
|
194
210
|
accepted: result.accepted ?? [],
|
|
195
211
|
messageId: result.messageId ?? '',
|
|
@@ -295,6 +311,23 @@ class EmailService {
|
|
|
295
311
|
}
|
|
296
312
|
return this.transportPromise;
|
|
297
313
|
}
|
|
314
|
+
clearResolvedTransport() {
|
|
315
|
+
this.resolvedTransport = undefined;
|
|
316
|
+
this.transportPromise = undefined;
|
|
317
|
+
}
|
|
318
|
+
async drainInFlightOperations() {
|
|
319
|
+
while (this.inFlightOperations.size > 0) {
|
|
320
|
+
await Promise.allSettled(Array.from(this.inFlightOperations));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async trackInFlightOperation(operation) {
|
|
324
|
+
this.inFlightOperations.add(operation);
|
|
325
|
+
try {
|
|
326
|
+
return await operation;
|
|
327
|
+
} finally {
|
|
328
|
+
this.inFlightOperations.delete(operation);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
298
331
|
async ensureReadyForDelivery() {
|
|
299
332
|
this.assertCanDeliver();
|
|
300
333
|
if (!this.options.verifyOnModuleInit) {
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"queue",
|
|
11
11
|
"mailer"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.
|
|
13
|
+
"version": "1.0.2",
|
|
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.
|
|
56
|
-
"@fluojs/di": "^1.
|
|
57
|
-
"@fluojs/notifications": "^1.0.
|
|
58
|
-
"@fluojs/runtime": "^1.
|
|
55
|
+
"@fluojs/core": "^1.0.3",
|
|
56
|
+
"@fluojs/di": "^1.1.0",
|
|
57
|
+
"@fluojs/notifications": "^1.0.2",
|
|
58
|
+
"@fluojs/runtime": "^1.1.8"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"nodemailer": "^6.10.1",
|
|
62
|
-
"@fluojs/queue": "^1.0.
|
|
62
|
+
"@fluojs/queue": "^1.0.2"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@fluojs/queue": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/nodemailer": "^8.0.0",
|
|
74
74
|
"vitest": "^3.2.4",
|
|
75
|
-
"@fluojs/queue": "^1.0.
|
|
75
|
+
"@fluojs/queue": "^1.0.2"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|