@shirudo/ddd-kit 0.16.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,18 +1,350 @@
1
- import { R as Result } from './result-jCwPSjFa.js';
2
- import { a as DeepEqualExceptOptions } from './deep-equal-except-C8yoSk4L.js';
1
+ import { Result } from '@shirudo/result';
2
+ import { DeepEqualExceptOptions } from './utils.js';
3
+ export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
3
4
 
5
+ /**
6
+ * Branded string ID. `Tag` carries the aggregate / entity name so two ids
7
+ * with different tags are not assignable to each other even though both
8
+ * are strings at runtime.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type UserId = Id<"UserId">;
13
+ * type OrderId = Id<"OrderId">;
14
+ *
15
+ * const u = "user-1" as UserId;
16
+ * const o: OrderId = u; // ❌ compile error
17
+ * ```
18
+ */
4
19
  type Id<Tag extends string> = string & {
5
20
  readonly __brand: Tag;
6
21
  };
7
- interface IdGenerator {
8
- next: <T extends string>() => Id<T>;
22
+ /**
23
+ * Produces fresh ids of a single, fixed tag. The tag is bound at the
24
+ * generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
25
+ * with no caller-side generic to abuse.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { ulid } from "ulid";
30
+ *
31
+ * const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
32
+ * const id = userIds.next(); // Id<"UserId">
33
+ * ```
34
+ *
35
+ * The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
36
+ * let callers pick `T` themselves — `gen.next<"AnyTag">()` typechecked
37
+ * even when the generator produced different-tag ids, silently defeating
38
+ * the brand.
39
+ */
40
+ interface IdGenerator<Tag extends string> {
41
+ next: () => Id<Tag>;
9
42
  }
10
43
 
11
44
  /**
12
- * Functional definition of an Entity via its capability.
13
- * An object is identifiable if it has an id.
45
+ * Factory function producing a fresh, unique event identifier for each call.
46
+ *
47
+ * The library ships a default that uses Web Crypto `crypto.randomUUID()`
48
+ * (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
49
+ * Cloudflare Workers, Vercel Edge, and any runtime that implements Web
50
+ * Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
51
+ * random) — for production event stores prefer a **time-ordered** id
52
+ * format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
53
+ * column stay clustered and `ORDER BY eventId` matches creation order.
54
+ * Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
55
+ */
56
+ type EventIdFactory = () => string;
57
+ /**
58
+ * Replaces the global event-id factory used by `createDomainEvent` and
59
+ * `createDomainEventWithMetadata`. Call once during application bootstrap,
60
+ * for example:
61
+ *
62
+ * ```ts
63
+ * import { ulid } from "ulid";
64
+ * import { setEventIdFactory } from "@shirudo/ddd-kit";
65
+ *
66
+ * setEventIdFactory(() => ulid());
67
+ * ```
68
+ *
69
+ * The per-call `options.eventId` override always wins over this factory.
70
+ *
71
+ * **Module-scoped — last setter wins.** The factory lives as a single
72
+ * module variable; importing two libraries that both call this races on
73
+ * load order. For multi-tenant request isolation (e.g. one factory per
74
+ * tenant in a single Worker invocation) **prefer the per-call
75
+ * `options.eventId`** instead of mutating the global. Same caveat applies
76
+ * to `setClockFactory`.
77
+ */
78
+ declare function setEventIdFactory(factory: EventIdFactory): void;
79
+ /**
80
+ * Restores the default event-id factory (`crypto.randomUUID()`).
81
+ * Intended for use in test `afterEach` hooks.
82
+ */
83
+ declare function resetEventIdFactory(): void;
84
+ /**
85
+ * Clock function producing a fresh `Date` for each call. The library
86
+ * defaults to `() => new Date()`; override globally via `setClockFactory`
87
+ * for deterministic event-sourcing tests, time-travel debugging, or any
88
+ * scenario where `occurredAt` must be reproducible.
14
89
  */
15
- type Identifiable<TId> = {
90
+ type ClockFactory = () => Date;
91
+ /**
92
+ * Replaces the global clock factory used by `createDomainEvent` and
93
+ * `createDomainEventWithMetadata`. Call once during application bootstrap
94
+ * (or per-test in deterministic test suites):
95
+ *
96
+ * ```ts
97
+ * import { setClockFactory } from "@shirudo/ddd-kit";
98
+ *
99
+ * setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
100
+ * ```
101
+ *
102
+ * The per-call `options.occurredAt` override always wins over this
103
+ * factory. Symmetric to `setEventIdFactory`.
104
+ */
105
+ declare function setClockFactory(factory: ClockFactory): void;
106
+ /**
107
+ * Restores the default clock factory (`() => new Date()`).
108
+ * Intended for use in test `afterEach` hooks.
109
+ */
110
+ declare function resetClockFactory(): void;
111
+ /**
112
+ * Metadata associated with a domain event for traceability and correlation.
113
+ * Used in event-driven architectures to track event flow across services.
114
+ */
115
+ interface EventMetadata {
116
+ /**
117
+ * Correlation ID for tracing events across multiple services/components.
118
+ * Typically used to group related events in a distributed system.
119
+ */
120
+ correlationId?: string;
121
+ /**
122
+ * Causation ID referencing the event or command that caused this event.
123
+ * Used to build event chains and understand causality.
124
+ */
125
+ causationId?: string;
126
+ /**
127
+ * User ID of the person or system that triggered the event.
128
+ */
129
+ userId?: string;
130
+ /**
131
+ * Source service or component that produced the event.
132
+ */
133
+ source?: string;
134
+ /**
135
+ * Additional custom metadata fields.
136
+ * Allows extensibility for domain-specific metadata.
137
+ */
138
+ [key: string]: unknown;
139
+ }
140
+ /**
141
+ * Domain Event represents something meaningful that happened in the domain.
142
+ * Events are immutable and carry information about what occurred.
143
+ *
144
+ * @template T - The event type name (e.g., "OrderCreated")
145
+ * @template P - The event payload type
146
+ */
147
+ interface DomainEvent<T extends string, P = void> {
148
+ /**
149
+ * Unique identifier for this specific event instance. Used by idempotent
150
+ * consumers, outbox dispatch tracking, and as the target of
151
+ * `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
152
+ * supplied.
153
+ */
154
+ eventId: string;
155
+ /**
156
+ * The type of the event, used for routing and handling.
157
+ */
158
+ type: T;
159
+ /**
160
+ * Identifier of the aggregate that produced the event. Optional at the
161
+ * library level — set it whenever the producing aggregate is known so
162
+ * downstream subscribers, outboxes, and projections can scope by entity.
163
+ */
164
+ aggregateId?: string;
165
+ /**
166
+ * Name of the aggregate type that produced the event (e.g. "Order").
167
+ * Pairs with `aggregateId` to fully qualify the source aggregate.
168
+ */
169
+ aggregateType?: string;
170
+ /**
171
+ * The event payload containing the domain data. The field is always
172
+ * present; its value is `undefined` when `P` is `void`.
173
+ */
174
+ payload: P;
175
+ /**
176
+ * Timestamp when the event occurred.
177
+ */
178
+ occurredAt: Date;
179
+ /**
180
+ * Event schema version for handling schema evolution.
181
+ * Required for safe schema migration in event-sourced systems.
182
+ * Use 1 for the initial schema version.
183
+ */
184
+ version: number;
185
+ /**
186
+ * Optional metadata for traceability, correlation, and auditing.
187
+ * Includes correlationId, causationId, userId, source, and custom fields.
188
+ */
189
+ metadata?: EventMetadata;
190
+ }
191
+ /**
192
+ * Shared option bag for the `createDomainEvent*` factories.
193
+ */
194
+ interface CreateDomainEventOptions {
195
+ /**
196
+ * Override for the auto-generated `eventId`. Pass an existing id (for
197
+ * replay, tests, or deterministic event sourcing) instead of letting the
198
+ * factory call `crypto.randomUUID()`.
199
+ */
200
+ eventId?: string;
201
+ /**
202
+ * Identifier of the aggregate that produced the event.
203
+ */
204
+ aggregateId?: string;
205
+ /**
206
+ * Name of the aggregate type that produced the event.
207
+ */
208
+ aggregateType?: string;
209
+ /**
210
+ * Override for the auto-generated `occurredAt` timestamp.
211
+ */
212
+ occurredAt?: Date;
213
+ /**
214
+ * Override for the default schema version (1).
215
+ */
216
+ version?: number;
217
+ /**
218
+ * Event metadata — correlation, causation, user, source, custom fields.
219
+ */
220
+ metadata?: EventMetadata;
221
+ }
222
+ /**
223
+ * Creates a domain event with default values.
224
+ * Sets occurredAt to current date and version to 1 if not provided.
225
+ *
226
+ * @param type - The event type
227
+ * @param payload - The event payload
228
+ * @param options - Optional event configuration
229
+ * @returns A domain event
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const event = createDomainEvent("OrderCreated", { orderId: "123" });
234
+ * ```
235
+ */
236
+ declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
237
+ declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
238
+ /**
239
+ * Creates a domain event with metadata for traceability.
240
+ * Convenience function for creating events with correlation and causation IDs.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * const event = createDomainEventWithMetadata(
245
+ * "OrderCreated",
246
+ * { orderId: "123" },
247
+ * { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
248
+ * );
249
+ * ```
250
+ */
251
+ declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
252
+ /**
253
+ * Copies metadata from a source event to a new event.
254
+ * Useful for maintaining correlation chains in event-driven architectures.
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const newEvent = createDomainEvent(
259
+ * "OrderShipped",
260
+ * { orderId: "123" },
261
+ * { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
262
+ * );
263
+ * ```
264
+ */
265
+ declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
266
+ /**
267
+ * Merges multiple metadata objects into one.
268
+ * Later metadata objects override earlier ones for the same keys.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const metadata = mergeMetadata(
273
+ * { correlationId: "corr-123" },
274
+ * { userId: "user-456" },
275
+ * { source: "order-service" }
276
+ * );
277
+ * ```
278
+ */
279
+ declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
280
+
281
+ /**
282
+ * Entity utilities and interfaces for Domain-Driven Design.
283
+ *
284
+ * In Domain-Driven Design, there are two types of entities:
285
+ *
286
+ * 1. **Aggregate Root Entity**: The parent Entity of an aggregate.
287
+ * - Has identity (id), state, and version
288
+ * - Implemented by classes extending `AggregateRoot` or `EventSourcedAggregate`
289
+ * - Represents the aggregate externally
290
+ * - Loaded/saved through repositories
291
+ *
292
+ * 2. **Child Entities**: Entities within an aggregate.
293
+ * - Have identity (id) and state, but no own version
294
+ * - Can extend `EntityBase<TState, TId>` for class-based entities
295
+ * - Or use functional style with `Identifiable<TId> & TProps`
296
+ * - Exist only within the aggregate boundary
297
+ * - Versioned through the Aggregate Root
298
+ * - Cannot be referenced directly from outside the aggregate
299
+ *
300
+ * This module provides:
301
+ * - `EntityBase<TState, TId>` - Base class for entities with state
302
+ * - `Entity<TId>` - Simple class for entities without state management
303
+ * - `Identifiable<TId>` - Minimal interface for objects with id
304
+ * - Helper functions for working with collections of entities
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * // Class-based child entity with logic
309
+ * class OrderItem extends EntityBase<OrderItemState, ItemId> {
310
+ * constructor(id: ItemId, initialState: OrderItemState) {
311
+ * super(id, initialState);
312
+ * }
313
+ *
314
+ * updateQuantity(quantity: number): void {
315
+ * this._state = { ...this._state, quantity };
316
+ * }
317
+ *
318
+ * calculateSubtotal(): number {
319
+ * return this._state.price * this._state.quantity;
320
+ * }
321
+ * }
322
+ *
323
+ * // Functional-style child entity (simpler, no logic)
324
+ * type OrderItem = Identifiable<ItemId> & {
325
+ * productId: string;
326
+ * quantity: number;
327
+ * price: number;
328
+ * };
329
+ *
330
+ * // Aggregate Root (Entity with version)
331
+ * class Order extends AggregateRoot<OrderState, OrderId> {
332
+ * // Order is an Aggregate Root Entity
333
+ * // OrderState contains OrderItem child entities
334
+ * }
335
+ * ```
336
+ */
337
+
338
+ /**
339
+ * Functional definition of an Entity via its capability — an object is
340
+ * identifiable if it has an `id`.
341
+ *
342
+ * `TId` is constrained to `Id<string>` so the brand discipline that
343
+ * `Id<Tag>` enforces is preserved end-to-end: an `Identifiable<UserId>`
344
+ * cannot accidentally be paired with an `Identifiable<OrderId>` or with
345
+ * a plain `string`.
346
+ */
347
+ type Identifiable<TId extends Id<string>> = {
16
348
  readonly id: TId;
17
349
  };
18
350
  /**
@@ -72,7 +404,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
72
404
  readonly id: TId;
73
405
  /**
74
406
  * Returns the current state of the entity.
75
- * State is readonly from outside to enforce encapsulation.
407
+ *
408
+ * The state object is **shallowly frozen** — direct property writes
409
+ * (`entity.state.foo = …`) throw in strict mode, but writes to nested
410
+ * objects (`entity.state.address.zip = …`) bypass the freeze. For deep
411
+ * immutability either model nested data with `vo()` (which freezes
412
+ * deeply) or reach for a structural-sharing library like Immer at the
413
+ * App layer. The shallow contract is intentional: deep freezing on
414
+ * every state write is too expensive for hot paths, and DDD aggregates
415
+ * normally treat their own state as private (`Tell, Don't Ask`).
76
416
  */
77
417
  get state(): TState;
78
418
  /**
@@ -82,12 +422,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
82
422
  protected _state: TState;
83
423
  protected constructor(id: TId, initialState: TState);
84
424
  /**
85
- * Optional validation hook to ensure state invariants.
86
- * Called during construction and whenever helpful.
87
- * Override this method to implement validation logic.
425
+ * Optional validation hook to ensure state invariants. Called during
426
+ * construction (from `Entity`'s constructor) and again on every
427
+ * `setState()` call. Throw to reject invalid state.
428
+ *
429
+ * **⚠️ Must not read subclass instance fields via `this`.** The
430
+ * constructor calls `validateState(initialState)` BEFORE the subclass's
431
+ * field initializers run, so `this.someField` is `undefined` at that
432
+ * point — a classic TypeScript/JavaScript constructor-ordering footgun.
433
+ * The `state` argument is the single source of truth; treat the method
434
+ * as pure with respect to `this`.
435
+ *
436
+ * If your invariants genuinely depend on per-instance configuration
437
+ * that isn't part of the state, pass that configuration into the state
438
+ * itself (DDD-canonical: the aggregate's state contains everything it
439
+ * needs) or perform the additional check after construction in a
440
+ * dedicated factory method.
88
441
  *
89
442
  * @param state - The state to validate
90
- * @throws Error if validation fails
443
+ * @throws Error (or `DomainError` subclass) if validation fails
91
444
  */
92
445
  protected validateState(_state: TState): void;
93
446
  /**
@@ -99,6 +452,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
99
452
  */
100
453
  protected setState(newState: TState): void;
101
454
  }
455
+ /**
456
+ * Shallow-freezes `value` when it's a non-null object or array, so that
457
+ * direct property writes throw in strict mode. Returns the value as-is for
458
+ * primitives. Used internally by `Entity` and its subclasses to prevent
459
+ * outside mutation of state read through the `state` getter without paying
460
+ * the cost of a deep clone on every read.
461
+ *
462
+ * Exported so that sibling classes (`EventSourcedAggregate`, `AggregateRoot`)
463
+ * can apply the same freeze when they bypass `setState` and assign
464
+ * `this._state` directly.
465
+ */
466
+ declare function freezeShallow<T>(value: T): T;
102
467
  /**
103
468
  * Checks if two entities have the same ID.
104
469
  * Works with any object that has an 'id' property.
@@ -116,7 +481,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
116
481
  * sameEntity(item1, item1); // true
117
482
  * ```
118
483
  */
119
- declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
484
+ declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
120
485
  /**
121
486
  * Finds an entity by ID in a collection.
122
487
  * Returns undefined if not found.
@@ -136,7 +501,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
136
501
  * // item is { id: itemId1, productId: "prod-1", quantity: 2 }
137
502
  * ```
138
503
  */
139
- declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
504
+ declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
140
505
  /**
141
506
  * Checks if an entity with the given ID exists in the collection.
142
507
  *
@@ -154,7 +519,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
154
519
  * hasEntityId(items, itemId2); // false
155
520
  * ```
156
521
  */
157
- declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
522
+ declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
158
523
  /**
159
524
  * Removes an entity with the given ID from the collection.
160
525
  * Returns a new array without the entity.
@@ -174,7 +539,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
174
539
  * // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
175
540
  * ```
176
541
  */
177
- declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
542
+ declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
178
543
  /**
179
544
  * Updates an entity with the given ID in the collection.
180
545
  * Returns a new array with the updated entity.
@@ -198,7 +563,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
198
563
  * // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
199
564
  * ```
200
565
  */
201
- declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
566
+ declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
202
567
  /**
203
568
  * Replaces an entity with the given ID in the collection.
204
569
  * Returns a new array with the replaced entity.
@@ -222,7 +587,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
222
587
  * });
223
588
  * ```
224
589
  */
225
- declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
590
+ declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
226
591
  /**
227
592
  * Extracts all IDs from a collection of entities.
228
593
  *
@@ -240,7 +605,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
240
605
  * // ids is [itemId1, itemId2]
241
606
  * ```
242
607
  */
243
- declare function entityIds<TId, T extends Identifiable<TId>>(entities: T[]): TId[];
608
+ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[]): TId[];
244
609
 
