@fluojs/cqrs 1.1.2 → 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 +7 -7
- package/README.md +7 -7
- package/dist/buses/command-bus.d.ts +9 -2
- package/dist/buses/command-bus.d.ts.map +1 -1
- package/dist/buses/command-bus.js +54 -24
- package/dist/buses/event-bus.d.ts +6 -7
- package/dist/buses/event-bus.d.ts.map +1 -1
- package/dist/buses/event-bus.js +52 -74
- package/dist/buses/event-handler-discovery.d.ts +12 -0
- package/dist/buses/event-handler-discovery.d.ts.map +1 -0
- package/dist/buses/event-handler-discovery.js +35 -0
- package/dist/buses/publish-drain-tracker.d.ts +38 -0
- package/dist/buses/publish-drain-tracker.d.ts.map +1 -0
- package/dist/buses/publish-drain-tracker.js +86 -0
- package/dist/buses/query-bus.d.ts +9 -2
- package/dist/buses/query-bus.d.ts.map +1 -1
- package/dist/buses/query-bus.js +54 -24
- package/dist/buses/saga-bus.d.ts +8 -9
- package/dist/buses/saga-bus.d.ts.map +1 -1
- package/dist/buses/saga-bus.js +37 -96
- package/dist/buses/saga-discovery.d.ts +12 -0
- package/dist/buses/saga-discovery.d.ts.map +1 -0
- package/dist/buses/saga-discovery.js +39 -0
- package/dist/buses/saga-drain.d.ts +9 -0
- package/dist/buses/saga-drain.d.ts.map +1 -0
- package/dist/buses/saga-drain.js +34 -0
- package/dist/buses/saga-topology.d.ts +15 -0
- package/dist/buses/saga-topology.d.ts.map +1 -0
- package/dist/buses/saga-topology.js +39 -0
- package/dist/discovery.d.ts +16 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +16 -9
- package/dist/dispatch-context.d.ts +33 -0
- package/dist/dispatch-context.d.ts.map +1 -0
- package/dist/dispatch-context.js +42 -0
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +8 -1
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +4 -0
- package/dist/types.d.ts +1 -1
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -139,7 +139,7 @@ class GetOrderSummaryHandler
|
|
|
139
139
|
}
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
`CqrsModule.forRoot(...)`를 import하는 애플리케이션 모듈에 projection handler, query handler, projection store를 singleton provider로 등록하세요. `CqrsEventBusService.publish(new OrderPlacedEvent(...))`는 일치하는 `@EventHandler(...)` provider를 saga와 위임 `@fluojs/event-bus` 발행보다 먼저 실행하므로, read model은 문서화된 CQRS event pipeline을 통해 write-side fact를 관찰합니다. Event replay, retry, 외부 transport가 같은 business fact를 두 번 이상 전달할 수 있으므로 projection handler는 idempotent하게 유지하세요.
|
|
142
|
+
`CqrsModule.forRoot(...)`를 import하는 애플리케이션 모듈에 projection handler, query handler, projection store를 singleton provider로 등록하세요. CQRS handler discovery는 provider registration만 검사합니다. HTTP controller는 request boundary에 남으며 controller class가 실수로 CQRS handler decorator를 가지고 있어도 무시됩니다. `CqrsModule.forRoot(...)`는 기본적으로 bus를 global로 export하며, `CqrsModule.forRoot({ global: false })`는 `eventBus.global`을 명시적으로 override하지 않는 한 해당 CQRS module을 import한 module을 통해서만 bus provider와 위임 `@fluojs/event-bus` provider가 보이도록 유지합니다. `CqrsEventBusService.publish(new OrderPlacedEvent(...))`는 일치하는 `@EventHandler(...)` provider를 saga와 위임 `@fluojs/event-bus` 발행보다 먼저 실행하므로, read model은 문서화된 CQRS event pipeline을 통해 write-side fact를 관찰합니다. Fan-out identity는 singleton provider token을 따릅니다. Decorated handler class 하나를 서로 다른 두 token으로 등록하면 두 registration이 모두 의도적으로 호출되고, 같은 token과 event route가 반복 discovery될 때만 deduplicate됩니다. Event replay, retry, 외부 transport가 같은 business fact를 두 번 이상 전달할 수 있으므로 projection handler는 idempotent하게 유지하세요.
|
|
143
143
|
|
|
144
144
|
### Saga 프로세스 매니저
|
|
145
145
|
|
|
@@ -168,19 +168,19 @@ class UserSaga implements ISaga<UserCreatedEvent> {
|
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Saga 실행은 같은 프로세스 안에서 동일
|
|
171
|
+
Saga 실행은 같은 프로세스 안에서 동일 provider-token/event route로 순환 재진입하거나 중첩 hop 수가 32를 넘으면 `SagaTopologyError`로 즉시 실패합니다. 같은 decorated saga class를 사용하는 서로 다른 singleton token은 별도의 fan-out route로 유지됩니다. 서로 다른 이벤트 단계를 순차 처리하는 multi-stage saga는 계속 허용되지만, in-process saga graph 전체는 비순환(acyclic) 구조를 유지해야 하며, 의도적인 순환/피드백 루프나 더 긴 체인은 외부 transport, scheduler, 또는 다른 bounded boundary 뒤로 이동해야 합니다.
|
|
172
172
|
|
|
173
|
-
Saga, command handler, query handler, event handler 안에서 다시 CQRS `execute(...)`, `publish(...)`, `publishAll(...)`를 호출할 때는 optional `CqrsDispatchContext` 인자를 그대로 전달하세요. CQRS는 이 명시적인 runtime-agnostic context로 Node.js async-local API에 의존하지 않고 nested dispatch 전반의 saga topology check를 유지합니다. 이 context는 opaque
|
|
173
|
+
Saga, command handler, query handler, event handler 안에서 다시 CQRS `execute(...)`, `publish(...)`, `publishAll(...)`를 호출할 때는 optional `CqrsDispatchContext` 인자를 그대로 전달하세요. CQRS는 이 명시적인 runtime-agnostic context로 Node.js async-local API에 의존하지 않고 nested dispatch 전반의 saga topology check를 유지합니다. 이 context는 opaque, frozen fieldless pass-through value이며, 신뢰하는 topology와 shutdown-drain state는 CQRS 내부에 비공개로 유지됩니다. Caller-shaped object와 복사된 값은 신뢰된 runtime state를 운반하지 않으므로 context를 직접 생성, 복제, 검사, mutate하지 마세요.
|
|
174
174
|
|
|
175
175
|
### Event 발행 계약
|
|
176
176
|
|
|
177
|
-
`CqrsEventBusService.publish(event)`는 CQRS event pipeline을 고정된 순서로 실행합니다. 먼저 일치하는 `@EventHandler(...)` provider를 실행하고, 그다음 일치하는 `@Saga(...)` provider를 실행한 뒤, 마지막으로 `@fluojs/event-bus`로 위임 발행합니다. `publishAll(events)`는 각 event의 CQRS handler, saga, 위임 발행 호출을 기다린 뒤 다음 event를 발행하므로 입력 순서를 보존합니다. 애플리케이션 shutdown 중에는 CQRS event bus가 진행 중인 `publish(...)` pipeline, `publishAll(...)` sequence, saga execution chain이 settle될 때까지 기다린 뒤 stopped 상태로 전환합니다.
|
|
177
|
+
`CqrsEventBusService.publish(event)`는 CQRS event pipeline을 고정된 순서로 실행합니다. 먼저 일치하는 `@EventHandler(...)` provider를 실행하고, 그다음 일치하는 `@Saga(...)` provider를 실행한 뒤, 마지막으로 `@fluojs/event-bus`로 위임 발행합니다. `publishAll(events)`는 각 event의 CQRS handler, saga, 위임 발행 호출을 기다린 뒤 다음 event를 발행하므로 입력 순서를 보존합니다. 애플리케이션 shutdown 중에는 CQRS event bus가 진행 중인 `publish(...)` pipeline, `publishAll(...)` sequence, saga execution chain이 settle될 때까지 기다린 뒤 stopped 상태로 전환합니다. Command bus와 query bus는 shutdown이 시작되면 새로운 `execute(...)` 호출을 거부하고 shutdown 중 preload된 handler cache를 정리하므로, close 이후 dispatch가 오래된 provider instance를 재사용할 수 없습니다. Shutdown이 시작되면 brand-new external `publish(...)`, `publishAll(...)`, direct saga dispatch 호출은 거부됩니다. 이미 활성화된 handler나 saga에서 호출되는 nested `publish(...)` 또는 `publishAll(...)`은 CQRS가 제공한 `CqrsDispatchContext`를 그대로 전달할 때만 계속 진행할 수 있습니다. 이렇게 하면 drain 작업은 활성 pipeline 안에 머무르고 관련 없는 caller는 계속 거부됩니다. 이미 진행 중인 publish와 saga 작업은 bounded shutdown window 안에서 계속 drain됩니다. Shutdown drain은 기본값이 5000ms인 `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`로 제한됩니다. CQRS handler, saga 또는 위임 publish chain이 이 bound 이후에도 멈춰 있으면 CQRS는 degraded status diagnostic을 기록하고 경고를 남긴 뒤 애플리케이션 close를 무기한 hang시키지 않고 계속 진행합니다. `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })`로 설정한 경우 위임 발행 호출은 일치하는 `@OnEvent(...)` subscriber가 완료되기 전에 resolve될 수 있으므로, 이 모드에서 `publish(...)`, `publishAll(...)`, shutdown drain 완료는 subscriber 완료를 의미하지 않습니다.
|
|
178
178
|
|
|
179
179
|
각 CQRS event handler와 saga는 매칭된 event prototype이 복원된 격리 event 복사본을 받습니다. 이 복사본을 mutate해도 변경은 현재 handler 또는 saga route 안에만 머물며, 다른 CQRS handler, saga, 원본 event 객체, 또는 위임된 `@fluojs/event-bus` subscriber에는 보이지 않습니다. 위임된 event-bus 발행은 CQRS side effect가 끝난 뒤 원본 event를 받으므로, `@OnEvent(...)` projection과 transport는 CQRS handler가 mutate한 복사본이 아니라 호출자가 소유한 payload를 관찰합니다.
|
|
180
180
|
|
|
181
181
|
Event class는 payload state를 clone 가능하고 enumerable하게 유지해야 합니다. 문자열 key와 symbol key를 가진 enumerable payload field는 shared core clone fallback으로 보존되지만, 열린 socket, function, process-local handle처럼 의도적으로 clone할 수 없는 resource는 발행 전에 ID나 다른 serializable boundary로 표현해야 합니다.
|
|
182
182
|
|
|
183
|
-
CQRS handler, event handler, saga는 singleton provider에서만 discovery됩니다. Non-singleton registration은 경고와 함께 건너뜁니다.
|
|
183
|
+
CQRS handler, event handler, saga는 singleton provider에서만 discovery됩니다. Non-singleton registration은 경고와 함께 건너뜁니다. Event handler와 saga fan-out은 singleton provider token으로 구분되므로 같은 decorated class를 사용해도 서로 다른 token은 별도 route로 유지됩니다.
|
|
184
184
|
|
|
185
185
|
### 심볼 토큰
|
|
186
186
|
|
|
@@ -199,7 +199,7 @@ class TokenInjectedService {
|
|
|
199
199
|
## 공개 API 개요
|
|
200
200
|
|
|
201
201
|
### 모듈 및 프로바이더
|
|
202
|
-
- `CqrsModule.forRoot(options)`: 메인 진입점입니다. 버스를 등록하고
|
|
202
|
+
- `CqrsModule.forRoot(options)`: 메인 진입점입니다. 버스를 등록하고 provider-only discovery를 시작합니다. Bus provider는 기본적으로 global이며 module-local visibility가 필요하면 `global: false`를 전달합니다.
|
|
203
203
|
- Module option은 명시적인 `commandHandlers`, `queryHandlers`, `eventHandlers`, `sagas`, 위임 `eventBus` option을 받을 수 있습니다.
|
|
204
204
|
- `CommandBusLifecycleService`: Command 실행을 위한 기본 서비스입니다.
|
|
205
205
|
- `QueryBusLifecycleService`: Query 실행을 위한 기본 서비스입니다.
|
|
@@ -219,7 +219,7 @@ class TokenInjectedService {
|
|
|
219
219
|
### 오류
|
|
220
220
|
- `CommandHandlerNotFoundException`, `QueryHandlerNotFoundException`: bus에 일치하는 handler가 없을 때 발생합니다.
|
|
221
221
|
- `DuplicateCommandHandlerError`, `DuplicateQueryHandlerError`: 서로 다른 singleton provider가 같은 command 또는 query type을 claim할 때 발생합니다.
|
|
222
|
-
- `DuplicateEventHandlerError`:
|
|
222
|
+
- `DuplicateEventHandlerError`: 호환성을 위해서만 export가 유지되며, event-handler discovery는 이 오류를 throw하거나 중복 registration을 failure로 취급하지 않습니다. 같은 provider token과 event route가 반복 discovery되면 조용히 deduplicate하고, 서로 다른 singleton provider token은 discovery 순서대로 fan-out되는 유효한 route로 유지합니다.
|
|
223
223
|
- `SagaExecutionError`: 예상하지 못한 non-Fluo saga 실패를 감쌉니다.
|
|
224
224
|
- `SagaTopologyError`: 자기 트리거, 순환, 또는 과도하게 깊은 in-process saga graph를 감지했을 때 발생합니다.
|
|
225
225
|
|
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ class GetOrderSummaryHandler
|
|
|
139
139
|
}
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Register the projection handler, query handler, and projection store as singleton providers in the same application module that imports `CqrsModule.forRoot(...)`. `CqrsEventBusService.publish(new OrderPlacedEvent(...))` runs matching `@EventHandler(...)` providers before sagas and delegated `@fluojs/event-bus` publication, so the read model observes the write-side fact through the documented CQRS event pipeline. Keep projection handlers idempotent because event replay, retries, or external transports can deliver the same business fact more than once.
|
|
142
|
+
Register the projection handler, query handler, and projection store as singleton providers in the same application module that imports `CqrsModule.forRoot(...)`. CQRS handler discovery inspects provider registrations only; HTTP controllers stay on the request boundary and are ignored even when a controller class accidentally carries a CQRS handler decorator. `CqrsModule.forRoot(...)` exports the buses globally by default, and `CqrsModule.forRoot({ global: false })` keeps those bus providers and the delegated `@fluojs/event-bus` providers visible only through modules that import the CQRS module unless `eventBus.global` is explicitly overridden. `CqrsEventBusService.publish(new OrderPlacedEvent(...))` runs matching `@EventHandler(...)` providers before sagas and delegated `@fluojs/event-bus` publication, so the read model observes the write-side fact through the documented CQRS event pipeline. Fan-out identity follows the singleton provider token: registering one decorated handler class under two distinct tokens intentionally invokes both registrations, while repeated discovery of the same token and event route is deduplicated. Keep projection handlers idempotent because event replay, retries, or external transports can deliver the same business fact more than once.
|
|
143
143
|
|
|
144
144
|
### Saga Process Managers
|
|
145
145
|
|
|
@@ -168,19 +168,19 @@ class UserSaga implements ISaga<UserCreatedEvent> {
|
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Saga execution fails fast with `SagaTopologyError` when an in-process publish chain re-enters the same
|
|
171
|
+
Saga execution fails fast with `SagaTopologyError` when an in-process publish chain re-enters the same provider-token/event route cyclically or exceeds 32 nested saga hops. Distinct singleton tokens that use the same decorated saga class remain distinct fan-out routes. Multi-stage sagas may still react to different event types in sequence, but in-process saga graphs must stay acyclic overall; move intentionally cyclic or long-running feedback loops behind an external transport, scheduler, or other bounded boundary.
|
|
172
172
|
|
|
173
|
-
When a saga, command handler, query handler, or event handler performs another CQRS `execute(...)`, `publish(...)`, or `publishAll(...)` call, pass the optional `CqrsDispatchContext` argument through unchanged. CQRS uses this explicit runtime-agnostic context to keep saga topology checks intact across nested dispatch without relying on Node.js async-local APIs. The context is opaque
|
|
173
|
+
When a saga, command handler, query handler, or event handler performs another CQRS `execute(...)`, `publish(...)`, or `publishAll(...)` call, pass the optional `CqrsDispatchContext` argument through unchanged. CQRS uses this explicit runtime-agnostic context to keep saga topology checks intact across nested dispatch without relying on Node.js async-local APIs. The context is an opaque, frozen fieldless pass-through value; trusted topology and shutdown-drain state remains private to CQRS. Do not construct, clone, inspect, or mutate it because caller-shaped objects and copied values do not carry trusted runtime state.
|
|
174
174
|
|
|
175
175
|
### Event Publishing Contracts
|
|
176
176
|
|
|
177
|
-
`CqrsEventBusService.publish(event)` runs the CQRS event pipeline in a fixed order: matching `@EventHandler(...)` providers first, matching `@Saga(...)` providers second, and delegated `@fluojs/event-bus` publication last. `publishAll(events)` preserves the input order by awaiting each event's CQRS handlers, sagas, and delegated publication call before publishing the next event. During application shutdown, the CQRS event bus waits for active `publish(...)` pipelines, `publishAll(...)` sequences, and saga execution chains to settle before marking itself stopped. Once shutdown starts, new `publish(...)`, `publishAll(...)`, and direct saga dispatch calls are rejected
|
|
177
|
+
`CqrsEventBusService.publish(event)` runs the CQRS event pipeline in a fixed order: matching `@EventHandler(...)` providers first, matching `@Saga(...)` providers second, and delegated `@fluojs/event-bus` publication last. `publishAll(events)` preserves the input order by awaiting each event's CQRS handlers, sagas, and delegated publication call before publishing the next event. During application shutdown, the CQRS event bus waits for active `publish(...)` pipelines, `publishAll(...)` sequences, and saga execution chains to settle before marking itself stopped. Command and query buses reject new `execute(...)` calls once shutdown starts and clear their preloaded handler caches during shutdown, so post-close dispatch cannot reuse stale provider instances. Once shutdown starts, brand-new external `publish(...)`, `publishAll(...)`, and direct saga dispatch calls are rejected. A nested `publish(...)` or `publishAll(...)` invoked from an already active handler or saga may continue only when it passes through the CQRS-provided `CqrsDispatchContext`; this keeps drain work inside the active pipeline while still rejecting unrelated callers. Already active publish and saga work continues draining inside the bounded shutdown window. Shutdown drain is bounded by `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`, which defaults to 5000ms; if a CQRS handler, saga, or delegated publish chain is still stuck after the bound, CQRS records degraded status diagnostics, logs a warning, and lets application close continue instead of hanging indefinitely. When `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })` is configured, the delegated publication call can resolve before matching `@OnEvent(...)` subscribers finish, so `publish(...)`, `publishAll(...)`, and shutdown drain completion do not imply subscriber completion in that mode.
|
|
178
178
|
|
|
179
179
|
Each CQRS event handler and saga receives an isolated event copy with the matched event prototype restored. Mutating that copy is local to the current handler or saga route; those mutations are not visible to other CQRS handlers, sagas, the original event object, or delegated `@fluojs/event-bus` subscribers. The delegated event-bus publication receives the original event after CQRS side effects complete, so `@OnEvent(...)` projections and transports observe the caller-owned payload rather than a CQRS handler's mutated copy.
|
|
180
180
|
|
|
181
181
|
Event classes should keep their payload state cloneable and enumerable. String-keyed and symbol-keyed enumerable payload fields are preserved by the shared core clone fallback, while intentionally non-cloneable resources such as open sockets, functions, or process-local handles should be represented by IDs or other serializable boundaries before publishing.
|
|
182
182
|
|
|
183
|
-
CQRS handlers, event handlers, and sagas are discovered only on singleton providers. Non-singleton registrations are skipped with warnings.
|
|
183
|
+
CQRS handlers, event handlers, and sagas are discovered only on singleton providers. Non-singleton registrations are skipped with warnings. Event-handler and saga fan-out is keyed by singleton provider token, so distinct tokens remain distinct routes even when they use the same decorated class.
|
|
184
184
|
|
|
185
185
|
### Symbol Tokens
|
|
186
186
|
|
|
@@ -199,7 +199,7 @@ class TokenInjectedService {
|
|
|
199
199
|
## Public API Overview
|
|
200
200
|
|
|
201
201
|
### Modules & Providers
|
|
202
|
-
- `CqrsModule.forRoot(options)`: Main entry point. Registers buses and starts discovery.
|
|
202
|
+
- `CqrsModule.forRoot(options)`: Main entry point. Registers buses and starts provider-only discovery. Bus providers are global by default; pass `global: false` for module-local visibility.
|
|
203
203
|
- Module options can provide explicit `commandHandlers`, `queryHandlers`, `eventHandlers`, `sagas`, and delegated `eventBus` options.
|
|
204
204
|
- `CommandBusLifecycleService`: Primary service for executing commands.
|
|
205
205
|
- `QueryBusLifecycleService`: Primary service for executing queries.
|
|
@@ -219,7 +219,7 @@ class TokenInjectedService {
|
|
|
219
219
|
### Errors
|
|
220
220
|
- `CommandHandlerNotFoundException`, `QueryHandlerNotFoundException`: Raised when a bus has no matching handler.
|
|
221
221
|
- `DuplicateCommandHandlerError`, `DuplicateQueryHandlerError`: Raised when different singleton providers claim the same command or query type.
|
|
222
|
-
- `DuplicateEventHandlerError`:
|
|
222
|
+
- `DuplicateEventHandlerError`: Retained only as a compatibility export; event-handler discovery does not throw it or treat duplicate registrations as failures. Repeated discovery of the same provider token and event route is silently deduplicated, while distinct singleton provider tokens remain valid fan-out routes in discovery order.
|
|
223
223
|
- `SagaExecutionError`: Wraps unexpected non-Fluo saga failures.
|
|
224
224
|
- `SagaTopologyError`: Raised when saga orchestration detects a self-triggering, cyclic, or over-deep in-process saga graph.
|
|
225
225
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Container } from '@fluojs/di';
|
|
2
|
+
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
|
|
2
3
|
import { CqrsBusBase } from '../discovery.js';
|
|
3
4
|
import type { CommandBus, CqrsDispatchContext, ICommand } from '../types.js';
|
|
4
5
|
/**
|
|
@@ -7,11 +8,15 @@ import type { CommandBus, CqrsDispatchContext, ICommand } from '../types.js';
|
|
|
7
8
|
* The command bus resolves singleton handlers only, warns on unsupported scopes,
|
|
8
9
|
* and throws explicit contract errors when no handler or multiple handlers exist.
|
|
9
10
|
*/
|
|
10
|
-
export declare class CommandBusLifecycleService extends CqrsBusBase implements CommandBus, OnApplicationBootstrap {
|
|
11
|
+
export declare class CommandBusLifecycleService extends CqrsBusBase implements CommandBus, OnApplicationBootstrap, OnApplicationShutdown {
|
|
11
12
|
private descriptors;
|
|
12
13
|
private discoveryPromise;
|
|
13
14
|
private discovered;
|
|
15
|
+
private lifecycleState;
|
|
16
|
+
private unregisterShutdownStartCleanup;
|
|
17
|
+
constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, registerRuntimeCleanup?: RuntimeCleanupRegistration);
|
|
14
18
|
onApplicationBootstrap(): Promise<void>;
|
|
19
|
+
onApplicationShutdown(): Promise<void>;
|
|
15
20
|
/**
|
|
16
21
|
* Executes one command by dispatching it to the discovered handler for its constructor.
|
|
17
22
|
*
|
|
@@ -23,6 +28,8 @@ export declare class CommandBusLifecycleService extends CqrsBusBase implements C
|
|
|
23
28
|
* @throws {InvariantError} When the resolved provider does not implement `execute(command)`.
|
|
24
29
|
*/
|
|
25
30
|
execute<TCommand extends ICommand, TResult = void>(command: TCommand, context?: CqrsDispatchContext): Promise<TResult>;
|
|
31
|
+
private assertAcceptingNewWork;
|
|
32
|
+
private markApplicationShutdownStarted;
|
|
26
33
|
private ensureDiscovered;
|
|
27
34
|
private discoverHandlers;
|
|
28
35
|
private discoverCommandDescriptors;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../../src/buses/command-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../../src/buses/command-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEpJ,OAAO,EAAE,WAAW,EAA4D,MAAM,iBAAiB,CAAC;AAGxG,OAAO,KAAK,EACV,UAAU,EAGV,mBAAmB,EACnB,QAAQ,EAET,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,0BAA2B,SAAQ,WAAY,YAAW,UAAU,EAAE,sBAAsB,EAAE,qBAAqB;IAC9H,OAAO,CAAC,WAAW,CAAoD;IACvE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,8BAA8B,CAA2B;gBAG/D,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,sBAAsB,GAAE,0BAAkD;IAStE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5C;;;;;;;;;OASG;IACG,OAAO,CAAC,QAAQ,SAAS,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB5H,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,8BAA8B;YAMxB,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,0BAA0B;CA2CnC"}
|
|
@@ -5,8 +5,8 @@ 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, InvariantError } from '@fluojs/core';
|
|
8
|
-
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
|
-
import { CqrsBusBase, createDuplicateHandlerMessage } from '../discovery.js';
|
|
8
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
|
+
import { CqrsBusBase, createDuplicateHandlerMessage, isSameHandlerRegistration } from '../discovery.js';
|
|
10
10
|
import { CommandHandlerNotFoundException, DuplicateCommandHandlerError } from '../errors.js';
|
|
11
11
|
import { getCommandHandlerMetadata } from '../metadata.js';
|
|
12
12
|
function isCommandHandler(value) {
|
|
@@ -25,13 +25,38 @@ function isCommandHandler(value) {
|
|
|
25
25
|
let _CommandBusLifecycleS;
|
|
26
26
|
class CommandBusLifecycleService extends CqrsBusBase {
|
|
27
27
|
static {
|
|
28
|
-
[_CommandBusLifecycleS, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], [], 0, void 0, CqrsBusBase).c;
|
|
28
|
+
[_CommandBusLifecycleS, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
|
|
29
29
|
}
|
|
30
30
|
descriptors = new Map();
|
|
31
31
|
discoveryPromise;
|
|
32
32
|
discovered = false;
|
|
33
|
+
lifecycleState = 'created';
|
|
34
|
+
unregisterShutdownStartCleanup;
|
|
35
|
+
constructor(runtimeContainer, compiledModules, logger, registerRuntimeCleanup = () => () => undefined) {
|
|
36
|
+
super(runtimeContainer, compiledModules, logger);
|
|
37
|
+
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
|
|
38
|
+
this.markApplicationShutdownStarted();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
33
41
|
async onApplicationBootstrap() {
|
|
34
|
-
|
|
42
|
+
this.lifecycleState = 'discovering';
|
|
43
|
+
try {
|
|
44
|
+
await this.ensureDiscovered();
|
|
45
|
+
this.lifecycleState = 'ready';
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.lifecycleState = 'failed';
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async onApplicationShutdown() {
|
|
52
|
+
this.markApplicationShutdownStarted();
|
|
53
|
+
this.unregisterShutdownStartCleanup?.();
|
|
54
|
+
this.unregisterShutdownStartCleanup = undefined;
|
|
55
|
+
this.descriptors.clear();
|
|
56
|
+
this.handlerInstances.clear();
|
|
57
|
+
this.discovered = false;
|
|
58
|
+
this.discoveryPromise = undefined;
|
|
59
|
+
this.lifecycleState = 'stopped';
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
/**
|
|
@@ -45,6 +70,7 @@ class CommandBusLifecycleService extends CqrsBusBase {
|
|
|
45
70
|
* @throws {InvariantError} When the resolved provider does not implement `execute(command)`.
|
|
46
71
|
*/
|
|
47
72
|
async execute(command, context) {
|
|
73
|
+
this.assertAcceptingNewWork('execute');
|
|
48
74
|
await this.ensureDiscovered();
|
|
49
75
|
const commandType = command.constructor;
|
|
50
76
|
const descriptor = this.descriptors.get(commandType);
|
|
@@ -57,6 +83,16 @@ class CommandBusLifecycleService extends CqrsBusBase {
|
|
|
57
83
|
}
|
|
58
84
|
return await instance.execute(command, context);
|
|
59
85
|
}
|
|
86
|
+
assertAcceptingNewWork(operation) {
|
|
87
|
+
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
|
|
88
|
+
throw new InvariantError(`CQRS command bus cannot ${operation} after shutdown has started.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
markApplicationShutdownStarted() {
|
|
92
|
+
if (this.lifecycleState !== 'stopped') {
|
|
93
|
+
this.lifecycleState = 'stopping';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
60
96
|
async ensureDiscovered() {
|
|
61
97
|
if (this.discovered) {
|
|
62
98
|
return;
|
|
@@ -82,7 +118,6 @@ class CommandBusLifecycleService extends CqrsBusBase {
|
|
|
82
118
|
}
|
|
83
119
|
discoverCommandDescriptors() {
|
|
84
120
|
const descriptors = new Map();
|
|
85
|
-
const seenByTarget = new WeakMap();
|
|
86
121
|
for (const candidate of this.discoveryCandidates()) {
|
|
87
122
|
const metadata = getCommandHandlerMetadata(candidate.targetType);
|
|
88
123
|
if (!metadata) {
|
|
@@ -92,27 +127,22 @@ class CommandBusLifecycleService extends CqrsBusBase {
|
|
|
92
127
|
this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @CommandHandler() but is registered with ${candidate.scope} scope. Command handlers are registered only for singleton providers.`, 'CommandBusLifecycleService');
|
|
93
128
|
continue;
|
|
94
129
|
}
|
|
95
|
-
const seenCommandTypes = seenByTarget.get(candidate.targetType) ?? new Set();
|
|
96
|
-
if (seenCommandTypes.has(metadata.commandType)) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
seenCommandTypes.add(metadata.commandType);
|
|
100
|
-
seenByTarget.set(candidate.targetType, seenCommandTypes);
|
|
101
130
|
const existing = descriptors.get(metadata.commandType);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
targetType: candidate.targetType,
|
|
113
|
-
token: candidate.token
|
|
114
|
-
});
|
|
131
|
+
const nextDescriptor = {
|
|
132
|
+
moduleName: candidate.moduleName,
|
|
133
|
+
targetType: candidate.targetType,
|
|
134
|
+
token: candidate.token
|
|
135
|
+
};
|
|
136
|
+
if (existing) {
|
|
137
|
+
if (isSameHandlerRegistration(existing, nextDescriptor)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw new DuplicateCommandHandlerError(createDuplicateHandlerMessage('command', metadata.commandType, existing, nextDescriptor));
|
|
115
141
|
}
|
|
142
|
+
descriptors.set(metadata.commandType, {
|
|
143
|
+
commandType: metadata.commandType,
|
|
144
|
+
...nextDescriptor
|
|
145
|
+
});
|
|
116
146
|
}
|
|
117
147
|
return descriptors;
|
|
118
148
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EventBus } from '@fluojs/event-bus';
|
|
2
|
-
import type { OnApplicationBootstrap, OnApplicationShutdown } from '@fluojs/runtime';
|
|
2
|
+
import type { OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
|
|
3
3
|
import { CqrsBusBase } from '../discovery.js';
|
|
4
4
|
import type { CqrsModuleOptions } from '../module.js';
|
|
5
5
|
import type { CqrsDispatchContext, CqrsEventBus, IEvent } from '../types.js';
|
|
@@ -17,10 +17,10 @@ export declare class CqrsEventBusService extends CqrsBusBase implements CqrsEven
|
|
|
17
17
|
private descriptors;
|
|
18
18
|
private discoveryPromise;
|
|
19
19
|
private discovered;
|
|
20
|
-
private readonly
|
|
21
|
-
private shutdownDrainTimeouts;
|
|
20
|
+
private readonly publishDrainTracker;
|
|
22
21
|
private lifecycleState;
|
|
23
|
-
|
|
22
|
+
private unregisterShutdownStartCleanup;
|
|
23
|
+
constructor(eventBus: EventBus, sagaService: CqrsSagaLifecycleService, runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions, registerRuntimeCleanup?: RuntimeCleanupRegistration);
|
|
24
24
|
onApplicationBootstrap(): Promise<void>;
|
|
25
25
|
onApplicationShutdown(): Promise<void>;
|
|
26
26
|
/**
|
|
@@ -50,9 +50,8 @@ export declare class CqrsEventBusService extends CqrsBusBase implements CqrsEven
|
|
|
50
50
|
private runPublishPipeline;
|
|
51
51
|
private runPublishAllPipeline;
|
|
52
52
|
private assertAcceptingNewWork;
|
|
53
|
-
private
|
|
54
|
-
private
|
|
55
|
-
private awaitShutdownDrain;
|
|
53
|
+
private markApplicationShutdownStarted;
|
|
54
|
+
private createPublishContext;
|
|
56
55
|
private resolveShutdownDrainTimeoutMs;
|
|
57
56
|
private matchEventDescriptors;
|
|
58
57
|
private ensureDiscovered;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/buses/event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/buses/event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGjH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAM9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAyC,MAAM,EAAiB,MAAM,aAAa,CAAC;AAGnI,OAAO,EAAiC,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAiBxF;;;;;GAKG;AACH,qBASa,mBAAoB,SAAQ,WAAY,YAAW,YAAY,EAAE,sBAAsB,EAAE,qBAAqB;IASvH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAI5B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAbhC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA0B;IAC9D,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,8BAA8B,CAA2B;gBAG9C,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,wBAAwB,EACtD,gBAAgB,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EAC9D,eAAe,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EAC7D,MAAM,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EACnC,aAAa,GAAE,iBAAsB,EACtD,sBAAsB,GAAE,0BAAkD;IAUtE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5C;;;;OAIG;IACH,4BAA4B;IAe5B;;;;;;;;OAQG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IASjG;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;YASlG,kBAAkB;YAiBlB,qBAAqB;IAMnC,OAAO,CAAC,sBAAsB;IAc9B,OAAO,CAAC,8BAA8B;IAMtC,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,qBAAqB;YAIf,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CAGjC"}
|
package/dist/buses/event-bus.js
CHANGED
|
@@ -6,13 +6,15 @@ function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.descrip
|
|
|
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, InvariantError } from '@fluojs/core';
|
|
8
8
|
import { EVENT_BUS as FLUO_EVENT_BUS } from '@fluojs/event-bus';
|
|
9
|
-
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
10
10
|
import { CqrsBusBase } from '../discovery.js';
|
|
11
|
+
import { createInternalCqrsDispatchContext, getInternalCqrsDispatchContextState } from '../dispatch-context.js';
|
|
11
12
|
import { createIsolatedEvent } from '../event-clone.js';
|
|
12
|
-
import { getEventHandlerMetadata } from '../metadata.js';
|
|
13
13
|
import { createCqrsPlatformStatusSnapshot } from '../status.js';
|
|
14
14
|
import { CQRS_MODULE_OPTIONS } from '../tokens.js';
|
|
15
|
-
import {
|
|
15
|
+
import { discoverEventHandlerDescriptors } from './event-handler-discovery.js';
|
|
16
|
+
import { CqrsPublishDrainTracker } from './publish-drain-tracker.js';
|
|
17
|
+
import { CQRS_SAGA_DRAIN_AUTHORIZATION, CqrsSagaLifecycleService } from './saga-bus.js';
|
|
16
18
|
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
17
19
|
function isEventHandler(value) {
|
|
18
20
|
if (typeof value !== 'object' || value === null) {
|
|
@@ -30,19 +32,23 @@ function isEventHandler(value) {
|
|
|
30
32
|
let _CqrsEventBusService;
|
|
31
33
|
class CqrsEventBusService extends CqrsBusBase {
|
|
32
34
|
static {
|
|
33
|
-
[_CqrsEventBusService, _initClass] = _applyDecs(this, [Inject(FLUO_EVENT_BUS, CqrsSagaLifecycleService, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
|
|
35
|
+
[_CqrsEventBusService, _initClass] = _applyDecs(this, [Inject(FLUO_EVENT_BUS, CqrsSagaLifecycleService, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
|
|
34
36
|
}
|
|
35
37
|
descriptors = [];
|
|
36
38
|
discoveryPromise;
|
|
37
39
|
discovered = false;
|
|
38
|
-
|
|
39
|
-
shutdownDrainTimeouts = 0;
|
|
40
|
+
publishDrainTracker;
|
|
40
41
|
lifecycleState = 'created';
|
|
41
|
-
|
|
42
|
+
unregisterShutdownStartCleanup;
|
|
43
|
+
constructor(eventBus, sagaService, runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
|
|
42
44
|
super(runtimeContainer, compiledModules, logger);
|
|
43
45
|
this.eventBus = eventBus;
|
|
44
46
|
this.sagaService = sagaService;
|
|
45
47
|
this.moduleOptions = moduleOptions;
|
|
48
|
+
this.publishDrainTracker = new CqrsPublishDrainTracker(logger);
|
|
49
|
+
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
|
|
50
|
+
this.markApplicationShutdownStarted();
|
|
51
|
+
});
|
|
46
52
|
}
|
|
47
53
|
async onApplicationBootstrap() {
|
|
48
54
|
this.lifecycleState = 'discovering';
|
|
@@ -55,9 +61,11 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
async onApplicationShutdown() {
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
this.markApplicationShutdownStarted();
|
|
65
|
+
this.unregisterShutdownStartCleanup?.();
|
|
66
|
+
this.unregisterShutdownStartCleanup = undefined;
|
|
67
|
+
if (this.publishDrainTracker.hasActivePipelines) {
|
|
68
|
+
await this.publishDrainTracker.drain(this.resolveShutdownDrainTimeoutMs());
|
|
61
69
|
}
|
|
62
70
|
this.lifecycleState = 'stopped';
|
|
63
71
|
}
|
|
@@ -77,7 +85,7 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
77
85
|
sagaShutdownDrainTimeouts: sagaSnapshot.shutdownDrainTimeouts,
|
|
78
86
|
sagasDiscovered: sagaSnapshot.sagasDiscovered,
|
|
79
87
|
shutdownDrainTimeoutMs: this.resolveShutdownDrainTimeoutMs(),
|
|
80
|
-
shutdownDrainTimeouts: this.shutdownDrainTimeouts
|
|
88
|
+
shutdownDrainTimeouts: this.publishDrainTracker.shutdownDrainTimeouts
|
|
81
89
|
});
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -91,8 +99,9 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
91
99
|
* @throws {InvariantError} When a discovered provider does not implement `handle(event)`.
|
|
92
100
|
*/
|
|
93
101
|
async publish(event, context) {
|
|
94
|
-
this.assertAcceptingNewWork('publish');
|
|
95
|
-
|
|
102
|
+
this.assertAcceptingNewWork('publish', context);
|
|
103
|
+
const publishContext = this.createPublishContext(context);
|
|
104
|
+
await this.publishDrainTracker.track(this.runPublishPipeline(event, publishContext.context), publishContext.drainToken);
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
/**
|
|
@@ -103,8 +112,9 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
103
112
|
* @returns A promise that resolves once all events are published.
|
|
104
113
|
*/
|
|
105
114
|
async publishAll(events, context) {
|
|
106
|
-
this.assertAcceptingNewWork('publishAll');
|
|
107
|
-
|
|
115
|
+
this.assertAcceptingNewWork('publishAll', context);
|
|
116
|
+
const publishContext = this.createPublishContext(context);
|
|
117
|
+
await this.publishDrainTracker.track(this.runPublishAllPipeline(events, publishContext.context), publishContext.drainToken);
|
|
108
118
|
}
|
|
109
119
|
async runPublishPipeline(event, context) {
|
|
110
120
|
await this.ensureDiscovered();
|
|
@@ -115,9 +125,7 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
115
125
|
}
|
|
116
126
|
await instance.handle(createIsolatedEvent(descriptor.eventType, event), context);
|
|
117
127
|
}
|
|
118
|
-
await this.sagaService.dispatch(event, context,
|
|
119
|
-
allowDuringShutdown: true
|
|
120
|
-
});
|
|
128
|
+
await this.sagaService.dispatch(event, context, CQRS_SAGA_DRAIN_AUTHORIZATION);
|
|
121
129
|
await this.eventBus.publish(event);
|
|
122
130
|
}
|
|
123
131
|
async runPublishAllPipeline(events, context) {
|
|
@@ -125,41 +133,38 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
125
133
|
await this.runPublishPipeline(event, context);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
|
-
assertAcceptingNewWork(operation) {
|
|
129
|
-
if (this.lifecycleState === '
|
|
136
|
+
assertAcceptingNewWork(operation, context) {
|
|
137
|
+
if (this.lifecycleState === 'stopped') {
|
|
130
138
|
throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} finally {
|
|
138
|
-
this.activePublishPipelines.delete(pipeline);
|
|
140
|
+
if (this.lifecycleState === 'stopping') {
|
|
141
|
+
const drainToken = getInternalCqrsDispatchContextState(context)?.publishDrainToken;
|
|
142
|
+
if (!drainToken || !this.publishDrainTracker.isActive(drainToken)) {
|
|
143
|
+
throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
|
|
144
|
+
}
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const drained = await this.awaitShutdownDrain(activePipelines, timeoutMs);
|
|
145
|
-
if (!drained) {
|
|
146
|
-
this.shutdownDrainTimeouts += 1;
|
|
147
|
-
this.logger.warn(`CQRS event shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activePipelines.length)} active publish pipeline(s); continuing shutdown.`, 'CqrsEventBusService');
|
|
147
|
+
markApplicationShutdownStarted() {
|
|
148
|
+
if (this.lifecycleState !== 'stopped') {
|
|
149
|
+
this.lifecycleState = 'stopping';
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} finally {
|
|
159
|
-
if (timeoutId) {
|
|
160
|
-
clearTimeout(timeoutId);
|
|
161
|
-
}
|
|
152
|
+
createPublishContext(context) {
|
|
153
|
+
const internalState = getInternalCqrsDispatchContextState(context);
|
|
154
|
+
const drainToken = internalState?.publishDrainToken ?? Symbol('fluo.cqrs.activePublishDrain');
|
|
155
|
+
if (context && internalState?.publishDrainToken) {
|
|
156
|
+
return {
|
|
157
|
+
context,
|
|
158
|
+
drainToken
|
|
159
|
+
};
|
|
162
160
|
}
|
|
161
|
+
return {
|
|
162
|
+
context: createInternalCqrsDispatchContext({
|
|
163
|
+
publishDrainToken: drainToken,
|
|
164
|
+
sagaTopology: internalState?.sagaTopology
|
|
165
|
+
}),
|
|
166
|
+
drainToken
|
|
167
|
+
};
|
|
163
168
|
}
|
|
164
169
|
resolveShutdownDrainTimeoutMs() {
|
|
165
170
|
const timeoutMs = this.moduleOptions.shutdown?.drainTimeoutMs;
|
|
@@ -195,34 +200,7 @@ class CqrsEventBusService extends CqrsBusBase {
|
|
|
195
200
|
}
|
|
196
201
|
}
|
|
197
202
|
discoverEventDescriptors() {
|
|
198
|
-
|
|
199
|
-
const seenByTarget = new WeakMap();
|
|
200
|
-
for (const candidate of this.discoveryCandidates()) {
|
|
201
|
-
const metadata = getEventHandlerMetadata(candidate.targetType);
|
|
202
|
-
if (!metadata) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (candidate.scope !== 'singleton') {
|
|
206
|
-
this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @EventHandler() but is registered with ${candidate.scope} scope. Event handlers are registered only for singleton providers.`, 'CqrsEventBusService');
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
|
|
210
|
-
if (seenEventTypes.has(metadata.eventType)) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
seenEventTypes.add(metadata.eventType);
|
|
214
|
-
seenByTarget.set(candidate.targetType, seenEventTypes);
|
|
215
|
-
const alreadyRegistered = descriptors.some(descriptor => descriptor.eventType === metadata.eventType && descriptor.targetType === candidate.targetType);
|
|
216
|
-
if (!alreadyRegistered) {
|
|
217
|
-
descriptors.push({
|
|
218
|
-
eventType: metadata.eventType,
|
|
219
|
-
moduleName: candidate.moduleName,
|
|
220
|
-
targetType: candidate.targetType,
|
|
221
|
-
token: candidate.token
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return descriptors;
|
|
203
|
+
return discoverEventHandlerDescriptors(this.discoveryCandidates(), this.logger);
|
|
226
204
|
}
|
|
227
205
|
static {
|
|
228
206
|
_initClass();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApplicationLogger } from '@fluojs/runtime';
|
|
2
|
+
import type { DiscoveryCandidate } from '../discovery.js';
|
|
3
|
+
import type { EventHandlerDescriptor } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Discovers singleton event-handler registrations by provider-token identity.
|
|
6
|
+
*
|
|
7
|
+
* @param candidates Provider registrations compiled from the application module graph.
|
|
8
|
+
* @param logger Application logger used for non-singleton discovery warnings.
|
|
9
|
+
* @returns Event-handler descriptors in discovery order.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverEventHandlerDescriptors(candidates: readonly DiscoveryCandidate[], logger: ApplicationLogger): EventHandlerDescriptor[];
|
|
12
|
+
//# sourceMappingURL=event-handler-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-handler-discovery.d.ts","sourceRoot":"","sources":["../../src/buses/event-handler-discovery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,KAAK,EAAiB,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,MAAM,EAAE,iBAAiB,GACxB,sBAAsB,EAAE,CAoC1B"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getEventHandlerMetadata } from '../metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discovers singleton event-handler registrations by provider-token identity.
|
|
4
|
+
*
|
|
5
|
+
* @param candidates Provider registrations compiled from the application module graph.
|
|
6
|
+
* @param logger Application logger used for non-singleton discovery warnings.
|
|
7
|
+
* @returns Event-handler descriptors in discovery order.
|
|
8
|
+
*/
|
|
9
|
+
export function discoverEventHandlerDescriptors(candidates, logger) {
|
|
10
|
+
const descriptors = [];
|
|
11
|
+
const seenEventTypesByToken = new Map();
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
const metadata = getEventHandlerMetadata(candidate.targetType);
|
|
14
|
+
if (!metadata) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (candidate.scope !== 'singleton') {
|
|
18
|
+
logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @EventHandler() but is registered with ${candidate.scope} scope. Event handlers are registered only for singleton providers.`, 'CqrsEventBusService');
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const seenEventTypes = seenEventTypesByToken.get(candidate.token) ?? new Set();
|
|
22
|
+
if (seenEventTypes.has(metadata.eventType)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
seenEventTypes.add(metadata.eventType);
|
|
26
|
+
seenEventTypesByToken.set(candidate.token, seenEventTypes);
|
|
27
|
+
descriptors.push({
|
|
28
|
+
eventType: metadata.eventType,
|
|
29
|
+
moduleName: candidate.moduleName,
|
|
30
|
+
targetType: candidate.targetType,
|
|
31
|
+
token: candidate.token
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return descriptors;
|
|
35
|
+
}
|