@shirudo/ddd-kit 1.0.0-rc.7 → 1.0.0-rc.9

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
@@ -52,6 +52,101 @@ interface IdGenerator<Tag extends string> {
52
52
  next: () => Id<Tag>;
53
53
  }
54
54
 
55
+ /**
56
+ * Abstract base for **domain-invariant violations**. Domain methods
57
+ * (aggregates, entity validation hooks, value-object constructors)
58
+ * throw `DomainError`-derived exceptions when a business rule is
59
+ * violated. Consumers derive their own concrete errors — e.g.
60
+ * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
61
+ * for `instanceof`-style catching at the App-Service boundary, where
62
+ * they typically map to HTTP 400 / business-rule responses.
63
+ *
64
+ * The library itself does **not** ship any concrete `DomainError`
65
+ * subclass — the kit can't know your invariants.
66
+ *
67
+ * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
68
+ * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
69
+ * `isRetryable`, …).
70
+ */
71
+ declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
72
+ }
73
+ /**
74
+ * Abstract base for **infrastructure / persistence failures** that the
75
+ * App-Service can recover from — typically by retrying, by returning
76
+ * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
77
+ * not domain-invariant violations (the business rules were not
78
+ * broken); they describe race conditions and missing rows at the
79
+ * storage boundary.
80
+ *
81
+ * Library-internal concrete subclasses: {@link AggregateNotFoundError},
82
+ * {@link ConcurrencyConflictError}.
83
+ */
84
+ declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
85
+ }
86
+ /**
87
+ * Thrown by `EventSourcedAggregate.apply()` when no handler is
88
+ * registered for the event's type. This means the aggregate's subclass
89
+ * forgot to add an entry to its `handlers` map — a programming /
90
+ * configuration bug, not a domain or infrastructure failure.
91
+ *
92
+ * Deliberately **not** on `DomainError` or `InfrastructureError` —
93
+ * a generic `catch (e instanceof DomainError)` handler at the App
94
+ * layer must not mask a forgotten handler; this should crash loud and
95
+ * fail the calling Use Case so the bug surfaces in development. The
96
+ * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
97
+ * also let it propagate uncaught instead of wrapping it in `Result.Err`.
98
+ *
99
+ * Use `isBaseError(e)` from `@shirudo/base-error` to detect
100
+ * "any structured error from the kit or any other BaseError-using
101
+ * library" at the App boundary.
102
+ */
103
+ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
104
+ readonly eventType: string;
105
+ constructor(eventType: string, cause?: unknown);
106
+ }
107
+ /**
108
+ * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
109
+ * given id does not exist. `InfrastructureError` because the storage
110
+ * boundary, not a business rule, decided the row is absent. Use the
111
+ * nullable variant `getById()` if "not found" is a valid outcome.
112
+ *
113
+ * Accepts an optional `cause` so a `Repository.save()` implementation
114
+ * can wrap a lower-level "row not found" / driver-level error without
115
+ * losing context. Cause-chain helpers (`getRootCause`,
116
+ * `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
117
+ *
118
+ * Not retryable — retrying won't make the row appear.
119
+ */
120
+ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
121
+ readonly aggregateType: string;
122
+ readonly id: string;
123
+ constructor(aggregateType: string, id: string, cause?: unknown);
124
+ }
125
+ /**
126
+ * Thrown by `IRepository.save()` when the aggregate's expected version
127
+ * does not match the version currently persisted — i.e. another writer
128
+ * updated the aggregate concurrently. The canonical optimistic-
129
+ * concurrency signal; the App-Service typically reloads, re-applies
130
+ * the use case, and retries, or surfaces HTTP 409 to the caller.
131
+ *
132
+ * `InfrastructureError` because the persistence layer (not a domain
133
+ * rule) detects the race. Marks itself as `retryable: true` so the
134
+ * `isRetryable` predicate from `@shirudo/base-error` picks it up.
135
+ */
136
+ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
137
+ readonly aggregateType: string;
138
+ readonly aggregateId: string;
139
+ readonly expectedVersion: number;
140
+ readonly actualVersion: number;
141
+ /**
142
+ * Marks this error as retryable so `isRetryable(err)` returns
143
+ * true. The canonical OCC pattern is to reload the aggregate, re-apply
144
+ * the use case, and retry on this exception.
145
+ */
146
+ readonly retryable: true;
147
+ constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
148
+ }
149
+
55
150
  /**
56
151
  * Factory function producing a fresh, unique event identifier for each call.
57
152
  *
@@ -314,9 +409,25 @@ interface CreateDomainEventOptions {
314
409
  * Creates a domain event with default values.
315
410
  * Sets occurredAt to current date and version to 1 if not provided.
316
411
  *
412
+ * **For aggregate-internal events, prefer `this.recordEvent(...)` on
413
+ * `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
414
+ * `aggregateId` (from `this.id`) and `aggregateType` (from the
415
+ * aggregate's declared `aggregateType` property), which downstream
416
+ * consumers — outbox dispatchers, projection handlers, audit logs —
417
+ * route by. The `withCommit` harvest boundary now validates both fields
418
+ * are present and throws if they're missing, so a direct
419
+ * `createDomainEvent(...)` call inside an aggregate that forgets the
420
+ * options is caught at runtime.
421
+ *
422
+ * Use `createDomainEvent(...)` directly for events that don't belong to
423
+ * an aggregate: system events, integration events, configuration events,
424
+ * test fixtures. For those, set `aggregateId` / `aggregateType` in
425
+ * `options` if downstream consumers expect routing metadata.
426
+ *
317
427
  * @param type - The event type
318
428
  * @param payload - The event payload
319
- * @param options - Optional event configuration
429
+ * @param options - Optional event configuration (including `aggregateId`
430
+ * and `aggregateType` for routing)
320
431
  * @returns A domain event
321
432
  *
322
433
  * @example
@@ -369,6 +480,93 @@ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?:
369
480
  */
370
481
  declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
371
482
 