245
610
  /**
246
611
  * Marker interface for Aggregate Roots.
@@ -280,45 +645,58 @@ interface IAggregateRoot<TId extends Id<string>> {
280
645
  * This version applies to the entire aggregate, including all child entities.
281
646
  */
282
647
  readonly version: Version;
648
+ /**
649
+ * Post-save hook: a `Repository.save()` implementation calls this with
650
+ * the persisted version after a successful write to push the new
651
+ * version back into the aggregate and clear any recorded domain events
652
+ * (they are now safely on the write side / in the outbox).
653
+ *
654
+ * Required by the interface so a Repository implementation can call it
655
+ * via the published `IAggregateRoot` contract without taking the
656
+ * abstract class as a compile-time dependency.
657
+ *
658
+ * @param version - The version assigned by the persistence layer
659
+ */
660
+ markPersisted(version: Version): void;
283
661
  }
284
662
  /**
285
663
  * Configuration options for AggregateRoot behavior.
286
664
  */
287
665
  interface AggregateConfig {
288
666
  /**
289
- * Whether to automatically bump the version when state changes.
290
- * Defaults to false. Set to true for automatic versioning.
667
+ * Whether `setState()` should bump the version automatically.
668
+ *
669
+ * Defaults to **`false`** for `AggregateRoot` — because `setState()`
670
+ * already takes an explicit `bumpVersion` argument per call, so adding
671
+ * an "always bump" config on top would be redundant. Keep it `false`
672
+ * unless you have a subclass that never passes `bumpVersion` and you
673
+ * want every state change to advance the version anyway.
674
+ *
675
+ * (Contrast with `EventSourcedAggregate`, which defaults this to
676
+ * `true` because every event-sourced state change is per definition a
677
+ * versioned commit.)
291
678
  */
292
679
  autoVersionBump?: boolean;
293
680
  }
294
681
  /**
295
- * Base class for creating Aggregate Roots (Entities) without Event Sourcing.
682
+ * Base class for Aggregate Roots without Event Sourcing.
296
683
  *
297
- * This class creates an Entity that serves as the Aggregate Root. The Aggregate Root
298
- * is the parent Entity of the aggregate and represents it externally. It has identity
299
- * (id), state, and version for optimistic concurrency control.
684
+ * In DDD (Evans), an Aggregate is a cluster of objects root entity, child entities,
685
+ * and value objects treated as a unit for consistency. The **Aggregate Root** is the
686
+ * root entity that represents the aggregate externally and is the only entry point
687
+ * for external code. This class serves as both: it IS the root entity and it contains
688
+ * the aggregate state (`TState`) which holds child entities and value objects.
300
689
  *
301
- * Extends `Entity<TState, TId>` to inherit:
302
- * - Identity (id)
303
- * - State management
304
- * - State validation
305
- *
306
- * Adds Aggregate Root specific functionality:
307
- * - Version management (for Optimistic Concurrency Control)
308
- * - Domain events tracking
690
+ * Provides:
691
+ * - Identity (id) and state management (via `Entity`)
692
+ * - Version management for optimistic concurrency control
693
+ * - Domain event tracking for side-effects
309
694
  * - Snapshot support for performance optimization
310
695
  *
311
- * The aggregate state (`TState`) contains:
312
- * - Child entities (Entities with id + state, but no own version)
313
- * - Value objects (immutable objects)
314
- *
315
- * All changes to child entities are versioned through the Aggregate Root. The version
316
- * applies to the entire aggregate, including all child entities.
696
+ * All changes to child entities within `TState` are versioned through this root.
697
+ * Use `setState()` for state mutations to ensure invariant validation.
317
698
  *
318
- * Implements `IAggregateRoot<TId>` to mark this as an Aggregate Root Entity.
319
- *
320
- * Use this class when you don't need Event Sourcing but still want
321
- * aggregate patterns with versioning and state management.
699
+ * For event sourcing, use `EventSourcedAggregate` instead.
322
700
  *
323
701
  * @template TState - The type of the aggregate state (contains child entities and value objects)
324
702
  * @template TId - The type of the aggregate root identifier
@@ -333,14 +711,15 @@ interface AggregateConfig {
333
711
  * }
334
712
  *
335
713
  * confirm(): void {
336
- * this._state = { ...this._state, status: "confirmed" };
337
- * this.bumpVersion(); // Versions the entire aggregate
714
+ * this.setState({ ...this.state, status: "confirmed" }, true);
338
715
  * }
339
716
  * }
340
717
  * ```
341
718
  */
