@shirudo/ddd-kit 1.3.0 → 2.2.0

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,10 +1,76 @@
1
- import { a as Id, A as AnyDomainEvent, I as IAggregateRoot, V as Version, b as AggregateSnapshot, C as CreateDomainEventOptions, c as IEventSourcedAggregate, D as DomainError, d as InfrastructureError } from './aggregate-BGdgvqKh.js';
2
- export { q as AggregateDeletedError, t as AggregateNotFoundError, f as ClockFactory, v as ConcurrencyConflictError, k as DomainEvent, u as DuplicateAggregateError, p as EventHarvestError, E as EventIdFactory, j as EventMetadata, x as IdGenerator, M as MissingHandlerError, U as UnenrolledChangesError, n as copyMetadata, l as createDomainEvent, m as createDomainEventWithMetadata, o as mergeMetadata, i as resetClockFactory, r as resetEventIdFactory, s as sameVersion, g as setClockFactory, e as setEventIdFactory, h as withClockFactory, w as withEventIdFactory } from './aggregate-BGdgvqKh.js';
1
+ import { a as Id, A as AnyDomainEvent, I as IAggregateRoot, V as Version, b as AggregateSnapshot, C as CreateDomainEventOptions, c as IEventSourcedAggregate, D as DomainError, d as InfrastructureError } from './aggregate-DFi6HlEh.js';
2
+ export { j as AggregateDeletedError, k as AggregateNotFoundError, l as AggregateNotFoundErrorOptions, n as ConcurrencyConflictError, o as ConcurrencyConflictErrorOptions, g as DomainEvent, p as DuplicateAggregateError, q as DuplicateAggregateErrorOptions, t as EventHarvestError, E as EventIdFactory, h as EventMetadata, z as IdGenerator, M as MissingHandlerError, S as SnapshotSchemaMismatchError, u as SnapshotSchemaMismatchErrorOptions, U as UnenrolledChangesError, v as UnregisteredHandlerError, x as UnregisteredHandlerErrorOptions, y as UnreplayableAggregateError, e as copyMetadata, f as createDomainEvent, m as mergeMetadata, r as resetEventIdFactory, s as sameVersion, i as setEventIdFactory, w as withEventIdFactory } from './aggregate-DFi6HlEh.js';
3
3
  import { Result } from '@shirudo/result';
4
4
  import { BaseError, ValidationError } from '@shirudo/base-error';
5
5
  import { DeepEqualExceptOptions } from './utils.js';
6
6
  export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
7
7
 
8
+ /**
9
+ * Module-internal home of the kit's swappable clock. The public surface
10
+ * (`ClockFactory`, `setClockFactory`, `withClockFactory`,
11
+ * `resetClockFactory`) is re-exported through `domain-event.ts`; the
12
+ * `now()` accessor stays internal so the barrel files never ship it.
13
+ * Living in its own module lets `createDomainEvent` (occurredAt) and
14
+ * `BaseAggregate.createSnapshot` (snapshotAt) read the same clock
15
+ * without an import cycle between those files.
16
+ */
17
+ /**
18
+ * Clock function producing a fresh `Date` for each call. The library
19
+ * defaults to `() => new Date()`; override globally via `setClockFactory`
20
+ * for deterministic event-sourcing tests, time-travel debugging, or any
21
+ * scenario where `occurredAt` / `snapshotAt` must be reproducible.
22
+ */
23
+ type ClockFactory = () => Date;
24
+ /**
25
+ * Replaces the global clock factory used by `createDomainEvent` and
26
+ * `createSnapshot`. Call once during application bootstrap (or per-test
27
+ * in deterministic test suites):
28
+ *
29
+ * ```ts
30
+ * import { setClockFactory } from "@shirudo/ddd-kit";
31
+ *
32
+ * setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
33
+ * ```
34
+ *
35
+ * The per-call `options.occurredAt` override always wins over this
36
+ * factory. Symmetric to `setEventIdFactory`.
37
+ *
38
+ * Module-scoped: see `setEventIdFactory` for the global-state
39
+ * caveats. For test isolation prefer {@link withClockFactory}; for
40
+ * multi-tenant request isolation prefer the per-call
41
+ * `options.occurredAt`.
42
+ */
43
+ declare function setClockFactory(factory: ClockFactory): void;
44
+ /**
45
+ * Scoped variant of {@link setClockFactory}: installs `factory`, runs
46
+ * `fn`, then restores the previous factory in a `finally` block.
47
+ * Synchronous-only, with the same constraints (and same runtime thenable
48
+ * guard) as `withEventIdFactory`.
49
+ *
50
+ * **When to prefer the per-call `options.occurredAt` instead.** Same
51
+ * trade-off as `withEventIdFactory`: passing `{ occurredAt }`
52
+ * to `createDomainEvent` is the strongest isolation for single-event
53
+ * cases. The scoped helper is for events constructed deep inside
54
+ * domain methods where threading an explicit timestamp is awkward.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * it("stamps events with a fixed clock", () => {
59
+ * const fixed = new Date("2026-01-01T00:00:00Z");
60
+ * withClockFactory(() => fixed, () => {
61
+ * const e = createDomainEvent("X", { v: 1 });
62
+ * expect(e.occurredAt).toEqual(fixed);
63
+ * });
64
+ * });
65
+ * ```
66
+ */
67
+ declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
68
+ /**
69
+ * Restores the default clock factory (`() => new Date()`).
70
+ * Intended for use in test `afterEach` hooks.
71
+ */
72
+ declare function resetClockFactory(): void;
73
+
8
74
  /**
9
75
  * Entity utilities and interfaces for Domain-Driven Design.
10
76
  *
@@ -18,32 +84,34 @@ export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit
18
84
  *
19
85
  * 2. **Child Entities**: Entities within an aggregate.
20
86
  * - Have identity (id) and state, but no own version
21
- * - Can extend `EntityBase<TState, TId>` for class-based entities
87
+ * - Can extend `Entity<TState, TId>` for class-based entities
22
88
  * - Or use functional style with `Identifiable<TId> & TProps`
23
89
  * - Exist only within the aggregate boundary
24
90
  * - Versioned through the Aggregate Root
25
91
  * - Cannot be referenced directly from outside the aggregate
26
92
  *
27
93
  * This module provides:
28
- * - `EntityBase<TState, TId>` - Base class for entities with state
29
- * - `Entity<TId>` - Simple class for entities without state management
94
+ * - `Entity<TState, TId>` - Base class for entities with state
95
+ * - `EntityConfig` - Construction options (opt-in deep freeze)
30
96
  * - `Identifiable<TId>` - Minimal interface for objects with id
31
97
  * - Helper functions for working with collections of entities
32
98
  *
33
99
  * @example
34
100
  * ```typescript
35
101
  * // Class-based child entity with logic
36
- * class OrderItem extends EntityBase<OrderItemState, ItemId> {
102
+ * class OrderItem extends Entity<OrderItemState, ItemId> {
37
103
  * constructor(id: ItemId, initialState: OrderItemState) {
38
104
  * super(id, initialState);
39
105
  * }
40
106
  *
41
107
  * updateQuantity(quantity: number): void {
42
- * this._state = { ...this._state, quantity };
108
+ * // setState runs validateState and re-freezes; a direct
109
+ * // `this._state = ...` assignment would skip both.
110
+ * this.setState({ ...this.state, quantity });
43
111
  * }
44
112
  *
45
113
  * calculateSubtotal(): number {
46
- * return this._state.price * this._state.quantity;
114
+ * return this.state.price * this.state.quantity;
47
115
  * }
48
116
  * }
49
117
  *
@@ -62,6 +130,31 @@ export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit
62
130
  * ```
63
131
  */
64
132
 
133
+ /**
134
+ * Construction options shared by `Entity` and (via `AggregateConfig`) the
135
+ * aggregate base classes.
136
+ */
137
+ interface EntityConfig {
138
+ /**
139
+ * Opt-in: freeze the WHOLE state graph (via `deepFreeze`) instead of
140
+ * the default shallow freeze, so nested outside writes
141
+ * (`entity.state.items.push(x)`) throw instead of silently bypassing
142
+ * `validateState`, the version bump, and the `changedKeys` dirty diff.
143
+ *
144
+ * Defaults to `false` (the documented shallow contract): deep freezing
145
+ * costs a full state-graph walk on every state write, which is why it
146
+ * is not the default on hot paths.
147
+ *
148
+ * **Only for plain-data states.** The deep freeze walks the entire
149
+ * graph: a class-based child entity inside the state would be frozen
150
+ * too, and its own mutation methods would start throwing. States
151
+ * carrying class-based children must keep the default shallow freeze.
152
+ * Note that the ownership transfer widens accordingly: nested objects
153
+ * passed into the constructor or `setState` are frozen IN PLACE (the
154
+ * shallow copy protects only the top-level input object).
155
+ */
156
+ deepFreezeState?: boolean;
157
+ }
65
158
  /**
66
159
  * Functional definition of an Entity via its capability: an object is
67
160
  * identifiable if it has an `id`.
@@ -122,7 +215,9 @@ interface IEntity<TId extends Id<string>, TState> extends Identifiable<TId> {
122
215
  * }
123
216
  *
124
217
  * updateQuantity(quantity: number): void {
125
- * this._state = { ...this._state, quantity };
218
+ * // setState runs validateState and re-freezes; a direct
219
+ * // `this._state = ...` assignment would skip both.
220
+ * this.setState({ ...this.state, quantity });
126
221
  * }
127
222
  * }
128
223
  * ```
@@ -132,30 +227,46 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
132
227
  /**
133
228
  * Returns the current state of the entity.
134
229
  *
135
- * The state object is **shallowly frozen**: direct property writes
136
- * (`entity.state.foo = …`) throw in strict mode, but writes to nested
137
- * objects (`entity.state.address.zip = …`) bypass the freeze. For deep
138
- * immutability either model nested data with `vo()` (which freezes
139
- * deeply) or reach for a structural-sharing library like Immer at the
140
- * App layer. The shallow contract is intentional: deep freezing on
141
- * every state write is too expensive for hot paths, and DDD aggregates
142
- * normally treat their own state as private (`Tell, Don't Ask`).
230
+ * By default the state object is **shallowly frozen**: direct property
231
+ * writes (`entity.state.foo = …`) throw in strict mode, but writes to
232
+ * nested objects (`entity.state.address.zip = …`) bypass the freeze.
233
+ * For deep immutability either enable the opt-in
234
+ * {@link EntityConfig.deepFreezeState} (plain-data states only), model
235
+ * nested data with `vo()` (which freezes deeply), or reach for a
236
+ * structural-sharing library like Immer at the App layer. The shallow
237
+ * default is intentional: deep freezing on every state write is too
238
+ * expensive for hot paths, and DDD aggregates normally treat their own
239
+ * state as private (`Tell, Don't Ask`).
143
240
  */
