@shirudo/ddd-kit 1.0.0-rc.1 → 1.0.0-rc.4

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,114 @@
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 { BaseError } from '@shirudo/base-error';
3
+ import { DeepEqualExceptOptions } from './utils.js';
4
+ export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
3
5
 
6
+ /**
7
+ * Branded string ID. `Tag` carries the aggregate / entity name so two ids
8
+ * with different tags are not assignable to each other even though both
9
+ * are strings at runtime.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * type UserId = Id<"UserId">;
14
+ * type OrderId = Id<"OrderId">;
15
+ *
16
+ * const u = "user-1" as UserId;
17
+ * const o: OrderId = u; // ❌ compile error
18
+ * ```
19
+ */
4
20
  type Id<Tag extends string> = string & {
5
21
  readonly __brand: Tag;
6
22
  };
7
- interface IdGenerator {
8
- next: <T extends string>() => Id<T>;
23
+ /**
24
+ * Produces fresh ids of a single, fixed tag. The tag is bound at the
25
+ * generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
26
+ * with no caller-side generic to abuse.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { ulid } from "ulid";
31
+ *
32
+ * const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
33
+ * const id = userIds.next(); // Id<"UserId">
34
+ * ```
35
+ *
36
+ * The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
37
+ * let callers pick `T` themselves — `gen.next<"AnyTag">()` typechecked
38
+ * even when the generator produced different-tag ids, silently defeating
39
+ * the brand.
40
+ */
41
+ interface IdGenerator<Tag extends string> {
42
+ next: () => Id<Tag>;
9
43
  }
10
44
 
45
+ /**
46
+ * Factory function producing a fresh, unique event identifier for each call.
47
+ *
48
+ * The library ships a default that uses Web Crypto `crypto.randomUUID()`
49
+ * (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
50
+ * Cloudflare Workers, Vercel Edge, and any runtime that implements Web
51
+ * Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
52
+ * random) — for production event stores prefer a **time-ordered** id
53
+ * format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
54
+ * column stay clustered and `ORDER BY eventId` matches creation order.
55
+ * Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
56
+ */
57
+ type EventIdFactory = () => string;
58
+ /**
59
+ * Replaces the global event-id factory used by `createDomainEvent` and
60
+ * `createDomainEventWithMetadata`. Call once during application bootstrap,
61
+ * for example:
62
+ *
63
+ * ```ts
64
+ * import { ulid } from "ulid";
65
+ * import { setEventIdFactory } from "@shirudo/ddd-kit";
66
+ *
67
+ * setEventIdFactory(() => ulid());
68
+ * ```
69
+ *
70
+ * The per-call `options.eventId` override always wins over this factory.
71
+ *
72
+ * **Module-scoped — last setter wins.** The factory lives as a single
73
+ * module variable; importing two libraries that both call this races on
74
+ * load order. For multi-tenant request isolation (e.g. one factory per
75
+ * tenant in a single Worker invocation) **prefer the per-call
76
+ * `options.eventId`** instead of mutating the global. Same caveat applies
77
+ * to `setClockFactory`.
78
+ */
79
+ declare function setEventIdFactory(factory: EventIdFactory): void;
80
+ /**
81
+ * Restores the default event-id factory (`crypto.randomUUID()`).
82
+ * Intended for use in test `afterEach` hooks.
83
+ */
84
+ declare function resetEventIdFactory(): void;
85
+ /**
86
+ * Clock function producing a fresh `Date` for each call. The library
87
+ * defaults to `() => new Date()`; override globally via `setClockFactory`
88
+ * for deterministic event-sourcing tests, time-travel debugging, or any
89
+ * scenario where `occurredAt` must be reproducible.
90
+ */
91
+ type ClockFactory = () => Date;
92
+ /**
93
+ * Replaces the global clock factory used by `createDomainEvent` and
94
+ * `createDomainEventWithMetadata`. Call once during application bootstrap
95
+ * (or per-test in deterministic test suites):
96
+ *
97
+ * ```ts
98
+ * import { setClockFactory } from "@shirudo/ddd-kit";
99
+ *
100
+ * setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
101
+ * ```
102
+ *
103
+ * The per-call `options.occurredAt` override always wins over this
104
+ * factory. Symmetric to `setEventIdFactory`.
105
+ */
106
+ declare function setClockFactory(factory: ClockFactory): void;
107
+ /**
108
+ * Restores the default clock factory (`() => new Date()`).
109
+ * Intended for use in test `afterEach` hooks.
110
+ */
111
+ declare function resetClockFactory(): void;
11
112
  /**
12
113
  * Metadata associated with a domain event for traceability and correlation.
13
114
  * Used in event-driven architectures to track event flow across services.
@@ -45,13 +146,31 @@ interface EventMetadata {
45
146
  * @template P - The event payload type
46
147
  */
47
148
  interface DomainEvent<T extends string, P = void> {
149
+ /**
150
+ * Unique identifier for this specific event instance. Used by idempotent
151
+ * consumers, outbox dispatch tracking, and as the target of
152
+ * `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
153
+ * supplied.
154
+ */
155
+ eventId: string;
48
156
  /**
49
157
  * The type of the event, used for routing and handling.
50
158
  */
51
159
  type: T;
52
160
  /**
53
- * The event payload containing the domain data.
54
- * Omitted when P is void (events without payload).
161
+ * Identifier of the aggregate that produced the event. Optional at the
162
+ * library level set it whenever the producing aggregate is known so
163
+ * downstream subscribers, outboxes, and projections can scope by entity.
164
+ */
165
+ aggregateId?: string;
166
+ /**
167
+ * Name of the aggregate type that produced the event (e.g. "Order").
168
+ * Pairs with `aggregateId` to fully qualify the source aggregate.
169
+ */
170
+ aggregateType?: string;
171
+ /**
172
+ * The event payload containing the domain data. The field is always
173
+ * present; its value is `undefined` when `P` is `void`.
55
174
  */
56
175
  payload: P;
57
176
  /**
@@ -70,6 +189,37 @@ interface DomainEvent<T extends string, P = void> {
70
189
  */
71
190
  metadata?: EventMetadata;
72
191
  }
192
+ /**
193
+ * Shared option bag for the `createDomainEvent*` factories.
194
+ */
195
+ interface CreateDomainEventOptions {
196
+ /**
197
+ * Override for the auto-generated `eventId`. Pass an existing id (for
198
+ * replay, tests, or deterministic event sourcing) instead of letting the
199
+ * factory call `crypto.randomUUID()`.
200
+ */
201
+ eventId?: string;
202
+ /**
203
+ * Identifier of the aggregate that produced the event.
204
+ */
205
+ aggregateId?: string;
206
+ /**
207
+ * Name of the aggregate type that produced the event.
208
+ */
209
+ aggregateType?: string;
210
+ /**
211
+ * Override for the auto-generated `occurredAt` timestamp.
212
+ */
213
+ occurredAt?: Date;
214
+ /**
215
+ * Override for the default schema version (1).
216
+ */
217
+ version?: number;
218
+ /**
219
+ * Event metadata — correlation, causation, user, source, custom fields.
220
+ */
221
+ metadata?: EventMetadata;
222
+ }
73
223
  /**
74
224
  * Creates a domain event with default values.
75
225
  * Sets occurredAt to current date and version to 1 if not provided.
@@ -84,16 +234,8 @@ interface DomainEvent<T extends string, P = void> {
84
234
  * const event = createDomainEvent("OrderCreated", { orderId: "123" });
85
235
  * ```
86
236
  */
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>;
237
+ declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
238
+ declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
97
239
  /**
98
240
  * Creates a domain event with metadata for traceability.
99
241
  * Convenience function for creating events with correlation and causation IDs.
@@ -107,10 +249,7 @@ declare function createDomainEvent<T extends string, P>(type: T, payload: P, opt
107
249
  * );
108
250
  * ```
109
251
  */
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>;
252
+ declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
114
253
  /**
115
254
  * Copies metadata from a source event to a new event.
116
255
  * Useful for maintaining correlation chains in event-driven architectures.
@@ -141,10 +280,72 @@ declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additio
141
280
  declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
142
281
 
143
282
  /**
144
- * Functional definition of an Entity via its capability.
145
- * An object is identifiable if it has an id.
283
+ * Entity utilities and interfaces for Domain-Driven Design.
284
+ *
285
+ * In Domain-Driven Design, there are two types of entities:
286
+ *
287
+ * 1. **Aggregate Root Entity**: The parent Entity of an aggregate.
288
+ * - Has identity (id), state, and version
289
+ * - Implemented by classes extending `AggregateRoot` or `EventSourcedAggregate`
290
+ * - Represents the aggregate externally
291
+ * - Loaded/saved through repositories
292
+ *
293
+ * 2. **Child Entities**: Entities within an aggregate.
294
+ * - Have identity (id) and state, but no own version
295
+ * - Can extend `EntityBase<TState, TId>` for class-based entities
296
+ * - Or use functional style with `Identifiable<TId> & TProps`
297
+ * - Exist only within the aggregate boundary
298
+ * - Versioned through the Aggregate Root
299
+ * - Cannot be referenced directly from outside the aggregate
300
+ *
301
+ * This module provides:
302
+ * - `EntityBase<TState, TId>` - Base class for entities with state
303
+ * - `Entity<TId>` - Simple class for entities without state management
304
+ * - `Identifiable<TId>` - Minimal interface for objects with id
305
+ * - Helper functions for working with collections of entities
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * // Class-based child entity with logic
310
+ * class OrderItem extends EntityBase<OrderItemState, ItemId> {
311
+ * constructor(id: ItemId, initialState: OrderItemState) {
312
+ * super(id, initialState);
313
+ * }
314
+ *
315
+ * updateQuantity(quantity: number): void {
316
+ * this._state = { ...this._state, quantity };
317
+ * }
318
+ *
319
+ * calculateSubtotal(): number {
320
+ * return this._state.price * this._state.quantity;
321
+ * }
322
+ * }
323
+ *
324
+ * // Functional-style child entity (simpler, no logic)
325
+ * type OrderItem = Identifiable<ItemId> & {
326
+ * productId: string;
327
+ * quantity: number;
328
+ * price: number;
329
+ * };
330
+ *
331
+ * // Aggregate Root (Entity with version)
332
+ * class Order extends AggregateRoot<OrderState, OrderId> {
333
+ * // Order is an Aggregate Root Entity
334
+ * // OrderState contains OrderItem child entities
335
+ * }
336
+ * ```
337
+ */
338
+
339
+ /**
340
+ * Functional definition of an Entity via its capability — an object is
341
+ * identifiable if it has an `id`.
342
+ *
343
+ * `TId` is constrained to `Id<string>` so the brand discipline that
344
+ * `Id<Tag>` enforces is preserved end-to-end: an `Identifiable<UserId>`
345
+ * cannot accidentally be paired with an `Identifiable<OrderId>` or with
346
+ * a plain `string`.
146
347
  */
147
- type Identifiable<TId> = {
348
+ type Identifiable<TId extends Id<string>> = {
148
349
  readonly id: TId;
149
350
  };
150
351
  /**
@@ -204,7 +405,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
204
405
  readonly id: TId;
205
406
  /**
206
407
  * Returns the current state of the entity.
207
- * State is readonly from outside to enforce encapsulation.
408
+ *
409
+ * The state object is **shallowly frozen** — direct property writes
410
+ * (`entity.state.foo = …`) throw in strict mode, but writes to nested
411
+ * objects (`entity.state.address.zip = …`) bypass the freeze. For deep
412
+ * immutability either model nested data with `vo()` (which freezes
413
+ * deeply) or reach for a structural-sharing library like Immer at the
414
+ * App layer. The shallow contract is intentional: deep freezing on
415
+ * every state write is too expensive for hot paths, and DDD aggregates
416
+ * normally treat their own state as private (`Tell, Don't Ask`).
208
417
  */
209
418
  get state(): TState;
210
419
  /**
@@ -214,12 +423,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
214
423
  protected _state: TState;
215
424
  protected constructor(id: TId, initialState: TState);
216
425
  /**
217
- * Optional validation hook to ensure state invariants.
218
- * Called during construction and whenever helpful.
219
- * Override this method to implement validation logic.
426
+ * Optional validation hook to ensure state invariants. Called during
427
+ * construction (from `Entity`'s constructor) and again on every
428
+ * `setState()` call. Throw to reject invalid state.
429
+ *
430
+ * **⚠️ Must not read subclass instance fields via `this`.** The
431
+ * constructor calls `validateState(initialState)` BEFORE the subclass's
432
+ * field initializers run, so `this.someField` is `undefined` at that
433
+ * point — a classic TypeScript/JavaScript constructor-ordering footgun.
434
+ * The `state` argument is the single source of truth; treat the method
435
+ * as pure with respect to `this`.
436
+ *
437
+ * If your invariants genuinely depend on per-instance configuration
438
+ * that isn't part of the state, pass that configuration into the state
439
+ * itself (DDD-canonical: the aggregate's state contains everything it
440
+ * needs) or perform the additional check after construction in a
441
+ * dedicated factory method.
220
442
  *
221
443
  * @param state - The state to validate
222
- * @throws Error if validation fails
444
+ * @throws Error (or `DomainError` subclass) if validation fails
223
445
  */
224
446
  protected validateState(_state: TState): void;
225
447
  /**
@@ -231,6 +453,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
231
453
  */
232
454
  protected setState(newState: TState): void;
233
455
  }
456
+ /**
457
+ * Shallow-freezes `value` when it's a non-null object or array, so that
458
+ * direct property writes throw in strict mode. Returns the value as-is for
459
+ * primitives. Used internally by `Entity` and its subclasses to prevent
460
+ * outside mutation of state read through the `state` getter without paying
461
+ * the cost of a deep clone on every read.
462
+ *
463
+ * Exported so that sibling classes (`EventSourcedAggregate`, `AggregateRoot`)
464
+ * can apply the same freeze when they bypass `setState` and assign
465
+ * `this._state` directly.
466
+ */
467
+ declare function freezeShallow<T>(value: T): T;
234
468
  /**
235
469
  * Checks if two entities have the same ID.
236
470
  * Works with any object that has an 'id' property.
@@ -248,7 +482,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
248
482
  * sameEntity(item1, item1); // true
249
483
  * ```
250
484
  */
251
- declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
485
+ declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
252
486
  /**
253
487
  * Finds an entity by ID in a collection.
254
488
  * Returns undefined if not found.
@@ -268,7 +502,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
268
502
  * // item is { id: itemId1, productId: "prod-1", quantity: 2 }
269
503
  * ```
270
504
  */
271
- declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
505
+ declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
272
506
  /**
273
507
  * Checks if an entity with the given ID exists in the collection.
274
508
  *
@@ -286,7 +520,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
286
520
  * hasEntityId(items, itemId2); // false
287
521
  * ```
288
522
  */
289
- declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
523
+ declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
290
524
  /**
291
525
  * Removes an entity with the given ID from the collection.
292
526
  * Returns a new array without the entity.
@@ -306,7 +540,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
306
540
  * // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
307
541
  * ```
308
542
  */
309
- declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
543
+ declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
310
544
  /**
311
545
  * Updates an entity with the given ID in the collection.
312
546
  * Returns a new array with the updated entity.
@@ -330,7 +564,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
330
564
  * // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
331
565
  * ```
332
566
  */
333
- declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
567
+ declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
334
568
  /**
335
569
  * Replaces an entity with the given ID in the collection.
336
570
  * Returns a new array with the replaced entity.
@@ -354,7 +588,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
354
588
  * });
355
589
  * ```
356
590
  */
357
- declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
591
+ declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
358
592
  /**
359
593
  * Extracts all IDs from a collection of entities.
360
594
  *
@@ -372,7 +606,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
372
606
  * // ids is [itemId1, itemId2]
373
607
  * ```
374
608
  */
375
- declare function entityIds<TId, T extends Identifiable<TId>>(entities: T[]): TId[];
609
+ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[]): TId[];
376
610
 