342
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = unknown> extends Entity<TState, TId> implements IAggregateRoot<TId> {
343
- version: Version;
719
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
720
+ private _version;
721
+ get version(): Version;
722
+ protected setVersion(version: Version): void;
344
723
  private readonly _config;
345
724
  private readonly _autoVersionBump;
346
725
  private _domainEvents;
@@ -354,12 +733,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
354
733
  * Call this after dispatching the events.
355
734
  */
356
735
  clearDomainEvents(): void;
736
+ /**
737
+ * Post-save hook called by a `Repository.save()` implementation to push
738
+ * the persisted version back into the in-memory aggregate and clear the
739
+ * recorded domain events (they are now safely on the write side / in
740
+ * the outbox).
741
+ *
742
+ * Use this so `save()` can keep its `Promise<void>` return type: the
743
+ * caller holds the aggregate reference, which is up to date after this
744
+ * call.
745
+ */
746
+ markPersisted(version: Version): void;
747
+ /**
748
+ * Mutates state and records the resulting domain events in the
749
+ * **canonical record-after-mutation order**. Use this instead of calling
750
+ * `setState` + `addDomainEvent` separately and you cannot trip the
751
+ * "event for a fact that never happened" footgun.
752
+ *
753
+ * Order of operations:
754
+ * 1. `setState(newState, true)` — runs `validateState` first.
755
+ * If it throws, the method propagates and **no event is recorded
756
+ * and no version is bumped**.
757
+ * 2. Each event in `events` is appended via `addDomainEvent`.
758
+ *
759
+ * `commit()` **always bumps the version**, regardless of the aggregate's
760
+ * `autoVersionBump` config. Recording a domain event implies "something
761
+ * happened that the outside world cares about", and optimistic-
762
+ * concurrency callers must see a fresh version every time. The config
763
+ * still governs the un-coupled `setState` path. If you need to mutate
764
+ * state without bumping (e.g. cosmetic caches), call `setState(newState,
765
+ * false)` and skip `commit` entirely.
766
+ *
767
+ * `events` accepts a single event or an array. Omit it (or pass `[]`)
768
+ * for state-only mutations.
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * confirm(): void {
773
+ * if (this.state.status === "confirmed") {
774
+ * throw new OrderAlreadyConfirmedError(this.id);
775
+ * }
776
+ * this.commit(
777
+ * { ...this.state, status: "confirmed" },
778
+ * { type: "OrderConfirmed", orderId: this.id },
779
+ * );
780
+ * }
781
+ * ```
782
+ *
783
+ * `EventSourcedAggregate.apply()` enforces the same ordering
784
+ * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
785
+ * where `setState` and `addDomainEvent` are otherwise decoupled and the
786
+ * ordering is convention-only.
787
+ *
788
+ * @param newState - The new state (validated by `validateState`)
789
+ * @param events - One event, an array of events, or none (default)
790
+ */
791
+ protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
357
792
  protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
358
793
  /**
359
- * Adds a domain event to the aggregate's list of changes.
360
- * Use this to record side-effects that should be published.
794
+ * Records a domain event for later publication.
795
+ *
796
+ * **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
797
+ * explicit: a domain event describes something that has just happened
798
+ * to the aggregate — its existence implies the state change already
799
+ * occurred. Concretely:
800
+ *
801
+ * ```ts
802
+ * confirm(): void {
803
+ * if (this.state.status === "confirmed") {
804
+ * throw new OrderAlreadyConfirmedError(this.id);
805
+ * }
806
+ * this.setState({ ...this.state, status: "confirmed" }, true);
807
+ * this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
808
+ * // ↑ post-mutation. The event represents the committed fact.
809
+ * }
810
+ * ```
811
+ *
812
+ * Recording before mutation is a footgun: if a subsequent invariant
813
+ * check throws, the event has already been queued but the state never
814
+ * actually changed — consumers see an event for a fact that did not
815
+ * happen.
361
816
  *
362
- * @param event - The domain event to add
817
+ * `EventSourcedAggregate.apply()` enforces this ordering structurally;
818
+ * `AggregateRoot` leaves it as a convention because the state-mutation
819
+ * path (`setState`) is decoupled from event recording.
820
+ *
821
+ * @param event - The domain event to record
363
822
  */
364
823
  protected addDomainEvent(event: TEvent): void;
365
824
  /**
@@ -410,6 +869,54 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
410
869
  restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
411
870
  }
412
871
 
872
+ /**
873
+ * Abstract base for all domain-layer exceptions in ddd-kit.
874
+ *
875
+ * Domain methods throw `DomainError`-derived exceptions when invariants are
876
+ * violated. Consumers derive their own concrete errors from this base
877
+ * (e.g. `OrderAlreadyShippedError extends DomainError`) so that domain
878
+ * failures have sprechende names and can be caught via `instanceof`.
879
+ *
880
+ * The library itself uses this base only for its own internal errors
881
+ * (`MissingHandlerError`, `AggregateNotFoundError`) — everything else
882
+ * is consumer territory.
883
+ */
884
+ declare abstract class DomainError extends Error {
885
+ constructor(message: string, options?: ErrorOptions);
886
+ }
887
+ /**
888
+ * Thrown by `EventSourcedAggregate.apply()` when no handler is registered
889
+ * for the given event type. Indicates a missing entry in the aggregate's
890
+ * `handlers` map — a programming error in the aggregate definition.
891
+ */
892
+ declare class MissingHandlerError extends DomainError {
893
+ readonly eventType: string;
894
+ constructor(eventType: string);
895
+ }
896
+ /**
897
+ * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the given
898
+ * id does not exist. Use the nullable variant `getById()` if "not found"
899
+ * is an expected outcome.
900
+ */
901
+ declare class AggregateNotFoundError extends DomainError {
902
+ readonly aggregateType: string;
903
+ readonly id: string;
904
+ constructor(aggregateType: string, id: string);
905
+ }
906
+ /**
907
+ * Thrown by `IRepository.save()` when the aggregate's expected version does
908
+ * not match the version currently persisted — i.e. another writer updated
909
+ * the aggregate concurrently. The canonical DDD optimistic-concurrency
910
+ * signal; callers typically reload, re-apply the use case, and retry.
911
+ */
912
+ declare class ConcurrencyConflictError extends DomainError {
913
+ readonly aggregateType: string;
914
+ readonly aggregateId: string;
915
+ readonly expectedVersion: number;
916
+ readonly actualVersion: number;
917
+ constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number);
918
+ }
919
+
413
920
  /**
414
921
  * Interface for Event-Sourced Aggregate Roots.
415
922
  * Defines the contract for aggregates that manage state changes via event sourcing.
@@ -417,17 +924,19 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
417
924
  * @template TId - The type of the aggregate root identifier
418
925
  * @template TEvent - The union type of all domain events
419
926
  */