483
+ type Version = number & {
484
+ readonly __v: true;
485
+ };
486
+ /**
487
+ * Snapshot of an aggregate state at a specific point in time.
488
+ * Used for optimizing event replay by starting from a snapshot
489
+ * instead of replaying all events from the beginning.
490
+ *
491
+ * @template TState - The type of the aggregate state
492
+ */
493
+ interface AggregateSnapshot<TState> {
494
+ /**
495
+ * The state of the aggregate at the time of the snapshot.
496
+ */
497
+ state: TState;
498
+ /**
499
+ * The version of the aggregate when the snapshot was taken.
500
+ */
501
+ version: Version;
502
+ /**
503
+ * Timestamp when the snapshot was created.
504
+ */
505
+ snapshotAt: Date;
506
+ }
507
+ /**
508
+ * Public contract every Aggregate Root satisfies. Implemented by
509
+ * `BaseAggregate` and inherited by both `AggregateRoot` and
510
+ * `EventSourcedAggregate`. Repository implementations type their
511
+ * `save(aggregate)` parameter against this interface rather than the
512
+ * concrete classes, so the repo layer does not take a compile-time
513
+ * dependency on the aggregate hierarchy.
514
+ *
515
+ * Full per-member documentation lives on the concrete `BaseAggregate`
516
+ * class; the interface is intentionally terse to avoid drift.
517
+ *
518
+ * @template TId - The aggregate root identifier (branded via `Id<Tag>`)
519
+ * @template TEvent - The domain-event union, defaults to `never`
520
+ */
521
+ interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
522
+ readonly id: TId;
523
+ readonly version: Version;
524
+ readonly persistedVersion: Version | undefined;
525
+ readonly pendingEvents: ReadonlyArray<TEvent>;
526
+ clearPendingEvents(): void;
527
+ markPersisted(version: Version): void;
528
+ }
529
+ /**
530
+ * Public contract for Event-Sourced Aggregate Roots. Extends
531
+ * `IAggregateRoot` with the replay-from-history boundary.
532
+ *
533
+ * @template TId - The aggregate root identifier
534
+ * @template TEvent - The union type of all domain events
535
+ */
536
+ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
537
+ /**
538
+ * Reconstitutes the aggregate from an event history. Returns
539
+ * `Result` because event-stream corruption is an expected
540
+ * recoverable failure at the infrastructure boundary.
541
+ */
542
+ loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
543
+ }
544
+ /**
545
+ * Checks if two aggregates are at the same version (same ID and version).
546
+ * Useful for optimistic concurrency control checks.
547
+ *
548
+ * Note: Two aggregates with the same ID ARE the same aggregate (identity).
549
+ * This function checks if they are at the same version — i.e., no concurrent modification.
550
+ *
551
+ * @example
552
+ * ```typescript
553
+ * const before = await repository.getById(id);
554
+ * // ... some operations ...
555
+ * const after = await repository.getById(id);
556
+ *
557
+ * if (!sameVersion(before, after)) {
558
+ * throw new Error("Aggregate was modified by another process");
559
+ * }
560
+ * ```
561
+ */
562
+ declare function sameVersion<TId extends Id<string>>(a: {
563
+ id: TId;
564
+ version: Version;
565
+ }, b: {
566
+ id: TId;
567
+ version: Version;
568
+ }): boolean;
569
+
372
570
  /**
373
571
  * Entity utilities and interfaces for Domain-Driven Design.
374
572
  *
@@ -696,491 +894,109 @@ declare function replaceEntityById<TId extends Id<string>, T extends Identifiabl
696
894
  * // ids is [itemId1, itemId2]
697
895
  * ```
698
896
  */
