@shirudo/ddd-kit 0.15.0 → 1.0.0-rc.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/README.md +106 -84
- package/dist/index.d.ts +342 -313
- package/dist/index.js +881 -1
- package/dist/index.js.map +1 -1
- package/dist/result.js +297 -1
- package/dist/result.js.map +1 -1
- package/dist/utils-array.js +241 -1
- package/dist/utils-array.js.map +1 -1
- package/dist/utils.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Composable TypeScript toolkit for tactical Domain-Driven Design.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> **Release Candidate**
|
|
6
6
|
>
|
|
7
|
-
> This library is
|
|
7
|
+
> This library is in Release Candidate phase. The API is considered stable and ready for production evaluation. Please report any issues before the final 1.0.0 release.
|
|
8
8
|
|
|
9
9
|
## Badges
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ Composable TypeScript toolkit for tactical Domain-Driven Design.
|
|
|
20
20
|
- **Repositories** - Persistence abstraction layer for aggregates with specification pattern support
|
|
21
21
|
- **Specifications** - Reusable query specifications for complex domain queries
|
|
22
22
|
- **Unit of Work** - Transaction management for maintaining consistency across operations
|
|
23
|
-
- **Result Type** - Functional error handling with `Result<T, E>` type for explicit success/failure states
|
|
23
|
+
- **Result Type** - Functional error handling with `Result<T, E>` type for explicit success/failure states. For advanced error handling with typed error hierarchies, see [`@shirudo/base-error`](https://www.npmjs.com/package/@shirudo/base-error)
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
@@ -72,7 +72,7 @@ In Domain-Driven Design, Entities are objects with identity and state. Unlike Va
|
|
|
72
72
|
- Has identity (id), state, and version for optimistic concurrency control
|
|
73
73
|
- Represents the aggregate externally
|
|
74
74
|
- Loaded/saved through repositories
|
|
75
|
-
- Created by extending `AggregateRoot` or `
|
|
75
|
+
- Created by extending `AggregateRoot` (state-based) or `EventSourcedAggregate` (event-sourced)
|
|
76
76
|
- Implements `IAggregateRoot<TId>`
|
|
77
77
|
|
|
78
78
|
2. **Child Entities**: Entities within an aggregate.
|
|
@@ -103,11 +103,11 @@ The Aggregate Root is an Entity (the parent Entity of the aggregate) that repres
|
|
|
103
103
|
|
|
104
104
|
The library provides:
|
|
105
105
|
|
|
106
|
-
- **`
|
|
106
|
+
- **`IAggregateRoot<TId>`** - Marker interface for Aggregate Root Entities. The Aggregate Root is an Entity with identity (id) and version for optimistic concurrency control. It represents the aggregate externally and is the only object that can be loaded/saved through repositories.
|
|
107
107
|
|
|
108
|
-
- **`
|
|
108
|
+
- **`AggregateRoot<TState, TId, TEvent?>`** - Base class for creating Aggregate Root Entities without Event Sourcing. Implements `IAggregateRoot<TId>`. The optional `TEvent` parameter (defaults to `unknown`) enables type-safe domain events — only aggregates that specify it get compile-time event validation. Provides ID and version management, state management, domain event tracking, and snapshot support. Use this when you don't need Event Sourcing but still want aggregate patterns with versioning and state management.
|
|
109
109
|
|
|
110
|
-
- **`
|
|
110
|
+
- **`EventSourcedAggregate<TState, TEvent, TId>`** - Base class for Event-Sourced Aggregate Roots. Extends `Entity` directly (not `AggregateRoot`) so that state changes can only happen through event handlers via `apply()`. Provides event tracking, event validation, history replay, and snapshot support.
|
|
111
111
|
|
|
112
112
|
Both classes support automatic versioning (configurable), snapshot creation/restoration, and optimistic concurrency control. The version applies to the entire aggregate, including all child entities.
|
|
113
113
|
|
|
@@ -117,7 +117,7 @@ CQRS separates read operations (Queries) from write operations (Commands), provi
|
|
|
117
117
|
|
|
118
118
|
### Domain Events
|
|
119
119
|
|
|
120
|
-
Domain Events represent something meaningful that happened in your domain. They are immutable records with a type, payload, timestamp,
|
|
120
|
+
Domain Events represent something meaningful that happened in your domain. They are immutable records with a type, payload, timestamp, version for schema evolution, and optional metadata for traceability. Events support versioning for handling schema changes over time and include metadata fields like `correlationId`, `causationId`, `userId`, and `source` for tracking event flow in distributed systems. Events are automatically tracked by aggregates and can be published to event buses or stored in outboxes for eventual consistency.
|
|
121
121
|
|
|
122
122
|
### Repositories
|
|
123
123
|
|
|
@@ -218,8 +218,8 @@ voEqualsExcept(address1, address2, {
|
|
|
218
218
|
|
|
219
219
|
```typescript
|
|
220
220
|
import {
|
|
221
|
-
|
|
222
|
-
type
|
|
221
|
+
AggregateRoot,
|
|
222
|
+
type IAggregateRoot,
|
|
223
223
|
type Id,
|
|
224
224
|
} from "@shirudo/ddd-kit";
|
|
225
225
|
|
|
@@ -233,7 +233,8 @@ type OrderState = {
|
|
|
233
233
|
status: "pending" | "confirmed" | "shipped";
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
// Without typed events (TEvent defaults to unknown)
|
|
237
|
+
class Order extends AggregateRoot<OrderState, OrderId> {
|
|
237
238
|
static create(id: OrderId, customerId: string): Order {
|
|
238
239
|
const initialState: OrderState = {
|
|
239
240
|
id,
|
|
@@ -246,32 +247,29 @@ class Order extends AggregateBase<OrderState, OrderId> implements AggregateRoot<
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
addItem(productId: string, quantity: number, price: number): void {
|
|
249
|
-
if (this.
|
|
250
|
+
if (this.state.status !== "pending") {
|
|
250
251
|
throw new Error("Cannot add items to a non-pending order");
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
this.
|
|
254
|
-
...this.
|
|
255
|
-
items: [...this.
|
|
256
|
-
total: this.
|
|
257
|
-
};
|
|
258
|
-
this.bumpVersion(); // Manual version bump for optimistic concurrency control
|
|
254
|
+
this.setState({
|
|
255
|
+
...this.state,
|
|
256
|
+
items: [...this.state.items, { productId, quantity, price }],
|
|
257
|
+
total: this.state.total + quantity * price,
|
|
258
|
+
}, true); // true = bump version for optimistic concurrency control
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
confirm(): void {
|
|
262
|
-
if (this.
|
|
262
|
+
if (this.state.status !== "pending") {
|
|
263
263
|
throw new Error("Only pending orders can be confirmed");
|
|
264
264
|
}
|
|
265
|
-
this.
|
|
266
|
-
this.bumpVersion();
|
|
265
|
+
this.setState({ ...this.state, status: "confirmed" }, true);
|
|
267
266
|
}
|
|
268
267
|
|
|
269
268
|
ship(): void {
|
|
270
|
-
if (this.
|
|
269
|
+
if (this.state.status !== "confirmed") {
|
|
271
270
|
throw new Error("Only confirmed orders can be shipped");
|
|
272
271
|
}
|
|
273
|
-
this.
|
|
274
|
-
this.bumpVersion();
|
|
272
|
+
this.setState({ ...this.state, status: "shipped" }, true);
|
|
275
273
|
}
|
|
276
274
|
}
|
|
277
275
|
|
|
@@ -285,11 +283,36 @@ console.log(order.version); // 3 (manually bumped)
|
|
|
285
283
|
console.log(order.state.status); // "shipped"
|
|
286
284
|
```
|
|
287
285
|
|
|
286
|
+
#### With Typed Domain Events
|
|
287
|
+
|
|
288
|
+
Use the optional third type parameter to get compile-time event validation:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
type OrderDomainEvent =
|
|
292
|
+
| { type: "OrderConfirmed" }
|
|
293
|
+
| { type: "OrderShipped"; trackingNumber: string };
|
|
294
|
+
|
|
295
|
+
class Order extends AggregateRoot<OrderState, OrderId, OrderDomainEvent> {
|
|
296
|
+
confirm(): void {
|
|
297
|
+
this.setState({ ...this.state, status: "confirmed" }, true);
|
|
298
|
+
this.addDomainEvent({ type: "OrderConfirmed" }); // type-safe
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ship(trackingNumber: string): void {
|
|
302
|
+
this.setState({ ...this.state, status: "shipped" }, true);
|
|
303
|
+
this.addDomainEvent({ type: "OrderShipped", trackingNumber }); // type-safe
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// order.domainEvents is ReadonlyArray<OrderDomainEvent> — no cast needed
|
|
308
|
+
// order.addDomainEvent({ type: "WrongEvent" }) → compile error
|
|
309
|
+
```
|
|
310
|
+
|
|
288
311
|
### Creating an Aggregate WITH Event Sourcing
|
|
289
312
|
|
|
290
313
|
```typescript
|
|
291
314
|
import {
|
|
292
|
-
|
|
315
|
+
EventSourcedAggregate,
|
|
293
316
|
createDomainEvent,
|
|
294
317
|
type AggregateRoot,
|
|
295
318
|
type Id,
|
|
@@ -306,12 +329,12 @@ type OrderState = {
|
|
|
306
329
|
};
|
|
307
330
|
|
|
308
331
|
type OrderCreated = DomainEvent<"OrderCreated", { customerId: string }>;
|
|
309
|
-
type OrderConfirmed = DomainEvent<"OrderConfirmed"
|
|
332
|
+
type OrderConfirmed = DomainEvent<"OrderConfirmed">;
|
|
310
333
|
type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
|
|
311
334
|
|
|
312
335
|
type OrderEvent = OrderCreated | OrderConfirmed | OrderShipped;
|
|
313
336
|
|
|
314
|
-
class Order extends
|
|
337
|
+
class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
315
338
|
static create(id: OrderId, customerId: string): Order {
|
|
316
339
|
const initialState: OrderState = {
|
|
317
340
|
id,
|
|
@@ -328,7 +351,7 @@ class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> imple
|
|
|
328
351
|
|
|
329
352
|
confirm(): void {
|
|
330
353
|
const result = this.apply(
|
|
331
|
-
createDomainEvent("OrderConfirmed"
|
|
354
|
+
createDomainEvent("OrderConfirmed") as OrderConfirmed
|
|
332
355
|
);
|
|
333
356
|
if (!result.ok) {
|
|
334
357
|
throw new Error(result.error);
|
|
@@ -347,7 +370,7 @@ class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> imple
|
|
|
347
370
|
// Or use unsafe variant (throws exception directly)
|
|
348
371
|
confirmUnsafe(): void {
|
|
349
372
|
this.applyUnsafe(
|
|
350
|
-
createDomainEvent("OrderConfirmed"
|
|
373
|
+
createDomainEvent("OrderConfirmed") as OrderConfirmed
|
|
351
374
|
);
|
|
352
375
|
}
|
|
353
376
|
|
|
@@ -388,9 +411,9 @@ console.log(order.version); // 3 (automatically bumped)
|
|
|
388
411
|
|
|
389
412
|
```typescript
|
|
390
413
|
import {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
414
|
+
AggregateRoot,
|
|
415
|
+
EventSourcedAggregate,
|
|
416
|
+
sameVersion,
|
|
394
417
|
type Id,
|
|
395
418
|
} from "@shirudo/ddd-kit";
|
|
396
419
|
|
|
@@ -413,11 +436,11 @@ const eventSourcedOrder = EventSourcedOrder.create("order-123" as OrderId, "cust
|
|
|
413
436
|
const eventsAfterSnapshot = [/* events that occurred after snapshot */];
|
|
414
437
|
eventSourcedOrder.restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot);
|
|
415
438
|
|
|
416
|
-
//
|
|
439
|
+
// Optimistic concurrency check
|
|
417
440
|
const order1 = await repository.getById(id);
|
|
418
441
|
// ... some operations ...
|
|
419
442
|
const order2 = await repository.getById(id);
|
|
420
|
-
if (!
|
|
443
|
+
if (!sameVersion(order1, order2)) {
|
|
421
444
|
throw new Error("Aggregate was modified by another process");
|
|
422
445
|
}
|
|
423
446
|
```
|
|
@@ -426,7 +449,7 @@ if (!sameAggregate(order1, order2)) {
|
|
|
426
449
|
|
|
427
450
|
```typescript
|
|
428
451
|
import {
|
|
429
|
-
|
|
452
|
+
EventSourcedAggregate,
|
|
430
453
|
createDomainEvent,
|
|
431
454
|
err,
|
|
432
455
|
ok,
|
|
@@ -441,7 +464,7 @@ type OrderState = { id: OrderId; status: "pending" | "confirmed" | "shipped" };
|
|
|
441
464
|
type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
|
|
442
465
|
type OrderEvent = OrderShipped;
|
|
443
466
|
|
|
444
|
-
class Order extends
|
|
467
|
+
class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
445
468
|
// Event validation
|
|
446
469
|
protected validateEvent(event: OrderEvent): Result<true, string> {
|
|
447
470
|
if (event.type === "OrderShipped" && this.state.status !== "confirmed") {
|
|
@@ -756,6 +779,11 @@ const orderCreated = createDomainEvent("OrderCreated", {
|
|
|
756
779
|
|
|
757
780
|
await eventBus.publish([orderCreated]);
|
|
758
781
|
// Both email and logging handlers will be called
|
|
782
|
+
|
|
783
|
+
// Wait for the next event of a given type (useful for tests and workflows)
|
|
784
|
+
const event = await eventBus.once<OrderCreated>("OrderCreated");
|
|
785
|
+
console.log("Order created:", event.payload.orderId);
|
|
786
|
+
// Automatically unsubscribes after the first event
|
|
759
787
|
```
|
|
760
788
|
|
|
761
789
|
### Creating Events with Metadata for Traceability
|
|
@@ -859,49 +887,42 @@ class Order extends AggregateRoot<OrderState, OrderId>
|
|
|
859
887
|
price,
|
|
860
888
|
};
|
|
861
889
|
|
|
862
|
-
this.
|
|
863
|
-
...this.
|
|
864
|
-
items: [...this.
|
|
865
|
-
total: this.
|
|
866
|
-
};
|
|
867
|
-
this.bumpVersion(); // Versions the entire aggregate (including child entities)
|
|
890
|
+
this.setState({
|
|
891
|
+
...this.state,
|
|
892
|
+
items: [...this.state.items, item],
|
|
893
|
+
total: this.state.total + price * quantity,
|
|
894
|
+
}, true); // true = bump version (versions the entire aggregate including child entities)
|
|
868
895
|
return itemId;
|
|
869
896
|
}
|
|
870
897
|
|
|
871
898
|
updateItemQuantity(itemId: ItemId, newQuantity: number): void {
|
|
872
|
-
const item = findEntityById(this.
|
|
899
|
+
const item = findEntityById(this.state.items, itemId);
|
|
873
900
|
if (!item) {
|
|
874
901
|
throw new Error("Item not found");
|
|
875
902
|
}
|
|
876
903
|
|
|
877
|
-
this.
|
|
878
|
-
...this.
|
|
879
|
-
items: updateEntityById(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
(i) => ({ ...i, quantity: newQuantity })
|
|
883
|
-
),
|
|
884
|
-
total: this._state.total - item.price * item.quantity + item.price * newQuantity,
|
|
885
|
-
};
|
|
886
|
-
this.bumpVersion(); // Versions the entire aggregate
|
|
904
|
+
this.setState({
|
|
905
|
+
...this.state,
|
|
906
|
+
items: updateEntityById(this.state.items, itemId, (i) => ({ ...i, quantity: newQuantity })),
|
|
907
|
+
total: this.state.total - item.price * item.quantity + item.price * newQuantity,
|
|
908
|
+
}, true);
|
|
887
909
|
}
|
|
888
910
|
|
|
889
911
|
removeItem(itemId: ItemId): void {
|
|
890
|
-
const item = findEntityById(this.
|
|
912
|
+
const item = findEntityById(this.state.items, itemId);
|
|
891
913
|
if (!item) {
|
|
892
914
|
throw new Error("Item not found");
|
|
893
915
|
}
|
|
894
916
|
|
|
895
|
-
this.
|
|
896
|
-
...this.
|
|
897
|
-
items: removeEntityById(this.
|
|
898
|
-
total: this.
|
|
899
|
-
};
|
|
900
|
-
this.bumpVersion(); // Versions the entire aggregate
|
|
917
|
+
this.setState({
|
|
918
|
+
...this.state,
|
|
919
|
+
items: removeEntityById(this.state.items, itemId),
|
|
920
|
+
total: this.state.total - item.price * item.quantity,
|
|
921
|
+
}, true);
|
|
901
922
|
}
|
|
902
923
|
|
|
903
924
|
getItem(itemId: ItemId): OrderItem | undefined {
|
|
904
|
-
return findEntityById(this.
|
|
925
|
+
return findEntityById(this.state.items, itemId);
|
|
905
926
|
}
|
|
906
927
|
}
|
|
907
928
|
|
|
@@ -950,15 +971,15 @@ class OrderItem extends Entity<OrderItemState, ItemId> {
|
|
|
950
971
|
if (newQuantity <= 0) {
|
|
951
972
|
throw new Error("Quantity must be greater than 0");
|
|
952
973
|
}
|
|
953
|
-
this.
|
|
974
|
+
this.setState({ ...this.state, quantity: newQuantity });
|
|
954
975
|
}
|
|
955
976
|
|
|
956
977
|
calculateSubtotal(): number {
|
|
957
|
-
return this.
|
|
978
|
+
return this.state.price * this.state.quantity;
|
|
958
979
|
}
|
|
959
980
|
|
|
960
981
|
isForProduct(productId: string): boolean {
|
|
961
|
-
return this.
|
|
982
|
+
return this.state.productId === productId;
|
|
962
983
|
}
|
|
963
984
|
|
|
964
985
|
protected validateState(state: OrderItemState): void {
|
|
@@ -995,17 +1016,16 @@ class Order extends AggregateRoot<OrderState, OrderId>
|
|
|
995
1016
|
const itemId = `item-${++this.itemCounter}` as ItemId;
|
|
996
1017
|
const item = new OrderItem(itemId, productId, quantity, price);
|
|
997
1018
|
|
|
998
|
-
this.
|
|
999
|
-
...this.
|
|
1000
|
-
items: [...this.
|
|
1001
|
-
};
|
|
1002
|
-
this.bumpVersion();
|
|
1019
|
+
this.setState({
|
|
1020
|
+
...this.state,
|
|
1021
|
+
items: [...this.state.items, item],
|
|
1022
|
+
}, true);
|
|
1003
1023
|
return itemId;
|
|
1004
1024
|
}
|
|
1005
1025
|
|
|
1006
1026
|
// Delegate to entity's business logic
|
|
1007
1027
|
updateItemQuantity(itemId: ItemId, newQuantity: number): void {
|
|
1008
|
-
const item = findEntityById(this.
|
|
1028
|
+
const item = findEntityById(this.state.items, itemId);
|
|
1009
1029
|
if (!item) throw new Error("Item not found");
|
|
1010
1030
|
|
|
1011
1031
|
item.updateQuantity(newQuantity); // Uses entity's logic
|
|
@@ -1014,18 +1034,17 @@ class Order extends AggregateRoot<OrderState, OrderId>
|
|
|
1014
1034
|
|
|
1015
1035
|
// Use entity's business logic
|
|
1016
1036
|
calculateTotal(): number {
|
|
1017
|
-
return this.
|
|
1037
|
+
return this.state.items.reduce(
|
|
1018
1038
|
(total, item) => total + item.calculateSubtotal(),
|
|
1019
1039
|
0
|
|
1020
1040
|
);
|
|
1021
1041
|
}
|
|
1022
1042
|
|
|
1023
1043
|
confirm(): void {
|
|
1024
|
-
if (this.
|
|
1044
|
+
if (this.state.items.length === 0) {
|
|
1025
1045
|
throw new Error("Cannot confirm an order without items");
|
|
1026
1046
|
}
|
|
1027
|
-
this.
|
|
1028
|
-
this.bumpVersion();
|
|
1047
|
+
this.setState({ ...this.state, status: "confirmed" }, true);
|
|
1029
1048
|
}
|
|
1030
1049
|
}
|
|
1031
1050
|
|
|
@@ -1167,11 +1186,11 @@ This package is written in TypeScript and provides full type definitions. All ty
|
|
|
1167
1186
|
Key exports include:
|
|
1168
1187
|
- `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
|
|
1169
1188
|
- `IAggregateRoot<TId>` - Marker interface for Aggregate Root Entities
|
|
1170
|
-
- `AggregateRoot<TState, TId
|
|
1171
|
-
- `
|
|
1172
|
-
- `AggregateConfig`, `
|
|
1189
|
+
- `AggregateRoot<TState, TId, TEvent?>` - Base class for creating Aggregate Root Entities without Event Sourcing (extends `Entity`, implements `IAggregateRoot<TId>`). Optional `TEvent` parameter enables type-safe domain events
|
|
1190
|
+
- `EventSourcedAggregate<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Roots (extends `Entity`, implements `IEventSourcedAggregate<TId, TEvent>`)
|
|
1191
|
+
- `AggregateConfig`, `EventSourcedAggregateConfig` - Configuration interfaces
|
|
1173
1192
|
- `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
|
|
1174
|
-
- `
|
|
1193
|
+
- `sameVersion()` - Optimistic concurrency check (same ID and version)
|
|
1175
1194
|
- `Entity<TState, TId>` - Base class for entities with state and business logic
|
|
1176
1195
|
- `IEntity<TId, TState>` - Entity interface
|
|
1177
1196
|
- `Identifiable<TId>` - Minimal interface for objects with id
|
|
@@ -1181,18 +1200,21 @@ Key exports include:
|
|
|
1181
1200
|
- `CommandBus`, `ICommandBus` - Command bus for centralized command execution
|
|
1182
1201
|
- `QueryBus`, `IQueryBus` - Query bus for centralized query execution (with `execute()` returning Result and `executeUnsafe()` throwing exceptions)
|
|
1183
1202
|
- `withCommit()` - Helper for transactional command execution with events
|
|
1184
|
-
- `DomainEvent<T, P
|
|
1185
|
-
- `
|
|
1203
|
+
- `DomainEvent<T, P?>` - Domain event interface (`P` defaults to `void` for payload-less events)
|
|
1204
|
+
- `EventMetadata` - Event metadata interface for traceability
|
|
1205
|
+
- `createDomainEvent()` - Event creation helper (payload is optional for payload-less events)
|
|
1206
|
+
- `createDomainEventWithMetadata()` - Event creation with metadata
|
|
1186
1207
|
- `copyMetadata()`, `mergeMetadata()` - Metadata utilities
|
|
1187
1208
|
- `EventBus<Evt>`, `EventBusImpl<Evt>` - Event bus interface and implementation for pub/sub pattern
|
|
1188
1209
|
- `EventHandler<Evt>` - Event handler function type
|
|
1189
1210
|
- `EventBus.subscribe()` - Subscribe handlers to event types
|
|
1190
|
-
- `EventBus.publish()` - Publish events to all subscribers
|
|
1211
|
+
- `EventBus.publish()` - Publish events to all subscribers (uses `Promise.allSettled` — all handlers run even if one fails)
|
|
1212
|
+
- `EventBus.once()` - Wait for the next event of a given type (returns Promise, auto-unsubscribes)
|
|
1191
1213
|
- `Result<T, E>`, `ok()`, `err()`, `isOk()`, `isErr()` - Result type and type guards
|
|
1192
1214
|
- `andThen()`, `map()`, `mapErr()` - Result composition utilities
|
|
1193
1215
|
- `unwrapOr()`, `unwrapOrElse()`, `match()` - Result unwrapping and pattern matching
|
|
1194
1216
|
- `Id<Tag>` - Branded ID type
|
|
1195
|
-
- `IRepository<
|
|
1217
|
+
- `IRepository<TAgg, TId>` - Repository interface
|
|
1196
1218
|
- `ISpecification<T>` - Specification interface
|
|
1197
1219
|
- `UnitOfWork` - Unit of Work interface
|
|
1198
1220
|
- `guard()` - Guard/validation helper
|
|
@@ -1313,7 +1335,7 @@ class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderId>
|
|
|
1313
1335
|
|
|
1314
1336
|
// 3. Save
|
|
1315
1337
|
await this.repository.save(order);
|
|
1316
|
-
await this.eventBus.publish(order.
|
|
1338
|
+
await this.eventBus.publish(order.domainEvents);
|
|
1317
1339
|
|
|
1318
1340
|
return ok(order.id);
|
|
1319
1341
|
// 4. Aggregate is garbage collected when method returns
|
|
@@ -1520,7 +1542,7 @@ class OrderService {
|
|
|
1520
1542
|
}
|
|
1521
1543
|
|
|
1522
1544
|
await this.repository.save(order);
|
|
1523
|
-
await this.eventBus.publish(order.
|
|
1545
|
+
await this.eventBus.publish(order.domainEvents);
|
|
1524
1546
|
|
|
1525
1547
|
return ok(order.id);
|
|
1526
1548
|
// order is garbage collected here
|