144
241
  get state(): TState;
145
242
  /**
146
243
  * The state is 'protected' so that only the subclass can modify it.
147
- * Subclasses can mutate this directly or use helper methods.
244
+ * Subclasses can mutate this directly or use helper methods; direct
245
+ * assignments should freeze through {@link freezeState} so the
246
+ * configured freeze mode is honored.
148
247
  */
149
248
  protected _state: TState;
249
+ private readonly _deepFreezeState;
150
250
  /**
151
251
  * **State ownership.** Plain-object and array states are shallow-copied
152
252
  * before the freeze, so the caller's own object stays mutable. A CLASS
153
253
  * INSTANCE passed as state is an ownership transfer: it is frozen
154
254
  * in place (a copy would strip its prototype). Do not keep mutating
155
255
  * the instance after handing it to the entity. The same contract
156
- * applies to {@link setState}.
256
+ * applies to {@link setState}. With
257
+ * {@link EntityConfig.deepFreezeState} enabled, the ownership transfer
258
+ * widens to the whole graph: NESTED objects are frozen in place too.
259
+ */
260
+ protected constructor(id: TId, initialState: TState, config?: EntityConfig);
261
+ /**
262
+ * Freezes a state value according to this entity's configured freeze
263
+ * mode: the default shallow freeze, or `deepFreeze` when
264
+ * {@link EntityConfig.deepFreezeState} was enabled at construction.
265
+ * Subclass code that assigns `this._state` directly (bypassing
266
+ * `setState`) must freeze through this method, not `freezeShallow`,
267
+ * or the opt-in silently degrades to shallow for that path.
157
268
  */
158
- protected constructor(id: TId, initialState: TState);
269
+ protected freezeState(value: TState): TState;
159
270
  /**
160
271
  * Optional validation hook to ensure state invariants. Called during
161
272
  * construction (from `Entity`'s constructor) and again on every
@@ -194,13 +305,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
194
305
  /**
195
306
  * Shallow-freezes `value` when it's a non-null object or array, so that
196
307
  * direct property writes throw in strict mode. Returns the value as-is for
197
- * primitives. Used internally by `Entity` and its subclasses to prevent
198
- * outside mutation of state read through the `state` getter without paying
199
- * the cost of a deep clone on every read.
200
- *
201
- * Exported so that sibling classes (`EventSourcedAggregate`, `AggregateRoot`)
202
- * can apply the same freeze when they bypass `setState` and assign
203
- * `this._state` directly.
308
+ * primitives. Used internally by `Entity` (via `freezeState`, which picks
309
+ * shallow or deep per the `deepFreezeState` config) to prevent outside
310
+ * mutation of state read through the `state` getter without paying the
311
+ * cost of a deep clone on every read.
312
+ *
313
+ * Subclass code that assigns `this._state` directly should freeze through
314
+ * the protected `freezeState(value)` method rather than calling this
315
+ * helper, so the configured freeze mode is honored. The export remains
316
+ * for consumers using it as a standalone utility.
204
317
  */
205
318
  declare function freezeShallow<T>(value: T): T;
206
319
  /**
@@ -536,8 +649,46 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
536
649
  *
537
650
  * The state is converted via {@link toSnapshotState}; the default
538
651
  * requires plain, serialisable data and fails fast otherwise.
652
+ *
653
+ * `snapshotAt` is read from the kit's swappable clock, the same one
654
+ * `createDomainEvent` stamps `occurredAt` from, so
655
+ * `setClockFactory` / `withClockFactory` pin snapshot timestamps in
656
+ * deterministic tests too. `schemaVersion` is stamped from
657
+ * {@link snapshotSchemaVersion} so a later restore can detect
658
+ * snapshots written against an older `TSnapshotState` shape.
539
659
  */
540
660
  createSnapshot(): AggregateSnapshot<TSnapshotState>;
661
+ /**
662
+ * Schema version of the shape {@link toSnapshotState} produces.
663
+ * Defaults to `1`. Bump it whenever `TSnapshotState` changes
664
+ * incompatibly (renamed or removed fields, changed representations):
665
+ * `createSnapshot` stamps it onto every snapshot, and the restore
666
+ * paths compare it, so an outdated stored snapshot surfaces as a
667
+ * `SnapshotSchemaMismatchError` at restore time (or is upgraded via
668
+ * {@link migrateSnapshotState}) instead of crashing on the first
669
+ * method call much later.
670
+ */
671
+ protected readonly snapshotSchemaVersion: number;
672
+ /**
673
+ * Resolves a stored snapshot's state against the aggregate's current
674
+ * snapshot schema: pass-through when the versions match (a missing
675
+ * `schemaVersion` counts as `1`, the pre-versioning era), otherwise
676
+ * routed through {@link migrateSnapshotState}. Called by both restore
677
+ * paths BEFORE anything is assigned, so a rejected snapshot leaves
678
+ * the aggregate untouched.
679
+ */
680
+ protected resolveSnapshotState(snapshot: AggregateSnapshot<TSnapshotState>): TSnapshotState;
681
+ /**
682
+ * Upgrade hook for snapshots written against an older
683
+ * `TSnapshotState` shape. Receives the stored state as `unknown`
684
+ * (its shape is, by definition, not the current `TSnapshotState`)
685
+ * plus the schema version it was written with, and returns the
686
+ * current shape. The default rejects with
687
+ * `SnapshotSchemaMismatchError`: discard-and-refold from the full
688
+ * event stream is the safe default strategy; override this only when
689
+ * upgrading in place is cheaper than refolding.
690
+ */
691
+ protected migrateSnapshotState(_stored: unknown, storedSchemaVersion: number): TSnapshotState;
541
692
  /**
542
693
  * Converts live aggregate state into the plain-data shape stored in a
543
694
  * snapshot. The default validates that the state graph is plain,
@@ -600,18 +751,33 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
600
751
  }
601
752
 
602
753
  /**
603
- * Configuration options for AggregateRoot behavior.
754
+ * Configuration options for AggregateRoot behavior. Inherits
755
+ * `deepFreezeState` from {@link EntityConfig} (opt-in deep freeze for
756
+ * plain-data states).
604
757
  */
605
- interface AggregateConfig {
758
+ interface AggregateConfig extends EntityConfig {
606
759
  /**
607
760
  * Whether `setState()` should bump the version automatically when the
608
761
  * caller omits the per-call `bumpVersion` argument.
609
762
  *
610
763
  * Defaults to **`false`**: `setState()` already takes an explicit
611
764
  * `bumpVersion` argument per call, so the config is just the default
612
- * the per-call argument falls back to. Set to `true` only if you have
613
- * a subclass that never passes `bumpVersion` and you want every state
614
- * change to advance the version anyway.
765
+ * the per-call argument falls back to.
766
+ *
767
+ * **OCC warning: an un-bumped mutation can be silently overwritten.**
768
+ * A save whose version did not move writes `WHERE version = v SET
769
+ * version = v`; a concurrent writer that loaded the same `v` then
770
+ * commits `WHERE version = v` successfully and replaces the state
771
+ * without any `ConcurrencyConflictError`. Skip the bump only for data
772
+ * whose loss under a concurrent write is acceptable (cosmetic caches,
773
+ * denormalized display fields), never for domain state.
774
+ *
775
+ * **Deprecation notice.** Relying on the implicit default (calling
776
+ * `setState(newState)` without a per-call `bumpVersion` and without
777
+ * setting this config) is deprecated. v3 makes the per-call argument
778
+ * required, so every mutation states its OCC intent explicitly at the
779
+ * call site; code that already passes `bumpVersion` or uses `commit()`
780
+ * is unaffected.
615
781
  */
616
782
  autoVersionBump?: boolean;
617
783
  }
@@ -810,6 +976,14 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
810
976
  * Sets the state and optionally bumps the version automatically.
811
977
  * Validates `newState` via `validateState()`.
812
978
  *
979
+ * **State the OCC intent explicitly.** With neither the per-call
980
+ * `bumpVersion` argument nor an `autoVersionBump` config, the version
981
+ * does not move, and an un-bumped mutation can be silently overwritten
982
+ * by a concurrent writer (see the {@link AggregateConfig.autoVersionBump}
983
+ * warning for the mechanics). Prefer `commit()` for domain mutations,
984
+ * or pass `bumpVersion` explicitly; relying on the implicit default is
985
+ * deprecated and v3 makes the argument required.
986
+ *
813
987
  * @param newState - The new state
814
988
  * @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
815
989
  */
@@ -927,9 +1101,21 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
927
1101
  * which never bumps it; only the final `markRestored` advances it.)