699
- declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
700
-
701
- /**
702
- * Marker interface for Aggregate Roots.
703
- *
704
- * In Domain-Driven Design, an Aggregate Root is an Entity (the parent Entity of the aggregate).
705
- * It represents the aggregate externally and is the only object that external code
706
- * is allowed to hold references to. All access to child entities within the aggregate
707
- * must go through the Aggregate Root.
708
- *
709
- * An Aggregate consists of:
710
- * - One Aggregate Root (Entity with id + version)
711
- * - Optional child entities (Entities with id + state, but no own version)
712
- * - Optional value objects
713
- *
714
- * The Aggregate Root has identity (id), state, and version for optimistic concurrency control.
715
- * Child entities exist only within the aggregate boundary and are versioned through
716
- * the Aggregate Root.
717
- *
718
- * @template TId - The type of the aggregate root identifier
719
- *
720
- * @example
721
- * ```typescript
722
- * class Order extends AggregateRoot<OrderState, OrderId> implements IAggregateRoot<OrderId> {
723
- * // Order is an Aggregate Root (an Entity with version)
724
- * // OrderState contains child entities (e.g., OrderItem) and value objects
725
- * }
726
- * ```
727
- */
728
- interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
729
- /**
730
- * Unique identifier of the aggregate root entity.
731
- */
732
- readonly id: TId;
733
- /**
734
- * Version number for optimistic concurrency control.
735
- * Incremented on each state change to detect concurrent modifications.
736
- * This version applies to the entire aggregate, including all child entities.
737
- */
738
- readonly version: Version;
739
- /**
740
- * Read-only list of domain events recorded on this aggregate that have
741
- * not yet been flushed to the outbox / persistence layer. Both state-
742
- * stored (`AggregateRoot`) and event-sourced (`EventSourcedAggregate`)
743
- * aggregates expose them under the same name, so Repository.save() can
744
- * harvest them uniformly without branching on the aggregate flavour.
745
- */
746
- readonly pendingEvents: ReadonlyArray<TEvent>;
747
- /**
748
- * Clears the pending-event list. Called by `markPersisted` after a
749
- * successful write — the events have been handed off to the outbox
750
- * / event store and are no longer the aggregate's responsibility.
751
- */
752
- clearPendingEvents(): void;
753
- /**
754
- * Post-save hook: a `Repository.save()` implementation calls this with
755
- * the persisted version after a successful write to push the new
756
- * version back into the aggregate and clear pendingEvents (they are
757
- * now safely on the write side / in the outbox).
758
- *
759
- * Required by the interface so a Repository implementation can call it
760
- * via the published `IAggregateRoot` contract without taking the
761
- * abstract class as a compile-time dependency.
762
- *
763
- * @param version - The version assigned by the persistence layer
764
- */
765
- markPersisted(version: Version): void;
766
- }
767
- /**
768
- * Configuration options for AggregateRoot behavior.
769
- */
770
- interface AggregateConfig {
771
- /**
772
- * Whether `setState()` should bump the version automatically when the
773
- * caller omits the per-call `bumpVersion` argument.
774
- *
775
- * Defaults to **`false`** — `setState()` already takes an explicit
776
- * `bumpVersion` argument per call, so the config is just the default
777
- * the per-call argument falls back to. Set to `true` only if you have
778
- * a subclass that never passes `bumpVersion` and you want every state
779
- * change to advance the version anyway.
780
- */
781
- autoVersionBump?: boolean;
782
- }
783
- /**
784
- * Base class for Aggregate Roots without Event Sourcing.
785
- *
786
- * In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
787
- * and value objects — treated as a unit for consistency. The **Aggregate Root** is the
788
- * root entity that represents the aggregate externally and is the only entry point
789
- * for external code. This class serves as both: it IS the root entity and it contains
790
- * the aggregate state (`TState`) which holds child entities and value objects.
791
- *
792
- * Provides:
793
- * - Identity (id) and state management (via `Entity`)
794
- * - Version management for optimistic concurrency control
795
- * - Domain event tracking for side-effects
796
- * - Snapshot support for performance optimization
797
- *
798
- * All changes to child entities within `TState` are versioned through this root.
799
- * Use `setState()` for state mutations to ensure invariant validation.
800
- *
801
- * For event sourcing, use `EventSourcedAggregate` instead.
802
- *
803
- * @template TState - The type of the aggregate state (contains child entities and value objects)
804
- * @template TId - The type of the aggregate root identifier
805
- * @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
806
- *
807
- * @example
808
- * ```typescript
809
- * // Order is an Aggregate Root (an Entity with version)
810
- * class Order extends AggregateRoot<OrderState, OrderId> {
811
- * constructor(id: OrderId, initialState: OrderState) {
812
- * super(id, initialState);
813
- * }
814
- *
815
- * confirm(): void {
816
- * this.setState({ ...this.state, status: "confirmed" }, true);
817
- * }
818
- * }
819
- * ```
820
- */
821
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
822
- private _version;
823
- get version(): Version;
824
- protected setVersion(version: Version): void;
825
- private readonly _config;
826
- private readonly _autoVersionBump;
827
- private _pendingEvents;
828
- /**
829
- * Read-only list of domain events recorded on this aggregate that have
830
- * not yet been flushed to the outbox / persistence layer.
831
- */
832
- get pendingEvents(): ReadonlyArray<TEvent>;
833
- /**
834
- * Clears the pending-event list. Call this after the events have been
835
- * dispatched (typically `markPersisted` handles it for you).
836
- */
837
- clearPendingEvents(): void;
838
- /**
839
- * **Framework lifecycle method — `@sealed`.** Called by `withCommit`
840
- * (or by your own orchestration code, after harvesting `pendingEvents`)
841
- * to push the persisted version back into the in-memory aggregate and
842
- * clear `pendingEvents`. TypeScript has no `final` keyword, but
843
- * subclasses **should not** override this method directly.
844
- *
845
- * Overriding without calling `super.markPersisted(version)` silently
846
- * leaks `pendingEvents` — the next `withCommit` will re-dispatch them
847
- * through the outbox, double-emitting events. This bug has been hit
848
- * in production by consumers; the {@link onPersisted} hook below is
849
- * the safer extension point.
850
- *
851
- * If you must override (legitimate cases are very rare), call
852
- * `super.markPersisted(version)` FIRST so the framework's cleanup
853
- * runs, then add your logic afterwards.
854
- *
855
- * @param version - The version assigned by the persistence layer
856
- * @see onPersisted — the safe extension point for subclasses
857
- */
858
- markPersisted(version: Version): void;
859
- /**
860
- * Subclass extension point — fires AFTER {@link markPersisted} has
861
- * updated the version and cleared `pendingEvents`. Override this for
862
- * post-persist logging, metrics, or cache-eviction without risk of
863
- * breaking the framework's pendingEvents cleanup.
864
- *
865
- * The default implementation is a no-op. Subclasses do NOT need to
866
- * call `super.onPersisted(version)` — there is nothing in the parent
867
- * implementation to preserve.
868
- *
869
- * **`onPersisted` deliberately receives only the version, not the
870
- * drained events.** Event-driven post-persist logic (aggregate-level
871
- * audit logging, per-event-type side effects) belongs in `EventBus`
872
- * subscribers or the outbox dispatcher — that is the proper
873
- * Aggregate-Boundary separation. Building event-aware logic into
874
- * `onPersisted` couples aggregate lifecycle to event processing and
875
- * recreates the boundary problems Vernon's aggregate discipline is
876
- * meant to prevent.
877
- *
878
- * **The hook must return synchronously.** `markPersisted` is `void`-
879
- * typed and calls `onPersisted` without `await`. TypeScript's
880
- * permissive `void` will accept an `async`-override returning
881
- * `Promise<void>`, but the returned promise is fire-and-forget —
882
- * any rejection becomes an unhandled rejection and `withCommit`
883
- * proceeds without waiting. For asynchronous work, subscribe to the
884
- * relevant domain event on the `EventBus` instead; that is the
885
- * properly awaited extension point.
886
- *
887
- * @param version - The version that was just persisted
888
- */
889
- protected onPersisted(_version: Version): void;
890
- /**
891
- * Mutates state and records the resulting domain events in the
892
- * **canonical record-after-mutation order**. Use this instead of calling
893
- * `setState` + `addDomainEvent` separately and you cannot trip the
894
- * "event for a fact that never happened" footgun.
895
- *
896
- * Order of operations:
897
- * 1. `setState(newState, true)` — runs `validateState` first.
898
- * If it throws, the method propagates and **no event is recorded
899
- * and no version is bumped**.
900
- * 2. Each event in `events` is appended via `addDomainEvent`.
901
- *
902
- * `commit()` **always bumps the version**, regardless of the aggregate's
903
- * `autoVersionBump` config. Recording a domain event implies "something
904
- * happened that the outside world cares about", and optimistic-
905
- * concurrency callers must see a fresh version every time. The config
906
- * still governs the un-coupled `setState` path. If you need to mutate
907
- * state without bumping (e.g. cosmetic caches), call `setState(newState,
908
- * false)` and skip `commit` entirely.
909
- *
910
- * `events` accepts a single event or an array. Omit it (or pass `[]`)
911
- * for state-only mutations.
912
- *
913
- * @example
914
- * ```ts
915
- * confirm(): void {
916
- * if (this.state.status === "confirmed") {
917
- * throw new OrderAlreadyConfirmedError(this.id);
918
- * }
919
- * this.commit(
920
- * { ...this.state, status: "confirmed" },
921
- * { type: "OrderConfirmed", orderId: this.id },
922
- * );
923
- * }
924
- * ```
925
- *
926
- * `EventSourcedAggregate.apply()` enforces the same ordering
927
- * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
928
- * where `setState` and `addDomainEvent` are otherwise decoupled and the
929
- * ordering is convention-only.
930
- *
931
- * @param newState - The new state (validated by `validateState`)
932
- * @param events - One event, an array of events, or none (default)
933
- */
934
- protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
935
- protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
936
- /**
937
- * Records a domain event for later publication.
938
- *
939
- * **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
940
- * explicit: a domain event describes something that has just happened
941
- * to the aggregate — its existence implies the state change already
942
- * occurred. Concretely:
943
- *
944
- * ```ts
945
- * confirm(): void {
946
- * if (this.state.status === "confirmed") {
947
- * throw new OrderAlreadyConfirmedError(this.id);
948
- * }
949
- * this.setState({ ...this.state, status: "confirmed" }, true);
950
- * this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
951
- * // ↑ post-mutation. The event represents the committed fact.
952
- * }
953
- * ```
954
- *
955
- * Recording before mutation is a footgun: if a subsequent invariant
956
- * check throws, the event has already been queued but the state never
957
- * actually changed — consumers see an event for a fact that did not
958
- * happen.
959
- *
960
- * `EventSourcedAggregate.apply()` enforces this ordering structurally;
961
- * `AggregateRoot` leaves it as a convention because the state-mutation
962
- * path (`setState`) is decoupled from event recording.
963
- *
964
- * @param event - The domain event to record
965
- */
966
- protected addDomainEvent(event: TEvent): void;
967
- /**
968
- * Manually bumps the aggregate version.
969
- * Call this after state changes for Optimistic Concurrency Control.
970
- *
971
- * If `autoVersionBump` is enabled, this is called automatically
972
- * when using `setState()`.
973
- */
974
- protected bumpVersion(): void;
975
- /**
976
- * Sets the state and optionally bumps the version automatically.
977
- * This is a convenience method for state mutations.
978
- * Automatically validates the newState using `validateState()`.
979
- * Overrides Entity.setState to add version bumping.
980
- *
981
- * @param newState - The new state
982
- * @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
983
- */
984
- protected setState(newState: TState, bumpVersion?: boolean): void;
985
- /**
986
- * Creates a snapshot of the current aggregate state.
987
- * Useful for performance optimization, backup/restore, and audit trails.
988
- *
989
- * @returns A snapshot containing the current state and version
990
- *
991
- * @example
992
- * ```typescript
993
- * const snapshot = aggregate.createSnapshot();
994
- * await snapshotRepository.save(aggregate.id, snapshot);
995
- * ```
996
- */
997
- createSnapshot(): AggregateSnapshot<TState>;
998
- /**
999
- * Restores the aggregate from a snapshot.
1000
- * This is useful for loading aggregates from snapshots instead of
1001
- * rebuilding them from scratch.
1002
- * Validates the restored state.
1003
- *
1004
- * @param snapshot - The snapshot to restore from
1005
- *
1006
- * @example
1007
- * ```typescript
1008
- * const snapshot = await snapshotRepository.getLatest(aggregateId);
1009
- * aggregate.restoreFromSnapshot(snapshot);
1010
- * ```
1011
- */
1012
- restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
1013
- }
1014
-
1015
- /**
1016
- * Abstract base for **domain-invariant violations**. Domain methods
1017
- * (aggregates, entity validation hooks, value-object constructors)
1018
- * throw `DomainError`-derived exceptions when a business rule is
1019
- * violated. Consumers derive their own concrete errors — e.g.
1020
- * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
1021
- * for `instanceof`-style catching at the App-Service boundary, where
1022
- * they typically map to HTTP 400 / business-rule responses.
1023
- *
1024
- * The library itself does **not** ship any concrete `DomainError`
1025
- * subclass — the kit can't know your invariants.
1026
- *
1027
- * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
1028
- * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
1029
- * `isRetryable`, …).
1030
- */
1031
- declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
1032
- }
1033
- /**
1034
- * Abstract base for **infrastructure / persistence failures** that the
1035
- * App-Service can recover from — typically by retrying, by returning
1036
- * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
1037
- * not domain-invariant violations (the business rules were not
1038
- * broken); they describe race conditions and missing rows at the
1039
- * storage boundary.
1040
- *
1041
- * Library-internal concrete subclasses: {@link AggregateNotFoundError},
1042
- * {@link ConcurrencyConflictError}.
1043
- */
1044
- declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
1045
- }
1046
- /**
1047
- * Thrown by `EventSourcedAggregate.apply()` when no handler is
1048
- * registered for the event's type. This means the aggregate's subclass
1049
- * forgot to add an entry to its `handlers` map — a programming /
1050
- * configuration bug, not a domain or infrastructure failure.
1051
- *
1052
- * Deliberately **not** on `DomainError` or `InfrastructureError` —
1053
- * a generic `catch (e instanceof DomainError)` handler at the App
1054
- * layer must not mask a forgotten handler; this should crash loud and
1055
- * fail the calling Use Case so the bug surfaces in development. The
1056
- * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
1057
- * also let it propagate uncaught instead of wrapping it in `Result.Err`.
1058
- *
1059
- * Use `isBaseError(e)` from `@shirudo/base-error` to detect
1060
- * "any structured error from the kit or any other BaseError-using
1061
- * library" at the App boundary.
1062
- */
1063
- declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
1064
- readonly eventType: string;
1065
- constructor(eventType: string, cause?: unknown);
1066
- }
1067
- /**
1068
- * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
1069
- * given id does not exist. `InfrastructureError` because the storage
1070
- * boundary, not a business rule, decided the row is absent. Use the
1071
- * nullable variant `getById()` if "not found" is a valid outcome.
1072
- *
1073
- * Accepts an optional `cause` so a `Repository.save()` implementation
1074
- * can wrap a lower-level "row not found" / driver-level error without
1075
- * losing context. Cause-chain helpers (`getRootCause`,
1076
- * `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
1077
- *
1078
- * Not retryable — retrying won't make the row appear.
1079
- */
1080
- declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
1081
- readonly aggregateType: string;
1082
- readonly id: string;
1083
- constructor(aggregateType: string, id: string, cause?: unknown);
1084
- }
1085
- /**
1086
- * Thrown by `IRepository.save()` when the aggregate's expected version
1087
- * does not match the version currently persisted — i.e. another writer
1088
- * updated the aggregate concurrently. The canonical optimistic-
1089
- * concurrency signal; the App-Service typically reloads, re-applies
1090
- * the use case, and retries, or surfaces HTTP 409 to the caller.
1091
- *
1092
- * `InfrastructureError` because the persistence layer (not a domain
1093
- * rule) detects the race. Marks itself as `retryable: true` so the
1094
- * `isRetryable` predicate from `@shirudo/base-error` picks it up.
1095
- */
1096
- declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
1097
- readonly aggregateType: string;
1098
- readonly aggregateId: string;
1099
- readonly expectedVersion: number;
1100
- readonly actualVersion: number;
1101
- /**
1102
- * Marks this error as retryable so `isRetryable(err)` returns
1103
- * true. The canonical OCC pattern is to reload the aggregate, re-apply
1104
- * the use case, and retry on this exception.
1105
- */
1106
- readonly retryable: true;
1107
- constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
1108
- }
1109
-
1110
- /**
1111
- * Interface for Event-Sourced Aggregate Roots.
1112
- * Defines the contract for aggregates that manage state changes via event sourcing.
1113
- *
1114
- * @template TId - The type of the aggregate root identifier
1115
- * @template TEvent - The union type of all domain events
1116
- */
1117
- interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
1118
- /**
1119
- * Reconstitutes the aggregate from an event history. Returns `Result`
1120
- * because event-stream corruption is an expected recoverable failure
1121
- * at the infrastructure boundary.
1122
- *
1123
- * @param history - An ordered list of past events
1124
- */
1125
- loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
1126
- }
1127
- type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
1128
- /**
1129
- * Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
1130
- *
1131
- * Like `AggregateRoot`, this is both the root entity and the aggregate boundary.
1132
- * The difference is persistence: state is derived from events, not stored directly.
1133
- * Events are the single source of truth — all state changes go through `apply()` → handler.
1134
- *
1135
- * Extends `Entity` directly (not `AggregateRoot`) so that `setState()` and
1136
- * `addDomainEvent()` are not available. This enforces the event sourcing pattern
1137
- * at the type level — there is no way to mutate state without going through an event handler.
1138
- *
1139
- * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
1140
- * invariant violations. Subclasses override `validateEvent()` to throw their
1141
- * own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
1142
- * infrastructure-boundary methods (`loadFromHistory`,
1143
- * `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
1144
- * during replay so callers can react to corrupted event streams without
1145
- * try/catch.
1146
- *
1147
- * @template TState - The type of the aggregate state (contains child entities and value objects)
1148
- * @template TEvent - The union type of all domain events
1149
- * @template TId - The type of the aggregate root identifier
1150
- *
1151
- * @example
1152
- * ```typescript
1153
- * class OrderAlreadyConfirmedError extends DomainError {
1154
- * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1155
- * }
1156
- *
1157
- * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1158
- * confirm(): void {
1159
- * this.apply(createDomainEvent("OrderConfirmed", {}));
1160
- * }
1161
- *
1162
- * protected validateEvent(event: OrderEvent): void {
1163
- * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1164
- * throw new OrderAlreadyConfirmedError(this.id);
1165
- * }
1166
- * }
1167
- *
1168
- * protected readonly handlers = {
1169
- * OrderConfirmed: (state: OrderState): OrderState => ({
1170
- * ...state,
1171
- * status: "confirmed",
1172
- * }),
1173
- * };
1174
- * }
1175
- * ```
1176
- */
1177
- declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
897
+ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
898
+
899
+ /**
900
+ * Shared base for both `AggregateRoot` (state-stored) and
901
+ * `EventSourcedAggregate`. Carries the lifecycle machinery that's
902
+ * identical across the two flavours: version + persistedVersion
903
+ * tracking, pending events buffer, the `markRestored` (Post-Load) /
904
+ * `markPersisted` (Post-Save) lifecycle markers, and the
905
+ * `recordEvent` helper that auto-injects `aggregateId` +
906
+ * `aggregateType` on every event the aggregate emits.
907
+ *
908
+ * Consumers do NOT extend this class directly extend
909
+ * `AggregateRoot` for state-stored aggregates or
910
+ * `EventSourcedAggregate` for event-sourced ones. The split between
911
+ * those two reflects the canonical Vernon §8 (state-stored) /
912
+ * Vernon §11 + Greg Young (event-sourced) distinction in how state
913
+ * is represented; the lifecycle machinery is the same for both.
914
+ *
915
+ * @template TState - The type of the aggregate state
916
+ * @template TId - The aggregate root identifier
917
+ * @template TEvent - The domain-event union. Defaults to `never` so
918
+ * aggregates without a declared event type cannot emit events
919
+ * (emitting any event becomes a compile error).
920
+ */
921
+ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
922
+ /**
923
+ * The aggregate's domain type as a string, used to populate
924
+ * `aggregateType` on events recorded via {@link recordEvent}.
925
+ *
926
+ * Subclasses MUST declare this as a string literal:
927
+ *
928
+ * ```ts
929
+ * class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
930
+ * protected readonly aggregateType = "Order";
931
+ * }
932
+ * ```
933
+ *
934
+ * The string is *the* identifier downstream consumers (outbox
935
+ * dispatchers, projection handlers, audit logs) use to route by
936
+ * aggregate kind. Use the same canonical name across your system —
937
+ * matching the class name is the obvious choice, but the value
938
+ * comes from this explicit declaration, not `constructor.name`
939
+ * (which is fragile under minification, bundler transforms, and
940
+ * subclass renaming).
941
+ */
942
+ protected abstract readonly aggregateType: string;
1178
943
  private _version;
