@shirudo/ddd-kit 1.0.0-rc.1 → 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,13 +1,113 @@
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
 
44
+ /**
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.
89
+ */
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;
11
111
  /**
12
112
  * Metadata associated with a domain event for traceability and correlation.
13
113
  * Used in event-driven architectures to track event flow across services.
@@ -45,13 +145,31 @@ interface EventMetadata {
45
145
  * @template P - The event payload type
46
146
  */
47
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;
48
155
  /**
49
156
  * The type of the event, used for routing and handling.
50
157
  */
51
158
  type: T;
52
159
  /**
53
- * The event payload containing the domain data.
54
- * Omitted when P is void (events without payload).
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`.
55
173
  */
56
174
  payload: P;
57
175
  /**
@@ -70,6 +188,37 @@ interface DomainEvent<T extends string, P = void> {
70
188
  */
71
189
  metadata?: EventMetadata;
72
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
+ }
73
222
  /**
74
223
  * Creates a domain event with default values.
75
224
  * Sets occurredAt to current date and version to 1 if not provided.
@@ -84,16 +233,8 @@ interface DomainEvent<T extends string, P = void> {
84
233
  * const event = createDomainEvent("OrderCreated", { orderId: "123" });
85
234
  * ```
86
235
  */
87
- declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: {
88
- occurredAt?: Date;
89
- version?: number;
90
- metadata?: EventMetadata;
91
- }): DomainEvent<T, void>;
92
- declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: {
93
- occurredAt?: Date;
94
- version?: number;
95
- metadata?: EventMetadata;
96
- }): DomainEvent<T, P>;
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>;
97
238
  /**
98
239
  * Creates a domain event with metadata for traceability.
99
240
  * Convenience function for creating events with correlation and causation IDs.
@@ -107,10 +248,7 @@ declare function createDomainEvent<T extends string, P>(type: T, payload: P, opt
107
248
  * );
108
249
  * ```
109
250
  */
110
- declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: {
111
- occurredAt?: Date;
112
- version?: number;
113
- }): DomainEvent<T, P>;
251
+ declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
114
252
  /**
115
253
  * Copies metadata from a source event to a new event.
116
254
  * Useful for maintaining correlation chains in event-driven architectures.
@@ -141,10 +279,72 @@ declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additio
141
279
  declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
142
280
 
143
281
  /**
144
- * Functional definition of an Entity via its capability.
145
- * An object is identifiable if it has an id.
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
+ * ```
146
336
  */
