@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/README.md +6 -3
- package/dist/{aggregate-BGdgvqKh.d.ts → aggregate-DFi6HlEh.d.ts} +143 -88
- 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 +916 -182
- package/dist/index.js +2017 -376
- 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 +124 -2
- package/dist/testing.js +535 -116
- package/dist/testing.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +160 -18
- package/dist/utils.js.map +1 -1
- package/package.json +20 -13
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-
|
|
2
|
-
export {
|
|
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 `
|
|
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
|
-
* - `
|
|
29
|
-
* - `
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
136
|
-
* (`entity.state.foo = …`) throw in strict mode, but writes to
|
|
137
|
-
* objects (`entity.state.address.zip = …`) bypass the freeze.
|
|
138
|
-
* immutability either
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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
|
|
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`
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* `
|
|
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.
|
|
613
|
-
*
|
|
614
|
-
*
|
|
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
|
|
931
|
-
*
|
|
932
|
-
*
|
|
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,
|
|
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
|
|
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"]],
|
|
1097
|
-
execute<C extends Command, R>(command: C): Promise<Result<R,
|
|
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"]],
|
|
1162
|
-
execute<C extends Command, R>(command: C): Promise<Result<R,
|
|
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
|
|
1323
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2022
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
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
|
-
*
|
|
2055
|
-
* const
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
*
|
|
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
|
-
* @
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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
|
-
*
|
|
2133
|
-
*
|
|
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
|
|
2139
|
-
private
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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 `
|
|
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
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
2644
|
-
*
|
|
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
|
|
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 };
|