1179
- get version(): Version;
1180
- private setVersion;
944
+ /**
945
+ * DB-baseline version. `undefined` until the aggregate has been
946
+ * persisted or restored at least once. Repository implementations
947
+ * route INSERT vs UPDATE on this field and use it as the OCC
948
+ * baseline. See `IRepository.save` JSDoc.
949
+ *
950
+ * Distinct from {@link version}, which is the in-memory
951
+ * post-mutation value. Mutations bump `_version` but never touch
952
+ * `_persistedVersion` — that field only moves on {@link markRestored}
953
+ * (Post-Load) and {@link markPersisted} (Post-Save).
954
+ */
955
+ private _persistedVersion;
1181
956
  private _pendingEvents;
957
+ get version(): Version;
958
+ get persistedVersion(): Version | undefined;
959
+ /**
960
+ * Read-only list of domain events recorded on this aggregate that
961
+ * have not yet been flushed to the outbox / persistence layer.
962
+ */
1182
963
  get pendingEvents(): ReadonlyArray<TEvent>;
964
+ /**
965
+ * Clears the pending-event list. Called by `markPersisted` after a
966
+ * successful write — the events have been handed off to the outbox
967
+ * / event store and are no longer the aggregate's responsibility.
968
+ */
1183
969
  clearPendingEvents(): void;