377
611
  /**
378
612
  * Marker interface for Aggregate Roots.
@@ -412,14 +646,36 @@ interface IAggregateRoot<TId extends Id<string>> {
412
646
  * This version applies to the entire aggregate, including all child entities.
413
647
  */
414
648
  readonly version: Version;
649
+ /**
650
+ * Post-save hook: a `Repository.save()` implementation calls this with
651
+ * the persisted version after a successful write to push the new
652
+ * version back into the aggregate and clear any recorded domain events
653
+ * (they are now safely on the write side / in the outbox).
654
+ *
655
+ * Required by the interface so a Repository implementation can call it
656
+ * via the published `IAggregateRoot` contract without taking the
657
+ * abstract class as a compile-time dependency.
658
+ *
659
+ * @param version - The version assigned by the persistence layer
660
+ */
661
+ markPersisted(version: Version): void;
415
662
  }
416
663
  /**
417
664
  * Configuration options for AggregateRoot behavior.
418
665
  */
419
666
  interface AggregateConfig {
420
667
  /**
421
- * Whether to automatically bump the version when state changes.
422
- * Defaults to false. Set to true for automatic versioning.
668
+ * Whether `setState()` should bump the version automatically.
669
+ *
670
+ * Defaults to **`false`** for `AggregateRoot` — because `setState()`
671
+ * already takes an explicit `bumpVersion` argument per call, so adding
672
+ * an "always bump" config on top would be redundant. Keep it `false`
673
+ * unless you have a subclass that never passes `bumpVersion` and you
674
+ * want every state change to advance the version anyway.
675
+ *
676
+ * (Contrast with `EventSourcedAggregate`, which defaults this to
677
+ * `true` because every event-sourced state change is per definition a
678
+ * versioned commit.)
423
679
  */
424
680
  autoVersionBump?: boolean;
425
681
  }
