@shirudo/ddd-kit 1.0.0-rc.6 → 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/dist/index.d.ts +281 -17
- package/dist/index.js +137 -14
- 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.
|
|
@@ -753,16 +836,57 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = ne
|
|
|
753
836
|
*/
|
|
754
837
|
clearPendingEvents(): void;
|
|
755
838
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
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.
|
|
760
850
|
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
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.
|
|
854
|
+
*
|
|
855
|
+
* @param version - The version assigned by the persistence layer
|
|
856
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
764
857
|
*/
|
|
765
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;
|
|
766
890
|
/**
|
|
767
891
|
* Mutates state and records the resulting domain events in the
|
|
768
892
|
* **canonical record-after-mutation order**. Use this instead of calling
|
|
@@ -1058,12 +1182,57 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1058
1182
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1059
1183
|
clearPendingEvents(): void;
|
|
1060
1184
|
/**
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
* `
|
|
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
|
|
1065
1203
|
*/
|
|
1066
1204
|
markPersisted(version: Version): void;
|
|
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;
|
|
1067
1236
|
protected constructor(id: TId, initialState: TState);
|
|
1068
1237
|
/**
|
|
1069
1238
|
* Validates an event before it is applied. Default is no-op.
|
|
@@ -1638,6 +1807,29 @@ interface TransactionScope<TCtx> {
|
|
|
1638
1807
|
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
1639
1808
|
* events persist atomically with the state change). Skipped when no
|
|
1640
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.
|
|
1641
1833
|
* 3. The transaction commits.
|
|
1642
1834
|
* 4. **After** the commit, `aggregate.markPersisted(aggregate.version)`
|
|
1643
1835
|
* fires on each returned aggregate — only now are pending events
|
|
@@ -1654,6 +1846,19 @@ interface TransactionScope<TCtx> {
|
|
|
1654
1846
|
* If the transaction rolls back, `markPersisted` is **not** called — the
|
|
1655
1847
|
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1656
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.
|
|
1861
|
+
*
|
|
1657
1862
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1658
1863
|
* ```typescript
|
|
1659
1864
|
* const result = await withCommit({ outbox, bus, scope }, async (tx) => {
|
|
@@ -2008,6 +2213,27 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2008
2213
|
* stored (optimistic concurrency).
|
|
2009
2214
|
* 2. Write the aggregate to durable storage.
|
|
2010
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
|
+
*
|
|
2011
2237
|
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2012
2238
|
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|
|
2013
2239
|
* pending events into the outbox, then mark persisted after commit).
|
|
@@ -2021,7 +2247,45 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2021
2247
|
*/
|
|
2022
2248
|
save(aggregate: TAgg): Promise<void>;
|
|
2023
2249
|
/**
|
|
2024
|
-
* 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.
|
|
2025
2289
|
*/
|
|
2026
2290
|
delete(id: TId): Promise<void>;
|
|
2027
2291
|
}
|
|
@@ -2295,4 +2559,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2295
2559
|
toJSON(): Readonly<T>;
|
|
2296
2560
|
}
|
|
2297
2561
|
|
|
2298
|
-
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, withCommit };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -399,6 +399,26 @@ function setEventIdFactory(factory) {
|
|
|
399
399
|
currentEventIdFactory = factory;
|
|
400
400
|
}
|
|
401
401
|
__name(setEventIdFactory, "setEventIdFactory");
|
|
402
|
+
function assertNotThenable(result, helperName) {
|
|
403
|
+
if (result !== null && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`${helperName}: fn returned a thenable. The factory is only installed for the synchronous portion of fn; awaited continuations would see the previous factory. For async-scoped factories use AsyncLocalStorage.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
__name(assertNotThenable, "assertNotThenable");
|
|
410
|
+
function withEventIdFactory(factory, fn) {
|
|
411
|
+
const previous = currentEventIdFactory;
|
|
412
|
+
currentEventIdFactory = factory;
|
|
413
|
+
try {
|
|
414
|
+
const result = fn();
|
|
415
|
+
assertNotThenable(result, "withEventIdFactory");
|
|
416
|
+
return result;
|
|
417
|
+
} finally {
|
|
418
|
+
currentEventIdFactory = previous;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
__name(withEventIdFactory, "withEventIdFactory");
|
|
402
422
|
function resetEventIdFactory() {
|
|
403
423
|
currentEventIdFactory = defaultEventIdFactory;
|
|
404
424
|
}
|
|
@@ -409,6 +429,18 @@ function setClockFactory(factory) {
|
|
|
409
429
|
currentClockFactory = factory;
|
|
410
430
|
}
|
|
411
431
|
__name(setClockFactory, "setClockFactory");
|
|
432
|
+
function withClockFactory(factory, fn) {
|
|
433
|
+
const previous = currentClockFactory;
|
|
434
|
+
currentClockFactory = factory;
|
|
435
|
+
try {
|
|
436
|
+
const result = fn();
|
|
437
|
+
assertNotThenable(result, "withClockFactory");
|
|
438
|
+
return result;
|
|
439
|
+
} finally {
|
|
440
|
+
currentClockFactory = previous;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
__name(withClockFactory, "withClockFactory");
|
|
412
444
|
function resetClockFactory() {
|
|
413
445
|
currentClockFactory = defaultClockFactory;
|
|
414
446
|
}
|
|
@@ -587,18 +619,61 @@ var AggregateRoot = class extends Entity {
|
|
|
587
619
|
this._pendingEvents = [];
|
|
588
620
|
}
|
|
589
621
|
/**
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
622
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
623
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
624
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
625
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
626
|
+
* subclasses **should not** override this method directly.
|
|
627
|
+
*
|
|
628
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
629
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
630
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
631
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
632
|
+
* the safer extension point.
|
|
633
|
+
*
|
|
634
|
+
* If you must override (legitimate cases are very rare), call
|
|
635
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
636
|
+
* runs, then add your logic afterwards.
|
|
594
637
|
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* call.
|
|
638
|
+
* @param version - The version assigned by the persistence layer
|
|
639
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
598
640
|
*/
|
|
599
641
|
markPersisted(version) {
|
|
600
642
|
this.setVersion(version);
|
|
601
643
|
this._pendingEvents = [];
|
|
644
|
+
this.onPersisted(version);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
648
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
649
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
650
|
+
* breaking the framework's pendingEvents cleanup.
|
|
651
|
+
*
|
|
652
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
653
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
654
|
+
* implementation to preserve.
|
|
655
|
+
*
|
|
656
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
657
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
658
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
659
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
660
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
661
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
662
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
663
|
+
* meant to prevent.
|
|
664
|
+
*
|
|
665
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
666
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
667
|
+
* permissive `void` will accept an `async`-override returning
|
|
668
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
669
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
670
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
671
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
672
|
+
* properly awaited extension point.
|
|
673
|
+
*
|
|
674
|
+
* @param version - The version that was just persisted
|
|
675
|
+
*/
|
|
676
|
+
onPersisted(_version) {
|
|
602
677
|
}
|
|
603
678
|
/**
|
|
604
679
|
* Mutates state and records the resulting domain events in the
|
|
@@ -834,14 +909,61 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
834
909
|
this._pendingEvents = [];
|
|
835
910
|
}
|
|
836
911
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
* `
|
|
912
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
913
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
914
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
915
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
916
|
+
* subclasses **should not** override this method directly.
|
|
917
|
+
*
|
|
918
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
919
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
920
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
921
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
922
|
+
* the safer extension point.
|
|
923
|
+
*
|
|
924
|
+
* If you must override (legitimate cases are very rare), call
|
|
925
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
926
|
+
* runs, then add your logic afterwards.
|
|
927
|
+
*
|
|
928
|
+
* @param version - The version assigned by the persistence layer
|
|
929
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
841
930
|
*/
|
|
842
931
|
markPersisted(version) {
|
|
843
932
|
this.setVersion(version);
|
|
844
933
|
this._pendingEvents = [];
|
|
934
|
+
this.onPersisted(version);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
938
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
939
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
940
|
+
* breaking the framework's pendingEvents cleanup.
|
|
941
|
+
*
|
|
942
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
943
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
944
|
+
* implementation to preserve.
|
|
945
|
+
*
|
|
946
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
947
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
948
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
949
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
950
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
951
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
952
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
953
|
+
* meant to prevent.
|
|
954
|
+
*
|
|
955
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
956
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
957
|
+
* permissive `void` will accept an `async`-override returning
|
|
958
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
959
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
960
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
961
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
962
|
+
* properly awaited extension point.
|
|
963
|
+
*
|
|
964
|
+
* @param version - The version that was just persisted
|
|
965
|
+
*/
|
|
966
|
+
onPersisted(_version) {
|
|
845
967
|
}
|
|
846
968
|
constructor(id, initialState) {
|
|
847
969
|
super(id, initialState);
|
|
@@ -987,13 +1109,14 @@ async function withCommit(deps, fn) {
|
|
|
987
1109
|
const { result, aggregates, events } = await deps.scope.transactional(
|
|
988
1110
|
async (ctx) => {
|
|
989
1111
|
const fnResult = await fn(ctx);
|
|
990
|
-
const
|
|
1112
|
+
const uniqueAggregates = Array.from(new Set(fnResult.aggregates));
|
|
1113
|
+
const harvested = uniqueAggregates.flatMap(
|
|
991
1114
|
(agg) => agg.pendingEvents
|
|
992
1115
|
);
|
|
993
1116
|
if (harvested.length > 0) {
|
|
994
1117
|
await deps.outbox.add(harvested);
|
|
995
1118
|
}
|
|
996
|
-
return { ...fnResult, events: harvested };
|
|
1119
|
+
return { ...fnResult, aggregates: uniqueAggregates, events: harvested };
|
|
997
1120
|
}
|
|
998
1121
|
);
|
|
999
1122
|
for (const agg of aggregates) {
|
|
@@ -1163,6 +1286,6 @@ var InMemoryOutbox = class {
|
|
|
1163
1286
|
}
|
|
1164
1287
|
};
|
|
1165
1288
|
|
|
1166
|
-
export { AggregateNotFoundError, AggregateRoot, CommandBus, ConcurrencyConflictError, DomainError, Entity, EventBusImpl, EventSourcedAggregate, InMemoryOutbox, InfrastructureError, MissingHandlerError, QueryBus, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };
|
|
1289
|
+
export { AggregateNotFoundError, AggregateRoot, CommandBus, ConcurrencyConflictError, DomainError, Entity, EventBusImpl, EventSourcedAggregate, InMemoryOutbox, InfrastructureError, MissingHandlerError, QueryBus, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
|
|
1167
1290
|
//# sourceMappingURL=index.js.map
|
|
1168
1291
|
//# sourceMappingURL=index.js.map
|