928
1102
  *
929
1103
  * Version advances additively: the aggregate's pre-existing version plus
930
- * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
931
- * an aggregate already at v=1 (e.g. after a creation event) loading
932
- * 2 events ends at v=3, not v=2.
1104
+ * `history.length`. A fresh aggregate (v=0) loading 3 events ends at
1105
+ * v=3; a PERSISTED aggregate at v=P (`persistedVersion === P`) catching
1106
+ * up on M newer events ends at v=P+M.
1107
+ *
1108
+ * **The replay target must be fresh or persisted.** An aggregate with
1109
+ * unflushed `pendingEvents`, or with an in-memory version that was
1110
+ * never persisted (a factory-created instance), throws
1111
+ * {@link UnreplayableAggregateError} BEFORE anything moves: replaying
1112
+ * onto it would `markRestored` a `persistedVersion` that counts
1113
+ * unpersisted history, flipping repository routing from INSERT to
1114
+ * UPDATE (or appending with a wrong expected version). The throw is
1115
+ * deliberate (crash-loud programming bug), never a `Result` `Err`, and
1116
+ * runs before the empty-history fast path so the misuse is caught
1117
+ * deterministically rather than only when the stream happens to be
1118
+ * non-empty.
933
1119
  */
934
1120
  loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
935
1121
  /**
@@ -941,8 +1127,26 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
941
1127
  * All-or-nothing: if any event mid-stream throws a `DomainError`, the
942
1128
  * aggregate is rolled back to its pre-call state + version. Partial
943
1129
  * restoration is never observable to the caller.
1130
+ *
1131
+ * **The restore target must not carry pending events.** Events recorded
1132
+ * before the restore are unrelated to the restored stream; harvesting
1133
+ * them after `markRestored` would emit them with a version baseline
1134
+ * they were never part of. Such a target throws
1135
+ * {@link UnreplayableAggregateError} before anything moves (crash-loud
1136
+ * programming bug, never a `Result` `Err`). Unlike `loadFromHistory`,
1137
+ * a never-persisted in-memory version is fine here: the snapshot
1138
+ * overwrites state and version entirely instead of adding to them.
944
1139
  */
945
1140
  restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TSnapshotState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
1141
+ /**
1142
+ * Replay-target freshness guard shared by `loadFromHistory` and
1143
+ * `restoreFromSnapshotWithEvents`. See {@link UnreplayableAggregateError}
1144
+ * for the desync both conditions prevent. `checkUnpersistedVersion` is
1145
+ * `true` for the additive `loadFromHistory` path only: the snapshot
1146
+ * restore overwrites version wholesale, so a never-persisted in-memory
1147
+ * version is harmless there.
1148
+ */
1149
+ private assertReplayTargetFresh;
946
1150
  /**
947
1151
  * A map of event types to their corresponding handlers.
948
1152
  * Subclasses MUST implement this property.
@@ -1006,6 +1210,8 @@ interface Command {
1006
1210
  *
1007
1211
  * @template C - The command type (must extend Command)
1008
1212
  * @template R - The result type
1213
+ * @template E - The error channel type. Defaults to `string`; widen it (e.g.
1214
+ * to a `DomainError` union) to carry typed failures through the bus.
1009
1215
  *
1010
1216
  * @example
1011
1217
  * ```typescript
@@ -1040,7 +1246,7 @@ interface Command {
1040
1246
  * });
1041
1247
  * ```
1042
1248
  */
1043
- type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string>>;
1249
+ type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Result<R, E>>;
1044
1250
 
1045
1251
  /**
1046
1252
  * Type map for command types to their return types.
@@ -1059,6 +1265,30 @@ type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string
1059
1265
  * ```
1060
1266
  */
1061
1267
  type CommandTypeMap = Record<string, unknown>;
1268
+ /**
1269
+ * Construction options for {@link CommandBus}.
1270
+ *
1271
+ * @template E - The error channel type of the bus.
1272
+ */
1273
+ interface CommandBusOptions<E = string> {
1274
+ /**
1275
+ * Maps a thrown value (a handler that throws, or dispatch to an
1276
+ * unregistered command type) into the bus's error channel `E`. Defaults to
1277
+ * {@link describeThrown}, which renders any thrown value as a `string`.
1278
+ * base-error's `toStructuredError` fits this slot directly when `E` is a
1279
+ * `StructuredError`.
1280
+ */
1281
+ errorMapper?: (thrown: unknown) => E;
1282
+ }
1283
+ /**
1284
+ * Constructor arguments for {@link CommandBus}. When `E` is the default
1285
+ * `string`, options are optional (the built-in {@link describeThrown} mapper
1286
+ * applies). When `E` is widened, an `errorMapper` is required, so a typed
1287
+ * channel can never silently fall back to string values.
1288
+ */
1289
+ type CommandBusArgs<E> = [E] extends [string] ? [options?: CommandBusOptions<E>] : [options: CommandBusOptions<E> & {
1290
+ errorMapper: (thrown: unknown) => E;
1291
+ }];
1062
1292
  /**
1063
1293
  * Command Bus interface for dispatching commands to their handlers.
1064
1294
  * Provides a centralized way to execute commands with handler registration.
@@ -1083,18 +1313,18 @@ type CommandTypeMap = Record<string, unknown>;
1083
1313
  * // result: Result<unknown, string>
1084
1314
  * ```
1085
1315
  */
1086
- interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1316
+ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap, E = string> {
1087
1317
  /**
1088
1318
  * Executes a command by dispatching it to the registered handler.
1089
1319
  * When a type map is provided, the return type is inferred from the command type.
1090
1320
  *
1091
1321
  * @param command - The command to execute
1092
- * @returns Result containing the success value or error message
1322
+ * @returns Result containing the success value or an error of type `E`
1093
1323
  */
1094
1324
  execute<C extends Command & {
1095
1325
  type: keyof TMap & string;
1096
- }>(command: C): Promise<Result<TMap[C["type"]], string>>;
1097
- execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
1326
+ }>(command: C): Promise<Result<TMap[C["type"]], E>>;
1327
+ execute<C extends Command, R>(command: C): Promise<Result<R, E>>;
1098
1328
  /**
1099
1329
  * Registers a handler for a specific command type.
1100
1330
  *
@@ -1111,7 +1341,7 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1111
1341
  type: K;
1112
1342
  } = Command & {
1113
1343
  type: K;
1114
- }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
1344
+ }>(commandType: K, handler: CommandHandler<C, TMap[K], E>): void;
1115
1345
  }
1116
1346
  /**
1117
1347
  * Simple in-memory command bus implementation.
@@ -1149,17 +1379,19 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1149
1379
  * const result = await bus.execute({ type: "CreateOrder", ... });
1150
1380
  * ```
1151
1381
  */
1152
- declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
1382
+ declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap, E = string> implements ICommandBus<TMap, E> {
1153
1383
  private readonly handlers;
1384
+ private readonly errorMapper;
1385
+ constructor(...args: CommandBusArgs<E>);
1154
1386
  register<K extends keyof TMap & string, C extends Command & {
1155
1387
  type: K;
1156
1388
  } = Command & {
1157
1389
  type: K;
1158
- }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
1390
+ }>(commandType: K, handler: CommandHandler<C, TMap[K], E>): void;
1159
1391
  execute<C extends Command & {
1160
1392
  type: keyof TMap & string;
1161
- }>(command: C): Promise<Result<TMap[C["type"]], string>>;
1162
- execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
1393
+ }>(command: C): Promise<Result<TMap[C["type"]], E>>;
1394
+ execute<C extends Command, R>(command: C): Promise<Result<R, E>>;
1163
1395
  }
1164
1396
 
1165
1397
  /**
@@ -1287,6 +1519,21 @@ interface OnceOptions {
1287
1519
  interface OutboxRecord<Evt extends AnyDomainEvent> {
1288
1520
  dispatchId: string;
1289
1521
  event: Evt;
1522
+ /**
1523
+ * Failed delivery attempts so far. Populated by implementations that
1524
+ * track dispatch failures (see {@link DispatchTrackingOutbox});
1525
+ * plain `Outbox` implementations may omit it.
1526
+ */
1527
+ attempts?: number;
1528
+ }
1529
+ /** A record that exhausted its delivery attempts; see {@link DispatchTrackingOutbox.deadLetters}. */
1530
+ interface DeadLetterRecord<Evt extends AnyDomainEvent> {
1531
+ dispatchId: string;
1532
+ event: Evt;
1533
+ /** Failed delivery attempts when the record was dead-lettered. */
1534
+ attempts: number;
1535
+ /** Human-readable rendering of the last delivery error, if recorded. */
1536
+ lastError?: string;
1290
1537
  }
1291
1538
  /**
1292
1539
  * Transactional outbox port: the bridge between the write-side
@@ -1319,8 +1566,15 @@ interface Outbox<Evt extends AnyDomainEvent> {
1319
1566
  add: (events: ReadonlyArray<Evt>) => Promise<void>;
1320
1567
  /**
1321
1568
  * Returns up to `limit` outbox records that have not yet been
1322
- * dispatched. The dispatcher polls this on a schedule. When `limit`
1323
- * is omitted, the implementation decides on a default page size.
1569
+ * dispatched, **in the order `add()` persisted them** (commit order).
1570
+ * The ordering is part of the port contract: `withCommit` promises
1571
+ * subscribers per-aggregate causal order, and a sequential dispatcher
1572
+ * can only honor that promise when this read is ordered. SQL-backed
1573
+ * implementations need a monotonic position column (an auto-increment
1574
+ * primary key works) and an `ORDER BY` on it; a bare `SELECT` returns
1575
+ * rows in storage order, not insertion order. The dispatcher polls
1576
+ * this on a schedule. When `limit` is omitted, the implementation
1577
+ * decides on a default page size.
1324
1578
  */