@@ -461,7 +717,7 @@ interface AggregateConfig {
461
717
  * }
462
718
  * ```
463
719
  */
464
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = unknown> extends Entity<TState, TId> implements IAggregateRoot<TId> {
720
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
465
721
  private _version;
466
722
  get version(): Version;
467
723
  protected setVersion(version: Version): void;
@@ -478,12 +734,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
478
734
  * Call this after dispatching the events.
479
735
  */
480
736
  clearDomainEvents(): void;
737
+ /**
738
+ * Post-save hook called by a `Repository.save()` implementation to push
739
+ * the persisted version back into the in-memory aggregate and clear the
740
+ * recorded domain events (they are now safely on the write side / in
741
+ * the outbox).
742
+ *
743
+ * Use this so `save()` can keep its `Promise<void>` return type: the
744
+ * caller holds the aggregate reference, which is up to date after this
745
+ * call.
746
+ */
747
+ markPersisted(version: Version): void;
748
+ /**
749
+ * Mutates state and records the resulting domain events in the
750
+ * **canonical record-after-mutation order**. Use this instead of calling
751
+ * `setState` + `addDomainEvent` separately and you cannot trip the
752
+ * "event for a fact that never happened" footgun.
753
+ *
754
+ * Order of operations:
755
+ * 1. `setState(newState, true)` — runs `validateState` first.
756
+ * If it throws, the method propagates and **no event is recorded
757
+ * and no version is bumped**.
758
+ * 2. Each event in `events` is appended via `addDomainEvent`.
759
+ *
760
+ * `commit()` **always bumps the version**, regardless of the aggregate's
761
+ * `autoVersionBump` config. Recording a domain event implies "something
762
+ * happened that the outside world cares about", and optimistic-
763
+ * concurrency callers must see a fresh version every time. The config
764
+ * still governs the un-coupled `setState` path. If you need to mutate
765
+ * state without bumping (e.g. cosmetic caches), call `setState(newState,
766
+ * false)` and skip `commit` entirely.
767
+ *
768
+ * `events` accepts a single event or an array. Omit it (or pass `[]`)
769
+ * for state-only mutations.
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * confirm(): void {
774
+ * if (this.state.status === "confirmed") {
775
+ * throw new OrderAlreadyConfirmedError(this.id);
776
+ * }
777
+ * this.commit(
778
+ * { ...this.state, status: "confirmed" },
779
+ * { type: "OrderConfirmed", orderId: this.id },
780
+ * );
781
+ * }
782
+ * ```
783
+ *
784
+ * `EventSourcedAggregate.apply()` enforces the same ordering
785
+ * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
786
+ * where `setState` and `addDomainEvent` are otherwise decoupled and the
787
+ * ordering is convention-only.
788
+ *
789
+ * @param newState - The new state (validated by `validateState`)
790
+ * @param events - One event, an array of events, or none (default)
791
+ */
792
+ protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
481
793
  protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
