@fluojs/email 1.0.0-beta.3 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +23 -9
- package/package.json +7 -7
package/README.ko.md
CHANGED
|
@@ -86,8 +86,9 @@ export class AppModule {}
|
|
|
86
86
|
import { Inject } from '@fluojs/core';
|
|
87
87
|
import { EmailService } from '@fluojs/email';
|
|
88
88
|
|
|
89
|
+
@Inject(EmailService)
|
|
89
90
|
export class WelcomeService {
|
|
90
|
-
constructor(
|
|
91
|
+
constructor(private readonly email: EmailService) {}
|
|
91
92
|
|
|
92
93
|
async sendWelcome(address: string) {
|
|
93
94
|
await this.email.send({
|
|
@@ -162,10 +163,13 @@ EmailModule.forRootAsync({
|
|
|
162
163
|
Behavioral contract 메모:
|
|
163
164
|
|
|
164
165
|
- `EmailService.send(...)`는 전달 전에 `defaultFrom`과 `defaultReplyTo`를 해석합니다.
|
|
166
|
+
- `EmailService.send(...)`는 빈 `to` 수신자를 transport handoff 전에 거부하므로 transport가 빈 전달 대상을 받지 않습니다.
|
|
167
|
+
- `EmailService.send(...)`와 `EmailService.sendNotification(...)`은 이미 abort된 `AbortSignal`을 템플릿 렌더링 또는 transport handoff 전에 반영합니다.
|
|
165
168
|
- `EmailService.send(...)`는 `accepted`, `pending`, `rejected` 수신자를 분리해 보존하므로 provider의 부분 실패가 호출자에게 그대로 보입니다.
|
|
166
169
|
- `EmailService.sendMany(...)`는 기본적으로 fail-fast입니다. 실패를 batch result에 수집하려면 `continueOnError: true`를 전달합니다.
|
|
167
170
|
- `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
|
|
168
171
|
- 서비스는 모듈 bootstrap 시 transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 시 닫습니다.
|
|
172
|
+
- transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
|
|
169
173
|
- 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
|
|
170
174
|
- 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
|
|
171
175
|
|
package/README.md
CHANGED
|
@@ -86,8 +86,9 @@ export class AppModule {}
|
|
|
86
86
|
import { Inject } from '@fluojs/core';
|
|
87
87
|
import { EmailService } from '@fluojs/email';
|
|
88
88
|
|
|
89
|
+
@Inject(EmailService)
|
|
89
90
|
export class WelcomeService {
|
|
90
|
-
constructor(
|
|
91
|
+
constructor(private readonly email: EmailService) {}
|
|
91
92
|
|
|
92
93
|
async sendWelcome(address: string) {
|
|
93
94
|
await this.email.send({
|
|
@@ -162,10 +163,13 @@ EmailModule.forRootAsync({
|
|
|
162
163
|
Behavioral contract notes:
|
|
163
164
|
|
|
164
165
|
- `EmailService.send(...)` resolves `defaultFrom` and `defaultReplyTo` before delivery.
|
|
166
|
+
- `EmailService.send(...)` rejects blank `to` recipients before handoff so transports never receive an empty delivery target.
|
|
167
|
+
- `EmailService.send(...)` and `EmailService.sendNotification(...)` honor an already-aborted `AbortSignal` before template rendering or transport handoff.
|
|
165
168
|
- `EmailService.send(...)` preserves `accepted`, `pending`, and `rejected` recipients separately so partial provider failures stay caller-visible.
|
|
166
169
|
- `EmailService.sendMany(...)` is fail-fast by default; pass `continueOnError: true` to collect failures in a batch result.
|
|
167
170
|
- `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
|
|
168
171
|
- The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
|
|
172
|
+
- Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
|
|
169
173
|
- Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
|
|
170
174
|
- The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
|
|
171
175
|
|
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,EAC9B,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,EAC9B,oBAAoB,EAEpB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAKf,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAyDtB;;;;;;;GAOG;AACH,qBACa,YAAa,YAAW,KAAK,EAAE,YAAY,EAAE,qBAAqB;IAKjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,cAAc,CAAmF;IACzG,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAE5D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAetC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBnC;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB3F;;;;;;;;;;;;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;YA4Bb,eAAe;IAe7B,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"}
|
package/dist/service.js
CHANGED
|
@@ -35,10 +35,23 @@ function createAbortError() {
|
|
|
35
35
|
error.name = 'AbortError';
|
|
36
36
|
return error;
|
|
37
37
|
}
|
|
38
|
+
function assertNotAborted(signal) {
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
throw createAbortError();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function createLifecycleError(message, cause) {
|
|
44
|
+
return new Error(message, {
|
|
45
|
+
cause
|
|
46
|
+
});
|
|
47
|
+
}
|
|
38
48
|
function assertMessageContent(message) {
|
|
39
49
|
if (message.to.length === 0) {
|
|
40
50
|
throw new EmailMessageValidationError('Email messages require at least one recipient in `to`.');
|
|
41
51
|
}
|
|
52
|
+
if (message.to.some(entry => entry.address.length === 0)) {
|
|
53
|
+
throw new EmailMessageValidationError('Email messages require non-empty recipients in `to`.');
|
|
54
|
+
}
|
|
42
55
|
if (!message.from.address) {
|
|
43
56
|
throw new EmailMessageValidationError('Email messages require a resolved `from` address.');
|
|
44
57
|
}
|
|
@@ -73,9 +86,9 @@ class EmailService {
|
|
|
73
86
|
await this.resolvedTransport.close();
|
|
74
87
|
}
|
|
75
88
|
this.lifecycleState = 'stopped';
|
|
76
|
-
} catch {
|
|
89
|
+
} catch (error) {
|
|
77
90
|
this.lifecycleState = 'failed';
|
|
78
|
-
throw
|
|
91
|
+
throw createLifecycleError('Email transport failed to close cleanly.', error);
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
async onModuleInit() {
|
|
@@ -86,9 +99,9 @@ class EmailService {
|
|
|
86
99
|
await transport.verify();
|
|
87
100
|
}
|
|
88
101
|
this.lifecycleState = 'ready';
|
|
89
|
-
} catch {
|
|
102
|
+
} catch (error) {
|
|
90
103
|
this.lifecycleState = 'failed';
|
|
91
|
-
throw
|
|
104
|
+
throw createLifecycleError('Email transport failed to initialize.', error);
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
|
|
@@ -128,12 +141,11 @@ class EmailService {
|
|
|
128
141
|
* ```
|
|
129
142
|
*/
|
|
130
143
|
async send(message, options = {}) {
|
|
131
|
-
|
|
132
|
-
throw createAbortError();
|
|
133
|
-
}
|
|
144
|
+
assertNotAborted(options.signal);
|
|
134
145
|
const transport = await this.ensureTransport();
|
|
135
146
|
const normalized = this.normalizeMessage(message);
|
|
136
147
|
assertMessageContent(normalized);
|
|
148
|
+
assertNotAborted(options.signal);
|
|
137
149
|
const result = await transport.send(normalized, options);
|
|
138
150
|
return {
|
|
139
151
|
accepted: result.accepted ?? [],
|
|
@@ -203,7 +215,8 @@ class EmailService {
|
|
|
203
215
|
*/
|
|
204
216
|
async sendNotification(notification, options = {}) {
|
|
205
217
|
const payload = notification.payload;
|
|
206
|
-
const rendered = await this.renderNotification(notification);
|
|
218
|
+
const rendered = await this.renderNotification(notification, options.signal);
|
|
219
|
+
assertNotAborted(options.signal);
|
|
207
220
|
return this.send({
|
|
208
221
|
attachments: payload.attachments,
|
|
209
222
|
bcc: payload.bcc,
|
|
@@ -256,10 +269,11 @@ class EmailService {
|
|
|
256
269
|
to: normalizeAddressList(message.to)
|
|
257
270
|
};
|
|
258
271
|
}
|
|
259
|
-
async renderNotification(notification) {
|
|
272
|
+
async renderNotification(notification, signal) {
|
|
260
273
|
if (!notification.template || !this.options.renderer) {
|
|
261
274
|
return undefined;
|
|
262
275
|
}
|
|
276
|
+
assertNotAborted(signal);
|
|
263
277
|
return this.options.renderer.render({
|
|
264
278
|
locale: notification.locale,
|
|
265
279
|
metadata: notification.metadata,
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"queue",
|
|
11
11
|
"mailer"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.0-beta.
|
|
13
|
+
"version": "1.0.0-beta.4",
|
|
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.0-beta.
|
|
56
|
-
"@fluojs/di": "^1.0.0-beta.
|
|
57
|
-
"@fluojs/notifications": "^1.0.0-beta.
|
|
58
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
55
|
+
"@fluojs/core": "^1.0.0-beta.5",
|
|
56
|
+
"@fluojs/di": "^1.0.0-beta.7",
|
|
57
|
+
"@fluojs/notifications": "^1.0.0-beta.4",
|
|
58
|
+
"@fluojs/runtime": "^1.0.0-beta.12"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"nodemailer": "^6.10.1",
|
|
62
|
-
"@fluojs/queue": "^1.0.0-beta.
|
|
62
|
+
"@fluojs/queue": "^1.0.0-beta.5"
|
|
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.0-beta.
|
|
75
|
+
"@fluojs/queue": "^1.0.0-beta.5"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|