1325
1579
  getPending: (limit?: number) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
1326
1580
  /**
@@ -1330,6 +1584,39 @@ interface Outbox<Evt extends AnyDomainEvent> {
1330
1584
  */
1331
1585
  markDispatched: (dispatchIds: ReadonlyArray<string>) => Promise<void>;
1332
1586
  }
1587
+ /**
1588
+ * Optional extension of {@link Outbox} for dispatchers that track
1589
+ * delivery failures. Without failure tracking, a poison message (an
1590
+ * event whose delivery always throws) is redelivered forever: it comes
1591
+ * back from every `getPending` poll, blocks per-aggregate ordering
1592
+ * behind it, and burns the dispatcher's cycles. This extension gives
1593
+ * the dispatcher a bounded-retry story: report each failed delivery via
1594
+ * {@link markFailed}; the implementation moves records past its
1595
+ * attempt ceiling to a dead-letter set that `getPending` no longer
1596
+ * returns, and {@link deadLetters} exposes them for alerting, manual
1597
+ * inspection, and redelivery (deliver by hand, then ack via
1598
+ * `markDispatched`, which also clears dead-lettered records).
1599
+ *
1600
+ * See the outbox guide's dispatcher recipe for the retry-then-dead-letter
1601
+ * loop this port shape supports.
1602
+ */
1603
+ interface DispatchTrackingOutbox<Evt extends AnyDomainEvent> extends Outbox<Evt> {
1604
+ /**
1605
+ * Records one failed delivery attempt for the given record:
1606
+ * increments its attempt count (surfaced as
1607
+ * {@link OutboxRecord.attempts}) and, once the implementation's
1608
+ * ceiling is reached, moves the record to the dead-letter set.
1609
+ * A no-op for unknown or already-dispatched ids (a late failure
1610
+ * report after a successful retry must not resurrect the record).
1611
+ */
1612
+ markFailed: (dispatchId: string, error?: unknown) => Promise<void>;
1613
+ /**
1614
+ * Records that exhausted their delivery attempts. They no longer
1615
+ * come back from `getPending`; wire this to alerting so poison
1616
+ * messages surface instead of rotting silently.
1617
+ */
1618
+ deadLetters: () => Promise<ReadonlyArray<DeadLetterRecord<Evt>>>;
1619
+ }
1333
1620
 
1334
1621
  /**
1335
1622
  * Transaction-scope abstraction.
@@ -1424,7 +1711,9 @@ interface TransactionScope<TCtx> {
1424
1711
  * events were recorded. Each harvested event is stamped with
1425
1712
  * `aggregateVersion` = the aggregate's commit version (onto a frozen
1426
1713
  * copy; a pre-set value wins) - consumers get the OCC version the
1427
- * row was written with, for ordering and idempotency watermarks.
1714
+ * row was written with, for cross-commit ordering and debugging.
1715
+ * All events of one commit share the stamp, so per-event dedup
1716
+ * belongs on `eventId`, not on this value (see the outbox guide).
1428
1717
  *
1429
1718
  * **Harvest order.** Events are concatenated in the order
1430
1719
  * aggregates appear in the returned `aggregates` array, then in
@@ -1525,6 +1814,18 @@ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
1525
1814
  * for delivery: the outbox dispatcher is the reliable path.
1526
1815
  */
1527
1816
  onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
1817
+ /**
1818
+ * Observer for post-commit persistence-cleanup failures: a throw from
1819
+ * `markPersisted`, the user-overridable `onPersisted` hook, or
1820
+ * `clearPendingEvents`. Called once per failing aggregate with the
1821
+ * error and that aggregate. Symmetric with {@link onPublishError}: the
1822
+ * transaction has already committed, so the failure must NOT reject the
1823
+ * write; without this observer it would otherwise vanish silently. The
1824
+ * hook is an observer only: if it throws, its error is swallowed so the
1825
+ * post-commit invariant holds, and the loop continues marking the
1826
+ * remaining aggregates.
1827
+ */
1828
+ onPersistError?: (error: unknown, aggregate: IAggregateRoot<Id<string>, Evt>) => void;
1528
1829
  /**
1529
1830
  * Cooperative-cancellation signal. If already aborted, `withCommit`
1530
1831
  * rejects with the signal's `reason` BEFORE opening the transaction.
@@ -1625,6 +1926,167 @@ interface Query {
1625
1926
  */
1626
1927
  type QueryHandler<Q extends Query, R> = (query: Q) => Promise<R>;
1627
1928
 
1929
+ /**
1930
+ * Type map for query types to their return types.
1931
+ * Used to improve type inference in QueryBus.
1932
+ *
1933
+ * @example
1934
+ * ```typescript
1935
+ * type MyQueryMap = {
1936
+ * GetOrder: Order | null;
1937
+ * ListOrders: Order[];
1938
+ * };
1939
+ *
1940
+ * const bus = new QueryBus<MyQueryMap>();
1941
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1942
+ * // result: Result<Order | null, string> ← automatically inferred
1943
+ * ```
1944
+ */
1945
+ type QueryTypeMap = Record<string, unknown>;
1946
+ /**
1947
+ * Construction options for {@link QueryBus}.
1948
+ *
1949
+ * @template E - The error channel type of the bus.
1950
+ */
1951
+ interface QueryBusOptions<E = string> {
1952
+ /**
1953
+ * Maps a thrown value (a handler that throws, or dispatch to an
1954
+ * unregistered query type) into the bus's error channel `E`. Defaults to
1955
+ * {@link describeThrown}, which renders any thrown value as a `string`.
1956
+ * base-error's `toStructuredError` fits this slot directly when `E` is a
1957
+ * `StructuredError`.
1958
+ */
1959
+ errorMapper?: (thrown: unknown) => E;
1960
+ }
1961
+ /**
1962
+ * Constructor arguments for {@link QueryBus}. When `E` is the default `string`,
1963
+ * options are optional (the built-in {@link describeThrown} mapper applies).
1964
+ * When `E` is widened, an `errorMapper` is required, so a typed channel can
1965
+ * never silently fall back to string values.
1966
+ */
1967
+ type QueryBusArgs<E> = [E] extends [string] ? [options?: QueryBusOptions<E>] : [options: QueryBusOptions<E> & {
1968
+ errorMapper: (thrown: unknown) => E;
1969
+ }];
1970
+ /**
1971
+ * Query Bus interface for dispatching queries to their handlers.
1972
+ * Provides a centralized way to execute queries with handler registration.
1973
+ *
1974
+ * Supports an optional type map (`TMap`) for automatic return type inference.
1975
+ * Without a type map, the return type must be specified manually or defaults to `unknown`.
1976
+ *
1977
+ * @template TMap - Optional mapping from query type strings to return types
1978
+ *
1979
+ * @example
1980
+ * ```typescript
1981
+ * // With type map (recommended) – return type is inferred
1982
+ * type MyQueries = { GetOrder: Order | null; ListOrders: Order[] };
1983
+ * const bus = new QueryBus<MyQueries>();
1984
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1985
+ * // result: Result<Order | null, string>
1986
+ *
1987
+ * // Without type map – works like before
1988
+ * const bus = new QueryBus();
1989
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
1990
+ * // result: Result<unknown, string>
1991
+ * ```
1992
+ */
1993
+ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap, E = string> {
1994
+ /**
1995
+ * Executes a query by dispatching it to the registered handler.
1996
+ * When a type map is provided, the return type is inferred from the query type.
1997
+ *
1998
+ * @param query - The query to execute
1999
+ * @returns Result containing the query result if successful, or an error message
2000
+ */
2001
+ execute<Q extends Query & {
2002
+ type: keyof TMap & string;
2003
+ }>(query: Q): Promise<Result<TMap[Q["type"]], E>>;
2004
+ execute<Q extends Query, R>(query: Q): Promise<Result<R, E>>;
2005
+ /**
2006
+ * Executes a query by dispatching it to the registered handler.
2007
+ * Throws an error if no handler is registered.
2008
+ *
2009
+ * @param query - The query to execute
2010
+ * @returns The query result
2011
+ * @throws Error if no handler is registered for the query type
2012
+ */
2013
+ executeUnsafe<Q extends Query & {
2014
+ type: keyof TMap & string;
2015
+ }>(query: Q): Promise<TMap[Q["type"]]>;
2016
+ executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
2017
+ /**
2018
+ * Registers a handler for a specific query type.
2019
+ *
2020
+ * When `TMap` is supplied, the `queryType` argument is restricted to its
2021
+ * keys and the handler signature is forced to match `TMap[K]` for the
2022
+ * return value: typos and wrong-typed handlers are compile errors.
2023
+ * Without `TMap` the registration is loose (any string key, any return
2024
+ * type) so the no-config path keeps working.
2025
+ *
2026
+ * @param queryType - The query type to register the handler for
2027
+ * @param handler - The handler function for this query type
2028
+ */
2029
+ register<K extends keyof TMap & string, Q extends Query & {
2030
+ type: K;
2031
+ } = Query & {
2032
+ type: K;
2033
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
2034
+ }
2035
+ /**
2036
+ * Simple in-memory query bus implementation.
2037
+ * Handlers are stored in a Map and dispatched based on query type.
2038
+ *
2039
+ * Supports an optional type map (`TMap`) for automatic return type inference.
2040
+ * When `TMap` is provided, `execute()` and `executeUnsafe()` infer the result type from the query type.
2041
+ * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
2042
+ *
2043
+ * **Note:** This is a basic implementation suitable for development and simple use cases.
2044
+ * For production environments, consider implementing or using a more feature-rich bus that includes:
2045
+ * - Middleware/Pipeline support (logging, caching, rate limiting)
2046
+ * - Error handling
2047
+ * - Timeout handling
2048
+ * - Metrics and observability
2049
+ * - Query result caching
2050
+ * - Rate limiting
2051
+ *
2052
+ * The `QueryHandler` type can still be used with external production-grade buses
2053
+ * (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
2054
+ *
2055
+ * @template TMap - Optional mapping from query type strings to return types
2056
+ *
2057
+ * @example
2058
+ * ```typescript
2059
+ * // With type map – full inference
2060
+ * type Queries = { GetOrder: Order | null; ListOrders: Order[] };
2061
+ * const bus = new QueryBus<Queries>();
2062
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2063
+ * // result: Result<Order | null, string>
2064
+ *
2065
+ * // Without type map – same as before
2066
+ * const bus = new QueryBus();
2067
+ * bus.register("GetOrder", async (query) => repository.getById(query.orderId));
2068
+ * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2069
+ * ```
2070
+ */
2071
+ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap, E = string> implements IQueryBus<TMap, E> {
2072
+ private readonly handlers;
2073
+ private readonly errorMapper;
2074
+ constructor(...args: QueryBusArgs<E>);
2075
+ register<K extends keyof TMap & string, Q extends Query & {
2076
+ type: K;
2077
+ } = Query & {
2078
+ type: K;
2079
+ }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
2080
+ execute<Q extends Query & {
2081
+ type: keyof TMap & string;
2082
+ }>(query: Q): Promise<Result<TMap[Q["type"]], E>>;
2083
+ execute<Q extends Query, R>(query: Q): Promise<Result<R, E>>;
2084
+ executeUnsafe<Q extends Query & {
2085
+ type: keyof TMap & string;
2086
+ }>(query: Q): Promise<TMap[Q["type"]]>;
2087
+ executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
2088
+ }
2089
+
1628
2090
  /**
1629
2091
  * A class reference used as the type key of the identity map. Keying
1630
2092
  * on the CLASS (not a name string) makes collisions impossible by
@@ -1935,6 +2397,13 @@ interface UnitOfWorkDeps<Evt extends AnyDomainEvent, TCtx, TRepos> {
1935
2397
  bus?: EventBus<Evt>;
1936
2398
  /** See `withCommit`: observer for post-commit `bus.publish` failures. */
1937
2399
  onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
2400
+ /**
2401
+ * See `withCommit`: observer for post-commit persistence-cleanup failures
2402
+ * (a throw from `markPersisted`, the `onPersisted` hook, or
2403
+ * `clearPendingEvents`). Called once per failing aggregate; observer only,
2404
+ * never rejects the committed write.
2405
+ */
2406
+ onPersistError?: (error: unknown, aggregate: IAggregateRoot<Id<string>, Evt>) => void;
1938
2407
  repositories: RepositoryFactories<TCtx, TRepos, Evt>;
1939
2408
  }
