@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/README.md +2 -2
- package/dist/{aggregate-DclYgG_D.d.ts → aggregate-CePTINEt.d.ts} +93 -33
- package/dist/http.d.ts +36 -13
- package/dist/http.js +15 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +249 -52
- package/dist/index.js +295 -71
- package/dist/index.js.map +1 -1
- package/dist/presentation.d.ts +54 -0
- package/dist/presentation.js +45 -0
- package/dist/presentation.js.map +1 -0
- package/dist/testing.d.ts +24 -23
- package/dist/testing.js.map +1 -1
- package/package.json +18 -11
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-
|
|
2
|
-
export { p as AggregateDeletedError,
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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
|
|
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,
|
|
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
|
|
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"]],
|
|
1097
|
-
execute<C extends Command, R>(command: C): Promise<Result<R,
|
|
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"]],
|
|
1162
|
-
execute<C extends Command, R>(command: C): Promise<Result<R,
|
|
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
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1752
|
-
* transaction commit itself. The kit cannot see inside
|
|
1753
|
-
* `TransactionScope.transactional`, so these
|
|
1754
|
-
*
|
|
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
|
|
1760
|
-
*
|
|
1761
|
-
* time serialization failure is
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
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
|
|
1840
|
-
* look like the escape hatch it is
|
|
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
|
|
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
|
|
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"]],
|
|
2016
|
-
execute<Q extends Query, R>(query: Q): Promise<Result<R,
|
|
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"]],
|
|
2093
|
-
execute<Q extends Query, R>(query: Q): Promise<Result<R,
|
|
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
|
|
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
|
|
2194
|
-
* is structurally typed
|
|
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 };
|