@shirudo/ddd-kit 1.0.0-rc.5 → 1.0.0-rc.7
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 +13 -16
- package/dist/index.d.ts +430 -147
- package/dist/index.js +192 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ type Id<Tag extends string> = string & {
|
|
|
25
25
|
* generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
26
26
|
* with no caller-side generic to abuse.
|
|
27
27
|
*
|
|
28
|
+
* **Your factory must produce unique ids under concurrent calls.**
|
|
29
|
+
* The kit makes no attempt to dedupe or detect collisions — a collision
|
|
30
|
+
* silently overwrites earlier rows (under unique-key constraints) or
|
|
31
|
+
* silently aliases two different entities (without them). Safe choices:
|
|
32
|
+
* `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
|
|
33
|
+
* KSUID — all collision-resistant by design. Unsafe choices: `Date.now()`
|
|
34
|
+
* alone (duplicates within the same millisecond), a process-local
|
|
35
|
+
* counter without persistence (resets to 1 on restart, collides with
|
|
36
|
+
* prior runs), a sequential id derived from non-atomic state.
|
|
37
|
+
*
|
|
28
38
|
* @example
|
|
29
39
|
* ```ts
|
|
30
40
|
* import { ulid } from "ulid";
|
|
@@ -71,12 +81,56 @@ type EventIdFactory = () => string;
|
|
|
71
81
|
*
|
|
72
82
|
* **Module-scoped — last setter wins.** The factory lives as a single
|
|
73
83
|
* module variable; importing two libraries that both call this races on
|
|
74
|
-
* load order
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
84
|
+
* load order, and parallel test workers will see each other's factory.
|
|
85
|
+
* For test isolation and short-lived contexts prefer
|
|
86
|
+
* {@link withEventIdFactory}; for multi-tenant request isolation
|
|
87
|
+
* (e.g. one factory per tenant in a single Worker invocation) **prefer
|
|
88
|
+
* the per-call `options.eventId`** instead of mutating the global. Same
|
|
89
|
+
* caveat applies to `setClockFactory`.
|
|
78
90
|
*/
|
|
79
91
|
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
92
|
+
/**
|
|
93
|
+
* Scoped variant of {@link setEventIdFactory}: installs `factory`,
|
|
94
|
+
* runs `fn`, then restores the previous factory in a `finally` block —
|
|
95
|
+
* so the restoration happens even if `fn` throws. Safe for parallel
|
|
96
|
+
* tests and for synchronous request handlers that need a tenant-
|
|
97
|
+
* specific factory without polluting the global.
|
|
98
|
+
*
|
|
99
|
+
* **Synchronous-only — enforced at runtime.** If `fn` returns a
|
|
100
|
+
* thenable (a `Promise` or any object with a `then` method), the
|
|
101
|
+
* helper throws *before* returning the value to the caller. This
|
|
102
|
+
* catches the async-misuse footgun where the factory would be
|
|
103
|
+
* restored before the awaited body of `fn` runs, leaving the awaited
|
|
104
|
+
* code reading the previous factory. For async scoping across `await`
|
|
105
|
+
* boundaries, use `AsyncLocalStorage` — out of scope for this helper;
|
|
106
|
+
* build it on top if you need it.
|
|
107
|
+
*
|
|
108
|
+
* Composes by nesting: an inner `withEventIdFactory` restores back to
|
|
109
|
+
* the outer's factory; the outer restores to the original.
|
|
110
|
+
*
|
|
111
|
+
* **When to prefer the per-call `options.eventId` instead.** If you're
|
|
112
|
+
* constructing a single event and want full control over its id,
|
|
113
|
+
* passing `{ eventId: "..." }` to `createDomainEvent` is the strongest
|
|
114
|
+
* isolation — it bypasses the factory mechanism entirely, no global
|
|
115
|
+
* mutation, no scope to manage. Reach for `withEventIdFactory` when
|
|
116
|
+
* the events are constructed deep inside domain methods you can't
|
|
117
|
+
* thread an explicit id through (typical test scenario), or when many
|
|
118
|
+
* events in a scope should share the same factory.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* // In a vitest test:
|
|
123
|
+
* it("emits deterministic ids", () => {
|
|
124
|
+
* withEventIdFactory(() => "evt-fixed", () => {
|
|
125
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
126
|
+
* expect(e.eventId).toBe("evt-fixed");
|
|
127
|
+
* });
|
|
128
|
+
* // Outside the callback the default crypto.randomUUID is restored,
|
|
129
|
+
* // even if the body had thrown.
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
80
134
|
/**
|
|
81
135
|
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
82
136
|
* Intended for use in test `afterEach` hooks.
|
|
@@ -102,8 +156,37 @@ type ClockFactory = () => Date;
|
|
|
102
156
|
*
|
|
103
157
|
* The per-call `options.occurredAt` override always wins over this
|
|
104
158
|
* factory. Symmetric to `setEventIdFactory`.
|
|
159
|
+
*
|
|
160
|
+
* Module-scoped — see {@link setEventIdFactory} for the global-state
|
|
161
|
+
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
162
|
+
* multi-tenant request isolation prefer the per-call
|
|
163
|
+
* `options.occurredAt`.
|
|
105
164
|
*/
|
|
106
165
|
declare function setClockFactory(factory: ClockFactory): void;
|
|
166
|
+
/**
|
|
167
|
+
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
168
|
+
* `fn`, then restores the previous factory in a `finally` block.
|
|
169
|
+
* Synchronous-only — same constraints (and same runtime thenable
|
|
170
|
+
* guard) as {@link withEventIdFactory}.
|
|
171
|
+
*
|
|
172
|
+
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
173
|
+
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
174
|
+
* to `createDomainEvent` is the strongest isolation for single-event
|
|
175
|
+
* cases. The scoped helper is for events constructed deep inside
|
|
176
|
+
* domain methods where threading an explicit timestamp is awkward.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* it("stamps events with a fixed clock", () => {
|
|
181
|
+
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
182
|
+
* withClockFactory(() => fixed, () => {
|
|
183
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
184
|
+
* expect(e.occurredAt).toEqual(fixed);
|
|
185
|
+
* });
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
107
190
|
/**
|
|
108
191
|
* Restores the default clock factory (`() => new Date()`).
|
|
109
192
|
* Intended for use in test `afterEach` hooks.
|
|
@@ -189,6 +272,13 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
189
272
|
*/
|
|
190
273
|
metadata?: EventMetadata;
|
|
191
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Upper-bound alias for "any `DomainEvent` shape". Use as a generic
|
|
277
|
+
* constraint when a type parameter should accept any concrete event
|
|
278
|
+
* union. The `unknown` payload is the upper bound — concrete unions
|
|
279
|
+
* still narrow via `Extract<Evt, { type: K }>` at the use-site.
|
|
280
|
+
*/
|
|
281
|
+
type AnyDomainEvent = DomainEvent<string, unknown>;
|
|
192
282
|
/**
|
|
193
283
|
* Shared option bag for the `createDomainEvent*` factories.
|
|
194
284
|
*/
|
|
@@ -263,7 +353,7 @@ declare function createDomainEventWithMetadata<T extends string, P>(type: T, pay
|
|
|
263
353
|
* );
|
|
264
354
|
* ```
|
|
265
355
|
*/
|
|
266
|
-
declare function copyMetadata(sourceEvent:
|
|
356
|
+
declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
267
357
|
/**
|
|
268
358
|
* Merges multiple metadata objects into one.
|
|
269
359
|
* Later metadata objects override earlier ones for the same keys.
|
|
@@ -502,7 +592,7 @@ declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Ide
|
|
|
502
592
|
* // item is { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
503
593
|
* ```
|
|
504
594
|
*/
|
|
505
|
-
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
595
|
+
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): T | undefined;
|
|
506
596
|
/**
|
|
507
597
|
* Checks if an entity with the given ID exists in the collection.
|
|
508
598
|
*
|
|
@@ -520,7 +610,7 @@ declare function findEntityById<TId extends Id<string>, T extends Identifiable<T
|
|
|
520
610
|
* hasEntityId(items, itemId2); // false
|
|
521
611
|
* ```
|
|
522
612
|
*/
|
|
523
|
-
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
613
|
+
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): boolean;
|
|
524
614
|
/**
|
|
525
615
|
* Removes an entity with the given ID from the collection.
|
|
526
616
|
* Returns a new array without the entity.
|
|
@@ -540,7 +630,7 @@ declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>
|
|
|
540
630
|
* // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
|
|
541
631
|
* ```
|
|
542
632
|
*/
|
|
543
|
-
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
633
|
+
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): T[];
|
|
544
634
|
/**
|
|
545
635
|
* Updates an entity with the given ID in the collection.
|
|
546
636
|
* Returns a new array with the updated entity.
|
|
@@ -564,7 +654,7 @@ declare function removeEntityById<TId extends Id<string>, T extends Identifiable
|
|
|
564
654
|
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
565
655
|
* ```
|
|
566
656
|
*/
|
|
567
|
-
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
657
|
+
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, updater: (entity: T) => T): T[];
|
|
568
658
|
/**
|
|
569
659
|
* Replaces an entity with the given ID in the collection.
|
|
570
660
|
* Returns a new array with the replaced entity.
|
|
@@ -588,7 +678,7 @@ declare function updateEntityById<TId extends Id<string>, T extends Identifiable
|
|
|
588
678
|
* });
|
|
589
679
|
* ```
|
|
590
680
|
*/
|
|
591
|
-
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
681
|
+
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, replacement: T): T[];
|
|
592
682
|
/**
|
|
593
683
|
* Extracts all IDs from a collection of entities.
|
|
594
684
|
*
|
|
@@ -606,7 +696,7 @@ declare function replaceEntityById<TId extends Id<string>, T extends Identifiabl
|
|
|
606
696
|
* // ids is [itemId1, itemId2]
|
|
607
697
|
* ```
|
|
608
698
|
*/
|
|
609
|
-
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
699
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
|
|
610
700
|
|
|
611
701
|
/**
|
|
612
702
|
* Marker interface for Aggregate Roots.
|
|
@@ -635,7 +725,7 @@ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(
|
|
|
635
725
|
* }
|
|
636
726
|
* ```
|
|
637
727
|
*/
|
|
638
|
-
interface IAggregateRoot<TId extends Id<string
|
|
728
|
+
interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
|
|
639
729
|
/**
|
|
640
730
|
* Unique identifier of the aggregate root entity.
|
|
641
731
|
*/
|
|
@@ -646,11 +736,25 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
646
736
|
* This version applies to the entire aggregate, including all child entities.
|
|
647
737
|
*/
|
|
648
738
|
readonly version: Version;
|
|
739
|
+
/**
|
|
740
|
+
* Read-only list of domain events recorded on this aggregate that have
|
|
741
|
+
* not yet been flushed to the outbox / persistence layer. Both state-
|
|
742
|
+
* stored (`AggregateRoot`) and event-sourced (`EventSourcedAggregate`)
|
|
743
|
+
* aggregates expose them under the same name, so Repository.save() can
|
|
744
|
+
* harvest them uniformly without branching on the aggregate flavour.
|
|
745
|
+
*/
|
|
746
|
+
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
747
|
+
/**
|
|
748
|
+
* Clears the pending-event list. Called by `markPersisted` after a
|
|
749
|
+
* successful write — the events have been handed off to the outbox
|
|
750
|
+
* / event store and are no longer the aggregate's responsibility.
|
|
751
|
+
*/
|
|
752
|
+
clearPendingEvents(): void;
|
|
649
753
|
/**
|
|
650
754
|
* Post-save hook: a `Repository.save()` implementation calls this with
|
|
651
755
|
* the persisted version after a successful write to push the new
|
|
652
|
-
* version back into the aggregate and clear
|
|
653
|
-
*
|
|
756
|
+
* version back into the aggregate and clear pendingEvents (they are
|
|
757
|
+
* now safely on the write side / in the outbox).
|
|
654
758
|
*
|
|
655
759
|
* Required by the interface so a Repository implementation can call it
|
|
656
760
|
* via the published `IAggregateRoot` contract without taking the
|
|
@@ -665,17 +769,14 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
665
769
|
*/
|
|
666
770
|
interface AggregateConfig {
|
|
667
771
|
/**
|
|
668
|
-
* Whether `setState()` should bump the version automatically
|
|
772
|
+
* Whether `setState()` should bump the version automatically when the
|
|
773
|
+
* caller omits the per-call `bumpVersion` argument.
|
|
669
774
|
*
|
|
670
|
-
* Defaults to **`false`**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* (Contrast with `EventSourcedAggregate`, which defaults this to
|
|
677
|
-
* `true` because every event-sourced state change is per definition a
|
|
678
|
-
* versioned commit.)
|
|
775
|
+
* Defaults to **`false`** — `setState()` already takes an explicit
|
|
776
|
+
* `bumpVersion` argument per call, so the config is just the default
|
|
777
|
+
* the per-call argument falls back to. Set to `true` only if you have
|
|
778
|
+
* a subclass that never passes `bumpVersion` and you want every state
|
|
779
|
+
* change to advance the version anyway.
|
|
679
780
|
*/
|
|
680
781
|
autoVersionBump?: boolean;
|
|
681
782
|
}
|
|
@@ -701,7 +802,7 @@ interface AggregateConfig {
|
|
|
701
802
|
*
|
|
702
803
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
703
804
|
* @template TId - The type of the aggregate root identifier
|
|
704
|
-
* @template TEvent - The type of domain events recorded by this aggregate (
|
|
805
|
+
* @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
|
|
705
806
|
*
|
|
706
807
|
* @example
|
|
707
808
|
* ```typescript
|
|
@@ -717,34 +818,75 @@ interface AggregateConfig {
|
|
|
717
818
|
* }
|
|
718
819
|
* ```
|
|
719
820
|
*/
|
|
720
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
|
|
821
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
721
822
|
private _version;
|
|
722
823
|
get version(): Version;
|
|
723
824
|
protected setVersion(version: Version): void;
|
|
724
825
|
private readonly _config;
|
|
725
826
|
private readonly _autoVersionBump;
|
|
726
|
-
private
|
|
827
|
+
private _pendingEvents;
|
|
727
828
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
829
|
+
* Read-only list of domain events recorded on this aggregate that have
|
|
830
|
+
* not yet been flushed to the outbox / persistence layer.
|
|
730
831
|
*/
|
|
731
|
-
get
|
|
832
|
+
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
732
833
|
/**
|
|
733
|
-
* Clears the list
|
|
734
|
-
*
|
|
834
|
+
* Clears the pending-event list. Call this after the events have been
|
|
835
|
+
* dispatched (typically `markPersisted` handles it for you).
|
|
735
836
|
*/
|
|
736
|
-
|
|
837
|
+
clearPendingEvents(): void;
|
|
737
838
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
839
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
840
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
841
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
842
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
843
|
+
* subclasses **should not** override this method directly.
|
|
844
|
+
*
|
|
845
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
846
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
847
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
848
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
849
|
+
* the safer extension point.
|
|
850
|
+
*
|
|
851
|
+
* If you must override (legitimate cases are very rare), call
|
|
852
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
853
|
+
* runs, then add your logic afterwards.
|
|
742
854
|
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
* call.
|
|
855
|
+
* @param version - The version assigned by the persistence layer
|
|
856
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
746
857
|
*/
|
|
747
858
|
markPersisted(version: Version): void;
|
|
859
|
+
/**
|
|
860
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
861
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
862
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
863
|
+
* breaking the framework's pendingEvents cleanup.
|
|
864
|
+
*
|
|
865
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
866
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
867
|
+
* implementation to preserve.
|
|
868
|
+
*
|
|
869
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
870
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
871
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
872
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
873
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
874
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
875
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
876
|
+
* meant to prevent.
|
|
877
|
+
*
|
|
878
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
879
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
880
|
+
* permissive `void` will accept an `async`-override returning
|
|
881
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
882
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
883
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
884
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
885
|
+
* properly awaited extension point.
|
|
886
|
+
*
|
|
887
|
+
* @param version - The version that was just persisted
|
|
888
|
+
*/
|
|
889
|
+
protected onPersisted(_version: Version): void;
|
|
748
890
|
/**
|
|
749
891
|
* Mutates state and records the resulting domain events in the
|
|
750
892
|
* **canonical record-after-mutation order**. Use this instead of calling
|
|
@@ -972,11 +1114,7 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
|
|
|
972
1114
|
* @template TId - The type of the aggregate root identifier
|
|
973
1115
|
* @template TEvent - The union type of all domain events
|
|
974
1116
|
*/
|
|
975
|
-
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends
|
|
976
|
-
/**
|
|
977
|
-
* Returns a read-only list of new, not-yet-persisted events.
|
|
978
|
-
*/
|
|
979
|
-
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
1117
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
980
1118
|
/**
|
|
981
1119
|
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
982
1120
|
* because event-stream corruption is an expected recoverable failure
|
|
@@ -984,45 +1122,9 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
|
|
|
984
1122
|
*
|
|
985
1123
|
* @param history - An ordered list of past events
|
|
986
1124
|
*/
|
|
987
|
-
loadFromHistory(history: TEvent
|
|
988
|
-
/**
|
|
989
|
-
* Clears the list of pending events.
|
|
990
|
-
*/
|
|
991
|
-
clearPendingEvents(): void;
|
|
992
|
-
/**
|
|
993
|
-
* Checks if the aggregate has any pending events.
|
|
994
|
-
*/
|
|
995
|
-
hasPendingEvents(): boolean;
|
|
996
|
-
/**
|
|
997
|
-
* Returns the number of pending events.
|
|
998
|
-
*/
|
|
999
|
-
getEventCount(): number;
|
|
1000
|
-
/**
|
|
1001
|
-
* Returns the latest pending event, if any.
|
|
1002
|
-
*/
|
|
1003
|
-
getLatestEvent(): TEvent | undefined;
|
|
1004
|
-
}
|
|
1005
|
-
type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
1006
|
-
/**
|
|
1007
|
-
* Configuration options for EventSourcedAggregate behavior.
|
|
1008
|
-
*/
|
|
1009
|
-
interface EventSourcedAggregateConfig {
|
|
1010
|
-
/**
|
|
1011
|
-
* Whether `apply()` should bump the version per event.
|
|
1012
|
-
*
|
|
1013
|
-
* Defaults to **`true`** for `EventSourcedAggregate` — each applied
|
|
1014
|
-
* event is by definition a versioned state change, so the canonical
|
|
1015
|
-
* event-sourcing pattern is "one event = one version bump". Set to
|
|
1016
|
-
* `false` only if your event store assigns version numbers itself
|
|
1017
|
-
* and you want the aggregate to track them via `bumpVersion()` /
|
|
1018
|
-
* `setVersion()` calls instead.
|
|
1019
|
-
*
|
|
1020
|
-
* (Contrast with `AggregateRoot`, which defaults this to `false`
|
|
1021
|
-
* because its `setState()` already takes a per-call `bumpVersion`
|
|
1022
|
-
* argument.)
|
|
1023
|
-
*/
|
|
1024
|
-
autoVersionBump?: boolean;
|
|
1125
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1025
1126
|
}
|
|
1127
|
+
type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
|
|
1026
1128
|
/**
|
|
1027
1129
|
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
1028
1130
|
*
|
|
@@ -1072,22 +1174,66 @@ interface EventSourcedAggregateConfig {
|
|
|
1072
1174
|
* }
|
|
1073
1175
|
* ```
|
|
1074
1176
|
*/
|
|
1075
|
-
declare abstract class EventSourcedAggregate<TState, TEvent extends
|
|
1177
|
+
declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
|
|
1076
1178
|
private _version;
|
|
1077
1179
|
get version(): Version;
|
|
1078
1180
|
private setVersion;
|
|
1079
1181
|
private _pendingEvents;
|
|
1080
|
-
private readonly _autoVersionBump;
|
|
1081
1182
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1082
1183
|
clearPendingEvents(): void;
|
|
1083
1184
|
/**
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
* `
|
|
1185
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
1186
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
1187
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
1188
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
1189
|
+
* subclasses **should not** override this method directly.
|
|
1190
|
+
*
|
|
1191
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
1192
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
1193
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
1194
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
1195
|
+
* the safer extension point.
|
|
1196
|
+
*
|
|
1197
|
+
* If you must override (legitimate cases are very rare), call
|
|
1198
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
1199
|
+
* runs, then add your logic afterwards.
|
|
1200
|
+
*
|
|
1201
|
+
* @param version - The version assigned by the persistence layer
|
|
1202
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
1088
1203
|
*/
|
|
1089
1204
|
markPersisted(version: Version): void;
|
|
1090
|
-
|
|
1205
|
+
/**
|
|
1206
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
1207
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
1208
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
1209
|
+
* breaking the framework's pendingEvents cleanup.
|
|
1210
|
+
*
|
|
1211
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
1212
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
1213
|
+
* implementation to preserve.
|
|
1214
|
+
*
|
|
1215
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
1216
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
1217
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
1218
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
1219
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
1220
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
1221
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
1222
|
+
* meant to prevent.
|
|
1223
|
+
*
|
|
1224
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
1225
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
1226
|
+
* permissive `void` will accept an `async`-override returning
|
|
1227
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
1228
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
1229
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
1230
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
1231
|
+
* properly awaited extension point.
|
|
1232
|
+
*
|
|
1233
|
+
* @param version - The version that was just persisted
|
|
1234
|
+
*/
|
|
1235
|
+
protected onPersisted(_version: Version): void;
|
|
1236
|
+
protected constructor(id: TId, initialState: TState);
|
|
1091
1237
|
/**
|
|
1092
1238
|
* Validates an event before it is applied. Default is no-op.
|
|
1093
1239
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1123,11 +1269,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1123
1269
|
* resolved via the (statically-sound) `handlers` map.
|
|
1124
1270
|
*/
|
|
1125
1271
|
private dispatchAndCommit;
|
|
1126
|
-
/**
|
|
1127
|
-
* Manually bumps the aggregate version.
|
|
1128
|
-
* Only needed if `autoVersionBump` is disabled.
|
|
1129
|
-
*/
|
|
1130
|
-
protected bumpVersion(): void;
|
|
1131
1272
|
/**
|
|
1132
1273
|
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1133
1274
|
* thrown during replay and returns it as an `Err` — this is the
|
|
@@ -1139,10 +1280,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1139
1280
|
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
1140
1281
|
* 2 events ends at v=3, not v=2.
|
|
1141
1282
|
*/
|
|
1142
|
-
loadFromHistory(history: TEvent
|
|
1143
|
-
hasPendingEvents(): boolean;
|
|
1144
|
-
getEventCount(): number;
|
|
1145
|
-
getLatestEvent(): TEvent | undefined;
|
|
1283
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1146
1284
|
/**
|
|
1147
1285
|
* Creates a snapshot of the current aggregate state.
|
|
1148
1286
|
*/
|
|
@@ -1157,7 +1295,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1157
1295
|
* aggregate is rolled back to its pre-call state + version. Partial
|
|
1158
1296
|
* restoration is never observable to the caller.
|
|
1159
1297
|
*/
|
|
1160
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent
|
|
1298
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1161
1299
|
/**
|
|
1162
1300
|
* A map of event types to their corresponding handlers.
|
|
1163
1301
|
* Subclasses MUST implement this property.
|
|
@@ -1456,9 +1594,7 @@ type EventHandler<Evt> = (event: Evt) => Promise<void> | void;
|
|
|
1456
1594
|
* await bus.publish([orderCreatedEvent, orderShippedEvent]);
|
|
1457
1595
|
* ```
|
|
1458
1596
|
*/
|
|
1459
|
-
interface EventBus<Evt extends {
|
|
1460
|
-
type: string;
|
|
1461
|
-
}> {
|
|
1597
|
+
interface EventBus<Evt extends AnyDomainEvent> {
|
|
1462
1598
|
/**
|
|
1463
1599
|
* Publishes events to all subscribed handlers.
|
|
1464
1600
|
*
|
|
@@ -1551,7 +1687,7 @@ interface OnceOptions {
|
|
|
1551
1687
|
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1552
1688
|
* primary key, or whatever the storage layer prefers.
|
|
1553
1689
|
*/
|
|
1554
|
-
interface OutboxRecord<Evt> {
|
|
1690
|
+
interface OutboxRecord<Evt extends AnyDomainEvent> {
|
|
1555
1691
|
dispatchId: string;
|
|
1556
1692
|
event: Evt;
|
|
1557
1693
|
}
|
|
@@ -1571,7 +1707,7 @@ interface OutboxRecord<Evt> {
|
|
|
1571
1707
|
* that's already marked is a no-op, not an error. This lets the
|
|
1572
1708
|
* dispatcher safely retry on partial-failure.
|
|
1573
1709
|
*/
|
|
1574
|
-
interface Outbox<Evt> {
|
|
1710
|
+
interface Outbox<Evt extends AnyDomainEvent> {
|
|
1575
1711
|
/**
|
|
1576
1712
|
* Persists events. Called from inside `withCommit`'s transactional
|
|
1577
1713
|
* callback, atomically with the aggregate write.
|
|
@@ -1607,11 +1743,15 @@ interface Outbox<Evt> {
|
|
|
1607
1743
|
* and rolls back if it throws.
|
|
1608
1744
|
*
|
|
1609
1745
|
* `TCtx` is the persistence layer's transaction handle — Drizzle's `tx`,
|
|
1610
|
-
* Prisma's `tx`, Mongo's session,
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1746
|
+
* Prisma's `tx`, Mongo's session, etc. The scope opens the transaction
|
|
1747
|
+
* and passes the handle to `fn`; the use case binds its repositories to
|
|
1748
|
+
* that handle (typically by constructing a tx-scoped repo from the ctx).
|
|
1749
|
+
*
|
|
1750
|
+
* No default for `TCtx`: every implementor names their context type
|
|
1751
|
+
* explicitly. For genuinely context-free scopes (in-memory tests, naive
|
|
1752
|
+
* no-tx scopes) use `TransactionScope<undefined>` — that's a conscious
|
|
1753
|
+
* "there is nothing meaningful here" statement, not an accidental
|
|
1754
|
+
* `unknown` fallback.
|
|
1615
1755
|
*
|
|
1616
1756
|
* Intentionally **not** Fowler's full Unit of Work (no change tracking,
|
|
1617
1757
|
* no `registerDirty` / `registerNew` / `registerDeleted`, no commit-time
|
|
@@ -1631,11 +1771,11 @@ interface Outbox<Evt> {
|
|
|
1631
1771
|
* ```typescript
|
|
1632
1772
|
* await scope.transactional(async (tx) => {
|
|
1633
1773
|
* // Construct tx-bound repos from ctx (your factory / DI of choice)
|
|
1634
|
-
* const
|
|
1774
|
+
* const orderRepository = makeOrderRepository(tx);
|
|
1635
1775
|
*
|
|
1636
|
-
* const order = await
|
|
1776
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1637
1777
|
* order.confirm();
|
|
1638
|
-
* await
|
|
1778
|
+
* await orderRepository.save(order);
|
|
1639
1779
|
* });
|
|
1640
1780
|
* ```
|
|
1641
1781
|
*
|
|
@@ -1645,60 +1785,98 @@ interface Outbox<Evt> {
|
|
|
1645
1785
|
* (constructor injection, factory functions, `withTx` chains); pick one
|
|
1646
1786
|
* and keep it consistent.
|
|
1647
1787
|
*/
|
|
1648
|
-
interface TransactionScope<TCtx
|
|
1788
|
+
interface TransactionScope<TCtx> {
|
|
1649
1789
|
transactional<T>(fn: (ctx: TCtx) => Promise<T>): Promise<T>;
|
|
1650
1790
|
}
|
|
1651
1791
|
|
|
1652
1792
|
/**
|
|
1653
1793
|
* Helper for executing a write Use Case inside a transaction scope.
|
|
1654
1794
|
*
|
|
1795
|
+
* The use-case callback returns the aggregates it touched; `withCommit`
|
|
1796
|
+
* owns the post-save lifecycle (harvest, outbox, mark-persisted, publish).
|
|
1797
|
+
* This matches the Vernon / Axon / EventFlow unit-of-work pattern:
|
|
1798
|
+
* `Repository.save` is pure persistence; "this aggregate has been
|
|
1799
|
+
* committed" is the orchestrator's call to make, not the repo's.
|
|
1800
|
+
*
|
|
1655
1801
|
* Order of operations:
|
|
1656
1802
|
* 1. `fn(ctx)` runs inside `scope.transactional(...)` — domain mutations
|
|
1657
1803
|
* + repo writes happen here. `ctx` is whatever transaction handle the
|
|
1658
1804
|
* `scope` exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or
|
|
1659
|
-
* `
|
|
1660
|
-
* 2.
|
|
1661
|
-
*
|
|
1662
|
-
* the state change.
|
|
1805
|
+
* `undefined` for context-free scopes).
|
|
1806
|
+
* 2. **Still inside the transaction**, `withCommit` harvests every
|
|
1807
|
+
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
1808
|
+
* events persist atomically with the state change). Skipped when no
|
|
1809
|
+
* events were recorded.
|
|
1810
|
+
*
|
|
1811
|
+
* **Harvest order.** Events are concatenated in the order
|
|
1812
|
+
* aggregates appear in the returned `aggregates` array, then in
|
|
1813
|
+
* each aggregate's `pendingEvents` order (insertion order via
|
|
1814
|
+
* `apply` / `commit` / `addDomainEvent`). So `aggregates: [a, b]`
|
|
1815
|
+
* with `a` emitting `[e1, e2]` and `b` emitting `[e3]` produces
|
|
1816
|
+
* `outbox.add([e1, e2, e3])` and `bus.publish([e1, e2, e3])` in
|
|
1817
|
+
* that exact order.
|
|
1818
|
+
*
|
|
1819
|
+
* **Two ordering guarantees, not one.** Within a single aggregate
|
|
1820
|
+
* the order is *causal* — events are recorded in the order the
|
|
1821
|
+
* domain methods ran, and subscribers (handlers, projections,
|
|
1822
|
+
* replay) MUST process them in that order. Across aggregates the
|
|
1823
|
+
* order in this batch is deterministic but *not* a domain
|
|
1824
|
+
* guarantee. Greg Young / Vernon IDDD §10: aggregates are
|
|
1825
|
+
* independent consistency boundaries; events across them are
|
|
1826
|
+
* eventually consistent. Subscribers should NOT engineer
|
|
1827
|
+
* dependencies on cross-aggregate ordering — use
|
|
1828
|
+
* `EventMetadata.causationId` to express true causation, or a
|
|
1829
|
+
* process manager to coordinate. The in-process EventBus delivers
|
|
1830
|
+
* this batch in order, sequential outbox-dispatchers preserve it
|
|
1831
|
+
* too, but parallel dispatchers or message brokers may reorder
|
|
1832
|
+
* across aggregates at delivery time.
|
|
1663
1833
|
* 3. The transaction commits.
|
|
1664
|
-
* 4. **After** the commit, `
|
|
1665
|
-
*
|
|
1834
|
+
* 4. **After** the commit, `aggregate.markPersisted(aggregate.version)`
|
|
1835
|
+
* fires on each returned aggregate — only now are pending events
|
|
1836
|
+
* considered flushed.
|
|
1837
|
+
* 5. `bus.publish(events)` fires for the in-process fast path (skipped
|
|
1838
|
+
* when no events or no `bus` is wired).
|
|
1666
1839
|
*
|
|
1667
1840
|
* Publishing AFTER commit prevents the classic "publish before commit"
|
|
1668
1841
|
* footgun: in-process subscribers can never react to events from a
|
|
1669
|
-
* transaction that later rolled back. If `bus.publish` itself
|
|
1842
|
+
* transaction that later rolled back. If `bus.publish` itself throws, the
|
|
1670
1843
|
* outbox still holds the events and an outbox-dispatcher will deliver
|
|
1671
1844
|
* them (eventual consistency).
|
|
1672
1845
|
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1846
|
+
* If the transaction rolls back, `markPersisted` is **not** called — the
|
|
1847
|
+
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1848
|
+
*
|
|
1849
|
+
* **Duplicate aggregates are deduped by reference.** If the returned
|
|
1850
|
+
* `aggregates` array contains the same instance twice — e.g. a use
|
|
1851
|
+
* case touches an order via two repository references that happen to
|
|
1852
|
+
* resolve to the same identity-map entry — `withCommit` dedupes by
|
|
1853
|
+
* JavaScript object identity before harvesting. Each event lands in
|
|
1854
|
+
* the outbox exactly once and `markPersisted` fires exactly once. Two
|
|
1855
|
+
* *different* instances with the same logical id cannot be detected
|
|
1856
|
+
* at this layer; that is a Repository contract violation (failure to
|
|
1857
|
+
* maintain Fowler's Identity Map per Unit of Work). See
|
|
1858
|
+
* `docs/guide/repository.md` → "Identity Map: one instance per
|
|
1859
|
+
* aggregate per Unit of Work" for the requirement on `IRepository`
|
|
1860
|
+
* implementations that makes this dedupe sound.
|
|
1681
1861
|
*
|
|
1682
1862
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1683
1863
|
* ```typescript
|
|
1684
1864
|
* const result = await withCommit({ outbox, bus, scope }, async (tx) => {
|
|
1685
|
-
* const
|
|
1686
|
-
* const order = await
|
|
1865
|
+
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
1866
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1687
1867
|
* order.confirm();
|
|
1688
|
-
* await
|
|
1689
|
-
* return { result: order.id,
|
|
1868
|
+
* await orderRepository.save(order); // pure persistence — does NOT call markPersisted
|
|
1869
|
+
* return { result: order.id, aggregates: [order] };
|
|
1690
1870
|
* });
|
|
1691
1871
|
* ```
|
|
1692
1872
|
*/
|
|
1693
|
-
declare function withCommit<Evt extends {
|
|
1694
|
-
type: string;
|
|
1695
|
-
}, R, TCtx = unknown>(deps: {
|
|
1873
|
+
declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
|
|
1696
1874
|
outbox: Outbox<Evt>;
|
|
1697
1875
|
bus?: EventBus<Evt>;
|
|
1698
1876
|
scope: TransactionScope<TCtx>;
|
|
1699
1877
|
}, fn: (ctx: TCtx) => Promise<{
|
|
1700
1878
|
result: R;
|
|
1701
|
-
|
|
1879
|
+
aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
|
|
1702
1880
|
}>): Promise<R>;
|
|
1703
1881
|
|
|
1704
1882
|
/**
|
|
@@ -1933,7 +2111,7 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
|
|
|
1933
2111
|
* // Both handlers will be called
|
|
1934
2112
|
* ```
|
|
1935
2113
|
*/
|
|
1936
|
-
declare class EventBusImpl<Evt extends
|
|
2114
|
+
declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt> {
|
|
1937
2115
|
private readonly handlers;
|
|
1938
2116
|
subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1939
2117
|
type: K;
|
|
@@ -1952,6 +2130,44 @@ declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements
|
|
|
1952
2130
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1953
2131
|
}
|
|
1954
2132
|
|
|
2133
|
+
/**
|
|
2134
|
+
* In-memory reference implementation of `Outbox<Evt>`.
|
|
2135
|
+
*
|
|
2136
|
+
* Intended for tests, single-process workers, and quick-start demos.
|
|
2137
|
+
* Uses the event's own `eventId` as the dispatch id — the common, clean
|
|
2138
|
+
* choice. Storage is a `Map<string, OutboxRecord<Evt>>` keyed by
|
|
2139
|
+
* `eventId`, so re-adding the same event is naturally idempotent (the
|
|
2140
|
+
* duplicate entry overwrites itself; `getPending` returns each event at
|
|
2141
|
+
* most once).
|
|
2142
|
+
*
|
|
2143
|
+
* For production, back the outbox with a transactional store so the
|
|
2144
|
+
* outbox row participates in the same transaction as the aggregate
|
|
2145
|
+
* write (see `TransactionScope` + `withCommit`). This class lives in
|
|
2146
|
+
* memory only — events are lost on process restart.
|
|
2147
|
+
*
|
|
2148
|
+
* @example
|
|
2149
|
+
* ```ts
|
|
2150
|
+
* import { InMemoryOutbox, EventBusImpl, withCommit } from "@shirudo/ddd-kit";
|
|
2151
|
+
*
|
|
2152
|
+
* const outbox = new InMemoryOutbox<OrderEvent>();
|
|
2153
|
+
* const bus = new EventBusImpl<OrderEvent>();
|
|
2154
|
+
*
|
|
2155
|
+
* await withCommit({ scope, outbox, bus }, async (tx) => {
|
|
2156
|
+
* const orderRepository = makeOrderRepository(tx);
|
|
2157
|
+
* const order = await orderRepository.getByIdOrFail(id);
|
|
2158
|
+
* order.confirm();
|
|
2159
|
+
* await orderRepository.save(order);
|
|
2160
|
+
* return { result: order.id, aggregates: [order] };
|
|
2161
|
+
* });
|
|
2162
|
+
* ```
|
|
2163
|
+
*/
|
|
2164
|
+
declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements Outbox<Evt> {
|
|
2165
|
+
private readonly pending;
|
|
2166
|
+
add(events: ReadonlyArray<Evt>): Promise<void>;
|
|
2167
|
+
getPending(limit?: number): Promise<ReadonlyArray<OutboxRecord<Evt>>>;
|
|
2168
|
+
markDispatched(dispatchIds: ReadonlyArray<string>): Promise<void>;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1955
2171
|
/**
|
|
1956
2172
|
* Core repository contract for Aggregate Roots.
|
|
1957
2173
|
*
|
|
@@ -1988,21 +2204,88 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
1988
2204
|
*/
|
|
1989
2205
|
exists(id: TId): Promise<boolean>;
|
|
1990
2206
|
/**
|
|
1991
|
-
* Persists the aggregate (insert or update). Implementations
|
|
2207
|
+
* Persists the aggregate (insert or update). Implementations are
|
|
2208
|
+
* responsible for **persistence only** — they must NOT touch the
|
|
2209
|
+
* aggregate's in-memory state:
|
|
1992
2210
|
*
|
|
1993
2211
|
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
1994
2212
|
* aggregate's expected version does not match the version currently
|
|
1995
2213
|
* stored (optimistic concurrency).
|
|
1996
|
-
* 2.
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2214
|
+
* 2. Write the aggregate to durable storage.
|
|
2215
|
+
*
|
|
2216
|
+
* **Insert vs update — library convention.** A fresh aggregate begins
|
|
2217
|
+
* at `version === 0` (the `Version` brand defaults to `0` in both
|
|
2218
|
+
* `AggregateRoot` and `EventSourcedAggregate`). After the first
|
|
2219
|
+
* versioned mutation (`setState(_, true)`, `apply()`, `commit()`) the
|
|
2220
|
+
* version is `> 0`. Implementations distinguish the two paths by the
|
|
2221
|
+
* incoming `aggregate.version`:
|
|
2222
|
+
*
|
|
2223
|
+
* - `aggregate.version === 0` → **INSERT** (no existing row to lock
|
|
2224
|
+
* against; the write succeeds unconditionally or fails the unique
|
|
2225
|
+
* constraint on `id`).
|
|
2226
|
+
* - `aggregate.version > 0` → **UPDATE** with the OCC predicate
|
|
2227
|
+
* `WHERE id = ? AND version = expected`. If the row count is `0`,
|
|
2228
|
+
* another writer raced you — throw `ConcurrencyConflictError`.
|
|
2229
|
+
*
|
|
2230
|
+
* The library does not formalise this in the type system because
|
|
2231
|
+
* version-bump semantics differ across the two aggregate flavours
|
|
2232
|
+
* (state-stored aggregates bump on the user's call to `setState(_,
|
|
2233
|
+
* true)`; event-sourced aggregates bump on every `apply()` by
|
|
2234
|
+
* definition). The `version === 0` invariant for "never persisted" is
|
|
2235
|
+
* the common contract.
|
|
2236
|
+
*
|
|
2237
|
+
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2238
|
+
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|
|
2239
|
+
* pending events into the outbox, then mark persisted after commit).
|
|
2240
|
+
* Calling `markPersisted` inside `save` clears pending events too early
|
|
2241
|
+
* and breaks the harvest path — and is also why the Vernon/Axon/
|
|
2242
|
+
* EventFlow pattern separates persistence from commit-events.
|
|
2243
|
+
*
|
|
2244
|
+
* If you are not using `withCommit` (custom orchestration), call
|
|
2245
|
+
* `aggregate.markPersisted(aggregate.version)` yourself **after** you
|
|
2246
|
+
* have harvested `aggregate.pendingEvents` for downstream dispatch.
|
|
2002
2247
|
*/
|
|
2003
2248
|
save(aggregate: TAgg): Promise<void>;
|
|
2004
2249
|
/**
|
|
2005
|
-
* Removes the aggregate by id.
|
|
2250
|
+
* Removes the aggregate's row by id. Pure persistence — does NOT
|
|
2251
|
+
* harvest pending events from the aggregate (the contract takes
|
|
2252
|
+
* only the id, so there is no aggregate to harvest from).
|
|
2253
|
+
*
|
|
2254
|
+
* Before reaching for `delete`, ask whether the user-facing "delete"
|
|
2255
|
+
* is the right domain verb. Most are actually state transitions
|
|
2256
|
+
* (*cancel*, *archive*, *close*, *deactivate*, *terminate*) with
|
|
2257
|
+
* proper domain names that should be modelled as state changes plus
|
|
2258
|
+
* a recorded event — not as row removal.
|
|
2259
|
+
*
|
|
2260
|
+
* `delete(id)` belongs in the toolkit for three distinct cases, in
|
|
2261
|
+
* decreasing order of common occurrence (see
|
|
2262
|
+
* `docs/guide/repository.md` → "Deletion and Domain Events" for
|
|
2263
|
+
* worked examples):
|
|
2264
|
+
*
|
|
2265
|
+
* 1. **State transition that records an event.** The user-facing
|
|
2266
|
+
* "delete" maps to a real domain operation (e.g. `order.cancel()`,
|
|
2267
|
+
* `order.archive()`). Call `save(aggregate)`; the row stays with
|
|
2268
|
+
* a status column. `delete(id)` is never called by the use case.
|
|
2269
|
+
*
|
|
2270
|
+
* 2. **Hard-delete with event harvest.** The row genuinely must
|
|
2271
|
+
* vanish (regulatory purge, retention-window expiry, true
|
|
2272
|
+
* termination) *and* the disappearance is a domain fact
|
|
2273
|
+
* subscribers care about. Inside `withCommit`'s transactional
|
|
2274
|
+
* callback, record the deletion event on the aggregate, then
|
|
2275
|
+
* call `delete(id)`. Return the aggregate in the `aggregates`
|
|
2276
|
+
* array so `withCommit` harvests its pending events into the
|
|
2277
|
+
* outbox before the row is gone.
|
|
2278
|
+
*
|
|
2279
|
+
* 3. **Hard-delete without event.** Deletion is invisible to the
|
|
2280
|
+
* domain (abandoned-cart cleanup, expired session rows). No
|
|
2281
|
+
* subscriber cares. If the entity has identity in the ubiquitous
|
|
2282
|
+
* language, you probably want path 1 or 2 instead.
|
|
2283
|
+
*
|
|
2284
|
+
* In pure event-sourced systems `delete` is rarely meaningful —
|
|
2285
|
+
* end-of-lifecycle there is a `Closed` / `Terminated` event in the
|
|
2286
|
+
* stream, and identity persists in the event log. `delete` applies
|
|
2287
|
+
* primarily to state-stored aggregates and snapshot / projection
|
|
2288
|
+
* tables.
|
|
2006
2289
|
*/
|
|
2007
2290
|
delete(id: TId): Promise<void>;
|
|
2008
2291
|
}
|
|
@@ -2276,4 +2559,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2276
2559
|
toJSON(): Readonly<T>;
|
|
2277
2560
|
}
|
|
2278
2561
|
|
|
2279
|
-
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type
|
|
2562
|
+
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type AnyDomainEvent, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IValueObject, type Id, type IdGenerator, type Identifiable, InMemoryOutbox, InfrastructureError, MissingHandlerError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type TransactionScope, type VO, ValueObject, type Version, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
|