1940
2409
  /**
@@ -2017,139 +2486,230 @@ declare class UnitOfWork<Evt extends AnyDomainEvent, TCtx, TRepos extends Record
2017
2486
  private buildRepositories;
2018
2487
  }
2019
2488
 
2489
+ /** Base shape for every input accepted by a domain state machine. */
2490
+ type DomainMachineInput = {
2491
+ readonly type: string;
2492
+ };
2493
+ /** Recursively readonly view of data accepted by the domain state machine. */
2494
+ type DomainMachineReadonly<TValue> = TValue extends bigint | boolean | null | number | string | symbol | undefined ? TValue : TValue extends (...args: never[]) => unknown ? TValue : TValue extends readonly unknown[] ? {
2495
+ readonly [TKey in keyof TValue]: DomainMachineReadonly<TValue[TKey]>;
2496
+ } : TValue extends object ? {
2497
+ readonly [TKey in keyof TValue]: DomainMachineReadonly<TValue[TKey]>;
2498
+ } : TValue;
2499
+ /** Immutable state and context value suitable for persistence. */
2500
+ type DomainMachineSnapshot<TState extends string, TContext> = {
2501
+ readonly state: TState;
2502
+ readonly context: DomainMachineReadonly<TContext>;
2503
+ };
2504
+ /** Optional context replacement and requested external work from a reducer. */
2505
+ type DomainTransitionResult<TContext, TOutput> = {
2506
+ readonly context?: TContext;
2507
+ readonly outputs?: readonly TOutput[];
2508
+ };
2509
+ /** A guard either allows, generically rejects, or returns a domain-specific rejection. */
2510
+ type DomainTransitionGuardResult = boolean | DomainError;
2511
+ /** A guarded state change and its optional pure context reducer. */
2512
+ type DomainTransition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput, TSource extends TState = TState> = {
2513
+ readonly target: TState;
2514
+ /**
2515
+ * Must be synchronous, deterministic, and side-effect-free. Return a
2516
+ * `DomainError` to provide a typed rejection that `can()` treats as false and
2517
+ * `dispatch()` throws unchanged.
2518
+ */
2519
+ readonly guard?: (input: {
2520
+ readonly state: TSource;
2521
+ readonly context: DomainMachineReadonly<TContext>;
2522
+ readonly input: DomainMachineReadonly<TInput>;
2523
+ }) => DomainTransitionGuardResult;
2524
+ /** Must be synchronous, deterministic, and side-effect-free. */
2525
+ readonly reduce?: (input: {
2526
+ readonly state: TSource;
2527
+ readonly context: DomainMachineReadonly<TContext>;
2528
+ readonly input: DomainMachineReadonly<TInput>;
2529
+ }) => DomainTransitionResult<TContext, TOutput> | undefined;
2530
+ };
2531
+ /** Configuration for one named control state. */
2532
+ type DomainStateNode<TState extends string, TContext, TInput extends DomainMachineInput, TOutput, TName extends TState = TState> = {
2533
+ readonly terminal?: boolean;
2534
+ /** State-specific context invariant. Must be synchronous, deterministic, and side-effect-free. */
2535
+ readonly validateContext?: (input: {
2536
+ readonly state: TName;
2537
+ readonly context: DomainMachineReadonly<TContext>;
2538
+ }) => boolean;
2539
+ readonly on?: {
2540
+ readonly [TType in TInput["type"]]?: DomainTransition<TState, TContext, Extract<TInput, {
2541
+ readonly type: TType;
2542
+ }>, TOutput, TName>;
2543
+ };
2544
+ };
2545
+ /** Complete, finite domain lifecycle definition. */
2546
+ type DomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> = {
2547
+ readonly initial: TState;
2548
+ readonly initialContext: () => TContext;
2549
+ /** Must be synchronous, deterministic, and side-effect-free. */
2550
+ readonly validateSnapshot?: (snapshot: DomainMachineSnapshot<TState, TContext>) => boolean;
2551
+ readonly states: {
2552
+ readonly [TName in TState]: DomainStateNode<TState, TContext, TInput, TOutput, TName>;
2553
+ };
2554
+ };
2555
+ /** Result of a successful transition. */
2556
+ type DomainTransitionOutcome<TState extends string, TContext, TOutput> = {
2557
+ readonly from: TState;
2558
+ readonly to: TState;
2559
+ readonly snapshot: DomainMachineSnapshot<TState, TContext>;
2560
+ readonly outputs: readonly DomainMachineReadonly<TOutput>[];
2561
+ };
2562
+
2563
+ type DomainMachineDefinitionDiagnostic<TState extends string> = {
2564
+ readonly code: "unreachable-state";
2565
+ readonly state: TState;
2566
+ } | {
2567
+ readonly code: "structural-dead-end";
2568
+ readonly state: TState;
2569
+ } | {
2570
+ readonly code: "no-terminal-path";
2571
+ readonly state: TState;
2572
+ };
2573
+ type DomainMachineTransitionDescription<TState extends string, TInputType extends string> = {
2574
+ readonly state: TState;
2575
+ readonly inputType: TInputType;
2576
+ readonly target: TState;
2577
+ readonly guarded: boolean;
2578
+ };
2579
+ type DomainMachineDefinitionAnalysis<TState extends string, TInputType extends string> = {
2580
+ readonly diagnostics: readonly DomainMachineDefinitionDiagnostic<TState>[];
2581
+ readonly transitions: readonly DomainMachineTransitionDescription<TState, TInputType>[];
2582
+ /** States reachable when every guard is assumed to allow its transition. */
2583
+ readonly structurallyReachableStates: readonly TState[];
2584
+ /** States with a graph path to a terminal state when every guard is assumed to allow it. */
2585
+ readonly statesWithTerminalPath: readonly TState[];
2586
+ };
2020
2587
  /**
2021
- * Type map for query types to their return types.
2022
- * Used to improve type inference in QueryBus.
2023
- *
2024
- * @example
2025
- * ```typescript
2026
- * type MyQueryMap = {
2027
- * GetOrder: Order | null;
2028
- * ListOrders: Order[];
2029
- * };
2030
- *
2031
- * const bus = new QueryBus<MyQueryMap>();
2032
- * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2033
- * // result: Result<Order | null, string> ← automatically inferred
2034
- * ```
2588
+ * Inspects the declarative transition graph without executing definition callbacks.
2589
+ * Guarded edges are treated as possible edges, so diagnostics never claim more
2590
+ * runtime reachability than the static graph can prove.
2035
2591
  */