482
794
  /**
483
- * Adds a domain event to the aggregate's list of changes.
484
- * Use this to record side-effects that should be published.
795
+ * Records a domain event for later publication.
796
+ *
797
+ * **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
798
+ * explicit: a domain event describes something that has just happened
799
+ * to the aggregate — its existence implies the state change already
800
+ * occurred. Concretely:
801
+ *
802
+ * ```ts
803
+ * confirm(): void {
804
+ * if (this.state.status === "confirmed") {
805
+ * throw new OrderAlreadyConfirmedError(this.id);
806
+ * }
807
+ * this.setState({ ...this.state, status: "confirmed" }, true);
808
+ * this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
809
+ * // ↑ post-mutation. The event represents the committed fact.
810
+ * }
811
+ * ```
812
+ *
813
+ * Recording before mutation is a footgun: if a subsequent invariant
814
+ * check throws, the event has already been queued but the state never
815
+ * actually changed — consumers see an event for a fact that did not
816
+ * happen.
817
+ *
818
+ * `EventSourcedAggregate.apply()` enforces this ordering structurally;
819
+ * `AggregateRoot` leaves it as a convention because the state-mutation
820
+ * path (`setState`) is decoupled from event recording.
485
821
  *
486
- * @param event - The domain event to add
822
+ * @param event - The domain event to record
487
823
  */
488
824
  protected addDomainEvent(event: TEvent): void;
489
825
  /**
@@ -534,6 +870,103 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
534
870
  restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
535
871
  }
536
872
 
873
+ /**
874
+ * Abstract base for **domain-invariant violations**. Domain methods
875
+ * (aggregates, entity validation hooks, value-object constructors)
876
+ * throw `DomainError`-derived exceptions when a business rule is
877
+ * violated. Consumers derive their own concrete errors — e.g.
878
+ * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
879
+ * for `instanceof`-style catching at the App-Service boundary, where
880
+ * they typically map to HTTP 400 / business-rule responses.
881
+ *
882
+ * The library itself does **not** ship any concrete `DomainError`
883
+ * subclass — the kit can't know your invariants. {@link MissingHandlerError},
884
+ * {@link AggregateNotFoundError}, and {@link ConcurrencyConflictError}
885
+ * deliberately sit on other branches of the hierarchy (see below) because
886
+ * they are not invariant violations.
887
+ *
888
+ * Extends `BaseError<Name>` from `@shirudo/base-error`, so derived
889
+ * classes get timestamps, `error.cause` traversal, `toJSON()`, i18n-
890
+ * aware `getUserMessage()`, and the `isRetryable` predicate for free.
891
+ */
892
+ declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
893
+ }
894
+ /**
895
+ * Abstract base for **infrastructure / persistence failures** that the
896
+ * App-Service can recover from — typically by retrying, by returning
897
+ * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
898
+ * not domain-invariant violations (the business rules were not
899
+ * broken); they describe race conditions and missing rows at the
900
+ * storage boundary.
901
+ *
902
+ * Library-internal concrete subclasses:
903
+ * - {@link AggregateNotFoundError}
904
+ * - {@link ConcurrencyConflictError}
905
+ *
906
+ * Extends `BaseError<Name>` from `@shirudo/base-error`.
907
+ */
908
+ declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
909
+ }
910
+ /**
911
+ * Thrown by `EventSourcedAggregate.apply()` when no handler is
912
+ * registered for the event's type. This means the aggregate's subclass
913
+ * forgot to add an entry to its `handlers` map — a programming /
914
+ * configuration bug, not a domain or infrastructure failure.
915
+ *
916
+ * Deliberately **not** on `DomainError` or `InfrastructureError` —
917
+ * a generic `catch (e instanceof DomainError)` handler at the App
918
+ * layer must not mask a forgotten handler; this should crash loud and
919
+ * fail the calling Use Case so the bug surfaces in development. The
920
+ * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
921
+ * also let it propagate uncaught instead of wrapping it in `Result.Err`.
922
+ *
923
+ * Use `isBaseError(e)` from `@shirudo/base-error` to detect
924
+ * "any structured error from the kit or any other BaseError-using
925
+ * library" at the App boundary.
926
+ */
927
+ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
928
+ readonly eventType: string;
929
+ constructor(eventType: string);
930
+ }
931
+ /**
932
+ * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
933
+ * given id does not exist. `InfrastructureError` because the storage
934
+ * boundary, not a business rule, decided the row is absent. Use the
935
+ * nullable variant `getById()` if "not found" is a valid outcome.
936
+ *
937
+ * Ships with a user-safe message via `withUserMessage`. Not retryable —
938
+ * retrying won't make the row appear.
939
+ */
940
+ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
941
+ readonly aggregateType: string;
942
+ readonly id: string;
943
+ constructor(aggregateType: string, id: string);
944
+ }
945
+ /**
946
+ * Thrown by `IRepository.save()` when the aggregate's expected version
947
+ * does not match the version currently persisted — i.e. another writer
948
+ * updated the aggregate concurrently. The canonical optimistic-
949
+ * concurrency signal; the App-Service typically reloads, re-applies
950
+ * the use case, and retries, or surfaces HTTP 409 to the caller.
951
+ *
952
+ * `InfrastructureError` because the persistence layer (not a domain
953
+ * rule) detects the race. Marks itself as `retryable: true` so the
954
+ * `isRetryable` predicate from `@shirudo/base-error` picks it up.
955
+ */
956
+ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
957
+ readonly aggregateType: string;
958
+ readonly aggregateId: string;
959
+ readonly expectedVersion: number;
960
+ readonly actualVersion: number;
961
+ /**
962
+ * Marks this error as retryable so `isRetryable(err)` returns
963
+ * true. The canonical OCC pattern is to reload the aggregate, re-apply
964
+ * the use case, and retry on this exception.
965
+ */
966
+ readonly retryable: true;
967
+ constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number);
968
+ }
969
+
537
970
  /**
538
971
  * Interface for Event-Sourced Aggregate Roots.
539
972
  * Defines the contract for aggregates that manage state changes via event sourcing.
@@ -547,11 +980,13 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
547
980
  */
548
981
  readonly pendingEvents: ReadonlyArray<TEvent>;
549
982
  /**
550
- * Reconstitutes the aggregate from an event history.
983
+ * Reconstitutes the aggregate from an event history. Returns `Result`
984
+ * because event-stream corruption is an expected recoverable failure
985
+ * at the infrastructure boundary.
551
986
  *
552
987
  * @param history - An ordered list of past events
553
988
  */
554
- loadFromHistory(history: TEvent[]): Result<void, string>;
989
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
555
990
  /**
556
991
  * Clears the list of pending events.
557
992
  */
@@ -575,8 +1010,18 @@ type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
575
1010
  */
576
1011
  interface EventSourcedAggregateConfig {
577
1012
  /**
578
- * Whether to automatically bump the version when applying new events.
579
- * Defaults to true. Set to false to manually control versioning.
1013
+ * Whether `apply()` should bump the version per event.
1014
+ *
1015
+ * Defaults to **`true`** for `EventSourcedAggregate` — each applied
1016
+ * event is by definition a versioned state change, so the canonical
1017
+ * event-sourcing pattern is "one event = one version bump". Set to
1018
+ * `false` only if your event store assigns version numbers itself
1019
+ * and you want the aggregate to track them via `bumpVersion()` /
1020
+ * `setVersion()` calls instead.
1021
+ *
1022
+ * (Contrast with `AggregateRoot`, which defaults this to `false`
1023
+ * because its `setState()` already takes a per-call `bumpVersion`
1024
+ * argument.)
580
1025
  */
581
1026
  autoVersionBump?: boolean;
582
1027
  }