420
- interface IAggregateEventSourced<TId extends Id<string>, TEvent extends DomainEvent<string, unknown>> extends IAggregateRoot<TId> {
927
+ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEvent<string, unknown>> extends IAggregateRoot<TId> {
421
928
  /**
422
929
  * Returns a read-only list of new, not-yet-persisted events.
423
930
  */
424
931
  readonly pendingEvents: ReadonlyArray<TEvent>;
425
932
  /**
426
- * Reconstitutes the aggregate from an event history.
933
+ * Reconstitutes the aggregate from an event history. Returns `Result`
934
+ * because event-stream corruption is an expected recoverable failure
935
+ * at the infrastructure boundary.
427
936
  *
428
937
  * @param history - An ordered list of past events
429
938
  */
430
- loadFromHistory(history: TEvent[]): Result<void, string>;
939
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
431
940
  /**
432
941
  * Clears the list of pending events.
433
942
  */
@@ -447,336 +956,174 @@ interface IAggregateEventSourced<TId extends Id<string>, TEvent extends DomainEv
447
956
  }
448
957
  type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
449
958
  /**
450
- * Extended configuration options for AggregateEventSourced behavior.
959
+ * Configuration options for EventSourcedAggregate behavior.
451
960
  */
452
- interface AggregateEventSourcedConfig extends AggregateConfig {
961
+ interface EventSourcedAggregateConfig {
453
962
  /**
454
- * Whether to automatically bump the version when applying new events.
455
- * Defaults to true. Set to false to manually control versioning.
456
- */
457
- autoVersionBump?: boolean;
458
- }
459
- /**
460
- * Base class for Event-Sourced Aggregate Roots (Entities).
461
- *
462
- * Extends `AggregateRoot` to create an Aggregate Root Entity with Event Sourcing capabilities.
463
- * The Aggregate Root is the parent Entity of the aggregate and represents it externally.
464
- *
465
- * The aggregate state (`TState`) contains:
466
- * - Child entities (Entities with id, but no own version)
467
- * - Value objects (immutable objects)
468
- *
469
- * All changes to child entities are versioned through the Aggregate Root. The version
470
- * applies to the entire aggregate, including all child entities.
471
- *
472
- * Extends `AggregateRoot` with Event Sourcing capabilities:
473
- * - Event tracking (pendingEvents)
474
- * - Event handlers for state transitions
475
- * - Event validation
476
- * - History replay
477
- *
478
- * Use this class when you want Event Sourcing with full event tracking
479
- * and replay capabilities.
480
- *
481
- * @template TState - The type of the aggregate state (contains child entities and value objects)
482
- * @template TEvent - The union type of all domain events
483
- * @template TId - The type of the aggregate root identifier
484
- *
485
- * @example
486
- * ```typescript
487
- * // Order is an Aggregate Root (an Entity) with Event Sourcing
488
- * class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> {
489
- * confirm(): void {
490
- * this.apply(createDomainEvent("OrderConfirmed", {}));
491
- * }
492
- *
493
- * protected readonly handlers = {
494
- * OrderConfirmed: (state: OrderState): OrderState => ({
495
- * ...state,
496
- * status: "confirmed",
497
- * }),
498
- * };
499
- * }
500
- * ```
501
- */
502
- declare abstract class AggregateEventSourced<TState, TEvent extends DomainEvent<string, unknown>, TId extends Id<string>> extends AggregateRoot<TState, TId, TEvent> implements IAggregateEventSourced<TId, TEvent> {
503
- private readonly _eventConfig;
504
- private readonly _eventAutoVersionBump;
505
- protected constructor(id: TId, initialState: TState, config?: AggregateEventSourcedConfig);
506
- /**
507
- * Returns a read-only list of new, not-yet-persisted events.
508
- */
509
- get pendingEvents(): ReadonlyArray<TEvent>;
510
- /**
511
- * Clears the list of pending events.
512
- * Typically called after the events have been persisted.
513
- */
514
- clearPendingEvents(): void;
515
- /**
516
- * Validates an event before it is applied.
517
- * Override this method to add custom validation logic.
518
- * Return `ok(true)` if the event is valid, `err(message)` otherwise.
519
- *
520
- * @param event - The event to validate
521
- * @returns Result indicating if the event is valid
522
- *
523
- * @example
524
- * ```typescript
525
- * protected validateEvent(event: OrderEvent): Result<true, string> {
526
- * if (event.type === "OrderShipped" && this.state.status !== "confirmed") {
527
- * return err("Order must be confirmed before shipping");
528
- * }
529
- * return ok(true);
530
- * }
531
- * ```
532
- */
533
- protected validateEvent(_event: TEvent): Result<true, string>;
534
- /**
535
- * Applies an event to change the state and adds it
536
- * to the list of pending events.
537
- * Returns a Result type instead of throwing an error.
538
- *
539
- * @param event - The domain event to apply
540
- * @param isNew - Indicates whether the event is new (and needs to be persisted)
541
- * or if it is being loaded from history
542
- * @returns Result<void, string> - ok if successful, err with error message if validation fails or handler is missing
543
- */
544
- protected apply(event: TEvent, isNew?: boolean): Result<void, string>;
545
- /**
546
- * Applies an event to change the state and adds it
547
- * to the list of pending events.
548
- * Throws an error if validation fails or handler is missing.
549
- *
550
- * @param event - The domain event to apply
551
- * @param isNew - Indicates whether the event is new (and needs to be persisted)
552
- * or if it is being loaded from history
553
- * @throws Error if event validation fails or handler is missing
554
- */
555
- protected applyUnsafe(event: TEvent, isNew?: boolean): void;
556
- /**
557
- * Manually bumps the aggregate version.
558
- * Only needed if `autoVersionBump` is disabled.
559
- */
560
- protected bumpVersion(): void;
561
- /**
562
- * Reconstitutes the aggregate from an event history.
563
- * Sets the version to the number of events in the history.
564
- *
565
- * @param history - An ordered list of past events
566
- */
567
- loadFromHistory(history: TEvent[]): Result<void, string>;
568
- /**
569
- * Checks if the aggregate has any pending events.
570
- *
571
- * @returns true if there are pending events, false otherwise
572
- */
573
- hasPendingEvents(): boolean;
574
- /**
575
- * Returns the number of pending events.
576
- *
577
- * @returns The count of pending events
578
- */
579
- getEventCount(): number;
580
- /**
581
- * Returns the latest pending event, if any.
582
- *
583
- * @returns The most recent event or undefined if no events exist
584
- */
585
- getLatestEvent(): TEvent | undefined;
586
- /**
587
- * Restores the aggregate from a snapshot and applies events that occurred after the snapshot.
588
- * This is more efficient than replaying all events from the beginning.
589
- *
590
- * @param snapshot - The snapshot to restore from
591
- * @param eventsAfterSnapshot - Events that occurred after the snapshot was taken
592
- *
593
- * @example
594
- * ```typescript
595
- * const snapshot = await snapshotRepository.getLatest(aggregateId);
596
- * const eventsAfter = await eventStore.getEventsAfter(aggregateId, snapshot.version);
597
- * aggregate.restoreFromSnapshotWithEvents(snapshot, eventsAfter);
598
- * ```
599
- */
600
- restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, string>;
601
- /**
602
- * A map of event types to their corresponding handlers.
603
- * Subclasses MUST implement this property.
604
- */
605
- protected abstract readonly handlers: {
606
- [K in TEvent["type"]]: Handler<TState, Extract<TEvent, {
607
- type: K;
608
- }>>;
609
- };
610
- }
611
-
612
- type Version = number & {
613
- readonly __v: true;
614
- };
615
- /**
616
- * Metadata associated with a domain event for traceability and correlation.
617
- * Used in event-driven architectures to track event flow across services.
618
- */
619
- interface EventMetadata {
620
- /**
621
- * Correlation ID for tracing events across multiple services/components.
622
- * Typically used to group related events in a distributed system.
623
- */
624
- correlationId?: string;
625
- /**
626
- * Causation ID referencing the event or command that caused this event.
627
- * Used to build event chains and understand causality.
628
- */
629
- causationId?: string;
630
- /**
631
- * User ID of the person or system that triggered the event.
632
- */
633
- userId?: string;
634
- /**
635
- * Source service or component that produced the event.
636
- */
637
- source?: string;
638
- /**
639
- * Additional custom metadata fields.
640
- * Allows extensibility for domain-specific metadata.
641
- */
642
- [key: string]: unknown;
643
- }
644
- /**
645
- * Domain Event represents something meaningful that happened in the domain.
646
- * Events are immutable and carry information about what occurred.
647
- *
648
- * @template T - The event type name (e.g., "OrderCreated")
649
- * @template P - The event payload type
650
- */
651
- interface DomainEvent<T extends string, P = void> {
652
- /**
653
- * The type of the event, used for routing and handling.
654
- */
655
- type: T;
656
- /**
657
- * The event payload containing the domain data.
658
- * Omitted when P is void (events without payload).
659
- */
660
- payload: P;
661
- /**
662
- * Timestamp when the event occurred.
663
- */
664
- occurredAt: Date;
665
- /**
666
- * Event schema version for handling schema evolution.
667
- * Defaults to 1 if not specified. Higher versions indicate schema changes.
668
- */
669
- version?: number;
670
- /**
671
- * Optional metadata for traceability, correlation, and auditing.
672
- * Includes correlationId, causationId, userId, source, and custom fields.
673
- */
674
- metadata?: EventMetadata;
675
- }
676
-
677
- /**
678
- * Structural interface representing an aggregate with state and events.
679
- * Used for type constraints in repositories and other infrastructure code.
680
- *
681
- * @template State - The type of the aggregate state
682
- * @template Evt - The union type of all domain events
683
- */
684
- interface Aggregate<State, Evt extends DomainEvent<string, unknown>> {
685
- state: Readonly<State>;
686
- version: Version;
687
- pendingEvents: ReadonlyArray<Evt>;
688
- }
689
- declare function aggregate<State, Evt extends DomainEvent<string, unknown>>(state: State, version?: Version): Aggregate<State, Evt>;
690
- declare function withEvent<S, E extends DomainEvent<string, unknown>>(agg: Aggregate<S, E>, evt: E): Aggregate<S, E>;
691
- declare function bump<S, E extends DomainEvent<string, unknown>>(agg: Aggregate<S, E>): Aggregate<S, E>;
692
- /**
693
- * Creates a domain event with default values.
694
- * Sets occurredAt to current date and version to 1 if not provided.
695
- *
696
- * @param type - The event type
697
- * @param payload - The event payload
698
- * @param options - Optional event configuration
699
- * @returns A domain event
700
- *
701
- * @example
702
- * ```typescript
703
- * const event = createDomainEvent("OrderCreated", { orderId: "123" });
704
- * ```
705
- */
706
- declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: {
707
- occurredAt?: Date;
708
- version?: number;
709
- metadata?: EventMetadata;
710
- }): DomainEvent<T, void>;
711
- declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: {
712
- occurredAt?: Date;
713
- version?: number;
714
- metadata?: EventMetadata;
715
- }): DomainEvent<T, P>;
963
+ * Whether `apply()` should bump the version per event.
964
+ *
965
+ * Defaults to **`true`** for `EventSourcedAggregate` — each applied
966
+ * event is by definition a versioned state change, so the canonical
967
+ * event-sourcing pattern is "one event = one version bump". Set to
968
+ * `false` only if your event store assigns version numbers itself
969
+ * and you want the aggregate to track them via `bumpVersion()` /
970
+ * `setVersion()` calls instead.
971
+ *
972
+ * (Contrast with `AggregateRoot`, which defaults this to `false`
973
+ * because its `setState()` already takes a per-call `bumpVersion`
974
+ * argument.)
975
+ */
976
+ autoVersionBump?: boolean;
977
+ }
716
978
  /**
717
- * Creates a domain event with metadata for traceability.
718
- * Convenience function for creating events with correlation and causation IDs.
979
+ * Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
719
980
  *
720
- * @param type - The event type
721
- * @param payload - The event payload
722
- * @param metadata - Event metadata for traceability
723
- * @param options - Optional event configuration
724
- * @returns A domain event with metadata
981
+ * Like `AggregateRoot`, this is both the root entity and the aggregate boundary.
982
+ * The difference is persistence: state is derived from events, not stored directly.
983
+ * Events are the single source of truth — all state changes go through `apply()` → handler.
725
984
  *
726
- * @example
727
- * ```typescript
728
- * const event = createDomainEventWithMetadata(
729
- * "OrderCreated",
730
- * { orderId: "123" },
731
- * {
732
- * correlationId: "corr-123",
733
- * causationId: "cmd-456",
734
- * userId: "user-789"
735
- * }
736
- * );
737
- * ```
738
- */
739
- declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: {
740
- occurredAt?: Date;
741
- version?: number;
742
- }): DomainEvent<T, P>;
743
- /**
744
- * Copies metadata from a source event to a new event.
745
- * Useful for maintaining correlation chains in event-driven architectures.
985
+ * Extends `Entity` directly (not `AggregateRoot`) so that `setState()` and
986
+ * `addDomainEvent()` are not available. This enforces the event sourcing pattern
987
+ * at the type level — there is no way to mutate state without going through an event handler.
746
988
  *
747
- * @param sourceEvent - The source event to copy metadata from
748
- * @param additionalMetadata - Additional metadata to merge in
749
- * @returns Event metadata with copied and merged values
989
+ * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
990
+ * invariant violations. Subclasses override `validateEvent()` to throw their
991
+ * own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
992
+ * infrastructure-boundary methods (`loadFromHistory`,
993
+ * `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
994
+ * during replay so callers can react to corrupted event streams without
995
+ * try/catch.
996
+ *
997
+ * @template TState - The type of the aggregate state (contains child entities and value objects)
998
+ * @template TEvent - The union type of all domain events
999
+ * @template TId - The type of the aggregate root identifier
750
1000
  *
751
1001
  * @example
752
1002
  * ```typescript
753
- * const newEvent = createDomainEvent(
754
- * "OrderShipped",
755
- * { orderId: "123" },
756
- * {
757
- * metadata: copyMetadata(previousEvent, { causationId: previousEvent.type })
1003
+ * class OrderAlreadyConfirmedError extends DomainError {
1004
+ * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1005
+ * }
1006
+ *
1007
+ * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1008
+ * confirm(): void {
1009
+ * this.apply(createDomainEvent("OrderConfirmed", {}));
758
1010
  * }
759
- * );
760
- * ```
761
- */
762
- declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
763
- /**
764
- * Merges multiple metadata objects into one.
765
- * Later metadata objects override earlier ones for the same keys.
766
1011
  *
767
- * @param metadataObjects - Array of metadata objects to merge
768
- * @returns Merged event metadata
1012
+ * protected validateEvent(event: OrderEvent): void {
1013
+ * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1014
+ * throw new OrderAlreadyConfirmedError(this.id);
1015
+ * }
1016
+ * }
769
1017
  *
770
- * @example
771
- * ```typescript
772
- * const metadata = mergeMetadata(
773
- * { correlationId: "corr-123" },
774
- * { userId: "user-456" },
775
- * { source: "order-service" }
776
- * );
1018
+ * protected readonly handlers = {
1019
+ * OrderConfirmed: (state: OrderState): OrderState => ({
1020
+ * ...state,
1021
+ * status: "confirmed",
1022
+ * }),
1023
+ * };
1024
+ * }
777
1025
  * ```
778
1026
  */