2036
- type QueryTypeMap = Record<string, unknown>;
2592
+ declare function analyzeDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): DomainMachineDefinitionAnalysis<TState, TInput["type"]>;
2593
+
2594
+ declare const preparedDefinitionBrand: unique symbol;
2037
2595
  /**
2038
- * Query Bus interface for dispatching queries to their handlers.
2039
- * Provides a centralized way to execute queries with handler registration.
2040
- *
2041
- * Supports an optional type map (`TMap`) for automatic return type inference.
2042
- * Without a type map, the return type must be specified manually or defaults to `unknown`.
2043
- *
2044
- * @template TMap - Optional mapping from query type strings to return types
2045
- *
2046
- * @example
2047
- * ```typescript
2048
- * // With type map (recommended) – return type is inferred
2049
- * type MyQueries = { GetOrder: Order | null; ListOrders: Order[] };
2050
- * const bus = new QueryBus<MyQueries>();
2051
- * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2052
- * // result: Result<Order | null, string>
2596
+ * A machine definition that `prepareDomainMachineDefinition` has
2597
+ * validated, defensively copied, and deeply frozen. Assignable wherever
2598
+ * a plain `DomainMachineDefinition` is accepted; the reverse does NOT
2599
+ * hold (the brand is required), so an API that demands a prepared
2600
+ * definition rejects raw ones at compile time. The pure functions and
2601
+ * the `DomainStateMachine` constructor recognize prepared definitions
2602
+ * at runtime and skip their per-call validate-and-copy.
2603
+ */
2604
+ type PreparedDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> = DomainMachineDefinition<TState, TContext, TInput, TOutput> & {
2605
+ readonly [preparedDefinitionBrand]: true;
2606
+ };
2607
+ /**
2608
+ * Validates and stabilizes a machine definition ONCE, for repeated use
2609
+ * with the pure functions. Without it, `transitionDomainState` and
2610
+ * `canTransitionDomainState` re-validate and defensively re-copy the
2611
+ * WHOLE definition on every call (the documented safety of the raw
2612
+ * path); on a hot dispatch path that is avoidable O(definition) work.
2613
+ * The `DomainStateMachine` class does the equivalent once in its
2614
+ * constructor; this export brings the same amortization to pure-API
2615
+ * users:
2053
2616
  *
2054
- * // Without type map – works like before
2055
- * const bus = new QueryBus();
2056
- * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2057
- * // result: Result<unknown, string>
2617
+ * ```ts
2618
+ * const prepared = prepareDomainMachineDefinition(orderLifecycle);
2619
+ * // per dispatch: no re-validation, no definition copy
2620
+ * const outcome = transitionDomainState(prepared, snapshot, input);
2058
2621
  * ```
2622
+ *
2623
+ * The returned definition is a deeply frozen copy, isolated from later
2624
+ * mutation of the input object. Preparing an already-prepared
2625
+ * definition returns it unchanged.
2059
2626
  */
2060
- interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
2061
- /**
2062
- * Executes a query by dispatching it to the registered handler.
2063
- * When a type map is provided, the return type is inferred from the query type.
2064
- *
2065
- * @param query - The query to execute
2066
- * @returns Result containing the query result if successful, or an error message
2067
- */
2068
- execute<Q extends Query & {
2069
- type: keyof TMap & string;
2070
- }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
2071
- execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
2072
- /**
2073
- * Executes a query by dispatching it to the registered handler.
2074
- * Throws an error if no handler is registered.
2075
- *
2076
- * @param query - The query to execute
2077
- * @returns The query result
2078
- * @throws Error if no handler is registered for the query type
2079
- */
2080
- executeUnsafe<Q extends Query & {
2081
- type: keyof TMap & string;
2082
- }>(query: Q): Promise<TMap[Q["type"]]>;
2083
- executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
2084
- /**
2085
- * Registers a handler for a specific query type.
2086
- *
2087
- * When `TMap` is supplied, the `queryType` argument is restricted to its
2088
- * keys and the handler signature is forced to match `TMap[K]` for the
2089
- * return value: typos and wrong-typed handlers are compile errors.
2090
- * Without `TMap` the registration is loose (any string key, any return
2091
- * type) so the no-config path keeps working.
2092
- *
2093
- * @param queryType - The query type to register the handler for
2094
- * @param handler - The handler function for this query type
2095
- */
2096
- register<K extends keyof TMap & string, Q extends Query & {
2097
- type: K;
2098
- } = Query & {
2099
- type: K;
2100
- }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
2627
+ declare function prepareDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): PreparedDomainMachineDefinition<TState, TContext, TInput, TOutput>;
2628
+
2629
+ /** No transition is defined for the input in the current state. */
2630
+ declare class InvalidDomainTransitionError extends DomainError<"InvalidDomainTransitionError"> {
2631
+ readonly state: string;
2632
+ readonly inputType: string;
2633
+ constructor(state: string, inputType: string);
2634
+ }
2635
+ /** A defined transition was rejected by its domain guard. */
2636
+ declare class DomainTransitionGuardRejectedError extends DomainError<"DomainTransitionGuardRejectedError"> {
2637
+ readonly state: string;
2638
+ readonly inputType: string;
2639
+ constructor(state: string, inputType: string);
2640
+ }
2641
+ /** The machine definition violates its runtime contract. */
2642
+ declare class InvalidDomainMachineDefinitionError extends BaseError<"InvalidDomainMachineDefinitionError"> {
2643
+ constructor(message: string, cause?: unknown);
2644
+ }
2645
+ /** Context contains unsupported or unsafe runtime data. */
2646
+ declare class InvalidDomainMachineContextError extends BaseError<"InvalidDomainMachineContextError"> {
2647
+ constructor(message: string, cause?: unknown);
2648
+ }
2649
+ /** A supplied or produced snapshot is malformed or violates invariants. */
2650
+ declare class InvalidDomainMachineSnapshotError extends BaseError<"InvalidDomainMachineSnapshotError"> {
2651
+ constructor(message: string, cause?: unknown);
2101
2652
  }
2653
+ /** An input is malformed or contains unsupported runtime data. */
2654
+ declare class InvalidDomainMachineInputError extends BaseError<"InvalidDomainMachineInputError"> {
2655
+ constructor(message: string, cause?: unknown);
2656
+ }
2657
+ /** A guard returned a value other than `boolean` or `DomainError`. */
2658
+ declare class InvalidDomainTransitionGuardResultError extends BaseError<"InvalidDomainTransitionGuardResultError"> {
2659
+ constructor(message: string, cause?: unknown);
2660
+ }
2661
+ /** A reducer returned a malformed result or unsupported output data. */
2662
+ declare class InvalidDomainTransitionResultError extends BaseError<"InvalidDomainTransitionResultError"> {
2663
+ constructor(message: string, cause?: unknown);
2664
+ }
2665
+ /** A callback attempted to evaluate the same stateful machine recursively. */
2666
+ declare class ReentrantDomainStateMachineEvaluationError extends BaseError<"ReentrantDomainStateMachineEvaluationError"> {
2667
+ constructor();
2668
+ }
2669
+
2670
+ /** Creates and validates a fresh initial snapshot from a machine definition. */
2671
+ declare function createInitialDomainMachineSnapshot<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): DomainMachineSnapshot<TState, TContext>;
2102
2672
  /**
2103
- * Simple in-memory query bus implementation.
2104
- * Handlers are stored in a Map and dispatched based on query type.
2105
- *
2106
- * Supports an optional type map (`TMap`) for automatic return type inference.
2107
- * When `TMap` is provided, `execute()` and `executeUnsafe()` infer the result type from the query type.
2108
- * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
2673
+ * Checks whether an input currently has an allowed transition.
2109
2674
  *
2110
- * **Note:** This is a basic implementation suitable for development and simple use cases.
2111
- * For production environments, consider implementing or using a more feature-rich bus that includes:
2112
- * - Middleware/Pipeline support (logging, caching, rate limiting)
2113
- * - Error handling
2114
- * - Timeout handling
2115
- * - Metrics and observability
2116
- * - Query result caching
2117
- * - Rate limiting
2118
- *
2119
- * The `QueryHandler` type can still be used with external production-grade buses
2120
- * (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
2121
- *
2122
- * @template TMap - Optional mapping from query type strings to return types
2675
+ * Returns `false` for missing transitions, terminal states, rejected guards,
2676
+ * and inputs without an own string `type` property. Invalid payload data for a
2677
+ * matching transition and broken guard code still throw structured errors.
2678
+ */
2679
+ declare function canTransitionDomainState<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext>, input: TInput): boolean;
2680
+ /**
2681
+ * Applies one input without mutating the input definition or snapshot.
2123
2682
  *
2124
- * @example
2125
- * ```typescript
2126
- * // With type map full inference
2127
- * type Queries = { GetOrder: Order | null; ListOrders: Order[] };
2128
- * const bus = new QueryBus<Queries>();
2129
- * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2130
- * // result: Result<Order | null, string>
2683
+ * @throws {@link InvalidDomainTransitionError} when no transition is defined.
2684
+ * @throws {@link DomainTransitionGuardRejectedError} when its guard rejects.
2685
+ * @throws A concrete `DomainError` returned by a rejecting guard.
2686
+ */
2687
+ declare function transitionDomainState<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext>, input: TInput): DomainTransitionOutcome<TState, TContext, TOutput>;
2688
+
2689
+ /**
2690
+ * Stateful convenience wrapper around the pure domain transition functions.
2131
2691
  *
2132
- * // Without type map same as before
2133
- * const bus = new QueryBus();
2134
- * bus.register("GetOrder", async (query) => repository.getById(query.orderId));
2135
- * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2136
- * ```
2692
+ * Persist {@link snapshot}, not the machine instance. Pass a restored snapshot
2693
+ * to the second constructor overload to validate and reconstitute a machine.
2137
2694
  */