@@ -591,17 +1036,35 @@ interface EventSourcedAggregateConfig {
591
1036
  * `addDomainEvent()` are not available. This enforces the event sourcing pattern
592
1037
  * at the type level — there is no way to mutate state without going through an event handler.
593
1038
  *
1039
+ * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
1040
+ * invariant violations. Subclasses override `validateEvent()` to throw their
1041
+ * own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
1042
+ * infrastructure-boundary methods (`loadFromHistory`,
1043
+ * `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
1044
+ * during replay so callers can react to corrupted event streams without
1045
+ * try/catch.
1046
+ *
594
1047
  * @template TState - The type of the aggregate state (contains child entities and value objects)
595
1048
  * @template TEvent - The union type of all domain events
596
1049
  * @template TId - The type of the aggregate root identifier
597
1050
  *
598
1051
  * @example
599
1052
  * ```typescript
1053
+ * class OrderAlreadyConfirmedError extends DomainError {
1054
+ * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1055
+ * }
1056
+ *
600
1057
  * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
601
1058
  * confirm(): void {
602
1059
  * this.apply(createDomainEvent("OrderConfirmed", {}));
603
1060
  * }
604
1061
  *
1062
+ * protected validateEvent(event: OrderEvent): void {
1063
+ * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1064
+ * throw new OrderAlreadyConfirmedError(this.id);
1065
+ * }
1066
+ * }
1067
+ *
605
1068
  * protected readonly handlers = {
606
1069
  * OrderConfirmed: (state: OrderState): OrderState => ({
607
1070
  * ...state,
@@ -619,36 +1082,66 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
619
1082
  private readonly _autoVersionBump;
620
1083
  get pendingEvents(): ReadonlyArray<TEvent>;
621
1084
  clearPendingEvents(): void;
1085
+ /**
1086
+ * Post-save hook called by a `Repository.save()` implementation to push
1087
+ * the persisted version back into the in-memory aggregate and clear the
1088
+ * pending events (they are now in the event store / outbox). Lets
1089
+ * `save()` keep its `Promise<void>` return type.
1090
+ */
1091
+ markPersisted(version: Version): void;
622
1092
  protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
623
1093
  /**
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.
1094
+ * Validates an event before it is applied. Default is no-op.
1095
+ * Subclasses override to throw a concrete `DomainError` subclass when
1096
+ * the event violates an invariant in the current state.
627
1097
  */
628
- protected validateEvent(_event: TEvent): Result<true, string>;
1098
+ protected validateEvent(_event: TEvent): void;
629
1099
  /**
630
- * Applies an event to change the state and adds it to pending events.
631
- * Returns a Result type instead of throwing an error.
1100
+ * Applies an event: validates, locates the handler, computes the next
1101
+ * state, then commits state + pending event + version bump atomically.
1102
+ *
1103
+ * Throws `DomainError` (or a subclass) on validation failure.
1104
+ * Throws `MissingHandlerError` if no handler is registered for `event.type`.
1105
+ *
1106
+ * State is not mutated if any step throws — the handler is invoked into
1107
+ * a local and only assigned to `_state` once all checks pass.
1108
+ *
1109
+ * The method is generic in the event tag `K`, so concrete callers
1110
+ * (`this.apply(orderCreated)`) narrow to the literal tag and the
1111
+ * dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
1112
+ * — no `as` cast required at the call site.
632
1113
  *
633
1114
  * @param event - The domain event to apply
634
- * @param isNew - Whether the event is new (needs persisting) or from history replay
1115
+ * @param isNew - Whether the event is new (needs persisting) or replayed from history
635
1116
  */
636
- protected apply(event: TEvent, isNew?: boolean): Result<void, string>;
1117
+ protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
1118
+ type: K;
1119
+ }>, isNew?: boolean): void;
637
1120
  /**
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.
1121
+ * Internal dispatch path used by `apply()` and the replay methods
1122
+ * (`loadFromHistory`, `restoreFromSnapshotWithEvents`). The replay loop
1123
+ * iterates over `TEvent[]` and therefore cannot supply a narrowed `K`
1124
+ * generic, so this helper accepts `TEvent` and the discriminator is
1125
+ * resolved via the (statically-sound) `handlers` map.
640
1126
  */
641
- protected applyUnsafe(event: TEvent, isNew?: boolean): void;
1127
+ private dispatchAndCommit;
642
1128
  /**
643
1129
  * Manually bumps the aggregate version.
644
1130
  * Only needed if `autoVersionBump` is disabled.
645
1131
  */
646
1132
  protected bumpVersion(): void;
647
1133
  /**
648
- * Reconstitutes the aggregate from an event history.
649
- * Sets the version to the number of events in the history.
1134
+ * Reconstitutes the aggregate from an event history. Catches `DomainError`
1135
+ * thrown during replay and returns it as an `Err` — this is the
1136
+ * infrastructure boundary, where event-stream corruption is an expected
1137
+ * recoverable failure. Unexpected (non-DomainError) throws propagate.
1138
+ *
1139
+ * Version advances additively: the aggregate's pre-existing version plus
1140
+ * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
1141
+ * an aggregate already at v=1 (e.g. after a creation event) loading
1142
+ * 2 events ends at v=3, not v=2.
650
1143
  */
651
- loadFromHistory(history: TEvent[]): Result<void, string>;
1144
+ loadFromHistory(history: TEvent[]): Result<void, DomainError>;
652
1145
  hasPendingEvents(): boolean;
653
1146
  getEventCount(): number;
654
1147
  getLatestEvent(): TEvent | undefined;
@@ -657,9 +1150,16 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
657
1150
  */
658
1151
  createSnapshot(): AggregateSnapshot<TState>;
659
1152
  /**
660
- * Restores the aggregate from a snapshot and applies events that occurred after.
1153
+ * Restores the aggregate from a snapshot and applies events that occurred
1154
+ * after. Same infrastructure-boundary semantics as `loadFromHistory`:
1155
+ * catches `DomainError` and returns it as an `Err`; non-domain throws
1156
+ * propagate.
1157
+ *
1158
+ * All-or-nothing: if any event mid-stream throws a `DomainError`, the
1159
+ * aggregate is rolled back to its pre-call state + version. Partial
1160
+ * restoration is never observable to the caller.
661
1161
  */
662
- restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, string>;
1162
+ restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
663
1163
  /**
664
1164
  * A map of event types to their corresponding handlers.
665
1165
  * Subclasses MUST implement this property.
@@ -674,33 +1174,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
674
1174
  type Version = number & {
675
1175
  readonly __v: true;
676
1176
  };
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
1177
  /**
705
1178
  * Snapshot of an aggregate state at a specific point in time.
706
1179
  * Used for optimizing event replay by starting from a snapshot
@@ -892,10 +1365,20 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
892
1365
  /**
893
1366
  * Registers a handler for a specific command type.
894
1367
  *
1368
+ * When `TMap` is supplied, the `commandType` argument is restricted to
1369
+ * its keys and the handler signature is forced to match `TMap[K]` for the
1370
+ * return value — typos and wrong-typed handlers are compile errors.
1371
+ * Without `TMap` the registration is loose (any string key, any return
1372
+ * type) so the no-config path keeps working.
1373
+ *
895
1374
  * @param commandType - The command type to register the handler for
896
1375
  * @param handler - The handler function for this command type
897
1376
  */
898
- register<C extends Command, R>(commandType: C["type"], handler: CommandHandler<C, R>): void;
1377
+ register<K extends keyof TMap & string, C extends Command & {
1378
+ type: K;
1379
+ } = Command & {
1380
+ type: K;
1381
+ }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
899
1382
  }
900
1383
  /**
901
1384
  * Simple in-memory command bus implementation.
@@ -935,7 +1418,11 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
935
1418
  */
936
1419
  declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
937
1420
  private readonly handlers;
938
- register<C extends Command, R>(commandType: C["type"], handler: CommandHandler<C, R>): void;
1421
+ register<K extends keyof TMap & string, C extends Command & {
1422
+ type: K;
1423
+ } = Command & {
1424
+ type: K;
1425
+ }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
939
1426
  execute<C extends Command & {
940
1427
  type: keyof TMap & string;
941
1428
  }>(command: C): Promise<Result<TMap[C["type"]], string>>;
@@ -976,7 +1463,28 @@ interface EventBus<Evt extends {
976
1463
  }> {
977
1464
  /**
978
1465
  * Publishes events to all subscribed handlers.
979
- * All handlers for each event type will be called.
1466
+ *
1467
+ * **Ordering & parallelism contract:**
1468
+ *
1469
+ * 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`,
1470
+ * awaits all of its handlers, then dispatches `b`, and so on. The
1471
+ * library never reorders or parallelises across events.
1472
+ * 2. **Handlers within a single event run in parallel.** All handlers
1473
+ * subscribed to `event.type` are awaited via `Promise.allSettled` —
1474
+ * none of them sees the others' errors and none is skipped if a
1475
+ * peer fails.
1476
+ * 3. **Errors are collected and thrown AFTER everything dispatches.**
1477
+ * If one handler throws, remaining handlers for that event still
1478
+ * run, and remaining events in the batch still publish. Once
1479
+ * `publish` reaches the end of the batch it throws — the single
1480
+ * error directly if there was one, or an `AggregateError`
1481
+ * ("Multiple event handlers failed") containing every captured
1482
+ * error otherwise. Callers that need fail-fast semantics should
1483
+ * publish events one at a time and not rely on batch atomicity.
1484
+ *
1485
+ * The contract is intentionally simple and in-process. For
1486
+ * cross-process delivery (RabbitMQ, Kafka, etc.), use the `Outbox`
1487
+ * port and a dedicated dispatcher.
980
1488
  *
981
1489
  * @param events - Array of events to publish
982
1490
  */
@@ -999,7 +1507,9 @@ interface EventBus<Evt extends {
999
1507
  * unsubscribe();
1000
1508
  * ```
1001
1509
  */
1002
- subscribe: <T extends Evt>(eventType: Evt["type"], handler: EventHandler<T>) => () => void;
1510
+ subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1511
+ type: K;
1512
+ }>>) => () => void;
1003
1513
  /**
1004
1514
  * Subscribes to the next occurrence of an event type.
1005
1515
  * Returns a Promise that resolves with the event data.
@@ -1014,24 +1524,125 @@ interface EventBus<Evt extends {
1014
1524
  * console.log("Order created:", event.payload.orderId);
1015
1525
  * ```
1016
1526
  */
1017
- once: <T extends Evt>(eventType: Evt["type"]) => Promise<T>;
1527
+ once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
1528
+ type: K;
1529
+ }>>;
1018
1530
  }
1531
+ /**
1532
+ * Options for `EventBus.once()`. Both fields are optional; without them
1533
+ * `once()` waits forever (the historical behaviour).
1534
+ */
1535
+ interface OnceOptions {
1536
+ /**
1537
+ * Aborts the wait. When `signal` fires, `once()` rejects with
1538
+ * `signal.reason` (or a generic abort error if none was supplied) and
1539
+ * the internal subscription is removed.
1540
+ */
1541
+ signal?: AbortSignal;
1542
+ /**
1543
+ * Rejects with a timeout error after this many milliseconds if no event
1544
+ * has arrived. The internal subscription and timer are cleaned up
1545
+ * regardless of which path settles the promise.
1546
+ */
1547
+ timeoutMs?: number;
1548
+ }
1549
+ /**
1550
+ * One pending event in the outbox plus the opaque id the implementation
1551
+ * needs to ack it via `markDispatched`. The library does not prescribe
1552
+ * what `dispatchId` looks like — an implementation can reuse the event's
1553
+ * own `eventId`, generate its own UUID, use the row's auto-increment
1554
+ * primary key, or whatever the storage layer prefers.
1555
+ */
1556
+ interface OutboxRecord<Evt> {
1557
+ dispatchId: string;
1558
+ event: Evt;
1559
+ }
1560
+ /**
1561
+ * Transactional outbox port — the bridge between the write-side
1562
+ * transaction and the (out-of-band) event dispatcher.
1563
+ *
1564
+ * Lifecycle:
1565
+ * 1. `add()` inside the write transaction (`withCommit` calls this) so
1566
+ * events persist atomically with the aggregate state.
1567
+ * 2. A separate outbox dispatcher polls `getPending()` and forwards the
1568
+ * events to subscribers / external brokers.
1569
+ * 3. After successful dispatch, the dispatcher calls `markDispatched()`
1570
+ * with the records' `dispatchId`s so they don't come back next poll.
1571
+ *
1572
+ * `markDispatched` is required to be idempotent — calling it with an id
1573
+ * that's already marked is a no-op, not an error. This lets the
1574
+ * dispatcher safely retry on partial-failure.
1575
+ */
1019
1576
  interface Outbox<Evt> {
1577
+ /**
1578
+ * Persists events. Called from inside `withCommit`'s transactional
1579
+ * callback, atomically with the aggregate write.
1580
+ *
1581
+ * **Idempotency:** implementations should dedupe on the event's
1582
+ * `eventId`. `withCommit` itself does not retry, but the surrounding
1583
+ * use case (a queue consumer, an HTTP retry, a transactional
1584
+ * outbox-dispatcher loop) may legitimately invoke the same write more
1585
+ * than once. A unique-key constraint on `(eventId)` in the outbox
1586
+ * table is the standard implementation.
1587
+ */
1020
1588
  add: (events: ReadonlyArray<Evt>) => Promise<void>;
1589
+ /**
1590
+ * Returns up to `limit` outbox records that have not yet been
1591
+ * dispatched. The dispatcher polls this on a schedule. When `limit`
1592
+ * is omitted, the implementation decides on a default page size.
1593
+ */
1594
+ getPending: (limit?: number) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
1595
+ /**
1596
+ * Marks the given dispatch records as delivered so subsequent
1597
+ * `getPending` calls don't return them. Must be idempotent on
1598
+ * already-marked ids.
1599
+ */
1600
+ markDispatched: (dispatchIds: ReadonlyArray<string>) => Promise<void>;
1021
1601
  }
1022
1602
 
1023
- interface UnitOfWork {
1603
+ /**
1604
+ * Transaction-scope abstraction.
1605
+ *
1606
+ * Wraps a block of work so it runs inside the persistence layer's native
1607
+ * transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, etc.). The block
1608
+ * commits when the callback resolves and rolls back if it throws.
1609
+ *
1610
+ * This is **not** Fowler's full Unit of Work (no change tracking, no
1611
+ * registerDirty/registerNew/registerDeleted, no commit-time flush). It is
1612
+ * intentionally minimal — change tracking is the ORM's job; the library
1613
+ * stays out of it. The name `TransactionScope` is therefore more honest
1614
+ * than `UnitOfWork`.
1615
+ *
1616
+ * @example
1617
+ * ```typescript
1618
+ * await scope.transactional(async () => {
1619
+ * const order = await repo.getByIdOrFail(orderId);
1620
+ * order.confirm();
1621
+ * await repo.save(order);
1622
+ * });
1623
+ * ```
1624
+ */
1625
+ interface TransactionScope {
1024
1626
  transactional<T>(fn: () => Promise<T>): Promise<T>;
1025
1627
  }
1026
- type RepoProvider<R> = (uow: UnitOfWork) => R;
1027
1628
 
1028
1629
  /**
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
1630
+ * Helper for executing a write Use Case inside a Unit of Work.
1631
+ *
1632
+ * Order of operations:
1633
+ * 1. `fn()` runs inside `uow.transactional(...)` domain mutations + repo
1634
+ * writes happen here.
1635
+ * 2. `outbox.add(events)` is also inside the transaction, so events
1636
+ * persist atomically with the state change (outbox pattern).
1637
+ * 3. The transaction commits.
1638
+ * 4. **After** the commit, `bus.publish(events)` fires for the in-process
1639
+ * fast path.
1640
+ *
1641
+ * Publishing AFTER commit prevents the classic "publish before commit"
1642
+ * footgun: in-process subscribers can never react to events from a
1643
+ * transaction that later rolled back. If `bus.publish` itself fails, the
1644
+ * outbox still holds the events and an outbox-dispatcher will deliver them
1645
+ * (eventual consistency).
1035
1646
  *
1036
1647
  * @example
1037
1648
  * ```typescript
@@ -1040,10 +1651,7 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
1040
1651
  * async () => {
1041
1652
  * const order = Order.create(customerId, items);
1042
1653
  * await repository.save(order);
1043
- * return {
1044
- * result: order.id,
1045
- * events: order.pendingEvents
1046
- * };
1654
+ * return { result: order.id, events: order.domainEvents };
1047
1655
  * }
1048
1656
  * );
1049
1657
  * ```
@@ -1053,7 +1661,7 @@ declare function withCommit<Evt extends {
1053
1661
  }, R>(deps: {
1054
1662
  outbox: Outbox<Evt>;
1055
1663
  bus?: EventBus<Evt>;
1056
- uow: UnitOfWork;
1664
+ scope: TransactionScope;
1057
1665
  }, fn: () => Promise<{
1058
1666
  result: R;
1059
1667
  events: ReadonlyArray<Evt>;
@@ -1201,10 +1809,20 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
1201
1809
  /**
1202
1810
  * Registers a handler for a specific query type.
1203
1811
  *
1812
+ * When `TMap` is supplied, the `queryType` argument is restricted to its
1813
+ * keys and the handler signature is forced to match `TMap[K]` for the
1814
+ * return value — typos and wrong-typed handlers are compile errors.
1815
+ * Without `TMap` the registration is loose (any string key, any return
1816
+ * type) so the no-config path keeps working.
1817
+ *
1204
1818
  * @param queryType - The query type to register the handler for
1205
1819
  * @param handler - The handler function for this query type
1206
1820
  */
1207
- register<Q extends Query, R>(queryType: Q["type"], handler: QueryHandler<Q, R>): void;
1821
+ register<K extends keyof TMap & string, Q extends Query & {
1822
+ type: K;
1823
+ } = Query & {
1824
+ type: K;
1825
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
1208
1826
  }
1209
1827
  /**
1210
1828
  * Simple in-memory query bus implementation.
@@ -1244,7 +1862,11 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
1244
1862
  */
1245
1863
  declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
1246
1864
  private readonly handlers;
1247
- register<Q extends Query, R>(queryType: Q["type"], handler: QueryHandler<Q, R>): void;
1865
+ register<K extends keyof TMap & string, Q extends Query & {
1866
+ type: K;
1867
+ } = Query & {
1868
+ type: K;
1869
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
1248
1870
  execute<Q extends Query & {
1249
1871
  type: keyof TMap & string;
1250
1872
  }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
@@ -1255,24 +1877,6 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
1255
1877
  executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
1256
1878
  }
1257
1879
 
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
1880
  /**
1277
1881
  * Simple in-memory event bus implementation.
1278
1882
  * Supports multiple subscribers per event type (pub/sub pattern).
@@ -1297,70 +1901,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
1297
1901
  */
1298
1902
  declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
1299
1903
  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>;
1904
+ subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1905
+ type: K;
1906
+ }>>): () => void;
1907
+ once<K extends Evt["type"]>(eventType: K, options?: OnceOptions): Promise<Extract<Evt, {
1908
+ type: K;
1909
+ }>>;
1910
+ /**
1911
+ * See {@link EventBus.publish} for the full ordering / parallelism /
1912
+ * error-aggregation contract this implementation realises:
1913
+ * - events in input order, sequentially;
1914
+ * - handlers within one event in parallel via `Promise.allSettled`;
1915
+ * - errors collected and thrown after the batch (single Error, or
1916
+ * `AggregateError` for multiple failures).
1917
+ */
1302
1918
  publish(events: ReadonlyArray<Evt>): Promise<void>;