970
+ protected setVersion(version: Version): void;
971
+ /**
972
+ * Manually bumps the aggregate version. Used by state-stored
973
+ * aggregates' `setState(_, true)` / `commit()` paths and by the
974
+ * event-sourced replay path after each applied event.
975
+ */
976
+ protected bumpVersion(): void;
977
+ /**
978
+ * **Lifecycle marker — Post-Load.** Syncs both `_version` and
979
+ * `_persistedVersion` to the DB-stored version. Used by
980
+ * `reconstitute(...)` factories to assemble an in-memory aggregate
981
+ * from a persisted row.
982
+ *
983
+ * Does NOT fire {@link onPersisted} — that hook has post-save
984
+ * semantics (metrics, audit, cache eviction), not post-load. The
985
+ * Factory-vs-Reconstitution distinction (Vernon §11) is honoured
986
+ * structurally: two separate markers, one for each transition.
987
+ *
988
+ * @param version - The version the row currently holds in the DB
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
993
+ * const order = new Order(id, state);
994
+ * order.markRestored(version);
995
+ * return order;
996
+ * }
997
+ * ```
998
+ */
999
+ protected markRestored(version: Version): void;
1184
1000
  /**
1185
1001
  * **Framework lifecycle method — `@sealed`.** Called by `withCommit`
1186
1002
  * (or by your own orchestration code, after harvesting `pendingEvents`)
@@ -1233,7 +1049,240 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1233
1049
  * @param version - The version that was just persisted
1234
1050
  */