779
- declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
1027
+ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<string, unknown>, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
1028
+ private _version;
1029
+ get version(): Version;
1030
+ private setVersion;
1031
+ private _pendingEvents;
1032
+ private readonly _autoVersionBump;
1033
+ get pendingEvents(): ReadonlyArray<TEvent>;
1034
+ clearPendingEvents(): void;
1035
+ /**
1036
+ * Post-save hook called by a `Repository.save()` implementation to push
1037
+ * the persisted version back into the in-memory aggregate and clear the
1038
+ * pending events (they are now in the event store / outbox). Lets
1039
+ * `save()` keep its `Promise<void>` return type.
1040
+ */
1041
+ markPersisted(version: Version): void;
1042
+ protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
1043
+ /**
1044
+ * Validates an event before it is applied. Default is no-op.
1045
+ * Subclasses override to throw a concrete `DomainError` subclass when
1046
+ * the event violates an invariant in the current state.
1047
+ */
1048
+ protected validateEvent(_event: TEvent): void;
1049
+ /**
1050
+ * Applies an event: validates, locates the handler, computes the next
1051
+ * state, then commits state + pending event + version bump atomically.
1052
+ *
1053
+ * Throws `DomainError` (or a subclass) on validation failure.
1054
+ * Throws `MissingHandlerError` if no handler is registered for `event.type`.
1055
+ *
1056
+ * State is not mutated if any step throws — the handler is invoked into
1057
+ * a local and only assigned to `_state` once all checks pass.
1058
+ *
1059
+ * The method is generic in the event tag `K`, so concrete callers
1060
+ * (`this.apply(orderCreated)`) narrow to the literal tag and the
1061
+ * dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
1062
+ * — no `as` cast required at the call site.
1063
+ *
1064
+ * @param event - The domain event to apply
1065
+ * @param isNew - Whether the event is new (needs persisting) or replayed from history
1066
+ */
1067
+ protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
1068
+ type: K;
1069
+ }>, isNew?: boolean): void;
1070
+ /**
1071
+ * Internal dispatch path used by `apply()` and the replay methods
1072
+ * (`loadFromHistory`, `restoreFromSnapshotWithEvents`). The replay loop
1073
+ * iterates over `TEvent[]` and therefore cannot supply a narrowed `K`
1074
+ * generic, so this helper accepts `TEvent` and the discriminator is
1075
+ * resolved via the (statically-sound) `handlers` map.
1076
+ */
1077
+ private dispatchAndCommit;
1078
+ /**
1079
+ * Manually bumps the aggregate version.
1080
+ * Only needed if `autoVersionBump` is disabled.
1081
+ */
1082
+ protected bumpVersion(): void;
1083
+ /**
1084
+ * Reconstitutes the aggregate from an event history. Catches `DomainError`
1085
+ * thrown during replay and returns it as an `Err` — this is the
1086
+ * infrastructure boundary, where event-stream corruption is an expected
1087
+ * recoverable failure. Unexpected (non-DomainError) throws propagate.
1088
+ *
1089
+ * Version advances additively: the aggregate's pre-existing version plus
1090
+ * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
1091
+ * an aggregate already at v=1 (e.g. after a creation event) loading
1092
+ * 2 events ends at v=3, not v=2.
1093
+ */
1094
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
1095
+ hasPendingEvents(): boolean;
1096
+ getEventCount(): number;
1097
+ getLatestEvent(): TEvent | undefined;
1098
+ /**
1099
+ * Creates a snapshot of the current aggregate state.
1100
+ */
1101
+ createSnapshot(): AggregateSnapshot<TState>;
1102
+ /**
1103
+ * Restores the aggregate from a snapshot and applies events that occurred
1104
+ * after. Same infrastructure-boundary semantics as `loadFromHistory`:
1105
+ * catches `DomainError` and returns it as an `Err`; non-domain throws
1106
+ * propagate.
1107
+ *
1108
+ * All-or-nothing: if any event mid-stream throws a `DomainError`, the
1109
+ * aggregate is rolled back to its pre-call state + version. Partial
1110
+ * restoration is never observable to the caller.
1111
+ */
1112
+ restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
1113
+ /**
1114
+ * A map of event types to their corresponding handlers.
1115
+ * Subclasses MUST implement this property.
1116
+ */
1117
+ protected abstract readonly handlers: {
1118
+ [K in TEvent["type"]]: Handler<TState, Extract<TEvent, {
1119
+ type: K;
1120
+ }>>;
1121
+ };
1122
+ }
1123
+
1124
+ type Version = number & {
1125
+ readonly __v: true;
1126
+ };
780
1127
  /**
781
1128
  * Snapshot of an aggregate state at a specific point in time.
782
1129
  * Used for optimizing event replay by starting from a snapshot
@@ -799,25 +1146,24 @@ interface AggregateSnapshot<TState> {
799
1146
  snapshotAt: Date;
800
1147
  }
801
1148
  /**
802
- * Checks if two aggregates are the same (same ID and version).
1149
+ * Checks if two aggregates are at the same version (same ID and version).
803
1150
  * Useful for optimistic concurrency control checks.
804
1151
  *
805
- * @param a - First aggregate
806
- * @param b - Second aggregate
807
- * @returns true if both aggregates have the same ID and version
1152
+ * Note: Two aggregates with the same ID ARE the same aggregate (identity).
1153
+ * This function checks if they are at the same version — i.e., no concurrent modification.
808
1154
  *
809
1155
  * @example
810
1156
  * ```typescript
811
- * const aggregate1 = await repository.getById(id);
1157
+ * const before = await repository.getById(id);
812
1158
  * // ... some operations ...
813
- * const aggregate2 = await repository.getById(id);
1159
+ * const after = await repository.getById(id);
814
1160
  *
815
- * if (!sameAggregate(aggregate1, aggregate2)) {
1161
+ * if (!sameVersion(before, after)) {
816
1162
  * throw new Error("Aggregate was modified by another process");
817
1163
  * }
818
1164
  * ```
819
1165
  */