147
- type Identifiable<TId> = {
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>> = {
148
348
  readonly id: TId;
149
349
  };
150
350
  /**
@@ -204,7 +404,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
204
404
  readonly id: TId;
205
405
  /**
206
406
  * Returns the current state of the entity.
207
- * 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`).
208
416
  */
209
417
  get state(): TState;
210
418
  /**
@@ -214,12 +422,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
214
422
  protected _state: TState;
215
423
  protected constructor(id: TId, initialState: TState);
216
424
  /**
217
- * Optional validation hook to ensure state invariants.
218
- * Called during construction and whenever helpful.
219
- * 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.
220
441
  *
221
442
  * @param state - The state to validate
222
- * @throws Error if validation fails
443
+ * @throws Error (or `DomainError` subclass) if validation fails
223
444
  */
224
445
  protected validateState(_state: TState): void;
225
446
  /**
@@ -231,6 +452,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
231
452
  */
232
453
  protected setState(newState: TState): void;
233
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;
234
467
  /**
235
468
  * Checks if two entities have the same ID.
236
469
  * Works with any object that has an 'id' property.
@@ -248,7 +481,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
248
481
  * sameEntity(item1, item1); // true
249
482
  * ```
250
483
  */
251
- 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;
252
485
  /**
253
486
  * Finds an entity by ID in a collection.
254
487
  * Returns undefined if not found.
@@ -268,7 +501,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
268
501
  * // item is { id: itemId1, productId: "prod-1", quantity: 2 }
269
502
  * ```
270
503
  */
271
- 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;
272
505
  /**
273
506
  * Checks if an entity with the given ID exists in the collection.
274
507
  *
@@ -286,7 +519,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
286
519
  * hasEntityId(items, itemId2); // false
287
520
  * ```
288
521
  */
289
- 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;
290
523
  /**
291
524
  * Removes an entity with the given ID from the collection.
292
525
  * Returns a new array without the entity.
@@ -306,7 +539,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
306
539
  * // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
307
540
  * ```
308
541
  */
309
- 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[];
310
543
  /**
311
544
  * Updates an entity with the given ID in the collection.
312
545
  * Returns a new array with the updated entity.
@@ -330,7 +563,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
330
563
  * // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
331
564
  * ```
332
565
  */
333
- 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[];
334
567
  /**
335
568
  * Replaces an entity with the given ID in the collection.
336
569
  * Returns a new array with the replaced entity.
@@ -354,7 +587,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
354
587
  * });
355
588
  * ```
356
589
  */
357
- 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[];
358
591
  /**
359
592
  * Extracts all IDs from a collection of entities.
360
593
  *
@@ -372,7 +605,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
372
605
  * // ids is [itemId1, itemId2]
373
606
  * ```
374
607
  */
375
- 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[];
376
609
 
377
610
  /**
378
611
  * Marker interface for Aggregate Roots.
@@ -412,14 +645,36 @@ interface IAggregateRoot<TId extends Id<string>> {
412
645
  * This version applies to the entire aggregate, including all child entities.
413
646
  */
414
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;
415
661
  }
416
662
  /**
417
663
  * Configuration options for AggregateRoot behavior.
418
664
  */
419
665
  interface AggregateConfig {
420
666
  /**
421
- * Whether to automatically bump the version when state changes.
422
- * 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.)
423
678
  */
424
679
  autoVersionBump?: boolean;
425
680
  }
@@ -461,7 +716,7 @@ interface AggregateConfig {
461
716
  * }
462
717
  * ```
463
718
  */
464
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = unknown> extends Entity<TState, TId> implements IAggregateRoot<TId> {
719
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
465
720
  private _version;
466
721
  get version(): Version;
467
722
  protected setVersion(version: Version): void;
@@ -478,12 +733,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
478
733
  * Call this after dispatching the events.
479
734
  */
480
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;
481
792
  protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
482
793
  /**
483
- * Adds a domain event to the aggregate's list of changes.
484
- * Use this to record side-effects that should be published.
794
+ * Records a domain event for later publication.
485
795
  *
486
- * @param event - The domain event to add
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.
816
+ *
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
487
822
  */
488
823
  protected addDomainEvent(event: TEvent): void;
489
824
  /**
@@ -534,6 +869,54 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
534
869
  restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
535
870
  }
536
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
+
537
920
  /**
538
921
  * Interface for Event-Sourced Aggregate Roots.
539
922
  * Defines the contract for aggregates that manage state changes via event sourcing.
@@ -547,11 +930,13 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
547
930
  */
548
931
  readonly pendingEvents: ReadonlyArray<TEvent>;
549
932
  /**
550
- * 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.
551
936
  *
552
937
  * @param history - An ordered list of past events
553
938
  */
554
- loadFromHistory(history: TEvent[]): Result<void, string>;
939
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
555
940
  /**
556
941
  * Clears the list of pending events.
557
942
  */
@@ -575,8 +960,18 @@ type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
575
960
  */
576
961
  interface EventSourcedAggregateConfig {
577
962
  /**
578
- * Whether to automatically bump the version when applying new events.
579
- * Defaults to true. Set to false to manually control versioning.
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.)
580
975
  */
581
976
  autoVersionBump?: boolean;
582
977
  }
@@ -591,17 +986,35 @@ interface EventSourcedAggregateConfig {
591
986
  * `addDomainEvent()` are not available. This enforces the event sourcing pattern
592
987
  * at the type level — there is no way to mutate state without going through an event handler.
593
988
  *
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
+ *
594
997
  * @template TState - The type of the aggregate state (contains child entities and value objects)
595
998
  * @template TEvent - The union type of all domain events
596
999
  * @template TId - The type of the aggregate root identifier
597
1000
  *
598
1001
  * @example
599
1002
  * ```typescript
1003
+ * class OrderAlreadyConfirmedError extends DomainError {
1004
+ * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1005
+ * }
1006
+ *
600
1007
  * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
601
1008
  * confirm(): void {
602
1009
  * this.apply(createDomainEvent("OrderConfirmed", {}));
603
1010
  * }
604
1011
  *
1012
+ * protected validateEvent(event: OrderEvent): void {
1013
+ * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1014
+ * throw new OrderAlreadyConfirmedError(this.id);
1015
+ * }
1016
+ * }
1017
+ *
605
1018
  * protected readonly handlers = {
606
1019
  * OrderConfirmed: (state: OrderState): OrderState => ({
607
1020
  * ...state,
@@ -619,36 +1032,66 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
619
1032
  private readonly _autoVersionBump;
620
1033
  get pendingEvents(): ReadonlyArray<TEvent>;
621
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;
622
1042
  protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
623
1043
  /**
624
- * Validates an event before it is applied.
625
- * Override this method to add custom validation logic.
626
- * Return `ok(true)` if the event is valid, `err(message)` otherwise.
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.
627
1047
  */
628
- protected validateEvent(_event: TEvent): Result<true, string>;
1048
+ protected validateEvent(_event: TEvent): void;
629
1049
  /**
630
- * Applies an event to change the state and adds it to pending events.
631
- * Returns a Result type instead of throwing an error.
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.
632
1063
  *
633
1064
  * @param event - The domain event to apply
634
- * @param isNew - Whether the event is new (needs persisting) or from history replay
1065
+ * @param isNew - Whether the event is new (needs persisting) or replayed from history
635
1066
  */
636
- protected apply(event: TEvent, isNew?: boolean): Result<void, string>;
1067
+ protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
1068
+ type: K;
1069
+ }>, isNew?: boolean): void;
637
1070
  /**
638
- * Applies an event to change the state and adds it to pending events.
639
- * Throws an error if validation fails or handler is missing.
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.
640
1076
  */
641
- protected applyUnsafe(event: TEvent, isNew?: boolean): void;
1077
+ private dispatchAndCommit;
642
1078
  /**
643
1079
  * Manually bumps the aggregate version.
644
1080
  * Only needed if `autoVersionBump` is disabled.
645
1081
  */
646
1082
  protected bumpVersion(): void;
647
1083
  /**
648
- * Reconstitutes the aggregate from an event history.
649
- * Sets the version to the number of events in the history.
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.
650
1093
  */
651
- loadFromHistory(history: TEvent[]): Result<void, string>;
1094
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
652
1095
  hasPendingEvents(): boolean;
653
1096
  getEventCount(): number;
654
1097
  getLatestEvent(): TEvent | undefined;
@@ -657,9 +1100,16 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
657
1100
  */
658
1101
  createSnapshot(): AggregateSnapshot<TState>;
659
1102
  /**
660
- * Restores the aggregate from a snapshot and applies events that occurred after.
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.
661
1111
  */
662
- restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, string>;
1112
+ restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
663
1113
  /**
664
1114
  * A map of event types to their corresponding handlers.
665
1115
  * Subclasses MUST implement this property.
@@ -674,33 +1124,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
674
1124
  type Version = number & {
675
1125
  readonly __v: true;
676
1126
  };
677
- /**
678
- * Lightweight functional aggregate state — state + version, no event sourcing.
679
- * This is a data projection, not a full Aggregate (which requires identity via IAggregateRoot).
680
- *
681
- * For event sourcing, use the class-based `EventSourcedAggregate` which enforces
682
- * that state changes happen through event handlers.
683
- *
684
- * @template State - The type of the aggregate state
685
- */
686
- interface AggregateState<State> {
687
- state: Readonly<State>;
688
- version: Version;
689
- }
690
- /**
691
- * Creates a lightweight functional aggregate state.
692
- *
693
- * @example
694
- * ```typescript
695
- * const order = aggregate<OrderState>({ status: "draft", items: [] });
696
- * ```
697
- */
698
- declare function aggregate<State>(state: State, version?: Version): AggregateState<State>;
699
- /**
700
- * Bumps the version of a functional aggregate state.
701
- * Returns a new aggregate state with incremented version.
702
- */
703
- declare function bump<S>(agg: AggregateState<S>): AggregateState<S>;
704
1127
  /**
705
1128
  * Snapshot of an aggregate state at a specific point in time.
706
1129
  * Used for optimizing event replay by starting from a snapshot
@@ -892,10 +1315,20 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
892
1315
  /**
893
1316
  * Registers a handler for a specific command type.
894
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
+ *
895
1324
  * @param commandType - The command type to register the handler for
896
1325
  * @param handler - The handler function for this command type
897
1326
  */
898
- 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;
899
1332
  }