2138
- declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
2139
- private readonly handlers;
2140
- register<K extends keyof TMap & string, Q extends Query & {
2141
- type: K;
2142
- } = Query & {
2143
- type: K;
2144
- }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
2145
- execute<Q extends Query & {
2146
- type: keyof TMap & string;
2147
- }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
2148
- execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
2149
- executeUnsafe<Q extends Query & {
2150
- type: keyof TMap & string;
2151
- }>(query: Q): Promise<TMap[Q["type"]]>;
2152
- executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
2695
+ declare class DomainStateMachine<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> {
2696
+ #private;
2697
+ private readonly definition;
2698
+ constructor(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>);
2699
+ constructor(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext> | undefined);
2700
+ /** Returns a defensive, deeply frozen copy of the current snapshot. */
2701
+ get snapshot(): DomainMachineSnapshot<TState, TContext>;
2702
+ /** Current named control state. */
2703
+ get state(): TState;
2704
+ /** Current deeply readonly context. */
2705
+ get context(): DomainMachineReadonly<TContext>;
2706
+ /** Whether the current state permanently forbids outgoing transitions. */
2707
+ isTerminal(): boolean;
2708
+ /** Checks a transition without changing the current snapshot. */
2709
+ can(input: TInput): boolean;
2710
+ /** Applies an input and advances the current snapshot on success. */
2711
+ dispatch(input: TInput): DomainTransitionOutcome<TState, TContext, TOutput>;
2712
+ private evaluate;
2153
2713
  }
2154
2714
 
2155
2715
  /**
@@ -2193,15 +2753,35 @@ declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt>
2193
2753
  publish(events: ReadonlyArray<Evt>): Promise<void>;
2194
2754
  }
2195
2755
 
2756
+ /** Construction options for {@link InMemoryOutbox}. */
2757
+ interface InMemoryOutboxOptions {
2758
+ /**
2759
+ * Failed-delivery ceiling: once `markFailed` has been reported this
2760
+ * many times for a record, it moves to {@link InMemoryOutbox.deadLetters}
2761
+ * and stops coming back from `getPending`. Default `5`.
2762
+ */
2763
+ maxDeliveryAttempts?: number;
2764
+ }
2196
2765
  /**
2197
- * In-memory reference implementation of `Outbox<Evt>`.
2766
+ * In-memory reference implementation of `DispatchTrackingOutbox<Evt>`
2767
+ * (and therefore of the plain `Outbox<Evt>` port).
2198
2768
  *
2199
2769
  * Intended for tests, single-process workers, and quick-start demos.
2200
2770
  * Uses the event's own `eventId` as the dispatch id: the common, clean
2201
- * choice. Storage is a `Map<string, OutboxRecord<Evt>>` keyed by
2202
- * `eventId`, so re-adding the same event is naturally idempotent (the
2203
- * duplicate entry overwrites itself; `getPending` returns each event at
2204
- * most once).
2771
+ * choice. Storage is a `Map` keyed by `eventId`, so re-adding the same
2772
+ * event is naturally idempotent (`getPending` returns each event at
2773
+ * most once; a re-add refreshes the stored copy, e.g. a retried
2774
+ * commit's freshly stamped `aggregateVersion`, while the delivery
2775
+ * attempt count survives), and insertion order is preserved:
2776
+ * `getPending` returns records in commit order, as the port contract
2777
+ * requires.
2778
+ *
2779
+ * Dispatch tracking: `markFailed` increments the record's attempt count
2780
+ * and, at `maxDeliveryAttempts`, moves it to the dead-letter set
2781
+ * exposed by `deadLetters()`. Re-`add`ing a dead-lettered event
2782
+ * requeues it with a fresh attempts budget (the operator-facing
2783
+ * inverse of `deadLetters()`); `markDispatched` acks pending AND
2784
+ * dead-lettered records (manual redelivery then ack).
2205
2785
  *
2206
2786
  * For production, back the outbox with a transactional store so the
2207
2787
  * outbox row participates in the same transaction as the aggregate
@@ -2230,11 +2810,156 @@ declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt>
2230
2810
  * });
2231
2811
  * ```
2232
2812
  */