820
- declare function sameAggregate<TId extends Id<string>>(a: {
1166
+ declare function sameVersion<TId extends Id<string>>(a: {
821
1167
  id: TId;
822
1168
  version: Version;
823
1169
  }, b: {
@@ -913,42 +1259,85 @@ interface Command {
913
1259
  */
914
1260
  type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string>>;
915
1261
 
1262
+ /**
1263
+ * Type map for command types to their return types.
1264
+ * Used to improve type inference in CommandBus.
1265
+ *
1266
+ * @example
1267
+ * ```typescript
1268
+ * type MyCommandMap = {
1269
+ * CreateOrder: OrderId;
1270
+ * CancelOrder: void;
1271
+ * };
1272
+ *
1273
+ * const bus = new CommandBus<MyCommandMap>();
1274
+ * const result = await bus.execute({ type: "CreateOrder", ... });
1275
+ * // result: Result<OrderId, string> ← automatically inferred
1276
+ * ```
1277
+ */
1278
+ type CommandTypeMap = Record<string, unknown>;
916
1279
  /**
917
1280
  * Command Bus interface for dispatching commands to their handlers.
918
1281
  * Provides a centralized way to execute commands with handler registration.
919
1282
  *
1283
+ * Supports an optional type map (`TMap`) for automatic return type inference.
1284
+ * Without a type map, the return type must be specified manually or defaults to `unknown`.
1285
+ *
1286
+ * @template TMap - Optional mapping from command type strings to return types
1287
+ *
920
1288
  * @example
921
1289
  * ```typescript
1290
+ * // With type map (recommended) – return type is inferred
1291
+ * type MyCommands = { CreateOrder: OrderId; CancelOrder: void };
1292
+ * const bus = new CommandBus<MyCommands>();
1293
+ * const result = await bus.execute({ type: "CreateOrder", ... });
1294
+ * // result: Result<OrderId, string>
1295
+ *
1296
+ * // Without type map – works like before
922
1297
  * const bus = new CommandBus();
923
1298
  * bus.register("CreateOrder", createOrderHandler);
924
- *
925
- * const result = await bus.execute({
926
- * type: "CreateOrder",
927
- * customerId: "123",
928
- * items: [...]
929
- * });
1299
+ * const result = await bus.execute({ type: "CreateOrder", ... });
1300
+ * // result: Result<unknown, string>
930
1301
  * ```
931
1302
  */
932
- interface ICommandBus {
1303
+ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
933
1304
  /**
934
1305
  * Executes a command by dispatching it to the registered handler.
1306
+ * When a type map is provided, the return type is inferred from the command type.
935
1307
  *
936
1308
  * @param command - The command to execute
937
1309
  * @returns Result containing the success value or error message
938
1310
  */
1311
+ execute<C extends Command & {
1312
+ type: keyof TMap & string;
1313
+ }>(command: C): Promise<Result<TMap[C["type"]], string>>;
939
1314
  execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
940
1315
  /**
941
1316
  * Registers a handler for a specific command type.
942
1317
  *
1318
+ * When `TMap` is supplied, the `commandType` argument is restricted to
1319
+ * its keys and the handler signature is forced to match `TMap[K]` for the
1320
+ * return value — typos and wrong-typed handlers are compile errors.
1321
+ * Without `TMap` the registration is loose (any string key, any return
1322
+ * type) so the no-config path keeps working.
1323
+ *
943
1324
  * @param commandType - The command type to register the handler for
944
1325
  * @param handler - The handler function for this command type
945
1326
  */
946
- register<C extends Command, R>(commandType: C["type"], handler: CommandHandler<C, R>): void;
1327
+ register<K extends keyof TMap & string, C extends Command & {
1328
+ type: K;
1329
+ } = Command & {
1330
+ type: K;
1331
+ }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
947
1332
  }
948
1333
  /**
949
1334
  * Simple in-memory command bus implementation.
950
1335
  * Handlers are stored in a Map and dispatched based on command type.
951
1336
  *
1337
+ * Supports an optional type map (`TMap`) for automatic return type inference.
1338
+ * When `TMap` is provided, `execute()` infers the result type from the command type.
1339
+ * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
1340
+ *
952
1341
  * **Note:** This is a basic implementation suitable for development and simple use cases.
953
1342
  * For production environments, consider implementing or using a more feature-rich bus that includes:
954
1343
  * - Middleware/Pipeline support (logging, validation, authorization)
@@ -961,20 +1350,32 @@ interface ICommandBus {
961
1350
  * The `CommandHandler` type can still be used with external production-grade buses
962
1351
  * (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
963
1352
  *
1353
+ * @template TMap - Optional mapping from command type strings to return types
1354
+ *
964
1355
  * @example
965
1356
  * ```typescript
966
- * const bus = new CommandBus();
967
- * bus.register("CreateOrder", async (cmd) => {
968
- * // ... handler logic
969
- * return ok(orderId);
970
- * });
1357
+ * // With type map – full inference
1358
+ * type Commands = { CreateOrder: OrderId; CancelOrder: void };
1359
+ * const bus = new CommandBus<Commands>();
1360
+ * const result = await bus.execute({ type: "CreateOrder", ... });
1361
+ * // result: Result<OrderId, string>
971
1362
  *
1363
+ * // Without type map – same as before
1364
+ * const bus = new CommandBus();
1365
+ * bus.register("CreateOrder", async (cmd) => ok(orderId));
972
1366
  * const result = await bus.execute({ type: "CreateOrder", ... });
973
1367
  * ```
974
1368
  */
975
- declare class CommandBus implements ICommandBus {
1369
+ declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
976
1370
  private readonly handlers;
977
- register<C extends Command, R>(commandType: C["type"], handler: CommandHandler<C, R>): void;
1371
+ register<K extends keyof TMap & string, C extends Command & {
1372
+ type: K;
1373
+ } = Command & {
1374
+ type: K;
1375
+ }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
1376
+ execute<C extends Command & {
1377
+ type: keyof TMap & string;
1378
+ }>(command: C): Promise<Result<TMap[C["type"]], string>>;
978
1379
  execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
979
1380
  }
980
1381
 
@@ -1007,10 +1408,33 @@ type EventHandler<Evt> = (event: Evt) => Promise<void> | void;
1007
1408
  * await bus.publish([orderCreatedEvent, orderShippedEvent]);
1008
1409
  * ```
1009
1410
  */
1010
- interface EventBus<Evt> {
1411
+ interface EventBus<Evt extends {
1412
+ type: string;
1413
+ }> {
1011
1414
  /**
1012
1415
  * Publishes events to all subscribed handlers.
1013
- * All handlers for each event type will be called.
1416
+ *
1417
+ * **Ordering & parallelism contract:**
1418
+ *
1419
+ * 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`,
1420
+ * awaits all of its handlers, then dispatches `b`, and so on. The
1421
+ * library never reorders or parallelises across events.
1422
+ * 2. **Handlers within a single event run in parallel.** All handlers
1423
+ * subscribed to `event.type` are awaited via `Promise.allSettled` —
1424
+ * none of them sees the others' errors and none is skipped if a
1425
+ * peer fails.
1426
+ * 3. **Errors are collected and thrown AFTER everything dispatches.**
1427
+ * If one handler throws, remaining handlers for that event still
1428
+ * run, and remaining events in the batch still publish. Once
1429
+ * `publish` reaches the end of the batch it throws — the single
1430
+ * error directly if there was one, or an `AggregateError`
1431
+ * ("Multiple event handlers failed") containing every captured
1432
+ * error otherwise. Callers that need fail-fast semantics should
1433
+ * publish events one at a time and not rely on batch atomicity.
1434
+ *
1435
+ * The contract is intentionally simple and in-process. For
1436
+ * cross-process delivery (RabbitMQ, Kafka, etc.), use the `Outbox`
1437
+ * port and a dedicated dispatcher.
1014
1438
  *
1015
1439
  * @param events - Array of events to publish
1016
1440
  */
@@ -1033,7 +1457,9 @@ interface EventBus<Evt> {
1033
1457
  * unsubscribe();
1034
1458
  * ```
1035
1459
  */
1036
- subscribe: <T extends Evt>(eventType: string, handler: EventHandler<T>) => () => void;
1460
+ subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1461
+ type: K;
1462
+ }>>) => () => void;
1037
1463
  /**
1038
1464
  * Subscribes to the next occurrence of an event type.
1039
1465
  * Returns a Promise that resolves with the event data.
@@ -1048,27 +1474,125 @@ interface EventBus<Evt> {
1048
1474
  * console.log("Order created:", event.payload.orderId);
1049
1475
  * ```
1050
1476
  */
1051
- once: <T extends Evt>(eventType: string) => Promise<T>;
1477
+ once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
1478
+ type: K;
1479
+ }>>;
1480
+ }
1481
+ /**
1482
+ * Options for `EventBus.once()`. Both fields are optional; without them
1483
+ * `once()` waits forever (the historical behaviour).
1484
+ */
1485
+ interface OnceOptions {
1486
+ /**
1487
+ * Aborts the wait. When `signal` fires, `once()` rejects with
1488
+ * `signal.reason` (or a generic abort error if none was supplied) and
1489
+ * the internal subscription is removed.
1490
+ */
1491
+ signal?: AbortSignal;
1492
+ /**
1493
+ * Rejects with a timeout error after this many milliseconds if no event
1494
+ * has arrived. The internal subscription and timer are cleaned up
1495
+ * regardless of which path settles the promise.
1496
+ */
1497
+ timeoutMs?: number;
1498
+ }
1499
+ /**
1500
+ * One pending event in the outbox plus the opaque id the implementation
1501
+ * needs to ack it via `markDispatched`. The library does not prescribe
1502
+ * what `dispatchId` looks like — an implementation can reuse the event's
1503
+ * own `eventId`, generate its own UUID, use the row's auto-increment
1504
+ * primary key, or whatever the storage layer prefers.
1505
+ */
1506
+ interface OutboxRecord<Evt> {
1507
+ dispatchId: string;
1508
+ event: Evt;
1052
1509
  }
1510
+ /**
1511
+ * Transactional outbox port — the bridge between the write-side
1512
+ * transaction and the (out-of-band) event dispatcher.
1513
+ *
1514
+ * Lifecycle:
1515
+ * 1. `add()` inside the write transaction (`withCommit` calls this) so
1516
+ * events persist atomically with the aggregate state.
1517
+ * 2. A separate outbox dispatcher polls `getPending()` and forwards the
1518
+ * events to subscribers / external brokers.
1519
+ * 3. After successful dispatch, the dispatcher calls `markDispatched()`
1520
+ * with the records' `dispatchId`s so they don't come back next poll.
1521
+ *
1522
+ * `markDispatched` is required to be idempotent — calling it with an id
1523
+ * that's already marked is a no-op, not an error. This lets the
1524
+ * dispatcher safely retry on partial-failure.
1525
+ */
1053
1526
  interface Outbox<Evt> {
1527
+ /**
1528
+ * Persists events. Called from inside `withCommit`'s transactional
1529
+ * callback, atomically with the aggregate write.
1530
+ *
1531
+ * **Idempotency:** implementations should dedupe on the event's
1532
+ * `eventId`. `withCommit` itself does not retry, but the surrounding
1533
+ * use case (a queue consumer, an HTTP retry, a transactional
1534
+ * outbox-dispatcher loop) may legitimately invoke the same write more
1535
+ * than once. A unique-key constraint on `(eventId)` in the outbox
1536
+ * table is the standard implementation.
1537
+ */
1054
1538
  add: (events: ReadonlyArray<Evt>) => Promise<void>;
1055
- }
1056
- interface Clock {
1057
- now: () => Date;
1539
+ /**
1540
+ * Returns up to `limit` outbox records that have not yet been
1541
+ * dispatched. The dispatcher polls this on a schedule. When `limit`
1542
+ * is omitted, the implementation decides on a default page size.
1543
+ */
1544
+ getPending: (limit?: number) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
1545
+ /**
1546
+ * Marks the given dispatch records as delivered so subsequent
1547
+ * `getPending` calls don't return them. Must be idempotent on
1548
+ * already-marked ids.
1549
+ */
1550
+ markDispatched: (dispatchIds: ReadonlyArray<string>) => Promise<void>;
1058
1551
  }
1059
1552
 
1060
- interface UnitOfWork {
1553
+ /**
1554
+ * Transaction-scope abstraction.
1555
+ *
1556
+ * Wraps a block of work so it runs inside the persistence layer's native
1557
+ * transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, etc.). The block
1558
+ * commits when the callback resolves and rolls back if it throws.
1559
+ *
1560
+ * This is **not** Fowler's full Unit of Work (no change tracking, no
1561
+ * registerDirty/registerNew/registerDeleted, no commit-time flush). It is
1562
+ * intentionally minimal — change tracking is the ORM's job; the library
1563
+ * stays out of it. The name `TransactionScope` is therefore more honest
1564
+ * than `UnitOfWork`.
1565
+ *
1566
+ * @example
1567
+ * ```typescript
1568
+ * await scope.transactional(async () => {
1569
+ * const order = await repo.getByIdOrFail(orderId);
1570
+ * order.confirm();
1571
+ * await repo.save(order);
1572
+ * });
1573
+ * ```
1574
+ */
1575
+ interface TransactionScope {
1061
1576
  transactional<T>(fn: () => Promise<T>): Promise<T>;
1062
1577
  }
1063
- type RepoProvider<R> = (uow: UnitOfWork) => R;
1064
1578
 
1065
1579
  /**
1066
- * Helper function for executing commands within a transaction.
1067
- * Handles event persistence via outbox and optional event bus publishing.
1068
- *
1069
- * @param deps - Dependencies including outbox, optional event bus, and unit of work
1070
- * @param fn - Function that returns result and events
1071
- * @returns The result wrapped in a transaction
1580
+ * Helper for executing a write Use Case inside a Unit of Work.
1581
+ *
1582
+ * Order of operations:
1583
+ * 1. `fn()` runs inside `uow.transactional(...)` domain mutations + repo
1584
+ * writes happen here.
1585
+ * 2. `outbox.add(events)` is also inside the transaction, so events
1586
+ * persist atomically with the state change (outbox pattern).
1587
+ * 3. The transaction commits.
1588
+ * 4. **After** the commit, `bus.publish(events)` fires for the in-process
1589
+ * fast path.
1590
+ *
1591
+ * Publishing AFTER commit prevents the classic "publish before commit"
1592
+ * footgun: in-process subscribers can never react to events from a
1593
+ * transaction that later rolled back. If `bus.publish` itself fails, the
1594
+ * outbox still holds the events and an outbox-dispatcher will deliver them
1595
+ * (eventual consistency).
1072
1596
  *
1073
1597
  * @example
1074
1598
  * ```typescript
@@ -1077,18 +1601,17 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
1077
1601
  * async () => {
1078
1602
  * const order = Order.create(customerId, items);
1079
1603
  * await repository.save(order);
1080
- * return {
1081
- * result: order.id,
1082
- * events: order.pendingEvents
1083
- * };
1604
+ * return { result: order.id, events: order.domainEvents };
1084
1605
  * }
1085
1606
  * );
1086
1607
  * ```
1087
1608
  */
1088
- declare function withCommit<Evt, R>(deps: {
1609
+ declare function withCommit<Evt extends {
1610
+ type: string;
1611
+ }, R>(deps: {
1089
1612
  outbox: Outbox<Evt>;
1090
1613
  bus?: EventBus<Evt>;
1091
- uow: UnitOfWork;
1614
+ scope: TransactionScope;
1092
1615
  }, fn: () => Promise<{
1093
1616
  result: R;
1094
1617
  events: ReadonlyArray<Evt>;
@@ -1169,29 +1692,57 @@ interface Query {
1169
1692
  */
1170
1693
  type QueryHandler<Q extends Query, R> = (query: Q) => Promise<R>;
1171
1694
 
1695
+ /**
1696
+ * Type map for query types to their return types.
1697
+ * Used to improve type inference in QueryBus.
1698
+ *
1699
+ * @example
1700
+ * ```typescript
1701
+ * type MyQueryMap = {
1702
+ * GetOrder: Order | null;
1703
+ * ListOrders: Order[];
1704
+ * };
1705
+ *
1706
+ * const bus = new QueryBus<MyQueryMap>();
1707
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1708
+ * // result: Result<Order | null, string> ← automatically inferred
1709
+ * ```
1710
+ */
1711
+ type QueryTypeMap = Record<string, unknown>;
1172
1712
  /**
1173
1713
  * Query Bus interface for dispatching queries to their handlers.
1174
1714
  * Provides a centralized way to execute queries with handler registration.
1175
1715
  *
1716
+ * Supports an optional type map (`TMap`) for automatic return type inference.
1717
+ * Without a type map, the return type must be specified manually or defaults to `unknown`.
1718
+ *
1719
+ * @template TMap - Optional mapping from query type strings to return types
1720
+ *
1176
1721
  * @example
1177
1722
  * ```typescript
1178
- * const bus = new QueryBus();
1179
- * bus.register("GetOrder", getOrderHandler);
1723
+ * // With type map (recommended) – return type is inferred
1724
+ * type MyQueries = { GetOrder: Order | null; ListOrders: Order[] };
1725
+ * const bus = new QueryBus<MyQueries>();
1726
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1727
+ * // result: Result<Order | null, string>
1180
1728
  *
1181
- * const order = await bus.execute({
1182
- * type: "GetOrder",
1183
- * orderId: "123"
1184
- * });
1729
+ * // Without type map – works like before
1730
+ * const bus = new QueryBus();
1731
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1732
+ * // result: Result<unknown, string>
1185
1733
  * ```
1186
1734
  */
1187
- interface IQueryBus {
1735
+ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
1188
1736
  /**
1189
1737
  * Executes a query by dispatching it to the registered handler.
1190
- * Returns a Result type instead of throwing an error.
1738
+ * When a type map is provided, the return type is inferred from the query type.
1191
1739
  *
1192
1740
  * @param query - The query to execute
1193
- * @returns Result containing the query result if successful, or an error message if no handler is registered
1741
+ * @returns Result containing the query result if successful, or an error message
1194
1742
  */
1743
+ execute<Q extends Query & {
1744
+ type: keyof TMap & string;
1745
+ }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
1195
1746
  execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
1196
1747
  /**
1197
1748
  * Executes a query by dispatching it to the registered handler.
@@ -1201,24 +1752,36 @@ interface IQueryBus {
1201
1752
  * @returns The query result
1202
1753
  * @throws Error if no handler is registered for the query type
1203
1754
  */
1755
+ executeUnsafe<Q extends Query & {
1756
+ type: keyof TMap & string;
1757
+ }>(query: Q): Promise<TMap[Q["type"]]>;
1204
1758
  executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
1205
1759
  /**
1206
1760
  * Registers a handler for a specific query type.
1207
1761
  *
1762
+ * When `TMap` is supplied, the `queryType` argument is restricted to its
1763
+ * keys and the handler signature is forced to match `TMap[K]` for the
1764
+ * return value — typos and wrong-typed handlers are compile errors.
1765
+ * Without `TMap` the registration is loose (any string key, any return
1766
+ * type) so the no-config path keeps working.
1767
+ *
1208
1768
  * @param queryType - The query type to register the handler for
1209
1769
  * @param handler - The handler function for this query type
1210
1770
  */
1211
- register<Q extends Query, R>(queryType: Q["type"], handler: QueryHandler<Q, R>): void;
1771
+ register<K extends keyof TMap & string, Q extends Query & {
1772
+ type: K;
1773
+ } = Query & {
1774
+ type: K;
1775
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
1212
1776
  }
1213
- /**
1214
- * Type map for query types to their return types.
1215
- * Used to improve type inference in QueryBus.
1216
- */
1217
- type QueryTypeMap = Record<string, unknown>;
1218
1777
  /**
1219
1778
  * Simple in-memory query bus implementation.
1220
1779
  * Handlers are stored in a Map and dispatched based on query type.
1221
1780
  *
1781
+ * Supports an optional type map (`TMap`) for automatic return type inference.
1782
+ * When `TMap` is provided, `execute()` and `executeUnsafe()` infer the result type from the query type.
1783
+ * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
1784
+ *
1222
1785
  * **Note:** This is a basic implementation suitable for development and simple use cases.
1223
1786
  * For production environments, consider implementing or using a more feature-rich bus that includes:
1224
1787
  * - Middleware/Pipeline support (logging, caching, rate limiting)
@@ -1231,44 +1794,39 @@ type QueryTypeMap = Record<string, unknown>;
1231
1794
  * The `QueryHandler` type can still be used with external production-grade buses
1232
1795
  * (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
1233
1796
  *
1797
+ * @template TMap - Optional mapping from query type strings to return types
1798
+ *
1234
1799
  * @example
1235
1800
  * ```typescript
1236
- * const bus = new QueryBus();
1237
- * bus.register("GetOrder", async (query) => {
1238
- * return await repository.getById(query.orderId);
1239
- * });
1801
+ * // With type map – full inference
1802
+ * type Queries = { GetOrder: Order | null; ListOrders: Order[] };
1803
+ * const bus = new QueryBus<Queries>();
1804
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1805
+ * // result: Result<Order | null, string>
1240
1806
  *
1241
- * const order = await bus.execute({ type: "GetOrder", orderId: "123" });
1807
+ * // Without type map same as before
1808
+ * const bus = new QueryBus();
1809
+ * bus.register("GetOrder", async (query) => repository.getById(query.orderId));
1810
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1242
1811
  * ```
1243
1812
  */
1244
- declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus {
1813
+ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
1245
1814
  private readonly handlers;
1246
- register<Q extends Query, R>(queryType: Q["type"], handler: QueryHandler<Q, R>): void;
1815
+ register<K extends keyof TMap & string, Q extends Query & {
1816
+ type: K;
1817
+ } = Query & {
1818
+ type: K;
1819
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
1247
1820
  execute<Q extends Query & {
1248
- type: keyof TMap;
1821
+ type: keyof TMap & string;
1249
1822
  }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
1250
1823
  execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
1824
+ executeUnsafe<Q extends Query & {
1825
+ type: keyof TMap & string;
1826
+ }>(query: Q): Promise<TMap[Q["type"]]>;
1251
1827
  executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
1252
1828
  }
1253
1829
 
1254
- /**
1255
- * Guard function that validates a condition and returns a Result.
1256
- * Returns `ok(true)` if the condition is met, otherwise `err(error)`.
1257
- *
1258
- * @param cond - The condition to check
1259
- * @param error - Error message if condition fails
1260
- * @returns Result<true, string>
1261
- *
1262
- * @example
1263
- * ```typescript
1264
- * const result = guard(id.length > 0, "ID cannot be empty");
1265
- * if (!result.ok) {
1266
- * return err(result.error);
1267
- * }
1268
- * ```
1269
- */
1270
- declare function guard(cond: boolean, error: string): Result<true, string>;
1271
-
1272
1830
  /**
1273
1831
  * Simple in-memory event bus implementation.
1274
1832
  * Supports multiple subscribers per event type (pub/sub pattern).
@@ -1293,68 +1851,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
1293
1851
  */
1294
1852
  declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
1295
1853
  private readonly handlers;
1296
- subscribe<T extends Evt>(eventType: string, handler: EventHandler<T>): () => void;
1297
- once<T extends Evt>(eventType: string): Promise<T>;
1854
+ subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1855
+ type: K;
1856
+ }>>): () => void;
1857
+ once<K extends Evt["type"]>(eventType: K, options?: OnceOptions): Promise<Extract<Evt, {
1858
+ type: K;
1859
+ }>>;
1860
+ /**
1861
+ * See {@link EventBus.publish} for the full ordering / parallelism /
1862
+ * error-aggregation contract this implementation realises:
1863
+ * - events in input order, sequentially;
1864
+ * - handlers within one event in parallel via `Promise.allSettled`;
1865
+ * - errors collected and thrown after the batch (single Error, or
1866
+ * `AggregateError` for multiple failures).
1867
+ */
1298
1868
  publish(events: ReadonlyArray<Evt>): Promise<void>;
1299
1869
  }
1300
1870
 
1301
1871
  /**
1302
- * A Specification is a named, standalone object that represents a business rule for a query.
1303
- * It is "translatable" into a concrete database query.
1872
+ * Core repository contract for Aggregate Roots.
1873
+ *
1874
+ * In DDD a Repository is a "collection illusion" for aggregates: load by
1875
+ * identity, save the whole aggregate, delete by identity. Querying by
1876
+ * arbitrary criteria is a separate concern (CQRS read-side, ad-hoc bulk
1877
+ * operations) and lives on the `IQueryableRepository` extension below — so
1878
+ * write-side repositories don't have to implement query plumbing they
1879
+ * don't need.
1880
+ *
1881
+ * Repositories work exclusively with Aggregate Root Entities. The Aggregate
1882
+ * Root represents the aggregate externally and is the only object that can
1883
+ * be loaded or saved through repositories. When loading, all child entities
1884
+ * and value objects inside the aggregate are loaded too; when saving, the
1885
+ * whole aggregate is persisted as a unit.
1886
+ *
1887
+ * @template TAgg - The aggregate root type (must implement IAggregateRoot)
1888
+ * @template TId - The type of the aggregate root identifier
1304
1889
  */
1305
- interface ISpecification<T> {
1306
- readonly _type: T;
1890
+ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
1891
+ /**
1892
+ * Loads an aggregate by id. Returns `null` when not found.
1893
+ */
1894
+ getById(id: TId): Promise<TAgg | null>;
1895
+ /**
1896
+ * Loads an aggregate by id and throws `AggregateNotFoundError` when not
1897
+ * found. Use this when "not found" is a programming/contract error in
1898
+ * the calling Use Case; use `getById` when null is a valid outcome.
1899
+ */
1900
+ getByIdOrFail(id: TId): Promise<TAgg>;
1901
+ /**
1902
+ * Returns whether an aggregate with the given id exists. Cheaper than
1903
+ * `getById !== null` if your storage supports `EXISTS`-style queries.
1904
+ */
1905
+ exists(id: TId): Promise<boolean>;
1906
+ /**
1907
+ * Persists the aggregate (insert or update). Implementations should:
1908
+ *
1909
+ * 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
1910
+ * aggregate's expected version does not match the version currently
1911
+ * stored (optimistic concurrency).
1912
+ * 2. After a successful write, call `aggregate.markPersisted(newVersion)`
1913
+ * so the in-memory aggregate reflects the new version and clears its
1914
+ * pending/domain events.
1915
+ *
1916
+ * Return type stays `void` — the caller already holds the aggregate
1917
+ * reference, which is now up to date.
1918
+ */
1919
+ save(aggregate: TAgg): Promise<void>;
1920
+ /**
1921
+ * Removes the aggregate by id.
1922
+ */
1923
+ delete(id: TId): Promise<void>;
1307
1924
  }
1308
-
1309
1925
  /**
1310
- * Repository interface for Aggregate Roots (Entities).
1926
+ * Repository extension that adds filter-based querying. `TFilter` is the
1927
+ * filter shape your persistence layer speaks: a Drizzle `SQL` expression, a
1928
+ * Prisma `WhereInput`, a MongoDB filter document, a plain
1929
+ * `(t: TAgg) => boolean` predicate for in-memory repos, or anything else.
1311
1930
  *
1312
- * Repositories work exclusively with Aggregate Root Entities. The Aggregate Root
1313
- * is the Entity that represents the aggregate externally and is the only object
1314
- * that can be loaded/saved through repositories.
1931
+ * The library does not prescribe a Specification or query DSL — the
1932
+ * Repository implementation owns its query language. This avoids the
1933
+ * phantom-interface trap of a library-level `ISpecification<T>` with no
1934
+ * methods and lets each Repository expose the strongest possible types for
1935
+ * its storage backend.
1315
1936
  *
1316
- * When loading an Aggregate Root, all child entities and value objects within
1317
- * the aggregate state are loaded as well. When saving, the entire aggregate
1318
- * (including all child entities) is persisted as a unit.
1937
+ * Aggregates that are only ever accessed by id should implement
1938
+ * `IRepository` directly and skip this extension.
1319
1939
  *
1320
- * Child entities cannot be loaded or saved independently - they exist only
1321
- * within the aggregate boundary and are managed through the Aggregate Root.
1940
+ * @template TAgg - The aggregate root type
1941
+ * @template TId - The aggregate root identifier type
1942
+ * @template TFilter - The filter shape understood by this repository
1322
1943
  *
1323
- * @template TState - The type of the aggregate state (contains child entities and value objects)
1324
- * @template TEvent - The union type of all domain events
1325
- * @template TAgg - The aggregate root type (must be an Aggregate Root Entity)
1326
- * @template TId - The type of the aggregate root identifier
1944
+ * @example
1945
+ * ```typescript
1946
+ * // In-memory repo with a predicate filter
1947
+ * type Predicate<T> = (t: T) => boolean;
1948
+ * class InMemoryOrders implements IQueryableRepository<Order, OrderId, Predicate<Order>> {
1949
+ * // ...
1950
+ * async find(filter: Predicate<Order>): Promise<Order[]> { ... }
1951
+ * async findOne(filter: Predicate<Order>): Promise<Order | null> { ... }
1952
+ * }
1953
+ *
1954
+ * // Drizzle repo with a SQL expression filter
1955
+ * import type { SQL } from "drizzle-orm";
1956
+ * class DrizzleOrders implements IQueryableRepository<Order, OrderId, SQL> {
1957
+ * // ...
1958
+ * }
1959
+ * ```
1327
1960
  */
1328
- interface IRepository<TState, TEvent extends DomainEvent<string, unknown>, TAgg extends IAggregateRoot<TId> & Aggregate<TState, TEvent>, TId extends Id<string>> {
1329
- getById(id: TId): Promise<TAgg | null>;
1330
- findOne(spec: ISpecification<TAgg>): Promise<TAgg | null>;
1331
- find(spec: ISpecification<TAgg>): Promise<TAgg[]>;
1332
- save(aggregate: TAgg): Promise<void>;
1333
- delete(id: TId): Promise<void>;
1961
+ interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>, TFilter> extends IRepository<TAgg, TId> {
1962
+ /**
1963
+ * Returns the first aggregate matching the filter, or `null` if none.
1964
+ */
1965
+ findOne(filter: TFilter): Promise<TAgg | null>;
1966
+ /**
1967
+ * Returns **every** aggregate matching the filter — no pagination,
1968
+ * no cursor. For unbounded result sets, prefer a read-side projection
1969
+ * (CQRS read model) over loading aggregates in bulk; aggregates are
1970
+ * write-side objects and rehydrating thousands of them by id is rarely
1971
+ * what you want. If you need pagination on the write side, declare a
1972
+ * domain-specific paged method on your concrete repository (e.g.
1973
+ * `findPage(filter, cursor)`) — the library does not prescribe a
1974
+ * pagination contract because cursor/offset/keyset semantics vary too
1975
+ * much across storage backends.
1976
+ */
1977
+ find(filter: TFilter): Promise<TAgg[]>;
1334
1978
  }
1335
1979
 
1336
1980
  type VO<T> = Readonly<T>;
1337
1981
  /**
1338
- * Deep freezes an object and all its nested properties recursively.
1339
- * This ensures true immutability for value objects with nested structures.
1340
- * Handles circular references by tracking visited objects.
1982
+ * Deep freezes an object and all its nested properties recursively, then
1983
+ * returns it. Iterates both string-keyed and symbol-keyed own properties
1984
+ * so the freeze symmetry matches `deepEqual` (which also considers symbol
1985
+ * keys). Handles circular references by tracking visited objects.
1986
+ *
1987
+ * Note: `deepFreeze` mutates its argument in place — it sets `[[Frozen]]`
1988
+ * on the object you pass in. Callers that need to avoid touching the
1989
+ * input (e.g. `vo()`) should deep-clone first.
1341
1990
  */
1342
1991
  declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
1343
1992
  /**
1344
1993
  * Creates a deeply immutable value object from the given data.
1345
- * All nested objects and arrays are frozen recursively.
1346
1994
  *
1347
- * @param t - The data to convert into a value object
1348
- * @returns A deeply frozen, immutable value object
1995
+ * The input is first deep-cloned with `structuredClone`, then the clone
1996
+ * is frozen so calling `vo(input)` never freezes the caller's own
1997
+ * object graph as a side-effect. Mutating the input afterwards does not
1998
+ * bleed into the VO.
1349
1999
  *
1350
2000
  * @example
1351
2001
  * ```typescript
1352
- * const address = vo({
1353
- * street: "Main St",
1354
- * city: "Berlin",
1355
- * coordinates: { lat: 52.5, lng: 13.4 }
1356
- * });
1357
- * // address.coordinates.lat = 99; // ❌ Error: Cannot assign to read-only property
2002
+ * const nested = { lat: 52.5, lng: 13.4 };
2003
+ * const address = vo({ street: "Main St", coordinates: nested });
2004
+ * address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
2005
+ * nested.lat = 0; // caller's input still mutable
1358
2006
  * ```
1359
2007
  */
1360
2008
  declare function vo<T>(t: T): VO<T>;
@@ -1453,26 +2101,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
1453
2101
  * ```
1454
2102
  */
1455
2103
  declare function voWithValidation<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): Result<VO<T>, string>;
1456
- /**
1457
- * Creates a value object with optional validation.
1458
- * Throws an error if validation fails.
1459
- *
1460
- * @param t - The data to convert into a value object
1461
- * @param validate - Validation function that returns true if valid
1462
- * @param errorMessage - Optional custom error message if validation fails
1463
- * @returns A deeply frozen, immutable value object
1464
- * @throws Error if validation fails
1465
- *
1466
- * @example
1467
- * ```typescript
1468
- * const money = voWithValidationUnsafe(
1469
- * { amount: 100, currency: "USD" },
1470
- * (m) => m.amount >= 0 && m.currency.length === 3,
1471
- * "Invalid money: amount must be non-negative and currency must be 3 characters"
1472
- * );
1473
- * ```
1474
- */
1475
- declare function voWithValidationUnsafe<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): VO<T>;
1476
2104
  /**
1477
2105
  * Interface for Value Objects.
1478
2106
  * Value Objects are immutable and defined by their properties.
@@ -1564,4 +2192,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
1564
2192
  toJSON(): Readonly<T>;
1565
2193
  }
1566
2194
 
1567
- export { type Aggregate, type AggregateConfig, AggregateEventSourced, type AggregateEventSourcedConfig, AggregateRoot, type AggregateSnapshot, type Clock, type Command, CommandBus, type CommandHandler, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventMetadata, type IAggregateEventSourced, type IAggregateRoot, type ICommandBus, type IEntity, type IQueryBus, type IRepository, type ISpecification, type IValueObject, type Id, type IdGenerator, type Identifiable, type Outbox, type Query, QueryBus, type QueryHandler, type RepoProvider, type UnitOfWork, type VO, ValueObject, type Version, aggregate, bump, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, guard, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, sameAggregate, sameEntity, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, voWithValidationUnsafe, withCommit, withEvent };
2195
+ export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type EventSourcedAggregateConfig, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IValueObject, type Id, type IdGenerator, type Identifiable, MissingHandlerError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type TransactionScope, type VO, ValueObject, type Version, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };