@shirudo/ddd-kit 1.2.0 → 2.0.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,5 +1,5 @@
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-DclYgG_D.js';
2
- export { p as AggregateDeletedError, q as AggregateNotFoundError, f as ClockFactory, u as ConcurrencyConflictError, k as DomainEvent, t as DuplicateAggregateError, E as EventIdFactory, j as EventMetadata, v as IdGenerator, M as MissingHandlerError, 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-DclYgG_D.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-CePTINEt.js';
2
+ export { p as AggregateDeletedError, t as AggregateNotFoundError, q as AggregateNotFoundErrorOptions, f as ClockFactory, y as ConcurrencyConflictError, x as ConcurrencyConflictErrorOptions, k as DomainEvent, v as DuplicateAggregateError, u as DuplicateAggregateErrorOptions, o as EventHarvestError, E as EventIdFactory, j as EventMetadata, z as IdGenerator, M as MissingHandlerError, U as UnenrolledChangesError, m as copyMetadata, l as createDomainEvent, n 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-CePTINEt.js';
3
3
  import { Result } from '@shirudo/result';
4
4
  import { BaseError, ValidationError } from '@shirudo/base-error';
5
5
  import { DeepEqualExceptOptions } from './utils.js';
@@ -446,7 +446,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
446
446
  * `changedKeys`/`hasChanges`. An override that skips `super` leaves
447
447
  * that baseline uncaptured: `changedKeys` permanently reports ALL
448
448
  * keys and `hasChanges` never returns `false`, so a partial-write
449
- * repository silently degrades to full writes on every save on top
449
+ * repository silently degrades to full writes on every save, on top
450
450
  * of the broken version sync.
451
451
  *
452
452
  * @param version - The version the row currently holds in the DB
@@ -694,7 +694,7 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
694
694
  * If you override this, call `super.markRestored(version)` FIRST:
695
695
  * skipping it leaves the baseline uncaptured, so `changedKeys`
696
696
  * permanently reports ALL keys and `hasChanges` never returns `false`
697
- * partial-write repositories silently degrade to full writes on
697
+ * (partial-write repositories silently degrade to full writes), on
698
698
  * top of breaking version sync.
699
699
  */
700
700
  protected markRestored(version: Version): void;
@@ -713,7 +713,7 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
713
713
  * **How it works.** `setState()` replaces state immutably and the
714
714
  * state object is shallow-frozen, so unchanged top-level sub-objects
715
715
  * keep reference identity across mutations. The diff is therefore a
716
- * shallow per-key `!==` against the baseline reference O(top-level
716
+ * shallow per-key `!==` against the baseline reference: O(top-level
717
717
  * keys), no proxies, no deep diff. A key also counts as dirty when its
718
718
  * *presence* differs (added or removed, even with an `undefined`
719
719
  * value). Computed fresh on every access (a new `Set` each time), so
@@ -726,12 +726,12 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
726
726
  * class-instance `TState` mutated through its own methods defeats
727
727
  * tracking entirely (the reference never changes). A keyless `TState`
728
728
  * (primitive, bare `Date`) has no keys to report, so `changedKeys`
729
- * stays empty for it use {@link hasChanges}, whose reference
729
+ * stays empty for it; use {@link hasChanges}, whose reference
730
730
  * fallback covers keyless states. A deep-equal but newly-referenced
731
731
  * value reports a false POSITIVE (harmless extra write); under the
732
732
  * contract above there are no false negatives.
733
733
  *
734
- * Granularity is per top-level key table-granular, not row-granular:
734
+ * Granularity is per top-level key, table-granular, not row-granular:
735
735
  * a dirty collection key means "this child table changed", not which
736
736
  * rows. `EventSourcedAggregate` deliberately has no `changedKeys`;
737
737
  * its `pendingEvents` are the change record.
@@ -741,16 +741,16 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
741
741
  * Safe skip signal: `false` only when there is genuinely nothing to
742
742
  * persist or flush. `true` when the aggregate has never been
743
743
  * persisted, the version moved past `persistedVersion`, there are
744
- * unflushed {@link pendingEvents}, any state key is dirty, or for
744
+ * unflushed {@link pendingEvents}, any state key is dirty, or, for
745
745
  * keyless states the per-key diff cannot see (primitive `TState`,
746
- * zero-own-key objects like a bare `Date`) the state reference
746
+ * zero-own-key objects like a bare `Date`), the state reference
747
747
  * changed since the baseline.
748
748
  *
749
749
  * The version clause is deliberate: `setState({...state}, true)` with
750
750
  * identical per-key values yields empty {@link changedKeys} but a
751
751
  * bumped version. If a repository skipped `save()` on a state-only
752
752
  * check, `withCommit` would still call `markPersisted(version)` after
753
- * commit, desyncing `persistedVersion` from the DB row and the next
753
+ * commit, desyncing `persistedVersion` from the DB row; and the next
754
754
  * uncontended save would throw a false `ConcurrencyConflictError`.
755
755
  *
756
756
  * The pending-events clause covers the sanctioned decoupled
@@ -1006,6 +1006,8 @@ interface Command {
1006
1006
  *
1007
1007
  * @template C - The command type (must extend Command)
1008
1008
  * @template R - The result type
1009
+ * @template E - The error channel type. Defaults to `string`; widen it (e.g.
1010
+ * to a `DomainError` union) to carry typed failures through the bus.
1009
1011
  *
1010
1012
  * @example
1011
1013
  * ```typescript
@@ -1040,7 +1042,7 @@ interface Command {
1040
1042
  * });
1041
1043
  * ```
1042
1044
  */
1043
- type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string>>;
1045
+ type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Result<R, E>>;
1044
1046
 
1045
1047
  /**
1046
1048
  * Type map for command types to their return types.
@@ -1059,6 +1061,30 @@ type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string
1059
1061
  * ```
1060
1062
  */
1061
1063
  type CommandTypeMap = Record<string, unknown>;
1064
+ /**
1065
+ * Construction options for {@link CommandBus}.
1066
+ *
1067
+ * @template E - The error channel type of the bus.
1068
+ */
1069
+ interface CommandBusOptions<E = string> {
1070
+ /**
1071
+ * Maps a thrown value (a handler that throws, or dispatch to an
1072
+ * unregistered command type) into the bus's error channel `E`. Defaults to
1073
+ * {@link describeThrown}, which renders any thrown value as a `string`.
1074
+ * base-error's `toStructuredError` fits this slot directly when `E` is a
1075
+ * `StructuredError`.
1076
+ */
1077
+ errorMapper?: (thrown: unknown) => E;
1078
+ }
1079
+ /**
1080
+ * Constructor arguments for {@link CommandBus}. When `E` is the default
1081
+ * `string`, options are optional (the built-in {@link describeThrown} mapper
1082
+ * applies). When `E` is widened, an `errorMapper` is required, so a typed
1083
+ * channel can never silently fall back to string values.
1084
+ */
1085
+ type CommandBusArgs<E> = [E] extends [string] ? [options?: CommandBusOptions<E>] : [options: CommandBusOptions<E> & {
1086
+ errorMapper: (thrown: unknown) => E;
1087
+ }];
1062
1088
  /**
1063
1089
  * Command Bus interface for dispatching commands to their handlers.
1064
1090
  * Provides a centralized way to execute commands with handler registration.
@@ -1083,18 +1109,18 @@ type CommandTypeMap = Record<string, unknown>;
1083
1109
  * // result: Result<unknown, string>
1084
1110
  * ```
1085
1111
  */
1086
- interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1112
+ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap, E = string> {
1087
1113
  /**
1088
1114
  * Executes a command by dispatching it to the registered handler.
1089
1115
  * When a type map is provided, the return type is inferred from the command type.
1090
1116
  *
1091
1117
  * @param command - The command to execute
1092
- * @returns Result containing the success value or error message
1118
+ * @returns Result containing the success value or an error of type `E`
1093
1119
  */
1094
1120
  execute<C extends Command & {
1095
1121
  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>>;
1122
+ }>(command: C): Promise<Result<TMap[C["type"]], E>>;
1123
+ execute<C extends Command, R>(command: C): Promise<Result<R, E>>;
1098
1124
  /**
1099
1125
  * Registers a handler for a specific command type.
1100
1126
  *
@@ -1111,7 +1137,7 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1111
1137
  type: K;
1112
1138
  } = Command & {
1113
1139
  type: K;
1114
- }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
1140
+ }>(commandType: K, handler: CommandHandler<C, TMap[K], E>): void;
1115
1141
  }
1116
1142
  /**
1117
1143
  * Simple in-memory command bus implementation.
@@ -1149,17 +1175,19 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
1149
1175
  * const result = await bus.execute({ type: "CreateOrder", ... });
1150
1176
  * ```
1151
1177
  */
1152
- declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
1178
+ declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap, E = string> implements ICommandBus<TMap, E> {
1153
1179
  private readonly handlers;
1180
+ private readonly errorMapper;
1181
+ constructor(...args: CommandBusArgs<E>);
1154
1182
  register<K extends keyof TMap & string, C extends Command & {
1155
1183
  type: K;
1156
1184
  } = Command & {
1157
1185
  type: K;
1158
- }>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
1186
+ }>(commandType: K, handler: CommandHandler<C, TMap[K], E>): void;
1159
1187
  execute<C extends Command & {
1160
1188
  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>>;
1189
+ }>(command: C): Promise<Result<TMap[C["type"]], E>>;
1190
+ execute<C extends Command, R>(command: C): Promise<Result<R, E>>;
1163
1191
  }
1164
1192
 
1165
1193
  /**
@@ -1386,8 +1414,22 @@ interface Outbox<Evt extends AnyDomainEvent> {
1386
1414
  * (constructor injection, factory functions, `withTx` chains); pick one
1387
1415
  * and keep it consistent.
1388
1416
  */
1417
+ /** Options passed to {@link TransactionScope.transactional}. */
1418
+ interface TransactionalOptions {
1419
+ /**
1420
+ * Cooperative-cancellation signal forwarded from `withCommit` /
1421
+ * `UnitOfWork.run`. The kit does not interrupt an in-flight query
1422
+ * itself: it pre-checks `aborted` before opening the transaction and
1423
+ * exposes the signal for the work callback to poll. A scope whose
1424
+ * driver supports cancellation (passing the signal to the query, an
1425
+ * interactive-transaction timeout) SHOULD honor it to abort work
1426
+ * already in progress; scopes that ignore it stay correct, just not
1427
+ * eagerly cancellable.
1428
+ */
1429
+ readonly signal?: AbortSignal;
1430
+ }
1389
1431
  interface TransactionScope<TCtx> {
1390
- transactional<T>(fn: (ctx: TCtx) => Promise<T>): Promise<T>;
1432
+ transactional<T>(fn: (ctx: TCtx) => Promise<T>, options?: TransactionalOptions): Promise<T>;
1391
1433
  }
1392
1434
 
1393
1435
  /**
@@ -1469,12 +1511,12 @@ interface TransactionScope<TCtx> {
1469
1511
  * version and (on `AggregateRoot`) re-baselines dirty tracking against
1470
1512
  * the CURRENT state. A mutation between `save` and the callback's return
1471
1513
  * therefore desyncs OCC (next save throws a false
1472
- * `ConcurrencyConflictError`) and under a partial-write repository
1514
+ * `ConcurrencyConflictError`); and under a partial-write repository
1473
1515
  * using `changedKeys`, an un-bumped mutation is silently marked clean
1474
1516
  * and never written. The `aggregateVersion` stamp widens the blast
1475
1517
  * radius further: harvested events would publicly claim a version the
1476
1518
  * committed row does not carry, poisoning every consumer's ordering
1477
- * and idempotency watermarks a cross-service inconsistency, not just
1519
+ * and idempotency watermarks: a cross-service inconsistency, not just
1478
1520
  * a local one. Mutate first, save last.
1479
1521
  *
1480
1522
  * **Duplicate aggregates are deduped by reference.** If the returned
@@ -1511,6 +1553,15 @@ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
1511
1553
  * for delivery: the outbox dispatcher is the reliable path.
1512
1554
  */
1513
1555
  onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
1556
+ /**
1557
+ * Cooperative-cancellation signal. If already aborted, `withCommit`
1558
+ * rejects with the signal's `reason` BEFORE opening the transaction.
1559
+ * Otherwise the signal is forwarded to `scope.transactional`, where a
1560
+ * cancellation-aware scope can abort an in-flight query. The kit does
1561
+ * not race the work promise: aborting does not kill a running query
1562
+ * unless the scope honors the signal.
1563
+ */
1564
+ signal?: AbortSignal;
1514
1565
  }, fn: (ctx: TCtx) => Promise<{
1515
1566
  result: R;
1516
1567
  aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
@@ -1518,7 +1569,7 @@ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
1518
1569
  * Optional marker: which of `aggregates` were DELETED in this unit
1519
1570
  * of work. Their pending events are harvested like any other
1520
1571
  * (deletion events must reach the outbox), but the post-commit
1521
- * lifecycle differs: `markPersisted` is NOT called on them — it
1572
+ * lifecycle differs: `markPersisted` is NOT called on them. It
1522
1573
  * would fire the user-overridable `onPersisted` hook, whose
1523
1574
  * post-save semantics (cache fill, read-model warm-up) are a lie
1524
1575
  * for a row that was just deleted. Their pending events are
@@ -1630,7 +1681,7 @@ type AggregateClass<TAgg> = (abstract new (...args: any[]) => TAgg) | (Function
1630
1681
  * `markPersisted`) is keyed on JavaScript object identity.
1631
1682
  *
1632
1683
  * Storage is two-level (per-type stores created lazily), so
1633
- * `Restaurant:123` and `Booking:123` can never collide the type key
1684
+ * `Restaurant:123` and `Booking:123` can never collide: the type key
1634
1685
  * is the aggregate CLASS, not the id alone and not a name string.
1635
1686
  *
1636
1687
  * Repository read-path contract:
@@ -1653,7 +1704,7 @@ type AggregateClass<TAgg> = (abstract new (...args: any[]) => TAgg) | (Function
1653
1704
  *
1654
1705
  * Deletion is final within an operation: {@link delete} removes the
1655
1706
  * entry AND records a tombstone, so a later {@link set} of the same
1656
- * type+id throws `AggregateDeletedError` a second instance of a
1707
+ * type+id throws `AggregateDeletedError`: a second instance of a
1657
1708
  * deleted aggregate can never sneak back into the unit of work, even
1658
1709
  * through a repository whose row delete is deferred.
1659
1710
  *
@@ -1664,6 +1715,7 @@ type AggregateClass<TAgg> = (abstract new (...args: any[]) => TAgg) | (Function
1664
1715
  declare class IdentityMap {
1665
1716
  private readonly _stores;
1666
1717
  private readonly _deleted;
1718
+ private readonly _pendingAtRegistration;
1667
1719
  /** The cached instance for type+id, or `undefined` (also after {@link delete}). */
1668
1720
  get<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): TAgg | undefined;
1669
1721
  /** Whether an instance is registered for type+id (false after {@link delete}). */
@@ -1671,7 +1723,7 @@ declare class IdentityMap {
1671
1723
  /**
1672
1724
  * Whether type+id was {@link delete}d in this unit of work. The
1673
1725
  * read path checks this BEFORE hydrating and returns `null`, so
1674
- * "deleted in this operation" reads uniformly as not-found
1726
+ * "deleted in this operation" reads uniformly as not-found,
1675
1727
  * regardless of whether the repository's physical delete already
1676
1728
  * removed the row or is deferred within the transaction. Without
1677
1729
  * the check, a read-only probe of a deleted aggregate would crash
@@ -1693,6 +1745,15 @@ declare class IdentityMap {
1693
1745
  * the operation.
1694
1746
  */
1695
1747
  set<TAgg>(type: AggregateClass<TAgg>, id: Id<string>, aggregate: TAgg): void;
1748
+ /**
1749
+ * Registered instances that have recorded MORE pending events than they
1750
+ * carried when first registered (loaded). Used by the unit of work's
1751
+ * end-of-run guard: an aggregate that gained events after load but was
1752
+ * never enrolled would silently drop them. A read-only load, or a
1753
+ * reconstitution that already carried events, shows no increase and is
1754
+ * not reported.
1755
+ */
1756
+ instancesWithNewPendingEvents(): unknown[];
1696
1757
  /**
1697
1758
  * Removes the entry for type+id and records a tombstone: subsequent
1698
1759
  * {@link get} / {@link has} report absence, and a subsequent
@@ -1748,20 +1809,23 @@ declare class TransactionClosedError extends BaseError<"TransactionClosedError">
1748
1809
  }
1749
1810
  /**
1750
1811
  * The unit of work failed AFTER the work callback completed
1751
- * successfully: during the event harvest, the outbox write, or the
1752
- * transaction commit itself. The kit cannot see inside
1753
- * `TransactionScope.transactional`, so these three are deliberately
1754
- * one error class - the underlying failure is attached as `cause`.
1812
+ * successfully, at the persistence boundary: the outbox write or the
1813
+ * transaction commit itself rejected. The kit cannot see inside
1814
+ * `TransactionScope.transactional`, so these are deliberately one error
1815
+ * class; the underlying failure is attached as `cause`.
1755
1816
  *
1756
1817
  * `InfrastructureError`: the business logic ran to completion; the
1757
1818
  * persistence boundary failed. The transaction rolled back (or never
1758
1819
  * committed), no aggregate was marked persisted, and pending events
1759
- * survive on the aggregates the operation left no partial state
1760
- * behind. **Whether a retry helps depends on the cause**: a commit-
1761
- * time serialization failure is transient, but `withCommit`'s harvest
1762
- * guard (an event missing `aggregateId` / `aggregateType` a
1763
- * programming bug) also lands here and will fail deterministically on
1764
- * every retry. Inspect the `cause` before routing into retry logic.
1820
+ * survive on the aggregates; the operation left no partial state behind.
1821
+ * A `CommitError` is the **potentially transient** post-completion
1822
+ * failure (a commit-time serialization failure is the classic case), so
1823
+ * it is the one a retrying caller should consider re-running. The
1824
+ * deterministic post-completion failure, a harvest-guard violation (an
1825
+ * event missing `aggregateId` / `aggregateType`, or an `aggregateVersion`
1826
+ * ahead of the commit version), is a programming bug and surfaces as
1827
+ * {@link EventHarvestError} instead, which does NOT extend
1828
+ * `InfrastructureError`, so it stays out of retry paths by construction.
1765
1829
  */
1766
1830
  declare class CommitError extends InfrastructureError<"CommitError"> {
1767
1831
  constructor(cause: unknown);
@@ -1836,8 +1900,8 @@ interface UnitOfWorkSession<Evt extends AnyDomainEvent = AnyDomainEvent> {
1836
1900
  }
1837
1901
  /**
1838
1902
  * What the work callback receives: repositories already bound to the
1839
- * live transaction, the enrollment session, and deliberately named to
1840
- * look like the escape hatch it is the raw transaction handle.
1903
+ * live transaction, the enrollment session, and, deliberately named to
1904
+ * look like the escape hatch it is, the raw transaction handle.
1841
1905
  *
1842
1906
  * All members throw {@link TransactionClosedError} once `run()` has
1843
1907
  * settled; do not let the context escape the callback.
@@ -1845,17 +1909,36 @@ interface UnitOfWorkSession<Evt extends AnyDomainEvent = AnyDomainEvent> {
1845
1909
  interface UnitOfWorkContext<TCtx, TRepos, Evt extends AnyDomainEvent = AnyDomainEvent> {
1846
1910
  readonly repositories: TRepos;
1847
1911
  /**
1848
- * **Escape hatch you are leaving the unit of work's guarantees.**
1912
+ * **Escape hatch: you are leaving the unit of work's guarantees.**
1849
1913
  * A write issued on the raw handle bypasses the repository contract,
1850
1914
  * enrollment (its aggregate's events are NOT harvested unless you
1851
1915
  * also call `session.enrollSaved`), and the identity map (a later
1852
- * `getById` of the same aggregate hydrates a SECOND instance
1916
+ * `getById` of the same aggregate hydrates a SECOND instance:
1853
1917
  * double harvest, double `markPersisted`). Use it only for writes no
1854
1918
  * repository covers, pair it with manual enrollment, and prefer
1855
1919
  * adding a repository method whenever one could exist.
1856
1920
  */
1857
1921
  readonly rawTransaction: TCtx;
1858
1922
  readonly session: UnitOfWorkSession<Evt>;
1923
+ /**
1924
+ * The cooperative-cancellation signal passed to {@link UnitOfWork.run},
1925
+ * or `undefined` if none was given. Poll `signal?.aborted` between
1926
+ * steps of a long operation and throw `signal.reason` to bail out; the
1927
+ * throw rolls the unit of work back like any other callback error. The
1928
+ * kit does not interrupt an in-flight query for you: actual query
1929
+ * cancellation depends on the `TransactionScope` honoring the signal.
1930
+ */
1931
+ readonly signal?: AbortSignal;
1932
+ }
1933
+ /** Options for a single {@link UnitOfWork.run} call. */
1934
+ interface RunOptions {
1935
+ /**
1936
+ * Cooperative-cancellation signal. If already aborted, `run()` rejects
1937
+ * with the signal's `reason` before opening a transaction. Otherwise it
1938
+ * is exposed on the context (poll `context.signal`) and forwarded to the
1939
+ * `TransactionScope`. Use `AbortSignal.timeout(ms)` for a deadline.
1940
+ */
1941
+ readonly signal?: AbortSignal;
1859
1942
  }
1860
1943
  /**
1861
1944
  * Per-repository factory map: for each key of `TRepos`, a function
@@ -1958,7 +2041,7 @@ declare class UnitOfWork<Evt extends AnyDomainEvent, TCtx, TRepos extends Record
1958
2041
  * run the post-commit lifecycle (markPersisted, publish) for every
1959
2042
  * enrolled aggregate. Returns the callback's result.
1960
2043
  */
1961
- run<R>(work: (context: UnitOfWorkContext<TCtx, TRepos, Evt>) => Promise<R>): Promise<R>;
2044
+ run<R>(work: (context: UnitOfWorkContext<TCtx, TRepos, Evt>) => Promise<R>, options?: RunOptions): Promise<R>;
1962
2045
  private buildRepositories;
1963
2046
  }
1964
2047
 
@@ -1979,6 +2062,30 @@ declare class UnitOfWork<Evt extends AnyDomainEvent, TCtx, TRepos extends Record
1979
2062
  * ```
1980
2063
  */
1981
2064
  type QueryTypeMap = Record<string, unknown>;
2065
+ /**
2066
+ * Construction options for {@link QueryBus}.
2067
+ *
2068
+ * @template E - The error channel type of the bus.
2069
+ */
2070
+ interface QueryBusOptions<E = string> {
2071
+ /**
2072
+ * Maps a thrown value (a handler that throws, or dispatch to an
2073
+ * unregistered query type) into the bus's error channel `E`. Defaults to
2074
+ * {@link describeThrown}, which renders any thrown value as a `string`.
2075
+ * base-error's `toStructuredError` fits this slot directly when `E` is a
2076
+ * `StructuredError`.
2077
+ */
2078
+ errorMapper?: (thrown: unknown) => E;
2079
+ }
2080
+ /**
2081
+ * Constructor arguments for {@link QueryBus}. When `E` is the default `string`,
2082
+ * options are optional (the built-in {@link describeThrown} mapper applies).
2083
+ * When `E` is widened, an `errorMapper` is required, so a typed channel can
2084
+ * never silently fall back to string values.
2085
+ */
2086
+ type QueryBusArgs<E> = [E] extends [string] ? [options?: QueryBusOptions<E>] : [options: QueryBusOptions<E> & {
2087
+ errorMapper: (thrown: unknown) => E;
2088
+ }];
1982
2089
  /**
1983
2090
  * Query Bus interface for dispatching queries to their handlers.
1984
2091
  * Provides a centralized way to execute queries with handler registration.
@@ -2002,7 +2109,7 @@ type QueryTypeMap = Record<string, unknown>;
2002
2109
  * // result: Result<unknown, string>
2003
2110
  * ```
2004
2111
  */
2005
- interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
2112
+ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap, E = string> {
2006
2113
  /**
2007
2114
  * Executes a query by dispatching it to the registered handler.
2008
2115
  * When a type map is provided, the return type is inferred from the query type.
@@ -2012,8 +2119,8 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
2012
2119
  */
2013
2120
  execute<Q extends Query & {
2014
2121
  type: keyof TMap & string;
2015
- }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
2016
- execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
2122
+ }>(query: Q): Promise<Result<TMap[Q["type"]], E>>;
2123
+ execute<Q extends Query, R>(query: Q): Promise<Result<R, E>>;
2017
2124
  /**
2018
2125
  * Executes a query by dispatching it to the registered handler.
2019
2126
  * Throws an error if no handler is registered.
@@ -2080,8 +2187,10 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
2080
2187
  * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2081
2188
  * ```
2082
2189
  */
2083
- declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
2190
+ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap, E = string> implements IQueryBus<TMap, E> {
2084
2191
  private readonly handlers;
2192
+ private readonly errorMapper;
2193
+ constructor(...args: QueryBusArgs<E>);
2085
2194
  register<K extends keyof TMap & string, Q extends Query & {
2086
2195
  type: K;
2087
2196
  } = Query & {
@@ -2089,8 +2198,8 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
2089
2198
  }>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
2090
2199
  execute<Q extends Query & {
2091
2200
  type: keyof TMap & string;
2092
- }>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
2093
- execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
2201
+ }>(query: Q): Promise<Result<TMap[Q["type"]], E>>;
2202
+ execute<Q extends Query, R>(query: Q): Promise<Result<R, E>>;
2094
2203
  executeUnsafe<Q extends Query & {
2095
2204
  type: keyof TMap & string;
2096
2205
  }>(query: Q): Promise<TMap[Q["type"]]>;
@@ -2151,7 +2260,7 @@ declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt>
2151
2260
  * For production, back the outbox with a transactional store so the
2152
2261
  * outbox row participates in the same transaction as the aggregate
2153
2262
  * write (see `TransactionScope` + `withCommit`). This class lives in
2154
- * memory only: events are lost on process restart and, sharper:
2263
+ * memory only: events are lost on process restart. Sharper still:
2155
2264
  * events `add()`ed inside a transaction that later rolls back are NOT
2156
2265
  * removed (the Map knows nothing about your scope's rollback). Tests
2157
2266
  * that assert rollback purity need an outbox that participates in the
@@ -2190,8 +2299,8 @@ declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements Outbox<Evt>
2190
2299
  * the deleted-cannot-be-resaved gate. Ids stay branded (`TId extends
2191
2300
  * Id<string>`) end-to-end.
2192
2301
  *
2193
- * Implementing this interface is optional the `UnitOfWork` registry
2194
- * is structurally typed but it is the single source of truth the
2302
+ * Implementing this interface is optional (the `UnitOfWork` registry
2303
+ * is structurally typed), but it is the single source of truth the
2195
2304
  * guide's examples and the repository contract test suite
2196
2305
  * (`@shirudo/ddd-kit/testing`, whose `ContractRepository` is the
2197
2306
  * minimal structural subset of this shape) are written against.
@@ -2377,6 +2486,94 @@ interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<
2377
2486
  find(filter: TFilter): Promise<TAgg[]>;
2378
2487
  }
2379
2488
 
2489
+ /**
2490
+ * Tuning for {@link RetryingTransactionScope}. All fields are optional;
2491
+ * the defaults suit optimistic-concurrency retries (a handful of writers
2492
+ * racing one aggregate), not high-fan-out hot-row contention.
2493
+ */
2494
+ interface RetryPolicy {
2495
+ /** Total tries, including the first. Default `3` (1 initial + 2 retries). */
2496
+ maxAttempts?: number;
2497
+ /** First backoff delay; doubles each retry. Default `50`ms. */
2498
+ baseDelayMs?: number;
2499
+ /** Ceiling for the backoff delay. Default `1000`ms. */
2500
+ maxDelayMs?: number;
2501
+ /**
2502
+ * Classifier deciding whether an error is worth retrying. Default
2503
+ * {@link someChainRetryable} (walks the cause chain for the loose
2504
+ * `retryable === true` marker, so `ConcurrencyConflictError` matches
2505
+ * even when an adapter wraps it). Override to add driver-specific
2506
+ * serialization codes (Postgres 40001, MySQL 1213, SQLite SQLITE_BUSY)
2507
+ * that your adapter has not mapped to a retryable kit error.
2508
+ */
2509
+ isRetryable?: (error: unknown) => boolean;
2510
+ /** Observer fired before each backoff wait (logging / metrics). */
2511
+ onRetry?: (info: {
2512
+ attempt: number;
2513
+ error: unknown;
2514
+ delayMs: number;
2515
+ }) => void;
2516
+ /** Backoff wait. Default an abortable `setTimeout`. Injectable for tests. */
2517
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
2518
+ /** Jitter source in `[0, 1)`. Default `Math.random`. Injectable for tests. */
2519
+ random?: () => number;
2520
+ }
2521
+ /**
2522
+ * Backoff delay for the attempt that just failed (1-based): exponential
2523
+ * (`baseDelayMs * 2^(attempt-1)`), capped at `maxDelayMs`, then a +/-20%
2524
+ * jitter band (`* random(0.8, 1.2)`) applied and re-clamped to the cap.
2525
+ * Pure and deterministic given `random`. Result is never negative.
2526
+ *
2527
+ * @internal Exported only so it can be unit-tested directly; not part of
2528
+ * the supported public API and may change without a major version.
2529
+ */
2530
+ declare function computeBackoffDelay(attempt: number, opts: {
2531
+ baseDelayMs: number;
2532
+ maxDelayMs: number;
2533
+ random: () => number;
2534
+ }): number;
2535
+ /**
2536
+ * A {@link TransactionScope} that retries its inner scope on transient
2537
+ * failures with exponential backoff and jitter. Compose it transparently:
2538
+ *
2539
+ * ```ts
2540
+ * const scope = new RetryingTransactionScope(drizzleScope, { maxAttempts: 5 });
2541
+ * const uow = new UnitOfWork({ scope, outbox, repositories });
2542
+ * ```
2543
+ *
2544
+ * **Retries the transaction only.** Each attempt re-invokes the inner
2545
+ * `transactional` with a fresh transaction, so the work callback must be
2546
+ * reload-safe (load aggregates via `getById` inside it, never capture an
2547
+ * aggregate from a previous attempt) and free of non-transactional side
2548
+ * effects before commit. `withCommit` publishes AFTER the commit, so the
2549
+ * in-process publish is outside the retried region and never duplicated;
2550
+ * publish failures are handled by `onPublishError`, not retried here.
2551
+ *
2552
+ * **Classification is by error, not by guesswork.** Only errors the
2553
+ * `isRetryable` predicate accepts are retried; everything else (a
2554
+ * `DomainError`, `EventHarvestError`, `UnenrolledChangesError`,
2555
+ * `DuplicateAggregateError`, a non-Error throw) surfaces immediately.
2556
+ * After `maxAttempts` the last error is rethrown unchanged, so a caller
2557
+ * can still match `ConcurrencyConflictError` and map it to HTTP 409.
2558
+ *
2559
+ * **Cancellation.** The `AbortSignal` from `transactional` options is
2560
+ * checked before each attempt and aborts the backoff wait, so an
2561
+ * `AbortSignal.timeout(ms)` bounds total elapsed time (there is
2562
+ * deliberately no separate max-elapsed knob).
2563
+ */
2564
+ declare class RetryingTransactionScope<TCtx> implements TransactionScope<TCtx> {
2565
+ private readonly inner;
2566
+ private readonly maxAttempts;
2567
+ private readonly baseDelayMs;
2568
+ private readonly maxDelayMs;
2569
+ private readonly isRetryable;
2570
+ private readonly sleep;
2571
+ private readonly random;
2572
+ private readonly onRetry?;
2573
+ constructor(inner: TransactionScope<TCtx>, policy?: RetryPolicy);
2574
+ transactional<T>(fn: (ctx: TCtx) => Promise<T>, options?: TransactionalOptions): Promise<T>;
2575
+ }
2576
+
2380
2577
  type VO<T> = Readonly<T>;
2381
2578
  /**
2382
2579
  * Deep freezes an object and all its nested properties recursively, then
@@ -2649,4 +2846,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
2649
2846
  */
2650
2847
  declare function voValidated<T>(t: T, validate: (issues: ValidationError, value: T) => void, message?: string): Result<VO<T>, ValidationError>;
2651
2848
 
2652
- 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, RollbackError, TransactionClosedError, type TransactionScope, UnitOfWork, type UnitOfWorkContext, type UnitOfWorkDeps, type UnitOfWorkSession, type VO, ValueObject, Version, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, removeEntityById, replaceEntityById, sameEntity, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withCommit };
2849
+ export { type AggregateClass, type AggregateConfig, AggregateRoot, AggregateSnapshot, AnyDomainEvent, type Command, CommandBus, type CommandBusOptions, 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 QueryBusOptions, 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 };