1303
1919
  }
1304
1920
 
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
1921
  /**
1318
- * Repository interface for Aggregate Roots (Entities).
1922
+ * Core repository contract for Aggregate Roots.
1319
1923
  *
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.
1924
+ * In DDD a Repository is a "collection illusion" for aggregates: load by
1925
+ * identity, save the whole aggregate, delete by identity. Querying by
1926
+ * arbitrary criteria is a separate concern (CQRS read-side, ad-hoc bulk
1927
+ * operations) and lives on the `IQueryableRepository` extension below — so
1928
+ * write-side repositories don't have to implement query plumbing they
1929
+ * don't need.
1323
1930
  *
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.
1327
- *
1328
- * Child entities cannot be loaded or saved independently - they exist only
1329
- * within the aggregate boundary and are managed through the Aggregate Root.
1931
+ * Repositories work exclusively with Aggregate Root Entities. The Aggregate
1932
+ * Root represents the aggregate externally and is the only object that can
1933
+ * be loaded or saved through repositories. When loading, all child entities
1934
+ * and value objects inside the aggregate are loaded too; when saving, the
1935
+ * whole aggregate is persisted as a unit.
1330
1936
  *
1331
1937
  * @template TAgg - The aggregate root type (must implement IAggregateRoot)
1332
- * @template TId - The type of the aggregate root identifier
1938
+ * @template TId - The type of the aggregate root identifier
1333
1939
  */