900
1333
  /**
901
1334
  * Simple in-memory command bus implementation.
@@ -935,7 +1368,11 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
935
1368
  */
936
1369
  declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
937
1370
  private readonly handlers;
938
- 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;
939
1376
  execute<C extends Command & {
940
1377
  type: keyof TMap & string;
941
1378
  }>(command: C): Promise<Result<TMap[C["type"]], string>>;
@@ -976,7 +1413,28 @@ interface EventBus<Evt extends {
976
1413
  }> {
977
1414
  /**
978
1415
  * Publishes events to all subscribed handlers.
979
- * 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.
980
1438
  *
981
1439
  * @param events - Array of events to publish
982
1440
  */
@@ -999,7 +1457,9 @@ interface EventBus<Evt extends {
999
1457
  * unsubscribe();
1000
1458
  * ```
1001
1459
  */
1002
- subscribe: <T extends Evt>(eventType: Evt["type"], handler: EventHandler<T>) => () => void;
1460
+ subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1461
+ type: K;
1462
+ }>>) => () => void;
1003
1463
  /**
1004
1464
  * Subscribes to the next occurrence of an event type.
1005
1465
  * Returns a Promise that resolves with the event data.
@@ -1014,24 +1474,125 @@ interface EventBus<Evt extends {
1014
1474
  * console.log("Order created:", event.payload.orderId);
1015
1475
  * ```
1016
1476
  */
1017
- once: <T extends Evt>(eventType: Evt["type"]) => Promise<T>;
1477
+ once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
1478
+ type: K;
1479
+ }>>;
1018
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;
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
+ */
1019
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
+ */
1020
1538
  add: (events: ReadonlyArray<Evt>) => Promise<void>;
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>;
1021
1551
  }
1022
1552
 
1023
- 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 {
1024
1576
  transactional<T>(fn: () => Promise<T>): Promise<T>;
1025
1577
  }
1026
- type RepoProvider<R> = (uow: UnitOfWork) => R;
1027
1578
 
