@fluojs/cqrs 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +153 -0
- package/README.md +153 -0
- package/dist/buses/command-bus.d.ts +29 -0
- package/dist/buses/command-bus.d.ts.map +1 -0
- package/dist/buses/command-bus.js +122 -0
- package/dist/buses/event-bus.d.ts +49 -0
- package/dist/buses/event-bus.d.ts.map +1 -0
- package/dist/buses/event-bus.js +165 -0
- package/dist/buses/query-bus.d.ts +29 -0
- package/dist/buses/query-bus.d.ts.map +1 -0
- package/dist/buses/query-bus.js +122 -0
- package/dist/buses/saga-bus.d.ts +46 -0
- package/dist/buses/saga-bus.d.ts.map +1 -0
- package/dist/buses/saga-bus.js +225 -0
- package/dist/decorators.d.ts +48 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +127 -0
- package/dist/discovery.d.ts +27 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +84 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +99 -0
- package/dist/event-clone.d.ts +10 -0
- package/dist/event-clone.d.ts.map +1 -0
- package/dist/event-clone.js +15 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/metadata.d.ts +87 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +235 -0
- package/dist/module.d.ts +30 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +74 -0
- package/dist/status.d.ts +17 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +69 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +6 -0
- package/dist/types.d.ts +161 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @fluojs/cqrs
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo 애플리케이션을 위한 CQRS 패키지입니다. 부트스트랩 시점 핸들러 탐색, Command/Query 디스패치, 그리고 `@fluojs/event-bus` 위임 기반 이벤트 발행 기능을 제공합니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [공통 패턴](#공통-패턴)
|
|
13
|
+
- [Saga 프로세스 매니저](#saga-프로세스-매니저)
|
|
14
|
+
- [심볼 토큰](#심볼-토큰)
|
|
15
|
+
- [공개 API 개요](#공개-api-개요)
|
|
16
|
+
- [관련 패키지](#관련-패키지)
|
|
17
|
+
- [예제 소스](#예제-소스)
|
|
18
|
+
|
|
19
|
+
## 설치
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @fluojs/cqrs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 사용 시점
|
|
26
|
+
|
|
27
|
+
- "의도"(Command/Query)와 "실행"(Handler)을 분리하고 싶을 때 사용합니다.
|
|
28
|
+
- 쓰기 모델과 읽기 모델의 명확한 분리가 필요한 복잡한 비즈니스 로직을 구현할 때 적합합니다.
|
|
29
|
+
- 도메인 이벤트에 의해 트리거되는 다단계 프로세스(Saga)를 오케스트레이션할 때 사용합니다.
|
|
30
|
+
- 단일 애플리케이션 내에서 Command, Query, Event를 위한 중앙 집중식 버스가 필요할 때 사용합니다.
|
|
31
|
+
|
|
32
|
+
## 빠른 시작
|
|
33
|
+
|
|
34
|
+
`CqrsModule`을 등록하고 첫 번째 Command와 Handler를 정의합니다.
|
|
35
|
+
|
|
36
|
+
`CqrsModule.forRoot(...)`를 사용해 CQRS 버스와 핸들러 탐색을 구성합니다.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Inject, Module } from '@fluojs/core';
|
|
40
|
+
import {
|
|
41
|
+
CqrsModule,
|
|
42
|
+
CommandHandler,
|
|
43
|
+
ICommand,
|
|
44
|
+
ICommandHandler,
|
|
45
|
+
CommandBusLifecycleService,
|
|
46
|
+
} from '@fluojs/cqrs';
|
|
47
|
+
|
|
48
|
+
// 1. Command 정의
|
|
49
|
+
class CreateUserCommand implements ICommand {
|
|
50
|
+
constructor(public readonly name: string) {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Handler 구현
|
|
54
|
+
@CommandHandler(CreateUserCommand)
|
|
55
|
+
class CreateUserHandler implements ICommandHandler<CreateUserCommand, string> {
|
|
56
|
+
async execute(command: CreateUserCommand): Promise<string> {
|
|
57
|
+
console.log(`사용자 생성 중: ${command.name}`);
|
|
58
|
+
return 'user-id-123';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Command Bus 사용
|
|
63
|
+
@Inject(CommandBusLifecycleService)
|
|
64
|
+
class UserService {
|
|
65
|
+
constructor(private readonly commandBus: CommandBusLifecycleService) {}
|
|
66
|
+
|
|
67
|
+
async create(name: string) {
|
|
68
|
+
return this.commandBus.execute(new CreateUserCommand(name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Module({
|
|
73
|
+
imports: [CqrsModule.forRoot()],
|
|
74
|
+
providers: [CreateUserHandler, UserService],
|
|
75
|
+
})
|
|
76
|
+
class AppModule {}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 공통 패턴
|
|
80
|
+
|
|
81
|
+
### Saga 프로세스 매니저
|
|
82
|
+
|
|
83
|
+
Saga를 사용하면 이벤트를 구독하고 새로운 Command를 트리거하여 복잡한 장기 실행 워크플로우를 구성할 수 있습니다.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Inject } from '@fluojs/core';
|
|
87
|
+
import { Saga, ISaga, IEvent, ICommand, CommandBusLifecycleService } from '@fluojs/cqrs';
|
|
88
|
+
|
|
89
|
+
class UserCreatedEvent implements IEvent {
|
|
90
|
+
constructor(public readonly userId: string) {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class SendWelcomeEmailCommand implements ICommand {
|
|
94
|
+
constructor(public readonly userId: string) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Inject(CommandBusLifecycleService)
|
|
98
|
+
@Saga(UserCreatedEvent)
|
|
99
|
+
class UserSaga implements ISaga<UserCreatedEvent> {
|
|
100
|
+
constructor(private readonly commandBus: CommandBusLifecycleService) {}
|
|
101
|
+
|
|
102
|
+
async handle(event: UserCreatedEvent): Promise<void> {
|
|
103
|
+
await this.commandBus.execute(new SendWelcomeEmailCommand(event.userId));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
이제 saga 실행은 같은 프로세스 안에서 동일 saga route로 순환 재진입하거나 중첩 hop 수가 32를 넘으면 `SagaTopologyError`로 즉시 실패합니다. 서로 다른 이벤트 단계를 순차 처리하는 multi-stage saga는 계속 허용되지만, in-process saga graph 전체는 비순환(acyclic) 구조를 유지해야 하며, 의도적인 순환/피드백 루프나 더 긴 체인은 외부 transport, scheduler, 또는 다른 bounded boundary 뒤로 이동해야 합니다.
|
|
109
|
+
|
|
110
|
+
### 심볼 토큰
|
|
111
|
+
|
|
112
|
+
CQRS 버스에 명시적인 Symbol 토큰이 필요하면 다음 익스포트를 사용할 수 있습니다.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Inject } from '@fluojs/core';
|
|
116
|
+
import { COMMAND_BUS, QUERY_BUS, EVENT_BUS } from '@fluojs/cqrs';
|
|
117
|
+
|
|
118
|
+
@Inject(COMMAND_BUS, QUERY_BUS, EVENT_BUS)
|
|
119
|
+
class TokenInjectedService {
|
|
120
|
+
constructor(commandBus, queryBus, eventBus) {}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 공개 API 개요
|
|
125
|
+
|
|
126
|
+
### 모듈 및 프로바이더
|
|
127
|
+
- `CqrsModule.forRoot(options)`: 메인 진입점입니다. 버스를 등록하고 탐색을 시작합니다.
|
|
128
|
+
- `CommandBusLifecycleService`: Command 실행을 위한 기본 서비스입니다.
|
|
129
|
+
- `QueryBusLifecycleService`: Query 실행을 위한 기본 서비스입니다.
|
|
130
|
+
- `CqrsEventBusService`: Event 발행을 위한 기본 서비스입니다.
|
|
131
|
+
|
|
132
|
+
### 데코레이터
|
|
133
|
+
- `@CommandHandler(Command)`: 클래스를 특정 Command와 연결합니다.
|
|
134
|
+
- `@QueryHandler(Query)`: 클래스를 특정 Query와 연결합니다.
|
|
135
|
+
- `@EventHandler(Event)`: 클래스를 특정 Event와 연결합니다.
|
|
136
|
+
- `@Saga(Event | Event[])`: 클래스를 Saga 리스너로 표시합니다.
|
|
137
|
+
|
|
138
|
+
### 인터페이스
|
|
139
|
+
- `ICommand`, `IQuery<T>`, `IEvent`: 메시지 마커 인터페이스입니다.
|
|
140
|
+
- `ICommandHandler<C, R>`, `IQueryHandler<Q, R>`, `IEventHandler<E>`, `ISaga<E>`: 핸들러 계약입니다.
|
|
141
|
+
|
|
142
|
+
### 오류
|
|
143
|
+
- `SagaTopologyError`: 자기 트리거, 순환, 또는 과도하게 깊은 in-process saga graph를 감지했을 때 발생합니다.
|
|
144
|
+
|
|
145
|
+
## 관련 패키지
|
|
146
|
+
|
|
147
|
+
- `@fluojs/event-bus`: `CqrsEventBusService`에서 사용하는 하위 이벤트 분산 패키지입니다.
|
|
148
|
+
- `@fluojs/core`: `@Module` 및 `@Inject` 데코레이터를 위해 필요합니다.
|
|
149
|
+
|
|
150
|
+
## 예제 소스
|
|
151
|
+
|
|
152
|
+
- `packages/cqrs/src/module.test.ts`: 모듈 등록 및 기본 버스 사용 예제.
|
|
153
|
+
- `packages/cqrs/src/public-api.test.ts`: 루트 배럴 공개 API 계약 검증 예제.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @fluojs/cqrs
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
CQRS primitives for fluo applications with bootstrap-time handler discovery, command/query dispatch, and event publishing delegation through `@fluojs/event-bus`.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [When to Use](#when-to-use)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Common Patterns](#common-patterns)
|
|
13
|
+
- [Saga Process Managers](#saga-process-managers)
|
|
14
|
+
- [Symbol Tokens](#symbol-tokens)
|
|
15
|
+
- [Public API Overview](#public-api-overview)
|
|
16
|
+
- [Related Packages](#related-packages)
|
|
17
|
+
- [Example Sources](#example-sources)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @fluojs/cqrs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## When to Use
|
|
26
|
+
|
|
27
|
+
- When you want to decouple the "intent" (Commands/Queries) from the "execution" (Handlers).
|
|
28
|
+
- When implementing complex business logic that requires clear separation between write models and read models.
|
|
29
|
+
- When orchestrating multi-step processes (Sagas) triggered by domain events.
|
|
30
|
+
- When you need a centralized bus for commands, queries, and events within a single application.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
Register the `CqrsModule` and define your first command and handler.
|
|
35
|
+
|
|
36
|
+
Use `CqrsModule.forRoot(...)` to wire CQRS buses and handler discovery.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Inject, Module } from '@fluojs/core';
|
|
40
|
+
import {
|
|
41
|
+
CqrsModule,
|
|
42
|
+
CommandHandler,
|
|
43
|
+
ICommand,
|
|
44
|
+
ICommandHandler,
|
|
45
|
+
CommandBusLifecycleService,
|
|
46
|
+
} from '@fluojs/cqrs';
|
|
47
|
+
|
|
48
|
+
// 1. Define a Command
|
|
49
|
+
class CreateUserCommand implements ICommand {
|
|
50
|
+
constructor(public readonly name: string) {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Implement the Handler
|
|
54
|
+
@CommandHandler(CreateUserCommand)
|
|
55
|
+
class CreateUserHandler implements ICommandHandler<CreateUserCommand, string> {
|
|
56
|
+
async execute(command: CreateUserCommand): Promise<string> {
|
|
57
|
+
console.log(`Creating user: ${command.name}`);
|
|
58
|
+
return 'user-id-123';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Use the Command Bus
|
|
63
|
+
@Inject(CommandBusLifecycleService)
|
|
64
|
+
class UserService {
|
|
65
|
+
constructor(private readonly commandBus: CommandBusLifecycleService) {}
|
|
66
|
+
|
|
67
|
+
async create(name: string) {
|
|
68
|
+
return this.commandBus.execute(new CreateUserCommand(name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Module({
|
|
73
|
+
imports: [CqrsModule.forRoot()],
|
|
74
|
+
providers: [CreateUserHandler, UserService],
|
|
75
|
+
})
|
|
76
|
+
class AppModule {}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Common Patterns
|
|
80
|
+
|
|
81
|
+
### Saga Process Managers
|
|
82
|
+
|
|
83
|
+
Sagas allow you to listen for events and trigger new commands, enabling complex long-running workflows.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Inject } from '@fluojs/core';
|
|
87
|
+
import { Saga, ISaga, IEvent, ICommand, CommandBusLifecycleService } from '@fluojs/cqrs';
|
|
88
|
+
|
|
89
|
+
class UserCreatedEvent implements IEvent {
|
|
90
|
+
constructor(public readonly userId: string) {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class SendWelcomeEmailCommand implements ICommand {
|
|
94
|
+
constructor(public readonly userId: string) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Inject(CommandBusLifecycleService)
|
|
98
|
+
@Saga(UserCreatedEvent)
|
|
99
|
+
class UserSaga implements ISaga<UserCreatedEvent> {
|
|
100
|
+
constructor(private readonly commandBus: CommandBusLifecycleService) {}
|
|
101
|
+
|
|
102
|
+
async handle(event: UserCreatedEvent): Promise<void> {
|
|
103
|
+
await this.commandBus.execute(new SendWelcomeEmailCommand(event.userId));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Saga execution now fails fast with `SagaTopologyError` when an in-process publish chain re-enters the same saga route cyclically or exceeds 32 nested saga hops. 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.
|
|
109
|
+
|
|
110
|
+
### Symbol Tokens
|
|
111
|
+
|
|
112
|
+
Use these exports when you want explicit symbol tokens for the CQRS buses:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Inject } from '@fluojs/core';
|
|
116
|
+
import { COMMAND_BUS, QUERY_BUS, EVENT_BUS } from '@fluojs/cqrs';
|
|
117
|
+
|
|
118
|
+
@Inject(COMMAND_BUS, QUERY_BUS, EVENT_BUS)
|
|
119
|
+
class TokenInjectedService {
|
|
120
|
+
constructor(commandBus, queryBus, eventBus) {}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Public API Overview
|
|
125
|
+
|
|
126
|
+
### Modules & Providers
|
|
127
|
+
- `CqrsModule.forRoot(options)`: Main entry point. Registers buses and starts discovery.
|
|
128
|
+
- `CommandBusLifecycleService`: Primary service for executing commands.
|
|
129
|
+
- `QueryBusLifecycleService`: Primary service for executing queries.
|
|
130
|
+
- `CqrsEventBusService`: Primary service for publishing events.
|
|
131
|
+
|
|
132
|
+
### Decorators
|
|
133
|
+
- `@CommandHandler(Command)`: Associates a class with a Command.
|
|
134
|
+
- `@QueryHandler(Query)`: Associates a class with a Query.
|
|
135
|
+
- `@EventHandler(Event)`: Associates a class with an Event.
|
|
136
|
+
- `@Saga(Event | Event[])`: Marks a class as a Saga listener.
|
|
137
|
+
|
|
138
|
+
### Interfaces
|
|
139
|
+
- `ICommand`, `IQuery<T>`, `IEvent`: Marker interfaces for messages.
|
|
140
|
+
- `ICommandHandler<C, R>`, `IQueryHandler<Q, R>`, `IEventHandler<E>`, `ISaga<E>`: Handler contracts.
|
|
141
|
+
|
|
142
|
+
### Errors
|
|
143
|
+
- `SagaTopologyError`: Raised when saga orchestration detects a self-triggering, cyclic, or over-deep in-process saga graph.
|
|
144
|
+
|
|
145
|
+
## Related Packages
|
|
146
|
+
|
|
147
|
+
- `@fluojs/event-bus`: Underlying event distribution used by `CqrsEventBusService`.
|
|
148
|
+
- `@fluojs/core`: Required for `@Module` and `@Inject` decorators.
|
|
149
|
+
|
|
150
|
+
## Example Sources
|
|
151
|
+
|
|
152
|
+
- `packages/cqrs/src/module.test.ts`: Module registration and basic bus usage.
|
|
153
|
+
- `packages/cqrs/src/public-api.test.ts`: Root-barrel public API contract coverage.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OnApplicationBootstrap } from '@fluojs/runtime';
|
|
2
|
+
import { CqrsBusBase } from '../discovery.js';
|
|
3
|
+
import type { CommandBus, ICommand } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Discovers and executes command handlers during application bootstrap and runtime dispatch.
|
|
6
|
+
*
|
|
7
|
+
* The command bus resolves singleton handlers only, warns on unsupported scopes,
|
|
8
|
+
* and throws explicit contract errors when no handler or multiple handlers exist.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CommandBusLifecycleService extends CqrsBusBase implements CommandBus, OnApplicationBootstrap {
|
|
11
|
+
private descriptors;
|
|
12
|
+
private discoveryPromise;
|
|
13
|
+
private discovered;
|
|
14
|
+
onApplicationBootstrap(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Executes one command by dispatching it to the discovered handler for its constructor.
|
|
17
|
+
*
|
|
18
|
+
* @param command Command instance to execute.
|
|
19
|
+
* @returns The resolved handler result.
|
|
20
|
+
*
|
|
21
|
+
* @throws {CommandHandlerNotFoundException} When no handler is registered for the command type.
|
|
22
|
+
* @throws {InvariantError} When the resolved provider does not implement `execute(command)`.
|
|
23
|
+
*/
|
|
24
|
+
execute<TCommand extends ICommand, TResult = void>(command: TCommand): Promise<TResult>;
|
|
25
|
+
private ensureDiscovered;
|
|
26
|
+
private discoverHandlers;
|
|
27
|
+
private discoverCommandDescriptors;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=command-bus.d.ts.map
|
|
@@ -0,0 +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;AAK9D,OAAO,EAAE,WAAW,EAAiC,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,EACV,UAAU,EAGV,QAAQ,EAET,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,0BAA2B,SAAQ,WAAY,YAAW,UAAU,EAAE,sBAAsB;IACvG,OAAO,CAAC,WAAW,CAAoD;IACvE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAErB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7C;;;;;;;;OAQG;IACG,OAAO,CAAC,QAAQ,SAAS,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAmB/E,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,0BAA0B;CAmDnC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
let _initClass;
|
|
2
|
+
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
3
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
4
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
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
|
+
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
|
+
import { Inject, InvariantError } from '@fluojs/core';
|
|
8
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
|
+
import { CommandHandlerNotFoundException, DuplicateCommandHandlerError } from '../errors.js';
|
|
10
|
+
import { getCommandHandlerMetadata } from '../metadata.js';
|
|
11
|
+
import { CqrsBusBase, createDuplicateHandlerMessage } from '../discovery.js';
|
|
12
|
+
function isCommandHandler(value) {
|
|
13
|
+
if (typeof value !== 'object' || value === null) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return typeof value.execute === 'function';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discovers and executes command handlers during application bootstrap and runtime dispatch.
|
|
21
|
+
*
|
|
22
|
+
* The command bus resolves singleton handlers only, warns on unsupported scopes,
|
|
23
|
+
* and throws explicit contract errors when no handler or multiple handlers exist.
|
|
24
|
+
*/
|
|
25
|
+
let _CommandBusLifecycleS;
|
|
26
|
+
class CommandBusLifecycleService extends CqrsBusBase {
|
|
27
|
+
static {
|
|
28
|
+
[_CommandBusLifecycleS, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], [], 0, void 0, CqrsBusBase).c;
|
|
29
|
+
}
|
|
30
|
+
descriptors = new Map();
|
|
31
|
+
discoveryPromise;
|
|
32
|
+
discovered = false;
|
|
33
|
+
async onApplicationBootstrap() {
|
|
34
|
+
await this.ensureDiscovered();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Executes one command by dispatching it to the discovered handler for its constructor.
|
|
39
|
+
*
|
|
40
|
+
* @param command Command instance to execute.
|
|
41
|
+
* @returns The resolved handler result.
|
|
42
|
+
*
|
|
43
|
+
* @throws {CommandHandlerNotFoundException} When no handler is registered for the command type.
|
|
44
|
+
* @throws {InvariantError} When the resolved provider does not implement `execute(command)`.
|
|
45
|
+
*/
|
|
46
|
+
async execute(command) {
|
|
47
|
+
await this.ensureDiscovered();
|
|
48
|
+
const commandType = command.constructor;
|
|
49
|
+
const descriptor = this.descriptors.get(commandType);
|
|
50
|
+
if (!descriptor) {
|
|
51
|
+
throw new CommandHandlerNotFoundException(`No command handler registered for ${commandType.name}.`);
|
|
52
|
+
}
|
|
53
|
+
const instance = await this.resolveHandlerInstance(descriptor.token);
|
|
54
|
+
if (!isCommandHandler(instance)) {
|
|
55
|
+
throw new InvariantError(`Command handler ${descriptor.targetType.name} must implement execute(command).`);
|
|
56
|
+
}
|
|
57
|
+
return await instance.execute(command);
|
|
58
|
+
}
|
|
59
|
+
async ensureDiscovered() {
|
|
60
|
+
if (this.discovered) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.discoveryPromise) {
|
|
64
|
+
await this.discoveryPromise;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.discoveryPromise = this.discoverHandlers();
|
|
68
|
+
await this.discoveryPromise;
|
|
69
|
+
}
|
|
70
|
+
async discoverHandlers() {
|
|
71
|
+
try {
|
|
72
|
+
this.descriptors = this.discoverCommandDescriptors();
|
|
73
|
+
this.handlerInstances.clear();
|
|
74
|
+
for (const descriptor of this.descriptors.values()) {
|
|
75
|
+
await this.preloadHandlerInstance(descriptor.token);
|
|
76
|
+
}
|
|
77
|
+
this.discovered = true;
|
|
78
|
+
} finally {
|
|
79
|
+
this.discoveryPromise = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
discoverCommandDescriptors() {
|
|
83
|
+
const descriptors = new Map();
|
|
84
|
+
const seenByTarget = new WeakMap();
|
|
85
|
+
for (const candidate of this.discoveryCandidates()) {
|
|
86
|
+
const metadata = getCommandHandlerMetadata(candidate.targetType);
|
|
87
|
+
if (!metadata) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (candidate.scope !== 'singleton') {
|
|
91
|
+
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');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const seenCommandTypes = seenByTarget.get(candidate.targetType) ?? new Set();
|
|
95
|
+
if (seenCommandTypes.has(metadata.commandType)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
seenCommandTypes.add(metadata.commandType);
|
|
99
|
+
seenByTarget.set(candidate.targetType, seenCommandTypes);
|
|
100
|
+
const existing = descriptors.get(metadata.commandType);
|
|
101
|
+
if (existing && existing.targetType !== candidate.targetType) {
|
|
102
|
+
throw new DuplicateCommandHandlerError(createDuplicateHandlerMessage('command', metadata.commandType, existing, {
|
|
103
|
+
moduleName: candidate.moduleName,
|
|
104
|
+
targetType: candidate.targetType
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
if (!existing) {
|
|
108
|
+
descriptors.set(metadata.commandType, {
|
|
109
|
+
commandType: metadata.commandType,
|
|
110
|
+
moduleName: candidate.moduleName,
|
|
111
|
+
targetType: candidate.targetType,
|
|
112
|
+
token: candidate.token
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return descriptors;
|
|
117
|
+
}
|
|
118
|
+
static {
|
|
119
|
+
_initClass();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export { _CommandBusLifecycleS as CommandBusLifecycleService };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type EventBus } from '@fluojs/event-bus';
|
|
2
|
+
import type { OnApplicationShutdown, OnApplicationBootstrap } from '@fluojs/runtime';
|
|
3
|
+
import { CqrsBusBase } from '../discovery.js';
|
|
4
|
+
import { CqrsSagaLifecycleService } from './saga-bus.js';
|
|
5
|
+
import type { CqrsEventBus, IEvent } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* CQRS-facing event bus that dispatches local event handlers, sagas, and the shared event transport.
|
|
8
|
+
*
|
|
9
|
+
* This service keeps CQRS event handlers singleton-only, fans events into saga orchestration,
|
|
10
|
+
* and delegates the final publication step to `@fluojs/event-bus`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CqrsEventBusService extends CqrsBusBase implements CqrsEventBus, OnApplicationBootstrap, OnApplicationShutdown {
|
|
13
|
+
private readonly eventBus;
|
|
14
|
+
private readonly sagaService;
|
|
15
|
+
private descriptors;
|
|
16
|
+
private discoveryPromise;
|
|
17
|
+
private discovered;
|
|
18
|
+
private lifecycleState;
|
|
19
|
+
constructor(eventBus: EventBus, sagaService: CqrsSagaLifecycleService, runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2]);
|
|
20
|
+
onApplicationBootstrap(): Promise<void>;
|
|
21
|
+
onApplicationShutdown(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a CQRS runtime status snapshot that includes local handler and saga state.
|
|
24
|
+
*
|
|
25
|
+
* @returns A structured snapshot describing CQRS event-handler discovery and saga lifecycle state.
|
|
26
|
+
*/
|
|
27
|
+
createPlatformStatusSnapshot(): import("../status.js").CqrsPlatformStatusSnapshot;
|
|
28
|
+
/**
|
|
29
|
+
* Publishes one event to matching CQRS handlers, sagas, and the shared event bus.
|
|
30
|
+
*
|
|
31
|
+
* @param event Event instance to publish.
|
|
32
|
+
* @returns A promise that resolves once all local CQRS side effects and delegated publication complete.
|
|
33
|
+
*
|
|
34
|
+
* @throws {InvariantError} When a discovered provider does not implement `handle(event)`.
|
|
35
|
+
*/
|
|
36
|
+
publish<TEvent extends IEvent>(event: TEvent): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Publishes a batch of events sequentially through the CQRS event pipeline.
|
|
39
|
+
*
|
|
40
|
+
* @param events Event instances to publish in order.
|
|
41
|
+
* @returns A promise that resolves once all events are published.
|
|
42
|
+
*/
|
|
43
|
+
publishAll<TEvent extends IEvent>(events: readonly TEvent[]): Promise<void>;
|
|
44
|
+
private matchEventDescriptors;
|
|
45
|
+
private ensureDiscovered;
|
|
46
|
+
private discoverHandlers;
|
|
47
|
+
private discoverEventDescriptors;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=event-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/buses/event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAA+B,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAyC,MAAM,EAAiB,MAAM,aAAa,CAAC;AAU9G;;;;;GAKG;AACH,qBACa,mBAAoB,SAAQ,WAAY,YAAW,YAAY,EAAE,sBAAsB,EAAE,qBAAqB;IAOvH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAP9B,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAsF;gBAGzF,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;IAKhD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;OAOG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBlE;;;;;OAKG;IACG,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjF,OAAO,CAAC,qBAAqB;YAIf,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CA4CjC"}
|