1334
1940
  interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
1941
+ /**
1942
+ * Loads an aggregate by id. Returns `null` when not found.
1943
+ */
1335
1944
  getById(id: TId): Promise<TAgg | null>;
1336
- findOne(spec: ISpecification<TAgg>): Promise<TAgg | null>;
1337
- find(spec: ISpecification<TAgg>): Promise<TAgg[]>;
1945
+ /**
1946
+ * Loads an aggregate by id and throws `AggregateNotFoundError` when not
1947
+ * found. Use this when "not found" is a programming/contract error in
1948
+ * the calling Use Case; use `getById` when null is a valid outcome.
1949
+ */
1950
+ getByIdOrFail(id: TId): Promise<TAgg>;
1951
+ /**
1952
+ * Returns whether an aggregate with the given id exists. Cheaper than
1953
+ * `getById !== null` if your storage supports `EXISTS`-style queries.
1954
+ */
1955
+ exists(id: TId): Promise<boolean>;
1956
+ /**
1957
+ * Persists the aggregate (insert or update). Implementations should:
1958
+ *
1959
+ * 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
1960
+ * aggregate's expected version does not match the version currently
1961
+ * stored (optimistic concurrency).
1962
+ * 2. After a successful write, call `aggregate.markPersisted(newVersion)`
1963
+ * so the in-memory aggregate reflects the new version and clears its
1964
+ * pending/domain events.
1965
+ *
1966
+ * Return type stays `void` — the caller already holds the aggregate
1967
+ * reference, which is now up to date.
1968
+ */
1338
1969
  save(aggregate: TAgg): Promise<void>;