1028
1579
  /**
1029
- * Helper function for executing commands within a transaction.
1030
- * Handles event persistence via outbox and optional event bus publishing.
1031
- *
1032
- * @param deps - Dependencies including outbox, optional event bus, and unit of work
1033
- * @param fn - Function that returns result and events
1034
- * @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).
1035
1596
  *
1036
1597
  * @example
1037
1598
  * ```typescript
@@ -1040,10 +1601,7 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
1040
1601
  * async () => {
1041
1602
  * const order = Order.create(customerId, items);
1042
1603
  * await repository.save(order);
1043
- * return {
1044
- * result: order.id,
1045
- * events: order.pendingEvents
1046
- * };
1604
+ * return { result: order.id, events: order.domainEvents };
1047
1605
  * }
1048
1606
  * );
1049
1607
  * ```
@@ -1053,7 +1611,7 @@ declare function withCommit<Evt extends {
1053
1611
  }, R>(deps: {
1054
1612
  outbox: Outbox<Evt>;
1055
1613
  bus?: EventBus<Evt>;
1056
- uow: UnitOfWork;
1614
+ scope: TransactionScope;
1057
1615
  }, fn: () => Promise<{
1058
1616
  result: R;
1059
1617
  events: ReadonlyArray<Evt>;
@@ -1201,10 +1759,20 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
1201
1759
  /**
1202
1760
  * Registers a handler for a specific query type.
1203
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
+ *
1204
1768
  * @param queryType - The query type to register the handler for
1205
1769
  * @param handler - The handler function for this query type
1206
1770
  */
1207
- 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;
1208
1776
  }
1209
1777
  /**
1210
1778
  * Simple in-memory query bus implementation.
@@ -1244,7 +1812,11 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
1244
1812
  */
1245
1813
  declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
1246
1814
  private readonly handlers;
1247
- 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;
1248
1820
  execute<Q extends Query & {
1249
1821
  type: keyof TMap & string;
1250
1822
  }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
@@ -1255,24 +1827,6 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
1255
1827
  executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
1256
1828
  }
1257
1829
 
1258
- /**
1259
- * Guard function that validates a condition and returns a Result.
1260
- * Returns `ok(true)` if the condition is met, otherwise `err(error)`.
1261
- *
1262
- * @param cond - The condition to check
1263
- * @param error - Error message if condition fails
1264
- * @returns Result<true, string>
1265
- *
1266
- * @example
1267
- * ```typescript
1268
- * const result = guard(id.length > 0, "ID cannot be empty");
1269
- * if (!result.ok) {
1270
- * return err(result.error);
1271
- * }
1272
- * ```
1273
- */
1274
- declare function guard(cond: boolean, error: string): Result<true, string>;
1275
-
1276
1830
  /**
1277
1831
  * Simple in-memory event bus implementation.
1278
1832
  * Supports multiple subscribers per event type (pub/sub pattern).
@@ -1297,70 +1851,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
1297
1851
  */
1298
1852
  declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
1299
1853
  private readonly handlers;
1300
- subscribe<T extends Evt>(eventType: Evt["type"], handler: EventHandler<T>): () => void;
1301
- once<T extends Evt>(eventType: Evt["type"]): 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
+ */
1302
1868
  publish(events: ReadonlyArray<Evt>): Promise<void>;
1303
1869
  }
1304
1870
 
1305
- declare const __specBrand: unique symbol;
1306
- /**
1307
- * A Specification is a named, standalone object that represents a business rule for a query.
1308
- * It is "translatable" into a concrete database query.
1309
- *
1310
- * Uses a branded type to carry the generic parameter without requiring
1311
- * implementors to add a runtime field.
1312
- */
1313
- interface ISpecification<T> {
1314
- readonly [__specBrand]?: T;
1315
- }
1316
-
1317
1871
  /**
1318
- * Repository interface for Aggregate Roots (Entities).
1319
- *
1320
- * Repositories work exclusively with Aggregate Root Entities. The Aggregate Root
1321
- * is the Entity that represents the aggregate externally and is the only object
1322
- * that can be loaded/saved through repositories.
1872
+ * Core repository contract for Aggregate Roots.
1323
1873
  *
1324
- * When loading an Aggregate Root, all child entities and value objects within
1325
- * the aggregate state are loaded as well. When saving, the entire aggregate
1326
- * (including all child entities) is persisted as a unit.
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.
1327
1880
  *
1328
- * Child entities cannot be loaded or saved independently - they exist only
1329
- * within the aggregate boundary and are managed through the Aggregate Root.
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.
1330
1886
  *
1331
1887
  * @template TAgg - The aggregate root type (must implement IAggregateRoot)
1332
- * @template TId - The type of the aggregate root identifier
1888
+ * @template TId - The type of the aggregate root identifier
1333
1889
  */
