@fluojs/notifications 1.0.1 → 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 +9 -6
- package/README.md +9 -6
- package/dist/service.d.ts +2 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +90 -28
- package/package.json +3 -3
package/README.ko.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
4
|
|
|
5
|
-
fluo를 위한 채널 중립(notification channel-agnostic) 알림 오케스트레이션 패키지입니다. 알림 채널의 공통 계약을 고정하고,
|
|
5
|
+
fluo를 위한 채널 중립(notification channel-agnostic) 알림 오케스트레이션 패키지입니다. 알림 채널의 공통 계약을 고정하고, 친숙한 dynamic-module 사용감을 가진 명시적 모듈 등록 API를 제공하며, 선택적인 큐 기반 전달 심(seam)과 라이프사이클 이벤트 발행 심을 노출합니다.
|
|
6
6
|
|
|
7
7
|
## 목차
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ npm install @fluojs/notifications
|
|
|
34
34
|
|
|
35
35
|
### 1. foundation 모듈 등록
|
|
36
36
|
|
|
37
|
-
알림 모듈 등록은 `NotificationsModule.forRoot(...)` 또는 `NotificationsModule.forRootAsync(...)`로 구성합니다.
|
|
37
|
+
알림 모듈 등록은 `channels`에 명시적인 `NotificationChannel` 값을 전달하는 `NotificationsModule.forRoot(...)` 또는 `NotificationsModule.forRootAsync(...)`로 구성합니다.
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
40
|
import { Module } from '@fluojs/core';
|
|
@@ -91,11 +91,13 @@ export class WelcomeService {
|
|
|
91
91
|
|
|
92
92
|
`NotificationsModule.forRoot(...)`와 `NotificationsModule.forRootAsync(...)`는 기본적으로 `NotificationsService`, `NOTIFICATIONS`, `NOTIFICATION_CHANNELS`를 global provider로 export합니다. 이 provider들이 notifications module을 import한 module 안에서만 보이도록 유지하려면 `global: false`를 설정합니다. 애플리케이션 서비스는 fluo의 class-level `@Inject(...)` decorator로 의존성을 선언해야 standard-decorator DI container가 parameter decorator 없이 서비스를 resolve할 수 있습니다.
|
|
93
93
|
|
|
94
|
+
Migration boundary: channel registration은 metadata 기반이 아니라 value 기반입니다. NestJS provider discovery, `@Injectable()` metadata, `emitDecoratorMetadata`에 기대어 channel이 등록된다고 가정하지 마세요. 애플리케이션 코드에서 `NotificationChannel` object를 만들거나 `NotificationsModule.forRootAsync({ inject, useFactory, global? })`에서 반환한 뒤, `channels` option으로 전달합니다.
|
|
95
|
+
|
|
94
96
|
## 일반적인 패턴
|
|
95
97
|
|
|
96
98
|
### 큐 기반 대량 전달
|
|
97
99
|
|
|
98
|
-
많은 알림을 백그라운드 워커로 넘기고 싶다면 선택적인 queue seam을 사용합니다.
|
|
100
|
+
많은 알림을 백그라운드 워커로 넘기고 싶다면 선택적인 queue seam을 사용합니다. Queue adapter는 애플리케이션 소유 integration이며, `@fluojs/notifications`는 abstract adapter contract만 호출합니다.
|
|
99
101
|
|
|
100
102
|
```typescript
|
|
101
103
|
NotificationsModule.forRoot({
|
|
@@ -124,11 +126,11 @@ Behavioral contract 메모:
|
|
|
124
126
|
- `dispatchMany(..., { continueOnError: true })`는 direct delivery 또는 순차 queue fallback enqueue에서 첫 실패를 던지는 대신 실패들을 수집합니다.
|
|
125
127
|
- queue enqueue가 실패하면 서비스는 enqueue 에러를 다시 던지기 전에 결정적인 `notification.dispatch.failed` 라이프사이클 이벤트를 발행합니다. queued bulk dispatch는 queue 미구성, channel 해석, provider/adapter failure 경로를 포함해 이미 `requested`를 발행한 모든 notification에 대해 terminal `queued` 또는 `failed` 이벤트도 발행합니다.
|
|
126
128
|
- `enqueueMany(...)`가 없으면 대량 queue delivery는 input order대로 각 job을 개별 enqueue하는 방식으로 fallback합니다. `continueOnError: true`이면 성공한 enqueue는 `results`에 남고 실패한 enqueue는 `failures`로 반환됩니다. 그렇지 않으면 첫 enqueue failure를 다시 던지기 전에 아직 terminal 상태가 없는 나머지 requested fallback job에 `failed` 라이프사이클 이벤트를 발행합니다.
|
|
127
|
-
- foundation 패키지는 특정 큐 구현을 가정하거나 import하지 않습니다.
|
|
129
|
+
- foundation 패키지는 특정 큐 구현을 가정하거나 import하지 않고, queue client/worker를 만들거나 애플리케이션 소유 queue resource를 close/drain하지 않습니다.
|
|
128
130
|
|
|
129
131
|
### 이벤트 발행자를 통한 라이프사이클 발행
|
|
130
132
|
|
|
131
|
-
foundation 패키지를 `@fluojs/event-bus` 구현에 직접 결합하지 않고도 caller-visible 라이프사이클 이벤트를 발행할 수 있습니다.
|
|
133
|
+
foundation 패키지를 `@fluojs/event-bus` 구현에 직접 결합하지 않고도 caller-visible 라이프사이클 이벤트를 발행할 수 있습니다. Event publisher 역시 애플리케이션 소유이며, foundation 패키지는 concrete event bus를 create/import/close/drain하지 않습니다.
|
|
132
134
|
|
|
133
135
|
```typescript
|
|
134
136
|
NotificationsModule.forRoot({
|
|
@@ -160,6 +162,7 @@ foundation 패키지는 의도적으로 다음을 **포함하지 않습니다**:
|
|
|
160
162
|
- 내장 email, Slack, Discord 구현
|
|
161
163
|
- 직접적인 `process.env` 접근
|
|
162
164
|
- `@fluojs/queue` 또는 `@fluojs/event-bus`의 concrete runtime 타입 의존성
|
|
165
|
+
- concrete queue 또는 event-bus resource를 create/import/close/drain하는 것. Queue adapter와 event publisher는 애플리케이션 소유 integration입니다.
|
|
163
166
|
- provider별 payload 의미를 공유 계약에 인코딩하는 것
|
|
164
167
|
|
|
165
168
|
이 제한 사항은 leaf 패키지가 하나의 안정적인 오케스트레이션 계층 위에서 독립적으로 진화할 수 있도록 하는 package contract의 일부입니다.
|
|
@@ -208,7 +211,7 @@ foundation 패키지는 의도적으로 다음을 **포함하지 않습니다**:
|
|
|
208
211
|
- `NotificationQueueNotConfiguredError`
|
|
209
212
|
|
|
210
213
|
상태 snapshot은 platform diagnostics를 위해 `operationMode`, dependency diagnostics, ownership, readiness, health 필드를 포함합니다.
|
|
211
|
-
Queue adapter가 구성되면 `details.dependencies`에 `notifications.queue-adapter`가 포함되고, lifecycle event가 event publisher를 통해 발행되면 `notifications.event-publisher`가 포함됩니다. 이러한 선택적 통합은 `ownership.externallyManaged: true`로 표시되지만, foundation 패키지가 concrete queue 또는 event-bus 리소스를
|
|
214
|
+
Queue adapter가 구성되면 `details.dependencies`에 `notifications.queue-adapter`가 포함되고, lifecycle event가 event publisher를 통해 발행되면 `notifications.event-publisher`가 포함됩니다. 이러한 선택적 통합은 `ownership.externallyManaged: true`로 표시되지만, foundation 패키지가 concrete queue 또는 event-bus 리소스를 create/close/drain하지 않으므로 `ownsResources: false`를 유지합니다.
|
|
212
215
|
|
|
213
216
|
## 관련 패키지
|
|
214
217
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
4
|
|
|
5
|
-
Channel-agnostic notification orchestration for fluo. It freezes the shared contract for notification channels, provides
|
|
5
|
+
Channel-agnostic notification orchestration for fluo. It freezes the shared contract for notification channels, provides explicit module registration with familiar dynamic-module ergonomics, and exposes optional queue-backed delivery and lifecycle event publication seams.
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ npm install @fluojs/notifications
|
|
|
34
34
|
|
|
35
35
|
### 1. Register the foundation module
|
|
36
36
|
|
|
37
|
-
Register notifications with `NotificationsModule.forRoot(...)` or `NotificationsModule.forRootAsync(...)`.
|
|
37
|
+
Register notifications with `NotificationsModule.forRoot(...)` or `NotificationsModule.forRootAsync(...)` by passing explicit `NotificationChannel` values in `channels`.
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
40
|
import { Module } from '@fluojs/core';
|
|
@@ -91,11 +91,13 @@ export class WelcomeService {
|
|
|
91
91
|
|
|
92
92
|
`NotificationsModule.forRoot(...)` and `NotificationsModule.forRootAsync(...)` export `NotificationsService`, `NOTIFICATIONS`, and `NOTIFICATION_CHANNELS` as global providers by default. Set `global: false` when these providers should stay visible only to the module that imports the notifications module. Application services should declare dependencies with fluo's class-level `@Inject(...)` decorator so the standard-decorator DI container can resolve the service without parameter decorators.
|
|
93
93
|
|
|
94
|
+
Migration boundary: channel registration is value-based, not metadata-based. Do not rely on NestJS provider discovery, `@Injectable()` metadata, or `emitDecoratorMetadata` to register channels. Build `NotificationChannel` objects in application code or return them from `NotificationsModule.forRootAsync({ inject, useFactory, global? })`, then pass them through the `channels` option.
|
|
95
|
+
|
|
94
96
|
## Common Patterns
|
|
95
97
|
|
|
96
98
|
### Queue-backed bulk delivery
|
|
97
99
|
|
|
98
|
-
Use the optional queue seam when many notifications should be deferred to background workers.
|
|
100
|
+
Use the optional queue seam when many notifications should be deferred to background workers. The queue adapter is an application-owned integration; `@fluojs/notifications` only calls the abstract adapter contract.
|
|
99
101
|
|
|
100
102
|
```typescript
|
|
101
103
|
NotificationsModule.forRoot({
|
|
@@ -124,11 +126,11 @@ Behavioral contract notes:
|
|
|
124
126
|
- `dispatchMany(..., { continueOnError: true })` collects failures instead of throwing on the first failed direct delivery or sequential queue fallback enqueue.
|
|
125
127
|
- When queue enqueue fails, the service emits deterministic `notification.dispatch.failed` lifecycle events before rethrowing the enqueue error to the caller. Queued bulk dispatch also publishes a terminal `queued` or `failed` event for every notification that already emitted `requested`, including queue-missing, channel-resolution, and provider/adapter failure paths.
|
|
126
128
|
- If `enqueueMany(...)` is unavailable, bulk queue delivery falls back to enqueueing each job individually in input order. With `continueOnError: true`, successful enqueues remain visible in `results` while failed enqueues are returned in `failures`; without it, the first enqueue failure is rethrown after the remaining requested fallback jobs receive `failed` lifecycle events.
|
|
127
|
-
- The foundation package does not assume or import a concrete queue implementation.
|
|
129
|
+
- The foundation package does not assume or import a concrete queue implementation, create queue clients/workers, or close/drain application-owned queue resources.
|
|
128
130
|
|
|
129
131
|
### Lifecycle publication through an event publisher
|
|
130
132
|
|
|
131
|
-
Publish caller-visible lifecycle events without coupling the foundation package to `@fluojs/event-bus` directly.
|
|
133
|
+
Publish caller-visible lifecycle events without coupling the foundation package to `@fluojs/event-bus` directly. The event publisher is also application-owned; the foundation package does not create, import, close, or drain a concrete event bus.
|
|
132
134
|
|
|
133
135
|
```typescript
|
|
134
136
|
NotificationsModule.forRoot({
|
|
@@ -160,6 +162,7 @@ The foundation package intentionally does **not**:
|
|
|
160
162
|
- ship built-in email, Slack, or Discord implementations
|
|
161
163
|
- inspect `process.env` directly
|
|
162
164
|
- depend on `@fluojs/queue` or `@fluojs/event-bus` concrete runtime types
|
|
165
|
+
- create, import, close, or drain concrete queue or event-bus resources; queue adapters and event publishers are application-owned integrations
|
|
163
166
|
- encode provider-specific payload semantics into the shared contract
|
|
164
167
|
|
|
165
168
|
These limitations are part of the package contract so leaf packages can evolve independently while sharing one stable orchestration layer.
|
|
@@ -208,7 +211,7 @@ These limitations are part of the package contract so leaf packages can evolve i
|
|
|
208
211
|
- `NotificationQueueNotConfiguredError`
|
|
209
212
|
|
|
210
213
|
Status snapshots include `operationMode`, dependency diagnostics, ownership, readiness, and health fields for platform diagnostics.
|
|
211
|
-
When a queue adapter is configured, `details.dependencies` includes `notifications.queue-adapter`; when lifecycle events are published through an event publisher, it includes `notifications.event-publisher`. Those optional integrations mark `ownership.externallyManaged: true` while the foundation package still reports `ownsResources: false` because it does not close concrete queue or event-bus resources.
|
|
214
|
+
When a queue adapter is configured, `details.dependencies` includes `notifications.queue-adapter`; when lifecycle events are published through an event publisher, it includes `notifications.event-publisher`. Those optional integrations mark `ownership.externallyManaged: true` while the foundation package still reports `ownsResources: false` because it does not create, close, or drain concrete queue or event-bus resources.
|
|
212
215
|
|
|
213
216
|
## Related Packages
|
|
214
217
|
|
package/dist/service.d.ts
CHANGED
|
@@ -57,9 +57,10 @@ export declare class NotificationsService implements Notifications {
|
|
|
57
57
|
private shouldQueueSingleDispatch;
|
|
58
58
|
private shouldQueue;
|
|
59
59
|
private publishLifecycleEvent;
|
|
60
|
-
private
|
|
60
|
+
private publishLifecycleEventBestEffort;
|
|
61
61
|
private publishFailureLifecycleEvent;
|
|
62
62
|
private publishFailureLifecycleEvents;
|
|
63
|
+
private publishRequestedLifecycleEvents;
|
|
63
64
|
private dispatchManyThroughSequentialQueueFallback;
|
|
64
65
|
}
|
|
65
66
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,oCAAoC,EACpC,mBAAmB,EACnB,+BAA+B,EAC/B,+BAA+B,EAC/B,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAE1B,aAAa,EAEd,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,qBACa,oBAAqB,YAAW,aAAa;IAItD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;gBAGtD,OAAO,EAAE,oCAAoC,EAC9D,QAAQ,EAAE,SAAS,mBAAmB,EAAE;IAO1C;;;;;;;;;;;;;;;;;;;OAmBG;IACG,QAAQ,CAAC,QAAQ,SAAS,2BAA2B,EACzD,YAAY,EAAE,QAAQ,EACtB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,oCAAoC,EACpC,mBAAmB,EACnB,+BAA+B,EAC/B,+BAA+B,EAC/B,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAE1B,aAAa,EAEd,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,qBACa,oBAAqB,YAAW,aAAa;IAItD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;gBAGtD,OAAO,EAAE,oCAAoC,EAC9D,QAAQ,EAAE,SAAS,mBAAmB,EAAE;IAO1C;;;;;;;;;;;;;;;;;;;OAmBG;IACG,QAAQ,CAAC,QAAQ,SAAS,2BAA2B,EACzD,YAAY,EAAE,QAAQ,EACtB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,0BAA0B,CAAC;IAmEtC;;;;;;;;OAQG;IACG,YAAY,CAAC,QAAQ,SAAS,2BAA2B,EAC7D,aAAa,EAAE,SAAS,QAAQ,EAAE,EAClC,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,+BAA+B,CAAC,QAAQ,CAAC,CAAC;IAiGrD;;;;OAIG;IACH,4BAA4B;IAS5B,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,4BAA4B;IAQpC,OAAO,CAAC,yBAAyB;IAIjC,OAAO,CAAC,WAAW;YAYL,qBAAqB;YA4BrB,+BAA+B;YAgB/B,4BAA4B;YAmB5B,6BAA6B;YA0B7B,+BAA+B;YAmB/B,0CAA0C;CA4DzD"}
|
package/dist/service.js
CHANGED
|
@@ -50,12 +50,12 @@ class NotificationsService {
|
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
52
52
|
async dispatch(notification, options = {}) {
|
|
53
|
-
await this.
|
|
53
|
+
const requestedPublicationError = await this.publishLifecycleEventBestEffort('notification.dispatch.requested', notification, options);
|
|
54
54
|
if (this.shouldQueueSingleDispatch(options)) {
|
|
55
55
|
try {
|
|
56
56
|
this.requireChannel(notification.channel);
|
|
57
57
|
} catch (error) {
|
|
58
|
-
await this.publishFailureLifecycleEvent(notification, options, error);
|
|
58
|
+
await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
61
61
|
const job = this.createQueueJob(notification);
|
|
@@ -67,10 +67,10 @@ class NotificationsService {
|
|
|
67
67
|
queued: true,
|
|
68
68
|
status: 'queued'
|
|
69
69
|
};
|
|
70
|
-
await this.
|
|
70
|
+
await this.publishLifecycleEventBestEffort('notification.dispatch.queued', notification, options, result.deliveryId);
|
|
71
71
|
return result;
|
|
72
72
|
} catch (error) {
|
|
73
|
-
await this.publishFailureLifecycleEvent(notification, options, error);
|
|
73
|
+
await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
|
|
74
74
|
throw error;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -78,7 +78,7 @@ class NotificationsService {
|
|
|
78
78
|
try {
|
|
79
79
|
channel = this.requireChannel(notification.channel);
|
|
80
80
|
} catch (error) {
|
|
81
|
-
await this.publishFailureLifecycleEvent(notification, options, error);
|
|
81
|
+
await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
|
|
82
82
|
throw error;
|
|
83
83
|
}
|
|
84
84
|
try {
|
|
@@ -92,10 +92,10 @@ class NotificationsService {
|
|
|
92
92
|
queued: delivery.status === 'queued',
|
|
93
93
|
status: delivery.status ?? 'delivered'
|
|
94
94
|
};
|
|
95
|
-
await this.
|
|
95
|
+
await this.publishLifecycleEventBestEffort(result.queued ? 'notification.dispatch.queued' : 'notification.dispatch.delivered', notification, options, result.deliveryId);
|
|
96
96
|
return result;
|
|
97
97
|
} catch (error) {
|
|
98
|
-
await this.publishFailureLifecycleEvent(notification, options, error);
|
|
98
|
+
await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -120,14 +120,12 @@ class NotificationsService {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
if (this.shouldQueue(notifications.length, options)) {
|
|
123
|
-
|
|
124
|
-
await this.publishLifecycleEventSafely('notification.dispatch.requested', notification, options);
|
|
125
|
-
}
|
|
123
|
+
const requestedPublicationErrors = await this.publishRequestedLifecycleEvents(notifications, options);
|
|
126
124
|
let queue;
|
|
127
125
|
try {
|
|
128
126
|
queue = this.requireQueueAdapter();
|
|
129
127
|
} catch (error) {
|
|
130
|
-
await this.publishFailureLifecycleEvents(notifications, options, error);
|
|
128
|
+
await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
|
|
131
129
|
throw error;
|
|
132
130
|
}
|
|
133
131
|
try {
|
|
@@ -135,18 +133,18 @@ class NotificationsService {
|
|
|
135
133
|
this.requireChannel(notification.channel);
|
|
136
134
|
}
|
|
137
135
|
} catch (error) {
|
|
138
|
-
await this.publishFailureLifecycleEvents(notifications, options, error);
|
|
136
|
+
await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
|
|
139
137
|
throw error;
|
|
140
138
|
}
|
|
141
139
|
const jobs = notifications.map(notification => this.createQueueJob(notification));
|
|
142
140
|
if (!queue.enqueueMany) {
|
|
143
|
-
return this.dispatchManyThroughSequentialQueueFallback(notifications, jobs, options);
|
|
141
|
+
return this.dispatchManyThroughSequentialQueueFallback(notifications, jobs, options, requestedPublicationErrors);
|
|
144
142
|
}
|
|
145
143
|
let ids;
|
|
146
144
|
try {
|
|
147
|
-
ids = await queue.enqueueMany(jobs);
|
|
145
|
+
ids = validateQueueBatchDeliveryIds(await queue.enqueueMany(jobs), jobs.length);
|
|
148
146
|
} catch (error) {
|
|
149
|
-
await this.publishFailureLifecycleEvents(notifications, options, error);
|
|
147
|
+
await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
|
|
150
148
|
throw error;
|
|
151
149
|
}
|
|
152
150
|
const results = notifications.map((notification, index) => ({
|
|
@@ -157,7 +155,7 @@ class NotificationsService {
|
|
|
157
155
|
}));
|
|
158
156
|
for (let index = 0; index < notifications.length; index += 1) {
|
|
159
157
|
const notification = notifications[index];
|
|
160
|
-
await this.
|
|
158
|
+
await this.publishLifecycleEventBestEffort('notification.dispatch.queued', notification, options, results[index]?.deliveryId);
|
|
161
159
|
}
|
|
162
160
|
return {
|
|
163
161
|
failed: 0,
|
|
@@ -276,28 +274,45 @@ class NotificationsService {
|
|
|
276
274
|
};
|
|
277
275
|
await this.options.events.publisher.publish(event);
|
|
278
276
|
}
|
|
279
|
-
async
|
|
277
|
+
async publishLifecycleEventBestEffort(name, notification, options, deliveryId, error) {
|
|
280
278
|
try {
|
|
281
279
|
await this.publishLifecycleEvent(name, notification, options, deliveryId, error);
|
|
282
|
-
} catch {
|
|
283
|
-
return;
|
|
280
|
+
} catch (publicationError) {
|
|
281
|
+
return publicationError;
|
|
284
282
|
}
|
|
283
|
+
return undefined;
|
|
285
284
|
}
|
|
286
|
-
async publishFailureLifecycleEvent(notification, options, error) {
|
|
285
|
+
async publishFailureLifecycleEvent(notification, options, error, ...precedingPublicationErrors) {
|
|
286
|
+
const priorPublicationErrors = precedingPublicationErrors.filter(entry => entry !== undefined);
|
|
287
287
|
try {
|
|
288
288
|
await this.publishLifecycleEvent('notification.dispatch.failed', notification, options, undefined, error);
|
|
289
289
|
} catch (publicationError) {
|
|
290
|
-
throw createLifecyclePublicationFailureError(error, publicationError);
|
|
290
|
+
throw createLifecyclePublicationFailureError(error, ...priorPublicationErrors, publicationError);
|
|
291
|
+
}
|
|
292
|
+
if (priorPublicationErrors.length > 0) {
|
|
293
|
+
throw createLifecyclePublicationFailureError(error, ...priorPublicationErrors);
|
|
291
294
|
}
|
|
292
295
|
}
|
|
293
|
-
async publishFailureLifecycleEvents(notifications, options, error) {
|
|
296
|
+
async publishFailureLifecycleEvents(notifications, options, error, precedingPublicationErrors = []) {
|
|
297
|
+
const priorPublicationErrors = precedingPublicationErrors.filter(entry => entry !== undefined);
|
|
294
298
|
const failurePublicationResults = await Promise.allSettled(notifications.map(notification => this.publishLifecycleEvent('notification.dispatch.failed', notification, options, undefined, error)));
|
|
295
299
|
const publicationFailures = failurePublicationResults.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
296
300
|
if (publicationFailures.length > 0) {
|
|
297
|
-
throw createLifecyclePublicationFailureError(error, ...publicationFailures);
|
|
301
|
+
throw createLifecyclePublicationFailureError(error, ...priorPublicationErrors, ...publicationFailures);
|
|
302
|
+
}
|
|
303
|
+
if (priorPublicationErrors.length > 0) {
|
|
304
|
+
throw createLifecyclePublicationFailureError(error, ...priorPublicationErrors);
|
|
298
305
|
}
|
|
299
306
|
}
|
|
300
|
-
async
|
|
307
|
+
async publishRequestedLifecycleEvents(notifications, options) {
|
|
308
|
+
const publicationErrors = [];
|
|
309
|
+
for (const notification of notifications) {
|
|
310
|
+
const publicationError = await this.publishLifecycleEventBestEffort('notification.dispatch.requested', notification, options);
|
|
311
|
+
publicationErrors.push(publicationError);
|
|
312
|
+
}
|
|
313
|
+
return publicationErrors;
|
|
314
|
+
}
|
|
315
|
+
async dispatchManyThroughSequentialQueueFallback(notifications, jobs, options, requestedPublicationErrors) {
|
|
301
316
|
const queue = this.requireQueueAdapter();
|
|
302
317
|
const results = [];
|
|
303
318
|
const failures = [];
|
|
@@ -316,18 +331,18 @@ class NotificationsService {
|
|
|
316
331
|
status: 'queued'
|
|
317
332
|
};
|
|
318
333
|
results.push(result);
|
|
319
|
-
await this.
|
|
334
|
+
await this.publishLifecycleEventBestEffort('notification.dispatch.queued', notification, options, deliveryId);
|
|
320
335
|
} catch (error) {
|
|
321
336
|
const failure = {
|
|
322
337
|
error: error instanceof Error ? error : new Error('Notification queue enqueue failed.'),
|
|
323
338
|
notification
|
|
324
339
|
};
|
|
325
340
|
if (!(options.continueOnError ?? false)) {
|
|
326
|
-
await this.publishFailureLifecycleEvents(notifications.slice(index), options, error);
|
|
341
|
+
await this.publishFailureLifecycleEvents(notifications.slice(index), options, error, requestedPublicationErrors.slice(index));
|
|
327
342
|
throw error;
|
|
328
343
|
}
|
|
329
344
|
try {
|
|
330
|
-
await this.publishFailureLifecycleEvent(notification, options, error);
|
|
345
|
+
await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationErrors[index]);
|
|
331
346
|
} catch (publicationError) {
|
|
332
347
|
failure.error = publicationError instanceof Error ? publicationError : createLifecyclePublicationFailureError(error, publicationError);
|
|
333
348
|
}
|
|
@@ -347,6 +362,31 @@ class NotificationsService {
|
|
|
347
362
|
}
|
|
348
363
|
}
|
|
349
364
|
export { _NotificationsService as NotificationsService };
|
|
365
|
+
function validateQueueBatchDeliveryIds(value, expectedCount) {
|
|
366
|
+
if (!Array.isArray(value)) {
|
|
367
|
+
throw createQueueBatchResultIntegrityError(`expected ${expectedCount} queue ids but received a non-array result`);
|
|
368
|
+
}
|
|
369
|
+
if (value.length !== expectedCount) {
|
|
370
|
+
throw createQueueBatchResultIntegrityError(`expected ${expectedCount} queue ids but received ${value.length}`);
|
|
371
|
+
}
|
|
372
|
+
const ids = [];
|
|
373
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
374
|
+
if (!Object.hasOwn(value, index)) {
|
|
375
|
+
throw createQueueBatchResultIntegrityError(`queue id at index ${index} must be present`);
|
|
376
|
+
}
|
|
377
|
+
const entry = value[index];
|
|
378
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
379
|
+
throw createQueueBatchResultIntegrityError(`queue id at index ${index} must be a non-empty string`);
|
|
380
|
+
}
|
|
381
|
+
ids.push(entry);
|
|
382
|
+
}
|
|
383
|
+
return ids;
|
|
384
|
+
}
|
|
385
|
+
function createQueueBatchResultIntegrityError(message) {
|
|
386
|
+
const error = new Error(`Notifications queue adapter returned an invalid enqueueMany() result: ${message}.`);
|
|
387
|
+
error.name = 'NotificationQueueResultIntegrityError';
|
|
388
|
+
return error;
|
|
389
|
+
}
|
|
350
390
|
function createLifecyclePublicationFailureError(dispatchError, ...publicationErrors) {
|
|
351
391
|
const primaryMessage = dispatchError instanceof Error ? dispatchError.message : 'Notification dispatch failed.';
|
|
352
392
|
return new AggregateError([dispatchError, ...publicationErrors], `Notification dispatch failed, and failed lifecycle event publication also failed: ${primaryMessage}`);
|
|
@@ -364,9 +404,31 @@ function stableStringify(value) {
|
|
|
364
404
|
if (value === null || typeof value !== 'object') {
|
|
365
405
|
return JSON.stringify(value) ?? String(value);
|
|
366
406
|
}
|
|
407
|
+
if (value instanceof Date) {
|
|
408
|
+
return Number.isNaN(value.getTime()) ? 'Date:Invalid' : `Date:${JSON.stringify(value.toISOString())}`;
|
|
409
|
+
}
|
|
410
|
+
if (value instanceof URL) {
|
|
411
|
+
return `URL:${JSON.stringify(value.href)}`;
|
|
412
|
+
}
|
|
413
|
+
if (value instanceof URLSearchParams) {
|
|
414
|
+
return `URLSearchParams:${JSON.stringify(value.toString())}`;
|
|
415
|
+
}
|
|
416
|
+
if (value instanceof RegExp) {
|
|
417
|
+
return `RegExp:${JSON.stringify(value.source)}/${value.flags}`;
|
|
418
|
+
}
|
|
419
|
+
if (value instanceof Map) {
|
|
420
|
+
const entries = Array.from(value.entries()).map(([key, entry]) => `[${stableStringify(key)},${stableStringify(entry)}]`).sort();
|
|
421
|
+
return `Map:{${entries.join(',')}}`;
|
|
422
|
+
}
|
|
423
|
+
if (value instanceof Set) {
|
|
424
|
+
const entries = Array.from(value.values()).map(entry => stableStringify(entry)).sort();
|
|
425
|
+
return `Set:[${entries.join(',')}]`;
|
|
426
|
+
}
|
|
367
427
|
if (Array.isArray(value)) {
|
|
368
428
|
return `[${value.map(entry => stableStringify(entry)).join(',')}]`;
|
|
369
429
|
}
|
|
430
|
+
const prototype = Object.getPrototypeOf(value);
|
|
431
|
+
const objectTag = prototype && prototype !== Object.prototype ? `${prototype.constructor?.name ?? 'Object'}:` : '';
|
|
370
432
|
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
|
|
371
|
-
return
|
|
433
|
+
return `${objectTag}{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(',')}}`;
|
|
372
434
|
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"event-bus",
|
|
10
10
|
"channels"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.2",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@fluojs/core": "^1.0.3",
|
|
40
|
-
"@fluojs/di": "^1.0
|
|
41
|
-
"@fluojs/runtime": "^1.1.
|
|
40
|
+
"@fluojs/di": "^1.1.0",
|
|
41
|
+
"@fluojs/runtime": "^1.1.8"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"vitest": "^3.2.4"
|