1970
+ /**
1971
+ * Removes the aggregate by id.
1972
+ */
1339
1973
  delete(id: TId): Promise<void>;
1340
1974
  }
1975
+ /**
1976
+ * Repository extension that adds filter-based querying. `TFilter` is the
1977
+ * filter shape your persistence layer speaks: a Drizzle `SQL` expression, a
1978
+ * Prisma `WhereInput`, a MongoDB filter document, a plain
1979
+ * `(t: TAgg) => boolean` predicate for in-memory repos, or anything else.
1980
+ *
1981
+ * The library does not prescribe a Specification or query DSL — the
1982
+ * Repository implementation owns its query language. This avoids the
1983
+ * phantom-interface trap of a library-level `ISpecification<T>` with no
1984
+ * methods and lets each Repository expose the strongest possible types for
1985
+ * its storage backend.
1986
+ *
1987
+ * Aggregates that are only ever accessed by id should implement
1988
+ * `IRepository` directly and skip this extension.
1989
+ *
1990
+ * @template TAgg - The aggregate root type
1991
+ * @template TId - The aggregate root identifier type
1992
+ * @template TFilter - The filter shape understood by this repository
1993
+ *
1994
+ * @example
1995
+ * ```typescript
1996
+ * // In-memory repo with a predicate filter
1997
+ * type Predicate<T> = (t: T) => boolean;
1998
+ * class InMemoryOrders implements IQueryableRepository<Order, OrderId, Predicate<Order>> {
1999
+ * // ...
2000
+ * async find(filter: Predicate<Order>): Promise<Order[]> { ... }
2001
+ * async findOne(filter: Predicate<Order>): Promise<Order | null> { ... }
2002
+ * }
2003
+ *
2004
+ * // Drizzle repo with a SQL expression filter
2005
+ * import type { SQL } from "drizzle-orm";
2006
+ * class DrizzleOrders implements IQueryableRepository<Order, OrderId, SQL> {
2007
+ * // ...
2008
+ * }
2009
+ * ```
2010
+ */
2011
+ interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>, TFilter> extends IRepository<TAgg, TId> {
2012
+ /**
2013
+ * Returns the first aggregate matching the filter, or `null` if none.
2014
+ */
2015
+ findOne(filter: TFilter): Promise<TAgg | null>;
2016
+ /**
2017
+ * Returns **every** aggregate matching the filter — no pagination,
2018
+ * no cursor. For unbounded result sets, prefer a read-side projection
2019
+ * (CQRS read model) over loading aggregates in bulk; aggregates are
2020
+ * write-side objects and rehydrating thousands of them by id is rarely
2021
+ * what you want. If you need pagination on the write side, declare a
2022
+ * domain-specific paged method on your concrete repository (e.g.
2023
+ * `findPage(filter, cursor)`) — the library does not prescribe a
2024
+ * pagination contract because cursor/offset/keyset semantics vary too
2025
+ * much across storage backends.
2026
+ */
2027
+ find(filter: TFilter): Promise<TAgg[]>;
2028
+ }
1341
2029
 
1342
2030
  type VO<T> = Readonly<T>;
1343
2031
  /**
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.
2032
+ * Deep freezes an object and all its nested properties recursively, then
2033
+ * returns it. Iterates both string-keyed and symbol-keyed own properties
2034
+ * so the freeze symmetry matches `deepEqual` (which also considers symbol
2035
+ * keys). Handles circular references by tracking visited objects.
2036
+ *
2037
+ * Note: `deepFreeze` mutates its argument in place — it sets `[[Frozen]]`
2038
+ * on the object you pass in. Callers that need to avoid touching the
2039
+ * input (e.g. `vo()`) should deep-clone first.
1347
2040
  */
1348
2041
  declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
1349
2042
  /**
1350
2043
  * Creates a deeply immutable value object from the given data.
1351
- * All nested objects and arrays are frozen recursively.
1352
2044
  *
1353
- * @param t - The data to convert into a value object
1354
- * @returns A deeply frozen, immutable value object
2045
+ * The input is first deep-cloned with `structuredClone`, then the clone
2046
+ * is frozen so calling `vo(input)` never freezes the caller's own
2047
+ * object graph as a side-effect. Mutating the input afterwards does not
2048
+ * bleed into the VO.
1355
2049
  *
1356
2050
  * @example
1357
2051
  * ```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
2052
+ * const nested = { lat: 52.5, lng: 13.4 };
2053
+ * const address = vo({ street: "Main St", coordinates: nested });
2054
+ * address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
2055
+ * nested.lat = 0; // caller's input still mutable
1364
2056
  * ```
1365
2057
  */
1366
2058
  declare function vo<T>(t: T): VO<T>;
@@ -1459,26 +2151,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
1459
2151
  * ```
1460
2152
  */
1461
2153
  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
2154
  /**
1483
2155
  * Interface for Value Objects.
1484
2156
  * Value Objects are immutable and defined by their properties.
@@ -1570,4 +2242,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
1570
2242
  toJSON(): Readonly<T>;
1571
2243
  }
1572
2244
 
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 };
2245
+ 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, InfrastructureError, 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 };