1235
1051
  protected onPersisted(_version: Version): void;
1236
- protected constructor(id: TId, initialState: TState);
1052
+ /**
1053
+ * Appends a domain event to the pending list. Prefer the higher-level
1054
+ * `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
1055
+ * (event-sourced) call sites — both wrap `addDomainEvent` in the
1056
+ * canonical record-AFTER-mutation order (Vernon §8). Calling
1057
+ * `addDomainEvent` directly is appropriate only when state and event
1058
+ * recording have already been decoupled deliberately (e.g. a
1059
+ * deletion event before a hard-delete; see `docs/guide/repository.md`).
1060
+ */
1061
+ protected addDomainEvent(event: TEvent): void;
1062
+ /**
1063
+ * Creates a snapshot of the current aggregate state — the state at
1064
+ * this moment plus the version. Useful for ES snapshot policies and
1065
+ * for state-stored backup / restore.
1066
+ */
1067
+ createSnapshot(): AggregateSnapshot<TState>;
1068
+ /**
1069
+ * Sugar for `createDomainEvent` that auto-injects `aggregateId`
1070
+ * (from `this.id`) and `aggregateType` (from {@link aggregateType})
1071
+ * into the event's metadata fields. This is the canonical path for
1072
+ * recording events from inside aggregate domain methods.
1073
+ *
1074
+ * Downstream consumers — outbox dispatchers, projection handlers,
1075
+ * audit logs — route by these two fields. Calling
1076
+ * `createDomainEvent(...)` directly inside an aggregate method
1077
+ * leaves them unset and is caught at the `withCommit` harvest
1078
+ * boundary, but `this.recordEvent(...)` makes the right thing
1079
+ * impossible to forget.
1080
+ *
1081
+ * @example
1082
+ * ```ts
1083
+ * class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
1084
+ * protected readonly aggregateType = "Order";
1085
+ *
1086
+ * confirm(): void {
1087
+ * this.commit(
1088
+ * { ...this.state, status: "confirmed" },
1089
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1090
+ * );
1091
+ * }
1092
+ * }
1093
+ * ```
1094
+ *
1095
+ * @param type - event type discriminator (must be one of `TEvent`'s tags)
1096
+ * @param payload - payload for that event subtype
1097
+ * @param options - any remaining `createDomainEvent` options
1098
+ * (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
1099
+ * and `aggregateType` are deliberately omitted — the helper sets
1100
+ * them.
1101
+ */
1102
+ protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
1103
+ }
1104
+
1105
+ /**
1106
+ * Configuration options for AggregateRoot behavior.
1107
+ */
1108
+ interface AggregateConfig {
1109
+ /**
1110
+ * Whether `setState()` should bump the version automatically when the
1111
+ * caller omits the per-call `bumpVersion` argument.
1112
+ *
1113
+ * Defaults to **`false`** — `setState()` already takes an explicit
1114
+ * `bumpVersion` argument per call, so the config is just the default
1115
+ * the per-call argument falls back to. Set to `true` only if you have
1116
+ * a subclass that never passes `bumpVersion` and you want every state
1117
+ * change to advance the version anyway.
1118
+ */
1119
+ autoVersionBump?: boolean;
1120
+ }
1121
+ /**
1122
+ * Base class for Aggregate Roots without Event Sourcing.
1123
+ *
1124
+ * In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
1125
+ * and value objects — treated as a unit for consistency. The **Aggregate Root** is the
1126
+ * root entity that represents the aggregate externally and is the only entry point
1127
+ * for external code. This class serves as both: it IS the root entity and it contains
1128
+ * the aggregate state (`TState`) which holds child entities and value objects.
1129
+ *
1130
+ * Provides:
1131
+ * - Identity (id) and state management (via `Entity`)
1132
+ * - Version + persistedVersion + pending-event tracking (via `BaseAggregate`)
1133
+ * - `setState`-based mutation with optional version bumping
1134
+ * - `commit()` record-after-mutation helper
1135
+ * - Snapshot support for performance optimization
1136
+ *
1137
+ * All changes to child entities within `TState` are versioned through this root.
1138
+ * Use `setState()` for state mutations to ensure invariant validation.
1139
+ *
1140
+ * For event sourcing, use `EventSourcedAggregate` instead.
1141
+ *
1142
+ * @template TState - The type of the aggregate state (contains child entities and value objects)
1143
+ * @template TId - The type of the aggregate root identifier
1144
+ * @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
1145
+ *
1146
+ * @example
1147
+ * ```typescript
1148
+ * // Order is an Aggregate Root (an Entity with version)
1149
+ * class Order extends AggregateRoot<OrderState, OrderId> {
1150
+ * protected readonly aggregateType = "Order";
1151
+ *
1152
+ * constructor(id: OrderId, initialState: OrderState) {
1153
+ * super(id, initialState);
1154
+ * }
1155
+ *
1156
+ * confirm(): void {
1157
+ * this.commit(
1158
+ * { ...this.state, status: "confirmed" },
1159
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1160
+ * );
1161
+ * }
1162
+ * }
1163
+ * ```
1164
+ */
1165
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
1166
+ private readonly _autoVersionBump;
1167
+ protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
1168
+ /**
1169
+ * Mutates state and records the resulting domain events in the
1170
+ * **canonical record-after-mutation order**. Use this instead of calling
1171
+ * `setState` + `addDomainEvent` separately and you cannot trip the
1172
+ * "event for a fact that never happened" footgun.
1173
+ *
1174
+ * Order of operations:
1175
+ * 1. `setState(newState, true)` — runs `validateState` first.
1176
+ * If it throws, the method propagates and **no event is recorded
1177
+ * and no version is bumped**.
1178
+ * 2. Each event in `events` is appended via `addDomainEvent`.
1179
+ *
1180
+ * `commit()` **always bumps the version**, regardless of the aggregate's
1181
+ * `autoVersionBump` config. Recording a domain event implies "something
1182
+ * happened that the outside world cares about", and optimistic-
1183
+ * concurrency callers must see a fresh version every time. The config
1184
+ * still governs the un-coupled `setState` path. If you need to mutate
1185
+ * state without bumping (e.g. cosmetic caches), call `setState(newState,
1186
+ * false)` and skip `commit` entirely.
1187
+ *
1188
+ * `events` accepts a single event or an array. Omit it (or pass `[]`)
1189
+ * for state-only mutations.
1190
+ *
1191
+ * @example
1192
+ * ```ts
1193
+ * confirm(): void {
1194
+ * if (this.state.status === "confirmed") {
1195
+ * throw new OrderAlreadyConfirmedError(this.id);
1196
+ * }
1197
+ * this.commit(
1198
+ * { ...this.state, status: "confirmed" },
1199
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1200
+ * );
1201
+ * }
1202
+ * ```
1203
+ *
1204
+ * `EventSourcedAggregate.apply()` enforces the same ordering
1205
+ * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
1206
+ * where `setState` and `addDomainEvent` are otherwise decoupled and the
1207
+ * ordering is convention-only.
1208
+ *
1209
+ * @param newState - The new state (validated by `validateState`)
1210
+ * @param events - One event, an array of events, or none (default)
1211
+ */
1212
+ protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
1213
+ /**
1214
+ * Sets the state and optionally bumps the version automatically.
1215
+ * Validates `newState` via `validateState()`.
1216
+ *
1217
+ * @param newState - The new state
1218
+ * @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
1219
+ */
1220
+ protected setState(newState: TState, bumpVersion?: boolean): void;
1221
+ /**
1222
+ * Restores the aggregate from a snapshot — loads state and aligns
1223
+ * `version` + `persistedVersion` to the snapshot version. Validates
1224
+ * the restored state.
1225
+ *
1226
+ * @param snapshot - The snapshot to restore from
1227
+ */
1228
+ restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
1229
+ }
1230
+
1231
+ type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
1232
+ /**
1233
+ * Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
1234
+ *
1235
+ * Like `AggregateRoot`, this is both the root entity and the aggregate
1236
+ * boundary. The difference is persistence: state is derived from events,
1237
+ * not stored directly. Events are the single source of truth — all state
1238
+ * changes go through `apply()` → handler.
1239
+ *
1240
+ * Extends `BaseAggregate` (the shared lifecycle machinery) but does NOT
1241
+ * expose `setState()` or `commit()` from `AggregateRoot`. This enforces
1242
+ * the event sourcing pattern at the type level — there is no way to
1243
+ * mutate state without going through an event handler.
1244
+ *
1245
+ * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions
1246
+ * on invariant violations. Subclasses override `validateEvent()` to
1247
+ * throw their own concrete subclasses (e.g. `OrderAlreadyConfirmedError`).
1248
+ * Only the infrastructure-boundary methods (`loadFromHistory`,
1249
+ * `restoreFromSnapshotWithEvents`) return `Result` — they catch
1250
+ * `DomainError` during replay so callers can react to corrupted event
1251
+ * streams without try/catch.
1252
+ *
1253
+ * @template TState - The aggregate state (contains child entities and value objects)
1254
+ * @template TEvent - The union type of all domain events
1255
+ * @template TId - The aggregate root identifier
1256
+ *
1257
+ * @example
1258
+ * ```typescript
1259
+ * class OrderAlreadyConfirmedError extends DomainError {
1260
+ * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1261
+ * }
1262
+ *
1263
+ * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1264
+ * protected readonly aggregateType = "Order";
1265
+ *
1266
+ * confirm(): void {
1267
+ * this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
1268
+ * }
1269
+ *
1270
+ * protected validateEvent(event: OrderEvent): void {
1271
+ * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1272
+ * throw new OrderAlreadyConfirmedError(this.id);
1273
+ * }
1274
+ * }
1275
+ *
1276
+ * protected readonly handlers = {
1277
+ * OrderConfirmed: (state: OrderState): OrderState => ({
1278
+ * ...state,
1279
+ * status: "confirmed",
1280
+ * }),
1281
+ * };
1282
+ * }
1283
+ * ```
1284
+ */
1285
+ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends BaseAggregate<TState, TId, TEvent> implements IEventSourcedAggregate<TId, TEvent> {
1237
1286
  /**
1238
1287
  * Validates an event before it is applied. Default is no-op.
1239
1288
  * Subclasses override to throw a concrete `DomainError` subclass when
@@ -1281,10 +1330,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1281
1330
  * 2 events ends at v=3, not v=2.
1282
1331
  */
1283
1332
  loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
1284
- /**
1285
- * Creates a snapshot of the current aggregate state.
1286
- */
1287
- createSnapshot(): AggregateSnapshot<TState>;
1288
1333
  /**
1289
1334
  * Restores the aggregate from a snapshot and applies events that occurred
1290
1335
  * after. Same infrastructure-boundary semantics as `loadFromHistory`:
@@ -1307,56 +1352,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1307
1352
  };
1308
1353
  }
1309
1354
 
1310
- type Version = number & {
1311
- readonly __v: true;
1312
- };
1313
- /**
1314
- * Snapshot of an aggregate state at a specific point in time.
1315
- * Used for optimizing event replay by starting from a snapshot
1316
- * instead of replaying all events from the beginning.
1317
- *
1318
- * @template TState - The type of the aggregate state
1319
- */
1320
- interface AggregateSnapshot<TState> {
1321
- /**
1322
- * The state of the aggregate at the time of the snapshot.
1323
- */
1324
- state: TState;
1325
- /**
1326
- * The version of the aggregate when the snapshot was taken.
1327
- */
1328
- version: Version;
1329
- /**
1330
- * Timestamp when the snapshot was created.
1331
- */
1332
- snapshotAt: Date;
1333
- }
1334
- /**
1335
- * Checks if two aggregates are at the same version (same ID and version).
1336
- * Useful for optimistic concurrency control checks.
1337
- *
1338
- * Note: Two aggregates with the same ID ARE the same aggregate (identity).
1339
- * This function checks if they are at the same version — i.e., no concurrent modification.
1340
- *
1341
- * @example
1342
- * ```typescript
1343
- * const before = await repository.getById(id);
1344
- * // ... some operations ...
1345
- * const after = await repository.getById(id);
1346
- *
1347
- * if (!sameVersion(before, after)) {
1348
- * throw new Error("Aggregate was modified by another process");
1349
- * }
1350
- * ```
1351
- */
1352
- declare function sameVersion<TId extends Id<string>>(a: {
1353
- id: TId;
1354
- version: Version;
1355
- }, b: {
1356
- id: TId;
1357
- version: Version;
1358
- }): boolean;
1359
-
1360
1355
  /**
1361
1356
  * Marker interface for Commands.
1362
1357
  * Commands represent write operations that change system state.
@@ -2213,26 +2208,25 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
2213
2208
  * stored (optimistic concurrency).
2214
2209
  * 2. Write the aggregate to durable storage.
2215
2210
  *
2216
- * **Insert vs update — library convention.** A fresh aggregate begins
2217
- * at `version === 0` (the `Version` brand defaults to `0` in both
2218
- * `AggregateRoot` and `EventSourcedAggregate`). After the first
2219
- * versioned mutation (`setState(_, true)`, `apply()`, `commit()`) the
2220
- * version is `> 0`. Implementations distinguish the two paths by the
2221
- * incoming `aggregate.version`:
2211
+ * **Insert vs update — `persistedVersion` convention.** Every aggregate
2212
+ * exposes two version fields with distinct roles:
2213
+ *
2214
+ * - `aggregate.version` — in-memory post-mutation value, bumped by
2215
+ * `setState(_, true)` / `commit()` / `apply()`. NOT the right
2216
+ * routing key, because mutations can advance it past zero while
2217
+ * the DB row still does not exist.
2218
+ * - `aggregate.persistedVersion` — what the persistence layer holds.
2219
+ * `undefined` until the aggregate has been persisted or restored
2220
+ * at least once. This is the routing key.
2222
2221
  *
2223
- * - `aggregate.version === 0` → **INSERT** (no existing row to lock
2224
- * against; the write succeeds unconditionally or fails the unique
2222
+ * - `aggregate.persistedVersion === undefined` → **INSERT** (never
2223
+ * persisted; write succeeds unconditionally or fails the unique
2225
2224
  * constraint on `id`).
2226
- * - `aggregate.version > 0` → **UPDATE** with the OCC predicate
2227
- * `WHERE id = ? AND version = expected`. If the row count is `0`,
2228
- * another writer raced you throw `ConcurrencyConflictError`.
2229
- *
2230
- * The library does not formalise this in the type system because
2231
- * version-bump semantics differ across the two aggregate flavours
2232
- * (state-stored aggregates bump on the user's call to `setState(_,
2233
- * true)`; event-sourced aggregates bump on every `apply()` by
2234
- * definition). The `version === 0` invariant for "never persisted" is
2235
- * the common contract.
2225
+ * - otherwise → **UPDATE** with the OCC predicate
2226
+ * `WHERE id = ? AND version = aggregate.persistedVersion` (the
2227
+ * load-time / last-save baseline, not the post-mutation in-memory
2228
+ * value). If the row count is `0`, another writer raced you —
2229
+ * throw `ConcurrencyConflictError`.
2236
2230
  *
2237
2231
  * Do **not** call `aggregate.markPersisted(...)` here. The library's
2238
2232
  * `withCommit` orchestrator handles the post-save lifecycle (harvest