1334
1890
  interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
1891
+ /**
1892
+ * Loads an aggregate by id. Returns `null` when not found.
1893
+ */
1335
1894
  getById(id: TId): Promise<TAgg | null>;
1336
- findOne(spec: ISpecification<TAgg>): Promise<TAgg | null>;
1337
- find(spec: ISpecification<TAgg>): Promise<TAgg[]>;
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
+ */
1338
1919
  save(aggregate: TAgg): Promise<void>;
1920
+ /**
1921
+ * Removes the aggregate by id.
1922
+ */
1339
1923
  delete(id: TId): Promise<void>;
1340
1924
  }
1925
+ /**
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.
1930
+ *
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.
1936
+ *
1937
+ * Aggregates that are only ever accessed by id should implement
1938
+ * `IRepository` directly and skip this extension.
1939
+ *
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
1943
+ *
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
+ * ```
1960
+ */
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[]>;
1978
+ }
1341
1979
 
1342
1980
  type VO<T> = Readonly<T>;
1343
1981
  /**
1344
- * Deep freezes an object and all its nested properties recursively.
1345
- * This ensures true immutability for value objects with nested structures.
1346
- * 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.
1347
1990
  */
1348
1991
  declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
1349
1992
  /**
1350
1993
  * Creates a deeply immutable value object from the given data.
1351
- * All nested objects and arrays are frozen recursively.
1352
1994
  *
1353
- * @param t - The data to convert into a value object
1354
- * @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.
1355
1999
  *
1356
2000
  * @example
1357
2001
  * ```typescript
1358
- * const address = vo({
1359
- * street: "Main St",
1360
- * city: "Berlin",
1361
- * coordinates: { lat: 52.5, lng: 13.4 }
1362
- * });
1363
- * // 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
1364
2006
  * ```
1365
2007
  */
1366
2008
  declare function vo<T>(t: T): VO<T>;
@@ -1459,26 +2101,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
1459
2101
  * ```
1460
2102
  */
1461
2103
  declare function voWithValidation<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): Result<VO<T>, string>;
1462
- /**
1463
- * Creates a value object with optional validation.
1464
- * Throws an error if validation fails.
1465
- *
1466
- * @param t - The data to convert into a value object
1467
- * @param validate - Validation function that returns true if valid
1468
- * @param errorMessage - Optional custom error message if validation fails
1469
- * @returns A deeply frozen, immutable value object
1470
- * @throws Error if validation fails
1471
- *
1472
- * @example
1473
- * ```typescript
1474
- * const money = voWithValidationUnsafe(
1475
- * { amount: 100, currency: "USD" },
1476
- * (m) => m.amount >= 0 && m.currency.length === 3,
1477
- * "Invalid money: amount must be non-negative and currency must be 3 characters"
1478
- * );
1479
- * ```
1480
- */
1481
- declare function voWithValidationUnsafe<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): VO<T>;
1482
2104
  /**
1483
2105
  * Interface for Value Objects.
1484
2106
  * Value Objects are immutable and defined by their properties.
@@ -1570,4 +2192,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
1570
2192
  toJSON(): Readonly<T>;
1571
2193
  }
1572
2194
 
1573
- export { type AggregateConfig, AggregateRoot, type AggregateSnapshot, type AggregateState, type Command, CommandBus, type CommandHandler, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventMetadata, EventSourcedAggregate, type EventSourcedAggregateConfig, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, 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, sameEntity, sameVersion, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, voWithValidationUnsafe, withCommit };
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 };