2233
- declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements Outbox<Evt> {
2813
+ declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements DispatchTrackingOutbox<Evt> {
2234
2814
  private readonly pending;
2815
+ private readonly dead;
2816
+ private readonly maxDeliveryAttempts;
2817
+ constructor(options?: InMemoryOutboxOptions);
2235
2818
  add(events: ReadonlyArray<Evt>): Promise<void>;
2236
2819
  getPending(limit?: number): Promise<ReadonlyArray<OutboxRecord<Evt>>>;
2237
2820
  markDispatched(dispatchIds: ReadonlyArray<string>): Promise<void>;
2821
+ markFailed(dispatchId: string, error?: unknown): Promise<void>;
2822
+ deadLetters(): Promise<ReadonlyArray<DeadLetterRecord<Evt>>>;
2823
+ }
2824
+
2825
+ /** Options for {@link EventStore.append}. */
2826
+ interface EventStoreAppendOptions {
2827
+ /**
2828
+ * The stream version the writer loaded (its optimistic-concurrency
2829
+ * baseline): the number of events the stream held when the aggregate
2830
+ * was reconstituted. `0` for a brand-new stream. For kit aggregates
2831
+ * this is `aggregate.persistedVersion ?? 0`; the aggregate version IS
2832
+ * the event count, so stream version and aggregate version align.
2833
+ */
2834
+ readonly expectedVersion: number;
2835
+ }
2836
+ /** Options for {@link EventStore.readStream}. */
2837
+ interface ReadStreamOptions {
2838
+ /**
2839
+ * Return only events AFTER this stream position (1-based event count),
2840
+ * the snapshot catch-up read: `readStream(id, { fromVersion:
2841
+ * snapshot.version })` yields exactly the events
2842
+ * `restoreFromSnapshotWithEvents` needs. Defaults to `0` (the whole
2843
+ * stream).
2844
+ */
2845
+ readonly fromVersion?: number;
2846
+ }
2847
+ /**
2848
+ * Driven port for event-sourced aggregate persistence: an append-only
2849
+ * store with one stream per aggregate (the stream id IS the aggregate
2850
+ * id, Greg Young's stream-per-aggregate default).
2851
+ *
2852
+ * The kit ships the port, the OCC error contract, `InMemoryEventStore`
2853
+ * as the reference implementation, and the event-sourced repository
2854
+ * contract suite (`@shirudo/ddd-kit/testing`). Your adapter implements
2855
+ * this port against a real store and must pass that suite: like the
2856
+ * state-stored `IRepository`, optimistic concurrency is a testable
2857
+ * adapter contract, not a kit guarantee.
2858
+ *
2859
+ * Repository usage (see the event-sourcing guide):
2860
+ *
2861
+ * ```ts
2862
+ * async getById(id: OrderId): Promise<Order | null> {
2863
+ * const cached = this.session.identityMap.get(Order, id);
2864
+ * if (cached) return cached;
2865
+ * const history = await this.eventStore.readStream(id);
2866
+ * if (history.length === 0) return null;
2867
+ * const order = Order.reconstitute(id); // bare instance, no events
2868
+ * const result = order.loadFromHistory(history);
2869
+ * if (result.isErr()) throw result.error; // corrupt stream
2870
+ * this.session.identityMap.set(Order, id, order);
2871
+ * return order;
2872
+ * }
2873
+ *
2874
+ * async save(order: Order): Promise<void> {
2875
+ * if (order.pendingEvents.length === 0) return;
2876
+ * this.session.enrollSaved(order);
2877
+ * await this.eventStore.append(order.id, order.pendingEvents, {
2878
+ * expectedVersion: order.persistedVersion ?? 0,
2879
+ * });
2880
+ * }
2881
+ * ```
2882
+ *
2883
+ * `save` appends the UNSTAMPED `pendingEvents` originals; `withCommit`
2884
+ * separately hands the outbox stamped copies. The store's own position
2885
+ * is the ordering authority for replay (see the event-sourcing guide's
2886
+ * note on `aggregateVersion`).
2887
+ *
2888
+ * **One save per aggregate per unit of work, after all mutations.**
2889
+ * `pendingEvents` are cleared and `persistedVersion` advances only AFTER
2890
+ * the commit (`markPersisted`), so a second `save` of the same instance
2891
+ * inside one unit of work would re-append the already-appended events
2892
+ * with a stale `expectedVersion` and deterministically conflict, with no
2893
+ * concurrent writer in sight. This is the same rule the state-stored
2894
+ * path documents as "mutate first, save last" on `withCommit`; it is
2895
+ * not specific to event sourcing.
2896
+ */
2897
+ interface EventStore<Evt extends AnyDomainEvent> {
2898
+ /**
2899
+ * Atomically appends `events` to the stream, guarded by optimistic
2900
+ * concurrency: the append succeeds only when the stream currently
2901
+ * holds exactly `options.expectedVersion` events.
2902
+ *
2903
+ * Contract for implementations:
2904
+ *
2905
+ * 1. **OCC:** on a version mismatch (stale writer, duplicate create
2906
+ * racing on `expectedVersion: 0`, or an expectedVersion ahead of
2907
+ * the stream), throw `ConcurrencyConflictError` from
2908
+ * `@shirudo/ddd-kit` carrying the expected and actual stream
2909
+ * versions; map your store's native conflict signal to it instead
2910
+ * of letting a raw driver error escape. One sanctioned exception:
2911
+ * an adapter that can DISTINGUISH the duplicate-create race
2912
+ * (`expectedVersion: 0` against a stream that already exists,
2913
+ * typically a unique violation on the first position) may throw
2914
+ * `DuplicateAggregateError` for that case instead, matching the
2915
+ * state-stored insert path. It is deliberately NOT retryable:
2916
+ * replaying the same append cannot succeed; the use case resolves
2917
+ * the create race (load the existing aggregate, or surface HTTP
2918
+ * 409). The contract suite accepts both errors for this race.
2919
+ * 2. **Atomicity:** all events land or none do; a rejected append
2920
+ * leaves the stream untouched.
2921
+ * 3. **Order:** events are stored in the given array order, appended
2922
+ * after the existing stream tail.
2923
+ * 4. **Append-only:** stored events are never edited or deleted;
2924
+ * corrections are new (compensating) events.
2925
+ *
2926
+ * An empty `events` array is a no-op; implementations resolve without
2927
+ * touching the store (an ES repository skips `save` for aggregates
2928
+ * without pending events anyway).
2929
+ */
2930
+ append(streamId: Id<string>, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
2931
+ /**
2932
+ * Reads the stream in append order. Returns an empty array for an
2933
+ * unknown stream (the repository maps that to `null`). With
2934
+ * `options.fromVersion`, returns only the events after that stream
2935
+ * position (snapshot catch-up). The returned array is owned by the
2936
+ * caller; implementations must not hand out live internal state.
2937
+ */
2938
+ readStream(streamId: Id<string>, options?: ReadStreamOptions): Promise<ReadonlyArray<Evt>>;
2939
+ }
2940
+
2941
+ /**
2942
+ * In-memory reference implementation of `EventStore<Evt>`.
2943
+ *
2944
+ * Intended for tests, single-process workers, and quick-start demos.
2945
+ * Implements the full port contract: expectedVersion-guarded appends
2946
+ * (throwing `ConcurrencyConflictError` on mismatch), atomic rejected
2947
+ * appends, append-order reads, and `fromVersion` slicing.
2948
+ *
2949
+ * For production, back the port with a durable store whose append and
2950
+ * the aggregate transaction share atomicity (a table with a
2951
+ * `(stream_id, position)` unique key inside the same transaction, or a
2952
+ * dedicated event store). Same caveat as `InMemoryOutbox`: this class
2953
+ * lives in memory only and knows nothing about your `TransactionScope`
2954
+ * rollbacks; events appended inside a transaction that later rolls back
2955
+ * are NOT removed. The event-sourced repository contract suite's
2956
+ * reference environment shows the snapshot/restore pattern for
2957
+ * rollback-pure in-memory testing.
2958
+ */
2959
+ declare class InMemoryEventStore<Evt extends AnyDomainEvent> implements EventStore<Evt> {
2960
+ private readonly streams;
2961
+ append(streamId: Id<string>, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
2962
+ readStream(streamId: Id<string>, options?: ReadStreamOptions): Promise<ReadonlyArray<Evt>>;
2238
2963
  }
2239
2964
 
2240
2965
  /**
@@ -2453,7 +3178,12 @@ interface RetryPolicy {
2453
3178
  * that your adapter has not mapped to a retryable kit error.
2454
3179
  */
2455
3180
  isRetryable?: (error: unknown) => boolean;
2456
- /** Observer fired before each backoff wait (logging / metrics). */
3181
+ /**
3182
+ * Observer fired before each backoff wait (logging / metrics).
3183
+ * Neutralised like the `withCommit` observers: a synchronous throw or
3184
+ * an async rejection is swallowed, so a buggy observer can neither
3185
+ * abort the retry loop nor mask the original retryable error.
3186
+ */
2457
3187
  onRetry?: (info: {
2458
3188
  attempt: number;
2459
3189
  error: unknown;
@@ -2555,7 +3285,12 @@ declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
2555
3285
  * `vo(input)` never freezes the caller's own object graph as a
2556
3286
  * side-effect. Mutating the input afterwards does not bleed into the VO.
2557
3287
  * Symbol-keyed properties are preserved (matching `voEquals`); function
2558
- * values are rejected (Value Objects are data, not behaviour).
3288
+ * values and custom class instances are rejected (Value Objects are plain
3289
+ * data, not behaviour-bearing object graphs). Inputs must be trusted and
3290
+ * Proxy-free: ECMAScript provides no portable way to identify a transparent
3291
+ * Proxy without potentially executing its traps, so `vo()` is not a sandbox
3292
+ * for hostile in-process objects. Built-ins that cannot provide immutable,
3293
+ * value-based semantics are rejected instead of weakening the VO contract.
2559
3294
  *
2560
3295
  * @example
2561
3296
  * ```typescript
@@ -2572,7 +3307,6 @@ declare function vo<T>(t: T): VO<T>;
2572
3307
  * - Nested objects and arrays
2573
3308
  * - Primitives (including NaN)
2574
3309
  * - Dates, Maps, Sets, RegExp
2575
- * - TypedArrays and DataView
2576
3310
  * - Symbol keys
2577
3311
  * - Circular references
2578
3312
  *
@@ -2640,8 +3374,8 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
2640
3374
  * Creates a value object with optional validation.
2641
3375
  * Returns a Result type instead of throwing an error.
2642
3376
  *
2643
- * Note: the Result covers VALIDATION failures only. Non-data values in
2644
- * the input (functions, Promise/WeakMap/WeakSet) still throw a
3377
+ * Note: the Result covers VALIDATION failures only. Non-data values and
3378
+ * built-ins without immutable value semantics still throw a
2645
3379
  * `TypeError` from `vo()`; they cannot occur in parsed JSON and signal
2646
3380
  * a programming error, not a validation failure.
2647
3381
  *
@@ -2709,7 +3443,7 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
2709
3443
  readonly props: Readonly<T>;
2710
3444
  /**
2711
3445
  * Creates a new ValueObject.
2712
- * The properties are deep-cloned (prototype-preserving) and then deeply
3446
+ * The plain-data properties are deep-cloned and then deeply
2713
3447
  * frozen, so the caller's own object graph is never frozen or mutated,
2714
3448
  * and later mutation of the input does not bleed into the value object.
2715
3449
  *
@@ -2792,4 +3526,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
2792
3526
  */
2793
3527
  declare function voValidated<T>(t: T, validate: (issues: ValidationError, value: T) => void, message?: string): Result<VO<T>, ValidationError>;
2794
3528
 
2795
- export { type AggregateClass, type AggregateConfig, AggregateRoot, AggregateSnapshot, AnyDomainEvent, type Command, CommandBus, type CommandHandler, CommitError, CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, Entity, type EventBus, EventBusImpl, type EventHandler, EventSourcedAggregate, IAggregateRoot, type ICommandBus, type IEntity, IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IUnitOfWorkRepository, type IValueObject, Id, type Identifiable, IdentityMap, InMemoryOutbox, InfrastructureError, NestedUnitOfWorkError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type RepositoryFactories, type RetryPolicy, RetryingTransactionScope, RollbackError, type RunOptions, TransactionClosedError, type TransactionScope, type TransactionalOptions, UnitOfWork, type UnitOfWorkContext, type UnitOfWorkDeps, type UnitOfWorkSession, type VO, ValueObject, Version, computeBackoffDelay, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, removeEntityById, replaceEntityById, sameEntity, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withCommit };
3529
+ export { type AggregateClass, type AggregateConfig, AggregateRoot, AggregateSnapshot, AnyDomainEvent, type ClockFactory, type Command, CommandBus, type CommandBusOptions, type CommandHandler, CommitError, CreateDomainEventOptions, type DeadLetterRecord, DeepEqualExceptOptions, type DispatchTrackingOutbox, DomainError, type DomainMachineDefinition, type DomainMachineDefinitionAnalysis, type DomainMachineDefinitionDiagnostic, type DomainMachineInput, type DomainMachineReadonly, type DomainMachineSnapshot, type DomainMachineTransitionDescription, DomainStateMachine, type DomainStateNode, type DomainTransition, DomainTransitionGuardRejectedError, type DomainTransitionGuardResult, type DomainTransitionOutcome, type DomainTransitionResult, Entity, type EntityConfig, type EventBus, EventBusImpl, type EventHandler, EventSourcedAggregate, type EventStore, type EventStoreAppendOptions, IAggregateRoot, type ICommandBus, type IEntity, IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IUnitOfWorkRepository, type IValueObject, Id, type Identifiable, IdentityMap, InMemoryEventStore, InMemoryOutbox, type InMemoryOutboxOptions, InfrastructureError, InvalidDomainMachineContextError, InvalidDomainMachineDefinitionError, InvalidDomainMachineInputError, InvalidDomainMachineSnapshotError, InvalidDomainTransitionError, InvalidDomainTransitionGuardResultError, InvalidDomainTransitionResultError, NestedUnitOfWorkError, type OnceOptions, type Outbox, type OutboxRecord, type PreparedDomainMachineDefinition, type Query, QueryBus, type QueryBusOptions, type QueryHandler, type ReadStreamOptions, ReentrantDomainStateMachineEvaluationError, type RepositoryFactories, type RetryPolicy, RetryingTransactionScope, RollbackError, type RunOptions, TransactionClosedError, type TransactionScope, type TransactionalOptions, UnitOfWork, type UnitOfWorkContext, type UnitOfWorkDeps, type UnitOfWorkSession, type VO, ValueObject, Version, analyzeDomainMachineDefinition, canTransitionDomainState, computeBackoffDelay, createInitialDomainMachineSnapshot, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, prepareDomainMachineDefinition, removeEntityById, replaceEntityById, resetClockFactory, sameEntity, setClockFactory, transitionDomainState, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withClockFactory, withCommit };