@shirudo/ddd-kit 3.0.0-rc.10 → 3.0.0-rc.11
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/chunks/kit-errors.d.ts +287 -108
- package/dist/chunks/kit-errors.js +250 -111
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/chunks/{ports.js → read-stream-pages.js} +531 -92
- package/dist/chunks/read-stream-pages.js.map +1 -0
- package/dist/chunks/snapshot-store.d.ts +542 -115
- package/dist/index.d.ts +598 -434
- package/dist/index.js +1508 -637
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +1 -1
- package/dist/testing.d.ts +106 -15
- package/dist/testing.js +546 -52
- package/dist/testing.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunks/ports.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as DomainError, ot as ReplayRejectedError } from "./kit-errors.js";
|
|
2
2
|
import { Result } from "@shirudo/result";
|
|
3
3
|
//#region src/internal/json-value.d.ts
|
|
4
4
|
/** A primitive value represented without loss by JSON. */
|
|
@@ -160,9 +160,9 @@ interface PublishedCommand<TType extends string = string, TPayload extends JsonV
|
|
|
160
160
|
*/
|
|
161
161
|
type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Result<R, E>>;
|
|
162
162
|
//#endregion
|
|
163
|
-
//#region src/domain/aggregate/aggregate-
|
|
163
|
+
//#region src/domain/aggregate/aggregate-identity.d.ts
|
|
164
164
|
/**
|
|
165
|
-
*
|
|
165
|
+
* The identity of one aggregate instance, as a value.
|
|
166
166
|
*
|
|
167
167
|
* Aggregate ids are type-scoped, so the raw id alone is not globally unique:
|
|
168
168
|
* `SalesOrder 1` and `FulfillmentOrder 1` are different aggregates. Event
|
|
@@ -170,13 +170,14 @@ type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Resu
|
|
|
170
170
|
* therefore carry both fields instead of defining boundary-specific variants.
|
|
171
171
|
*
|
|
172
172
|
* `aggregateType` is a stable technical stream category. Renaming it changes
|
|
173
|
-
* persistence keys and orphans checkpoints unless the stored
|
|
174
|
-
* migrated. When bounded contexts share infrastructure and
|
|
175
|
-
* name, qualify it at the source (`sales.order`,
|
|
173
|
+
* persistence keys and orphans checkpoints unless the stored aggregate
|
|
174
|
+
* identities are migrated. When bounded contexts share infrastructure and
|
|
175
|
+
* reuse a domain name, qualify it at the source (`sales.order`,
|
|
176
|
+
* `fulfillment.order`). The kit
|
|
176
177
|
* deliberately adds no separate `boundedContext` field: qualification remains
|
|
177
178
|
* the consumer's naming decision.
|
|
178
179
|
*/
|
|
179
|
-
interface
|
|
180
|
+
interface AggregateIdentity<TAggregateId extends string = string> {
|
|
180
181
|
readonly aggregateType: string;
|
|
181
182
|
readonly aggregateId: TAggregateId;
|
|
182
183
|
}
|
|
@@ -323,8 +324,9 @@ type AnyDomainEvent = DomainEvent<string, unknown>;
|
|
|
323
324
|
* A domain event accepted by an aggregate but not yet given its recording
|
|
324
325
|
* identity, recording time, or delivery metadata.
|
|
325
326
|
*
|
|
326
|
-
* The aggregate owns the event type, payload, source
|
|
327
|
-
* schema version because those values describe the business
|
|
327
|
+
* The aggregate owns the event type, payload, source aggregate identity,
|
|
328
|
+
* and payload schema version because those values describe the business
|
|
329
|
+
* fact it produced.
|
|
328
330
|
* The application shell later turns this value into a {@link DomainEvent}.
|
|
329
331
|
*/
|
|
330
332
|
interface UncommittedDomainEvent<T extends string, P = void> {
|
|
@@ -434,6 +436,27 @@ interface DomainEventFactory {
|
|
|
434
436
|
*/
|
|
435
437
|
readonly now: () => Date;
|
|
436
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Creates an immutable, instance-bound domain-event factory.
|
|
441
|
+
*
|
|
442
|
+
* The supplied functions are read once and captured by value. The returned
|
|
443
|
+
* object is frozen, so another request, test, or library cannot replace its
|
|
444
|
+
* policy. Its {@link DomainEventFactory.createStamp} method is the
|
|
445
|
+
* application-shell bridge that records an accepted aggregate decision.
|
|
446
|
+
* Passing the factory through `AggregateConfig`
|
|
447
|
+
* enables the explicitly named convenience methods, whose defaults read time
|
|
448
|
+
* and randomness.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* const domainEvents = createDomainEventFactory({
|
|
453
|
+
* eventIdFactory: () => uuidv7(),
|
|
454
|
+
* clock: () => new Date(),
|
|
455
|
+
* });
|
|
456
|
+
* order.confirm();
|
|
457
|
+
* recordPendingEvents(order, domainEvents);
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
437
460
|
declare function createDomainEventFactory(options?: DomainEventFactoryOptions): DomainEventFactory;
|
|
438
461
|
/**
|
|
439
462
|
* Immutable UUID-v4/platform-clock factory used by the top-level
|
|
@@ -446,9 +469,9 @@ declare function createUncommittedDomainEvent<T extends string, P>(type: T, payl
|
|
|
446
469
|
/**
|
|
447
470
|
* Attaches shell-owned recording data to an accepted aggregate decision.
|
|
448
471
|
*
|
|
449
|
-
* The decision supplies the domain type, payload, source
|
|
450
|
-
* schema version. The stamp supplies only event
|
|
451
|
-
* trace metadata.
|
|
472
|
+
* The decision supplies the domain type, payload, source aggregate
|
|
473
|
+
* identity, and payload schema version. The stamp supplies only event
|
|
474
|
+
* identity, recording time, and trace metadata.
|
|
452
475
|
*/
|
|
453
476
|
declare function recordDomainEvent<T extends string, P>(event: UncommittedDomainEvent<T, P>, stamp: DomainEventStamp): DomainEvent<T, P>;
|
|
454
477
|
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
@@ -531,7 +554,7 @@ type EventCommitCandidatePosition = Omit<CommitPosition, "previousEventfulAggreg
|
|
|
531
554
|
*/
|
|
532
555
|
interface EventCommitCandidate<Evt extends AnyDomainEvent> {
|
|
533
556
|
readonly event: Evt;
|
|
534
|
-
readonly source:
|
|
557
|
+
readonly source: AggregateIdentity;
|
|
535
558
|
readonly position: EventCommitCandidatePosition;
|
|
536
559
|
}
|
|
537
560
|
/**
|
|
@@ -541,7 +564,7 @@ interface EventCommitCandidate<Evt extends AnyDomainEvent> {
|
|
|
541
564
|
*/
|
|
542
565
|
interface CommittedDomainEvent<Evt extends AnyDomainEvent> {
|
|
543
566
|
readonly event: Evt;
|
|
544
|
-
readonly source:
|
|
567
|
+
readonly source: AggregateIdentity;
|
|
545
568
|
readonly position: CommitPosition;
|
|
546
569
|
}
|
|
547
570
|
//#endregion
|
|
@@ -625,6 +648,22 @@ interface OutboxWriter<Evt extends AnyDomainEvent> {
|
|
|
625
648
|
* should reject it rather than replace or silently reinterpret it as a retry.
|
|
626
649
|
*/
|
|
627
650
|
add: (events: ReadonlyArray<EventCommitCandidate<Evt>>) => Promise<void>;
|
|
651
|
+
/**
|
|
652
|
+
* Ends the event sources of removed aggregates. Called from inside
|
|
653
|
+
* `withCommit`'s transactional callback, after `add()`, atomically with
|
|
654
|
+
* the removal.
|
|
655
|
+
*
|
|
656
|
+
* For every source that has a head, the adapter marks the head as ended
|
|
657
|
+
* in the SAME transaction. The mark must outlive any retention of heads:
|
|
658
|
+
* a pruned ended head opens the source again. A source without a head
|
|
659
|
+
* has no events, so it has no continuity to protect: it stays open, and
|
|
660
|
+
* its first event starts a new head. After the mark, `add()` must reject
|
|
661
|
+
* a new event of that source: its aggregate was created again under a
|
|
662
|
+
* removed identity, which the kit does not support. An exact retry of a
|
|
663
|
+
* stored event stays idempotent, and ending a source twice is not an
|
|
664
|
+
* error.
|
|
665
|
+
*/
|
|
666
|
+
endEventSources: (sources: ReadonlyArray<AggregateIdentity>) => Promise<void>;
|
|
628
667
|
}
|
|
629
668
|
/**
|
|
630
669
|
* Transactional outbox port: the bridge between the write-side
|
|
@@ -773,7 +812,7 @@ interface DurableCommandMessage<C extends PublishedCommand> extends CommandMessa
|
|
|
773
812
|
*/
|
|
774
813
|
interface CommandCommitOriginCandidate {
|
|
775
814
|
readonly eventId: string;
|
|
776
|
-
readonly source:
|
|
815
|
+
readonly source: AggregateIdentity;
|
|
777
816
|
readonly position: EventCommitCandidatePosition;
|
|
778
817
|
}
|
|
779
818
|
/**
|
|
@@ -801,6 +840,14 @@ interface CommandOutboxCommitCandidate<C extends PublishedCommand> {
|
|
|
801
840
|
*/
|
|
802
841
|
interface CommandOutboxWriter<C extends PublishedCommand> {
|
|
803
842
|
add(commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>): Promise<void>;
|
|
843
|
+
/**
|
|
844
|
+
* Ends the source cursors of removed aggregates, in the same transaction
|
|
845
|
+
* as the removal. After the mark, `add()` must reject a new commit whose
|
|
846
|
+
* origin comes from an ended source; an exact retry by `origin.eventId`
|
|
847
|
+
* stays idempotent. A source without a cursor stays open, and ending a
|
|
848
|
+
* source twice is not an error. See `OutboxWriter.endEventSources`.
|
|
849
|
+
*/
|
|
850
|
+
endEventSources(sources: ReadonlyArray<AggregateIdentity>): Promise<void>;
|
|
804
851
|
}
|
|
805
852
|
/** Maps one private accepted event to zero or more addressed commands. */
|
|
806
853
|
type CommandOutboxMapper<Evt extends AnyDomainEvent, C extends PublishedCommand> = (event: Evt) => ReadonlyArray<CommandMessageContent<C>>;
|
|
@@ -937,6 +984,8 @@ interface AggregateSnapshot<TState> {
|
|
|
937
984
|
*/
|
|
938
985
|
interface Aggregate<TId extends Id<string>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
|
|
939
986
|
readonly id: TId;
|
|
987
|
+
/** The aggregate type and the id: the full identity of the aggregate. */
|
|
988
|
+
readonly aggregateIdentity: AggregateIdentity<TId>;
|
|
940
989
|
readonly version: Version;
|
|
941
990
|
readonly pendingEvents: ReadonlyArray<PendingDomainEvent<TEvent>>;
|
|
942
991
|
}
|
|
@@ -953,7 +1002,8 @@ interface ReplayableAggregate<TId extends Id<string>, TEvent extends AnyDomainEv
|
|
|
953
1002
|
* `Result` because event-stream corruption is an expected
|
|
954
1003
|
* recoverable failure at the infrastructure boundary: a `DomainError`
|
|
955
1004
|
* thrown by a fold arrives as `Err`. Every other failure propagates
|
|
956
|
-
* after the all-or-nothing rollback.
|
|
1005
|
+
* after the all-or-nothing rollback. The pending-decisions guard runs
|
|
1006
|
+
* for an empty history as well; an empty history is otherwise a no-op.
|
|
957
1007
|
*
|
|
958
1008
|
* @throws ForeignEventError when a history event names another aggregate
|
|
959
1009
|
* @throws UnreplayableAggregateError when the target carries pending
|
|
@@ -1238,6 +1288,15 @@ interface TransactionalOptions {
|
|
|
1238
1288
|
* eagerly cancellable.
|
|
1239
1289
|
*/
|
|
1240
1290
|
readonly signal?: AbortSignal;
|
|
1291
|
+
/**
|
|
1292
|
+
* Tells the caller that a new attempt starts. A scope that can open more
|
|
1293
|
+
* than one transaction for one call (a retrying scope) calls it before
|
|
1294
|
+
* each attempt opens its transaction. The caller can then tell a failure
|
|
1295
|
+
* to open the next attempt apart from a failure of the previous one. A
|
|
1296
|
+
* scope that opens one transaction per call can ignore it. A scope that
|
|
1297
|
+
* wraps another scope passes the options on unchanged.
|
|
1298
|
+
*/
|
|
1299
|
+
readonly onAttemptStart?: () => void;
|
|
1241
1300
|
}
|
|
1242
1301
|
/**
|
|
1243
1302
|
* Transaction-scope abstraction.
|
|
@@ -1759,15 +1818,15 @@ type IdempotencyClaim = {
|
|
|
1759
1818
|
* production pattern and the only family that proves atomic command effect +
|
|
1760
1819
|
* idempotency completion without reconciliation.
|
|
1761
1820
|
*
|
|
1762
|
-
* A NON-transactional store (the in-memory reference, a separate
|
|
1763
|
-
* store) cannot see commits or rollbacks. Every fresh claim
|
|
1764
|
-
* a store-minted token and bounded lease. The wrapper
|
|
1765
|
-
* transaction runs; `complete`, `renew`, `confirm`,
|
|
1766
|
-
* compare the token so a stale owner cannot
|
|
1767
|
-
* expired PENDING claim may be replaced. An
|
|
1768
|
-
* replayed or released automatically:
|
|
1769
|
-
* `reconciliation-required`, and the application must
|
|
1770
|
-
* truth. `unknown` keeps it blocked.
|
|
1821
|
+
* A NON-transactional store (the in-memory reference on its own, a separate
|
|
1822
|
+
* durable store) cannot see commits or rollbacks. Every fresh claim
|
|
1823
|
+
* therefore returns a store-minted token and bounded lease. The wrapper
|
|
1824
|
+
* renews it while the transaction runs; `complete`, `renew`, `confirm`,
|
|
1825
|
+
* `abandon`, and `reconcile` compare the token so a stale owner cannot
|
|
1826
|
+
* mutate a successor claim. An expired PENDING claim may be replaced. An
|
|
1827
|
+
* expired STAGED outcome is never replayed or released automatically:
|
|
1828
|
+
* `claim` returns `reconciliation-required`, and the application must
|
|
1829
|
+
* consult the source of truth. `unknown` keeps it blocked.
|
|
1771
1830
|
*
|
|
1772
1831
|
* A lease is coordination, not a security or exactly-once boundary. To return
|
|
1773
1832
|
* `not-committed` safely, the source transaction must persist an idempotency
|
|
@@ -2026,23 +2085,23 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
|
|
|
2026
2085
|
* Implementations may serialize more than the requested keys, but never
|
|
2027
2086
|
* less. The callback is non-reentrant for an overlapping key set.
|
|
2028
2087
|
*/
|
|
2029
|
-
withCheckpointLocks<R>(ctx: TCtx, projection: string,
|
|
2088
|
+
withCheckpointLocks<R>(ctx: TCtx, projection: string, identities: ReadonlyArray<AggregateIdentity>, work: () => Promise<R>): Promise<R>;
|
|
2030
2089
|
/**
|
|
2031
|
-
* The stored watermark receipt for `(projection,
|
|
2090
|
+
* The stored watermark receipt for `(projection, identity)`, or `undefined`
|
|
2032
2091
|
* when this projection has never applied an event of that
|
|
2033
2092
|
* aggregate. Called inside the projector's transaction.
|
|
2034
2093
|
*/
|
|
2035
|
-
load(ctx: TCtx, projection: string,
|
|
2094
|
+
load(ctx: TCtx, projection: string, identity: AggregateIdentity): Promise<ProjectionCheckpoint | undefined>;
|
|
2036
2095
|
/**
|
|
2037
2096
|
* Persists the watermark receipt, overwriting a previous one (last write
|
|
2038
2097
|
* wins; the projector only calls this with advancing checkpoints).
|
|
2039
2098
|
* Called inside the projector's transaction, after the read-model
|
|
2040
2099
|
* update it accounts for.
|
|
2041
2100
|
*/
|
|
2042
|
-
save(ctx: TCtx, projection: string,
|
|
2101
|
+
save(ctx: TCtx, projection: string, identity: AggregateIdentity, checkpoint: ProjectionCheckpoint): Promise<void>;
|
|
2043
2102
|
/**
|
|
2044
2103
|
* The wait-for-version building block: `true` when the stored
|
|
2045
|
-
* watermark for `(projection,
|
|
2104
|
+
* watermark for `(projection, identity)` is at or past
|
|
2046
2105
|
* `position`. Runs OUTSIDE any transaction (a query-side poll).
|
|
2047
2106
|
*
|
|
2048
2107
|
* Pass the position of the LAST event your commit emitted: all
|
|
@@ -2050,7 +2109,7 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
|
|
|
2050
2109
|
* on the version alone would report "reached" while later events
|
|
2051
2110
|
* of the same commit are still unapplied.
|
|
2052
2111
|
*/
|
|
2053
|
-
hasReached(projection: string,
|
|
2112
|
+
hasReached(projection: string, identity: AggregateIdentity, position: ProjectionPosition): Promise<boolean>;
|
|
2054
2113
|
/**
|
|
2055
2114
|
* Deletes every checkpoint of `projection` (other projections'
|
|
2056
2115
|
* checkpoints are untouched): the rebuild entry point. Called
|
|
@@ -2067,7 +2126,7 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
|
|
|
2067
2126
|
* read-model writes.
|
|
2068
2127
|
*
|
|
2069
2128
|
* The projector feed MUST contain every committed envelope for each aggregate
|
|
2070
|
-
*
|
|
2129
|
+
* identity it carries, including event types this read model does not use.
|
|
2071
2130
|
* Handle those events as explicit no-ops in `apply`: the projector still
|
|
2072
2131
|
* advances their cursor. Filtering a broker subscription by event type drops
|
|
2073
2132
|
* positions from the source chain and turns the next commit into a real gap.
|
|
@@ -2106,6 +2165,298 @@ interface Projection<Evt extends AnyDomainEvent, TCtx = unknown> {
|
|
|
2106
2165
|
truncate?(ctx: TCtx): Promise<void>;
|
|
2107
2166
|
}
|
|
2108
2167
|
//#endregion
|
|
2168
|
+
//#region src/persistence/repository/identity-map.d.ts
|
|
2169
|
+
/**
|
|
2170
|
+
* A class reference used as the type key of the identity map. Keying
|
|
2171
|
+
* on the CLASS (not a name string) makes collisions impossible by
|
|
2172
|
+
* construction: `Restaurant` and `Booking` are different keys even if
|
|
2173
|
+
* someone names two aggregates identically across modules, and there
|
|
2174
|
+
* is no string-discipline to maintain.
|
|
2175
|
+
*
|
|
2176
|
+
* `Function & { prototype: TAgg }` carries this alone. The kit's aggregate
|
|
2177
|
+
* convention is a **protected constructor** plus static factories, and
|
|
2178
|
+
* TypeScript rejects assigning such a class to a construct-signature type, so
|
|
2179
|
+
* the prototype witness is what accepts them. It infers `TAgg` as well.
|
|
2180
|
+
* Measured: a construct signature beside it changes neither what the type
|
|
2181
|
+
* accepts nor what it infers, and it was the last `any` in the published
|
|
2182
|
+
* types.
|
|
2183
|
+
*/
|
|
2184
|
+
type AggregateClass<TAgg> = Function & {
|
|
2185
|
+
prototype: TAgg;
|
|
2186
|
+
};
|
|
2187
|
+
/**
|
|
2188
|
+
* Per-unit-of-work Identity Map (Fowler, PoEAA): within one operation,
|
|
2189
|
+
* one aggregate type+id maps to exactly ONE in-memory instance.
|
|
2190
|
+
*
|
|
2191
|
+
* This is the shipped implementation of the contract the
|
|
2192
|
+
* Repository guide (`docs/guide/repository.md`) places on
|
|
2193
|
+
* `AggregatePersistence` implementations: two `findById(id)` calls in the same
|
|
2194
|
+
* unit of work MUST return the same instance, because commit-token
|
|
2195
|
+
* write registration dedupes by JavaScript object identity. Two instances for
|
|
2196
|
+
* one logical aggregate can otherwise produce two tokens, two harvests,
|
|
2197
|
+
* and two post-commit lifecycle calls.
|
|
2198
|
+
*
|
|
2199
|
+
* Storage is two-level (per-type stores created lazily), so
|
|
2200
|
+
* `Restaurant:123` and `Booking:123` can never collide: the type key
|
|
2201
|
+
* is the aggregate CLASS, not the id alone and not a name string.
|
|
2202
|
+
*
|
|
2203
|
+
* Repository read-path contract:
|
|
2204
|
+
*
|
|
2205
|
+
* ```ts
|
|
2206
|
+
* async findById(id: OrderId): Promise<Order | undefined> {
|
|
2207
|
+
* const cached = this.tracking.identityMap.get(Order, id);
|
|
2208
|
+
* if (cached) return cached;
|
|
2209
|
+
* // Deleted in this unit of work = gone, even if the physical
|
|
2210
|
+
* // delete is deferred and the row is still visible in the tx.
|
|
2211
|
+
* if (this.tracking.identityMap.isDeleted(Order, id)) return undefined;
|
|
2212
|
+
*
|
|
2213
|
+
* const row = await this.loadRow(id);
|
|
2214
|
+
* if (!row) return undefined;
|
|
2215
|
+
* const order = Order.reconstitute(row.id, row.state, row.version);
|
|
2216
|
+
* return this.tracking.trackLoaded(order);
|
|
2217
|
+
* }
|
|
2218
|
+
* ```
|
|
2219
|
+
*
|
|
2220
|
+
* Deletion is final within an operation: {@link delete} removes the
|
|
2221
|
+
* entry AND records a tombstone, so a later {@link set} of the same
|
|
2222
|
+
* type+id throws `AggregateDeletedError`: a second instance of a
|
|
2223
|
+
* deleted aggregate can never sneak back into the unit of work, even
|
|
2224
|
+
* through a repository whose row delete is deferred.
|
|
2225
|
+
*
|
|
2226
|
+
* Lifetime is ONE unit of work: the `UnitOfWork` creates a fresh map
|
|
2227
|
+
* per `run()` and clears it on close. Never cache across operations;
|
|
2228
|
+
* that would silently bypass optimistic concurrency control.
|
|
2229
|
+
*/
|
|
2230
|
+
declare class IdentityMap {
|
|
2231
|
+
private readonly _stores;
|
|
2232
|
+
private readonly _deleted;
|
|
2233
|
+
private _pendingAtRegistration;
|
|
2234
|
+
/** The cached instance for type+id, or `undefined` (also after {@link delete}). */
|
|
2235
|
+
get<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): TAgg | undefined;
|
|
2236
|
+
/** Whether an instance is registered for type+id (false after {@link delete}). */
|
|
2237
|
+
has<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
|
|
2238
|
+
/**
|
|
2239
|
+
* Whether type+id was {@link delete}d in this unit of work. The
|
|
2240
|
+
* read path checks this BEFORE hydrating and returns `null`, so
|
|
2241
|
+
* "deleted in this operation" reads uniformly as not-found,
|
|
2242
|
+
* regardless of whether the repository's physical delete already
|
|
2243
|
+
* removed the row or is deferred within the transaction. Without
|
|
2244
|
+
* the check, a read-only probe of a deleted aggregate would crash
|
|
2245
|
+
* in {@link set} for deferred-write repositories and return `null`
|
|
2246
|
+
* for immediate-write ones.
|
|
2247
|
+
*/
|
|
2248
|
+
isDeleted<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
|
|
2249
|
+
/**
|
|
2250
|
+
* Registers the hydrated instance under its class and the id of its
|
|
2251
|
+
* `aggregateIdentity`.
|
|
2252
|
+
*
|
|
2253
|
+
* - Re-registering the SAME instance is a no-op (idempotent).
|
|
2254
|
+
* - Registering a DIFFERENT instance for an occupied type+id throws:
|
|
2255
|
+
* that is precisely the identity-map violation this class exists
|
|
2256
|
+
* to prevent (the repository hydrated twice instead of checking
|
|
2257
|
+
* {@link get} first), and letting it pass would double-harvest
|
|
2258
|
+
* events downstream.
|
|
2259
|
+
* - Registering a type+id that was {@link delete}d in this unit of
|
|
2260
|
+
* work throws `AggregateDeletedError`: deletion is final within
|
|
2261
|
+
* the operation.
|
|
2262
|
+
*/
|
|
2263
|
+
set<TAgg extends {
|
|
2264
|
+
readonly aggregateIdentity: AggregateIdentity;
|
|
2265
|
+
}>(type: AggregateClass<TAgg>, aggregate: TAgg): void;
|
|
2266
|
+
/**
|
|
2267
|
+
* Registered instances that have recorded MORE pending events than they
|
|
2268
|
+
* carried when first registered (loaded). Used by the unit of work's
|
|
2269
|
+
* end-of-run guard: an aggregate that gained events after load but was
|
|
2270
|
+
* never enrolled would silently drop them. A read-only load, or a
|
|
2271
|
+
* reconstitution that already carried events, shows no increase and is
|
|
2272
|
+
* not reported.
|
|
2273
|
+
*/
|
|
2274
|
+
instancesWithNewPendingEvents(): unknown[];
|
|
2275
|
+
/**
|
|
2276
|
+
* Takes back a registration WITHOUT a tombstone: the entry is removed
|
|
2277
|
+
* only when the stored instance IS the given one, and a later
|
|
2278
|
+
* {@link set} of the same type+id stays legal. The Unit of Work calls
|
|
2279
|
+
* this when a registration step fails after {@link set} already ran, so
|
|
2280
|
+
* the failed instance cannot be served by `findById` as a phantom.
|
|
2281
|
+
* This is rollback, not deletion; deletion finality belongs to
|
|
2282
|
+
* {@link delete}.
|
|
2283
|
+
*/
|
|
2284
|
+
discard<TAgg>(type: AggregateClass<TAgg>, id: Id<string>, aggregate: TAgg): void;
|
|
2285
|
+
/**
|
|
2286
|
+
* Removes the entry for type+id and records a tombstone: subsequent
|
|
2287
|
+
* {@link get} / {@link has} report absence, and a subsequent
|
|
2288
|
+
* {@link set} of the same type+id throws `AggregateDeletedError`.
|
|
2289
|
+
* The Unit of Work calls this as part of `repository.remove(aggregate)`;
|
|
2290
|
+
* repository adapters receive only the read-only
|
|
2291
|
+
* identity-map view.
|
|
2292
|
+
*/
|
|
2293
|
+
delete<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): void;
|
|
2294
|
+
/** Empties all stores and tombstones. Called by the unit of work on close. */
|
|
2295
|
+
clear(): void;
|
|
2296
|
+
}
|
|
2297
|
+
//#endregion
|
|
2298
|
+
//#region src/persistence/repository/persistence-model.d.ts
|
|
2299
|
+
/** Whether a baseline represents an existing row or a pending insert. */
|
|
2300
|
+
type PersistenceLifecycle = "loaded" | "new";
|
|
2301
|
+
/**
|
|
2302
|
+
* Adapter-owned projection and change derivation for one aggregate type.
|
|
2303
|
+
*
|
|
2304
|
+
* The domain model does not implement this contract. A repository adapter
|
|
2305
|
+
* chooses what it persists, how that projection is captured at load, and
|
|
2306
|
+
* whether a change set is a partial diff or a full replacement.
|
|
2307
|
+
*/
|
|
2308
|
+
interface PersistenceModel<TAggregate, TBaseline, TChangeSet> {
|
|
2309
|
+
/**
|
|
2310
|
+
* Captures the adapter's persistence projection at the current moment.
|
|
2311
|
+
* Return a detached value or an immutable value object: the Unit of Work
|
|
2312
|
+
* retains it as a baseline and cannot make an arbitrary adapter type safe.
|
|
2313
|
+
*
|
|
2314
|
+
* Capture must be deterministic for an unchanged aggregate: the Unit of
|
|
2315
|
+
* Work compares successive captures to detect mutation after write
|
|
2316
|
+
* registration. A capture that embeds ambient values (clock reads,
|
|
2317
|
+
* random ids) would make every commit look mutated. The default
|
|
2318
|
+
* comparison is the package's structural deep equality, which matches
|
|
2319
|
+
* `Set` members and `Map` keys by reference (JS `SameValueZero`
|
|
2320
|
+
* semantics): a capture that re-materializes object Set members or Map
|
|
2321
|
+
* keys on every call must supply {@link captureEquals}.
|
|
2322
|
+
*
|
|
2323
|
+
* A column that the store owns stays out of the capture, for example an
|
|
2324
|
+
* `updatedAt` that the adapter stamps; `flush` writes it. A captured store
|
|
2325
|
+
* column pushes a stale value back on the next write, or it marks every
|
|
2326
|
+
* row as changed.
|
|
2327
|
+
*/
|
|
2328
|
+
capture(aggregate: TAggregate): TBaseline;
|
|
2329
|
+
/**
|
|
2330
|
+
* Adapter-owned equality for two captures of the persistence projection.
|
|
2331
|
+
* Optional: the default is the package's structural deep equality (Set
|
|
2332
|
+
* members and Map keys by reference). Supply it when the capture shape
|
|
2333
|
+
* needs domain-specific comparison, for example rebuilt value-object Set
|
|
2334
|
+
* members compared by value.
|
|
2335
|
+
*/
|
|
2336
|
+
readonly captureEquals?: (a: TBaseline, b: TBaseline) => boolean;
|
|
2337
|
+
/**
|
|
2338
|
+
* Derives the adapter's write payload from its own baseline.
|
|
2339
|
+
*
|
|
2340
|
+
* `baseline` is absent for a new aggregate. `lifecycle` disambiguates that
|
|
2341
|
+
* case from an adapter whose loaded baseline type itself admits `undefined`.
|
|
2342
|
+
* The returned payload must not share mutable references with the aggregate;
|
|
2343
|
+
* it is the exact value later handed to `flush`.
|
|
2344
|
+
*/
|
|
2345
|
+
changes(baseline: TBaseline | undefined, aggregate: TAggregate, lifecycle: PersistenceLifecycle): TChangeSet;
|
|
2346
|
+
/** Tells orchestration whether the derived state write is empty. */
|
|
2347
|
+
isEmpty(changes: TChangeSet): boolean;
|
|
2348
|
+
}
|
|
2349
|
+
declare const persistenceBaselineBrand: unique symbol;
|
|
2350
|
+
/**
|
|
2351
|
+
* Opaque, typed receipt for an adapter-owned persistence baseline.
|
|
2352
|
+
*
|
|
2353
|
+
* It intentionally exposes no data. The Unit of Work may retain the token and
|
|
2354
|
+
* ask the owning adapter capability to derive changes, but cannot branch on or
|
|
2355
|
+
* couple itself to the baseline's shape.
|
|
2356
|
+
*/
|
|
2357
|
+
interface PersistenceBaseline<TAggregate, TChangeSet> {
|
|
2358
|
+
readonly [persistenceBaselineBrand]: (aggregate: TAggregate) => TChangeSet;
|
|
2359
|
+
}
|
|
2360
|
+
/** A derived adapter change set plus its adapter-defined emptiness result. */
|
|
2361
|
+
interface PersistenceChanges<TChangeSet> {
|
|
2362
|
+
readonly value: TChangeSet;
|
|
2363
|
+
readonly empty: boolean;
|
|
2364
|
+
}
|
|
2365
|
+
/** Captures a baseline for an aggregate restored by a repository adapter. */
|
|
2366
|
+
declare function capturePersistenceBaseline<TAggregate, TBaseline, TChangeSet>(model: PersistenceModel<TAggregate, TBaseline, TChangeSet>, aggregate: TAggregate): PersistenceBaseline<TAggregate, TChangeSet>;
|
|
2367
|
+
/** Creates the explicit no-row baseline for a newly added aggregate. */
|
|
2368
|
+
declare function insertPersistenceBaseline<TAggregate, TBaseline, TChangeSet>(model: PersistenceModel<TAggregate, TBaseline, TChangeSet>): PersistenceBaseline<TAggregate, TChangeSet>;
|
|
2369
|
+
/**
|
|
2370
|
+
* Captures the aggregate's current adapter projection using the capability
|
|
2371
|
+
* carried by an existing baseline. Used to seal persistence-last registration.
|
|
2372
|
+
*/
|
|
2373
|
+
declare function recapturePersistenceBaseline<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): PersistenceBaseline<TAggregate, TChangeSet>;
|
|
2374
|
+
/**
|
|
2375
|
+
* Recaptures the adapter projection and reports whether it drifted from the
|
|
2376
|
+
* baseline's stored capture, using the model's `captureEquals` when supplied
|
|
2377
|
+
* and structural deep equality otherwise.
|
|
2378
|
+
*
|
|
2379
|
+
* This, not `changes()`/`isEmpty()`, is the mutation detector: the
|
|
2380
|
+
* `PersistenceModel` contract explicitly permits a full-replacement change
|
|
2381
|
+
* set whose `isEmpty` is never true, so a non-empty change set proves
|
|
2382
|
+
* nothing about mutation. Comparing capture to capture asks the honest
|
|
2383
|
+
* question independent of the model's diffing strategy. A `"new"` lifecycle
|
|
2384
|
+
* baseline has no stored capture and never reports drift.
|
|
2385
|
+
*/
|
|
2386
|
+
declare function persistenceProjectionDrifted<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): boolean;
|
|
2387
|
+
/** Derives a typed adapter change set without exposing the stored baseline. */
|
|
2388
|
+
declare function derivePersistenceChanges<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): PersistenceChanges<TChangeSet>;
|
|
2389
|
+
//#endregion
|
|
2390
|
+
//#region src/application/unit-of-work/persistence-contract.d.ts
|
|
2391
|
+
/** The explicit persistence intent registered for one tracked aggregate. */
|
|
2392
|
+
type AggregateWriteIntent = "add" | "update" | "remove";
|
|
2393
|
+
/** Read-only Identity Map operations available to repository adapters. */
|
|
2394
|
+
type UnitOfWorkIdentityMap = Pick<IdentityMap, "get" | "has" | "isDeleted">;
|
|
2395
|
+
/**
|
|
2396
|
+
* Repository-specific tracking capability handed only to its adapter.
|
|
2397
|
+
*
|
|
2398
|
+
* Read paths call {@link RepositoryTracking.trackLoaded} before returning a
|
|
2399
|
+
* restored aggregate.
|
|
2400
|
+
* Write methods exposed to application code are replaced by Unit-of-Work-owned
|
|
2401
|
+
* `add`, `update`, and `remove` registrations, so an adapter implementation of
|
|
2402
|
+
* those methods cannot perform durable I/O early or skip event harvesting.
|
|
2403
|
+
*
|
|
2404
|
+
* Contract for repository implementations:
|
|
2405
|
+
* - `findById(id)` checks `identityMap.get` BEFORE hydrating, treats
|
|
2406
|
+
* `identityMap.isDeleted` as not-found (`undefined`), and returns
|
|
2407
|
+
* `tracking.trackLoaded(aggregate)` after hydration. This captures the
|
|
2408
|
+
* expected version before application code can mutate the instance.
|
|
2409
|
+
* - Adapter objects have no lifecycle methods; the facade installs the
|
|
2410
|
+
* Unit-of-Work-owned `add`, `update` unless the definition is append-only,
|
|
2411
|
+
* and `remove` with `physicalRemoval`. An adapter that defines `add`,
|
|
2412
|
+
* `update`, or `remove` fails with `InvalidRepositoryAdapterError`.
|
|
2413
|
+
* - Other repository methods are reads. A custom method that performs a write
|
|
2414
|
+
* would bypass the Unit of Work and violates the adapter contract.
|
|
2415
|
+
*/
|
|
2416
|
+
interface RepositoryTracking<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
|
|
2417
|
+
/**
|
|
2418
|
+
* Registers an aggregate restored by a repository before returning it to
|
|
2419
|
+
* application code. The Unit of Work identity-maps the instance and captures
|
|
2420
|
+
* its current version as the optimistic-concurrency expectation.
|
|
2421
|
+
*
|
|
2422
|
+
* Return the result, not the argument. If the Unit of Work already tracks
|
|
2423
|
+
* an instance with the same identity, the result is that instance: when
|
|
2424
|
+
* two loads of one id overlap, the first tracked instance wins.
|
|
2425
|
+
*/
|
|
2426
|
+
trackLoaded(aggregate: TAggregate): TAggregate;
|
|
2427
|
+
/**
|
|
2428
|
+
* Read-only view of the per-operation Identity Map (Fowler): one aggregate type+id,
|
|
2429
|
+
* one in-memory instance. Created fresh per `run()`, cleared on
|
|
2430
|
+
* close; accessing it after close throws
|
|
2431
|
+
* {@link TransactionClosedError}.
|
|
2432
|
+
*/
|
|
2433
|
+
readonly identityMap: UnitOfWorkIdentityMap;
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Immutable adapter input for one registered aggregate write.
|
|
2437
|
+
*
|
|
2438
|
+
* `expectedVersion` is captured when a loaded aggregate joins the Unit of
|
|
2439
|
+
* Work and is absent for `add`. `version`, `changes`, and `events` describe
|
|
2440
|
+
* the exact moment at which the application registered its write intent.
|
|
2441
|
+
* Adapters must use the expected/current version pair for their OCC predicate
|
|
2442
|
+
* and must not read mutable write state back from an aggregate reference.
|
|
2443
|
+
*
|
|
2444
|
+
* The predicate itself is adapter code. The compare-and-set must run in the
|
|
2445
|
+
* store's own write statement. The kit does not know the store, so it cannot
|
|
2446
|
+
* write that statement. The kit owns the version pair and
|
|
2447
|
+
* {@link ConcurrencyConflictError}; the repository contract suite proves the
|
|
2448
|
+
* predicate.
|
|
2449
|
+
*/
|
|
2450
|
+
interface AggregatePersistenceWrite<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>, TChangeSet> {
|
|
2451
|
+
readonly intent: AggregateWriteIntent;
|
|
2452
|
+
/** The aggregate type and the id of the written aggregate. */
|
|
2453
|
+
readonly aggregateIdentity: AggregateIdentity<TAggregate["id"]>;
|
|
2454
|
+
readonly expectedVersion: Version | undefined;
|
|
2455
|
+
readonly version: Version;
|
|
2456
|
+
readonly changes: PersistenceChanges<TChangeSet>;
|
|
2457
|
+
readonly events: TAggregate["pendingEvents"];
|
|
2458
|
+
}
|
|
2459
|
+
//#endregion
|
|
2109
2460
|
//#region src/persistence/event-store/event-store.d.ts
|
|
2110
2461
|
/** Options for {@link EventStore.append}. */
|
|
2111
2462
|
interface EventStoreAppendOptions {
|
|
@@ -2123,18 +2474,15 @@ interface ReadStreamOptions {
|
|
|
2123
2474
|
/**
|
|
2124
2475
|
* Maximum number of events returned by this page. Required so callers
|
|
2125
2476
|
* cannot accidentally materialize an unbounded stream. Must be a positive
|
|
2126
|
-
* safe integer. An adapter may return fewer events,
|
|
2127
|
-
*
|
|
2477
|
+
* safe integer. An adapter may return fewer events, for example when it
|
|
2478
|
+
* clamps `limit` to a maximum of its own. It must return at least one
|
|
2479
|
+
* event while unread events remain inside the requested window.
|
|
2128
2480
|
*/
|
|
2129
2481
|
readonly limit: number;
|
|
2130
2482
|
/**
|
|
2131
2483
|
* Return only events AFTER this stream position (1-based event count),
|
|
2132
|
-
* the snapshot
|
|
2133
|
-
*
|
|
2134
|
-
* `aggregate.replayHistory`; the caller checks that the aggregate
|
|
2135
|
-
* ends at the pinned head ({@link ReplayHeadMismatchError}). Defaults
|
|
2136
|
-
* to `0` (the first stream page).
|
|
2137
|
-
* Must be a non-negative safe integer when present.
|
|
2484
|
+
* for example the version of a snapshot. Defaults to `0` (the first
|
|
2485
|
+
* stream page). Must be a non-negative safe integer when present.
|
|
2138
2486
|
*/
|
|
2139
2487
|
readonly fromVersion?: number;
|
|
2140
2488
|
/**
|
|
@@ -2143,10 +2491,16 @@ interface ReadStreamOptions {
|
|
|
2143
2491
|
* `(fromVersion, toVersion]`. Defaults to the actual stream head.
|
|
2144
2492
|
* `0` therefore returns an empty window; a value beyond the head clamps
|
|
2145
2493
|
* to the head; and `fromVersion >= toVersion` is an empty interval, not
|
|
2146
|
-
* an error.
|
|
2147
|
-
* Must be a non-negative safe integer when present.
|
|
2494
|
+
* an error. Must be a non-negative safe integer when present.
|
|
2148
2495
|
*/
|
|
2149
2496
|
readonly toVersion?: number;
|
|
2497
|
+
/**
|
|
2498
|
+
* Cooperative-cancellation signal of the surrounding operation. An
|
|
2499
|
+
* adapter either rejects with the `reason` of an aborted signal, or
|
|
2500
|
+
* bounds each page read with the timeout of its driver. So a page read
|
|
2501
|
+
* that hangs cannot hang its caller.
|
|
2502
|
+
*/
|
|
2503
|
+
readonly signal?: AbortSignal;
|
|
2150
2504
|
}
|
|
2151
2505
|
/**
|
|
2152
2506
|
* State returned by {@link EventStore.readStream}.
|
|
@@ -2155,7 +2509,7 @@ interface ReadStreamOptions {
|
|
|
2155
2509
|
* of the requested read window and page limit. `exists: true` implies
|
|
2156
2510
|
* `lastVersion >= 1`: an
|
|
2157
2511
|
* existing stream has at least one event, while metadata or tombstones without
|
|
2158
|
-
* events must be reported as `exists: false`.
|
|
2512
|
+
* events must be reported as `exists: false`. An absent stream is therefore
|
|
2159
2513
|
* distinguishable from an existing stream whose requested window is empty.
|
|
2160
2514
|
* Snapshot-backed repositories use that distinction to reject a snapshot whose
|
|
2161
2515
|
* version lies beyond the current authoritative stream head.
|
|
@@ -2169,9 +2523,50 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
|
|
|
2169
2523
|
readonly lastVersion: number;
|
|
2170
2524
|
readonly events: ReadonlyArray<Evt>;
|
|
2171
2525
|
};
|
|
2526
|
+
/**
|
|
2527
|
+
* The read half of the store: one bounded page of a stream at a time. A
|
|
2528
|
+
* store implements both halves; a read-only implementation offers this
|
|
2529
|
+
* role alone.
|
|
2530
|
+
*/
|
|
2531
|
+
interface EventStreamReader<Evt extends AnyDomainEvent> {
|
|
2532
|
+
/**
|
|
2533
|
+
* Reads one bounded page of the qualified stream in append order. An absent stream returns
|
|
2534
|
+
* `{ exists: false, lastVersion: 0, events: [] }`; an existing stream keeps
|
|
2535
|
+
* `exists: true` even when its requested window is empty. An existing stream
|
|
2536
|
+
* has at least one event, so `exists: true` implies `lastVersion >= 1`;
|
|
2537
|
+
* metadata or tombstones without events must be reported as absent.
|
|
2538
|
+
* `lastVersion` is always the actual stream head. `options.limit` is
|
|
2539
|
+
* mandatory and caps the returned array. An adapter may return fewer than
|
|
2540
|
+
* the requested limit, but if the requested window still contains unread
|
|
2541
|
+
* events it must return a non-empty contiguous prefix so callers can make
|
|
2542
|
+
* progress. Each returned event occupies one stream position, so the
|
|
2543
|
+
* events of a page fill the positions after `fromVersion` without a gap.
|
|
2544
|
+
* `options.fromVersion`
|
|
2545
|
+
* excludes positions at or below its 1-based event count;
|
|
2546
|
+
* `options.toVersion` includes positions through its count, so both bounds
|
|
2547
|
+
* describe `(fromVersion, toVersion]`. `toVersion: 0` and inverted ranges
|
|
2548
|
+
* return an empty existing window, while a bound beyond the head clamps to
|
|
2549
|
+
* the head. This distinction is load-bearing for snapshot catch-up and
|
|
2550
|
+
* point-in-time reconstruction: a repository can verify the requested
|
|
2551
|
+
* historical window against the authoritative head. `limit` must be a
|
|
2552
|
+
* positive safe integer; present bounds must be non-negative safe integers.
|
|
2553
|
+
* Invalid options reject with `RangeError` before querying storage.
|
|
2554
|
+
*
|
|
2555
|
+
* Each page's `exists`, `lastVersion`, and `events` must describe one
|
|
2556
|
+
* consistent view of the stream. Multiple page reads are not one database
|
|
2557
|
+
* snapshot. A caller that reads a stream in several pages pins a target
|
|
2558
|
+
* version on the first page, passes it as `toVersion` on every
|
|
2559
|
+
* continuation, and advances `fromVersion` by the number of events each
|
|
2560
|
+
* page returned. Because streams are append-only, that yields a stable
|
|
2561
|
+
* prefix even if new events arrive while replay is in progress. The returned
|
|
2562
|
+
* event array is owned by the caller; implementations must not hand out
|
|
2563
|
+
* mutable live internal state.
|
|
2564
|
+
*/
|
|
2565
|
+
readStream(stream: AggregateIdentity, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
|
|
2566
|
+
}
|
|
2172
2567
|
/**
|
|
2173
2568
|
* Driven port for event-sourced aggregate persistence: an append-only
|
|
2174
|
-
* store with one stream per aggregate. Each stream is
|
|
2569
|
+
* store with one stream per aggregate. Each stream is identified by the
|
|
2175
2570
|
* qualified tuple `(aggregateType, aggregateId)`, because aggregate ids
|
|
2176
2571
|
* are type-scoped rather than globally unique.
|
|
2177
2572
|
*
|
|
@@ -2187,58 +2582,38 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
|
|
|
2187
2582
|
* Repository usage (see the event-sourcing guide):
|
|
2188
2583
|
*
|
|
2189
2584
|
* ```ts
|
|
2190
|
-
* private stream(id: OrderId):
|
|
2585
|
+
* private stream(id: OrderId): AggregateIdentity<OrderId> {
|
|
2191
2586
|
* return { aggregateType: "Order", aggregateId: id };
|
|
2192
2587
|
* }
|
|
2193
2588
|
*
|
|
2194
2589
|
* async findById(id: OrderId): Promise<Order | undefined> {
|
|
2195
2590
|
* const cached = this.tracking.identityMap.get(Order, id);
|
|
2196
2591
|
* if (cached) return cached;
|
|
2197
|
-
* const
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
* const
|
|
2592
|
+
* const read = await readStreamPages(this.eventStore, this.stream(id), {
|
|
2593
|
+
* limit: 256,
|
|
2594
|
+
* });
|
|
2595
|
+
* if (!read.reachable) return undefined;
|
|
2596
|
+
* const loaded = await reconstituteAggregateFromStreamPages(
|
|
2202
2597
|
* () => Order.reconstitute(id), // bare instance, no events
|
|
2203
|
-
*
|
|
2598
|
+
* read,
|
|
2204
2599
|
* );
|
|
2205
|
-
* if (
|
|
2206
|
-
*
|
|
2207
|
-
* let fromVersion = first.events.length;
|
|
2208
|
-
* while (fromVersion < targetVersion) {
|
|
2209
|
-
* const page = await this.eventStore.readStream(address, {
|
|
2210
|
-
* fromVersion,
|
|
2211
|
-
* toVersion: targetVersion,
|
|
2212
|
-
* limit: 256,
|
|
2213
|
-
* });
|
|
2214
|
-
* if (!page.exists || page.events.length === 0) {
|
|
2215
|
-
* throw new NonProgressingEventStreamPageError({
|
|
2216
|
-
* ...address,
|
|
2217
|
-
* fromVersion,
|
|
2218
|
-
* targetVersion,
|
|
2219
|
-
* });
|
|
2220
|
-
* }
|
|
2221
|
-
* const catchUp = order.replayHistory(page.events);
|
|
2222
|
-
* if (catchUp.isErr()) throw catchUp.error; // corrupt stream
|
|
2223
|
-
* fromVersion += page.events.length;
|
|
2224
|
-
* }
|
|
2225
|
-
* if (order.version !== targetVersion) {
|
|
2226
|
-
* throw new ReplayHeadMismatchError({
|
|
2227
|
-
* ...address,
|
|
2228
|
-
* targetVersion,
|
|
2229
|
-
* actualVersion: order.version,
|
|
2230
|
-
* });
|
|
2231
|
-
* }
|
|
2232
|
-
* return this.tracking.trackLoaded(order);
|
|
2600
|
+
* if (loaded.isErr()) throw loaded.error; // corrupt stream
|
|
2601
|
+
* return this.tracking.trackLoaded(loaded.value);
|
|
2233
2602
|
* }
|
|
2234
2603
|
*
|
|
2235
2604
|
* flush(write: AggregatePersistenceWrite<Order, number | undefined>) {
|
|
2236
|
-
* return this.eventStore.append(
|
|
2605
|
+
* return this.eventStore.append(write.aggregateIdentity, write.events, {
|
|
2237
2606
|
* expectedVersion: write.expectedVersion ?? 0,
|
|
2238
2607
|
* });
|
|
2239
2608
|
* }
|
|
2240
2609
|
* ```
|
|
2241
2610
|
*
|
|
2611
|
+
* `readStreamPages` pins the head on the first page as the target version
|
|
2612
|
+
* and reads the rest in bounded pages up to it.
|
|
2613
|
+
* `reconstituteAggregateFromStreamPages` replays the pages and checks that
|
|
2614
|
+
* the replay ends at the target version. The long form, for an adapter that
|
|
2615
|
+
* pages on its own, is in the event-sourcing guide.
|
|
2616
|
+
*
|
|
2242
2617
|
* `flush` appends the exact event batch registered by `add` or `update`;
|
|
2243
2618
|
* `withCommit`
|
|
2244
2619
|
* separately composes them into outbox envelopes. The event store's own
|
|
@@ -2247,7 +2622,7 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
|
|
|
2247
2622
|
* The exact appended event batch is acknowledged only after the surrounding
|
|
2248
2623
|
* transaction commits. Rollback leaves it pending.
|
|
2249
2624
|
*/
|
|
2250
|
-
interface EventStore<Evt extends AnyDomainEvent> {
|
|
2625
|
+
interface EventStore<Evt extends AnyDomainEvent> extends EventStreamReader<Evt> {
|
|
2251
2626
|
/**
|
|
2252
2627
|
* Atomically appends `events` to the stream, guarded by optimistic
|
|
2253
2628
|
* concurrency: the append succeeds only when the stream currently
|
|
@@ -2292,44 +2667,96 @@ interface EventStore<Evt extends AnyDomainEvent> {
|
|
|
2292
2667
|
* touching the store (an ES repository skips `append` for aggregates
|
|
2293
2668
|
* without pending events anyway).
|
|
2294
2669
|
*
|
|
2670
|
+
* A load that read its pages through a replica still appends here with
|
|
2671
|
+
* the pinned target as `expectedVersion`. The primary rejects a stale
|
|
2672
|
+
* head, so a lagging replica cannot lose an update.
|
|
2673
|
+
*
|
|
2295
2674
|
* Treat `aggregateType` as a stable technical stream category. If two
|
|
2296
2675
|
* bounded contexts share one physical store and reuse a domain name,
|
|
2297
2676
|
* qualify it at the source (`sales.order`, `fulfillment.order`). Renaming
|
|
2298
2677
|
* it changes the stream key and therefore requires a data migration.
|
|
2299
2678
|
*/
|
|
2300
|
-
append(stream:
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
*
|
|
2320
|
-
*
|
|
2679
|
+
append(stream: AggregateIdentity, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
|
|
2680
|
+
}
|
|
2681
|
+
//#endregion
|
|
2682
|
+
//#region src/persistence/event-store/reconstitute-from-stream-pages.d.ts
|
|
2683
|
+
/**
|
|
2684
|
+
* The shape the replay reads: the stream, the window, and the pages.
|
|
2685
|
+
* `readStreamPages` returns it as the reachable branch of a kit read. An
|
|
2686
|
+
* adapter that pages on its own builds it directly, after it pinned the
|
|
2687
|
+
* target version with `pinTargetVersion`.
|
|
2688
|
+
*/
|
|
2689
|
+
interface ReplayableStreamPages<Evt extends AnyDomainEvent> {
|
|
2690
|
+
/** The qualified stream the pages come from. */
|
|
2691
|
+
readonly stream: AggregateIdentity;
|
|
2692
|
+
/**
|
|
2693
|
+
* The cursor the read started at: `fromVersion`, or `0`. The pages hold
|
|
2694
|
+
* the events after it, so the replay target must stand at this version.
|
|
2695
|
+
*/
|
|
2696
|
+
readonly fromVersion: number;
|
|
2697
|
+
/**
|
|
2698
|
+
* The version the replay must end at: the head at read time, or a
|
|
2699
|
+
* `toVersion` at or below it. Never a version taken from a snapshot, and
|
|
2700
|
+
* never below `fromVersion`. Later appends do not move it.
|
|
2701
|
+
*/
|
|
2702
|
+
readonly targetVersion: number;
|
|
2703
|
+
/**
|
|
2704
|
+
* The events after the cursor through the target version, in append
|
|
2705
|
+
* order, one bounded page per iteration. Every iteration starts again
|
|
2706
|
+
* from the first page, so a second replay of one read sees the same
|
|
2707
|
+
* prefix. A page holds at least one event, and the pages end at the
|
|
2708
|
+
* target version. The replay rejects an empty page, and a page that
|
|
2709
|
+
* would run past the target version, with
|
|
2710
|
+
* {@link InvalidEventStreamPageError} before any row of it reaches the
|
|
2711
|
+
* aggregate. `createReplayableStreamPagesContractTests` proves these
|
|
2712
|
+
* rules for an adapter.
|
|
2321
2713
|
*
|
|
2322
|
-
*
|
|
2323
|
-
*
|
|
2324
|
-
*
|
|
2325
|
-
*
|
|
2326
|
-
* returned. Because streams are append-only, that yields a stable prefix
|
|
2327
|
-
* even if new events arrive while replay is in progress. The returned
|
|
2328
|
-
* event array is owned by the caller; implementations must not hand out
|
|
2329
|
-
* mutable live internal state.
|
|
2714
|
+
* The events of a page are stored facts: no consumer, upcaster, or fold
|
|
2715
|
+
* changes one in place. `readStreamPages` reads fresh events on every
|
|
2716
|
+
* later iteration, and `createReplayableStreamPages` hands out frozen
|
|
2717
|
+
* ones, so a consumer that breaks this rule fails in its own tests.
|
|
2330
2718
|
*/
|
|
2331
|
-
|
|
2719
|
+
readonly pages: AsyncIterable<ReadonlyArray<Evt>>;
|
|
2332
2720
|
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Reconstitutes an event-sourced aggregate from the pages of a stream read
|
|
2723
|
+
* and yields it only when the replay ends at the target version.
|
|
2724
|
+
*
|
|
2725
|
+
* This is the paged form of `reconstituteAggregateFromHistory`.
|
|
2726
|
+
* `createReplayTarget` builds the replay target: a fresh instance for a
|
|
2727
|
+
* full replay, or an instance reconstituted from a snapshot for a read that
|
|
2728
|
+
* starts at `snapshot.version`. `read` is the reachable branch of a kit
|
|
2729
|
+
* read, or the value that an adapter that pages on its own built.
|
|
2730
|
+
*
|
|
2731
|
+
* The call checks the window first. `targetVersion` must be a positive
|
|
2732
|
+
* safe integer, and `fromVersion` a non-negative one at or below it. A bad
|
|
2733
|
+
* window rejects with `RangeError` before the replay target is built.
|
|
2734
|
+
*
|
|
2735
|
+
* The call then primes the replay target with an empty history, so the
|
|
2736
|
+
* aggregate runs its own guard even when the read holds no page: a replay
|
|
2737
|
+
* target with pending decisions throws `UnreplayableAggregateError`. The
|
|
2738
|
+
* replay target must stand at `read.fromVersion`. A replay target at
|
|
2739
|
+
* another version throws {@link ReplayTargetMismatchError} before the
|
|
2740
|
+
* first page, even when it also rejected the empty history: a wiring
|
|
2741
|
+
* defect wins over a rejection that the data cannot explain.
|
|
2742
|
+
*
|
|
2743
|
+
* Each page goes through `replayHistory` on the replay target, so
|
|
2744
|
+
* allocation stays bounded by the size of a page; `readStreamPages` rejects
|
|
2745
|
+
* a page over its `limit`. An empty page, and a page that would run past
|
|
2746
|
+
* the target version, throw {@link InvalidEventStreamPageError} before any
|
|
2747
|
+
* row of them reaches the aggregate. Events carry no stream position, so
|
|
2748
|
+
* only the call can find pages that end short of the target version. After
|
|
2749
|
+
* the last page it compares the final version with `read.targetVersion` and
|
|
2750
|
+
* throws {@link ReplayTargetMismatchError} on a difference.
|
|
2751
|
+
*
|
|
2752
|
+
* When the aggregate rejects a stored event with a `DomainError`, the call
|
|
2753
|
+
* stops reading and returns {@link ReplayRejectedError} as `Err`. The
|
|
2754
|
+
* error names the stream and the window of the rejected page, and it holds
|
|
2755
|
+
* the `DomainError` as `cause`. The replay target never escapes on that
|
|
2756
|
+
* path. Wiring errors and a foreign row throw, as in `replayHistory`. What
|
|
2757
|
+
* `createReplayTarget` throws propagates.
|
|
2758
|
+
*/
|
|
2759
|
+
declare function reconstituteAggregateFromStreamPages<TAggregate extends ReplayableAggregate<Id<string>, AnyDomainEvent>>(createReplayTarget: () => TAggregate, read: ReplayableStreamPages<Parameters<TAggregate["replayHistory"]>[0][number]>): Promise<Result<TAggregate, ReplayRejectedError>>;
|
|
2333
2760
|
//#endregion
|
|
2334
2761
|
//#region src/persistence/snapshot-store/snapshot-store.d.ts
|
|
2335
2762
|
/**
|
|
@@ -2371,14 +2798,14 @@ interface SnapshotStore<TState = unknown> {
|
|
|
2371
2798
|
* The latest snapshot for the aggregate, or `undefined` when none
|
|
2372
2799
|
* exists. The repository falls back to a full replay then.
|
|
2373
2800
|
*/
|
|
2374
|
-
load(
|
|
2801
|
+
load(identity: AggregateIdentity): Promise<AggregateSnapshot<TState> | undefined>;
|
|
2375
2802
|
/**
|
|
2376
2803
|
* Persists `snapshot` as the new latest for the aggregate,
|
|
2377
2804
|
* replacing any previous one. Called AFTER the write transaction
|
|
2378
2805
|
* committed (see the port docs); a single-row upsert is the
|
|
2379
2806
|
* standard implementation.
|
|
2380
2807
|
*/
|
|
2381
|
-
save(
|
|
2808
|
+
save(identity: AggregateIdentity, snapshot: AggregateSnapshot<TState>): Promise<void>;
|
|
2382
2809
|
/**
|
|
2383
2810
|
* Removes the aggregate's snapshot; a no-op when none exists. The
|
|
2384
2811
|
* two callers: the schema-migration fallback (a
|
|
@@ -2390,8 +2817,8 @@ interface SnapshotStore<TState = unknown> {
|
|
|
2390
2817
|
* which a stale snapshot resurrects the erased aggregate on the
|
|
2391
2818
|
* snapshot load path; snapshot-first degrades to a full replay.
|
|
2392
2819
|
*/
|
|
2393
|
-
delete(
|
|
2820
|
+
delete(identity: AggregateIdentity): Promise<void>;
|
|
2394
2821
|
}
|
|
2395
2822
|
//#endregion
|
|
2396
|
-
export {
|
|
2823
|
+
export { EventHandler as $, Command as $t, IdempotencyClaimHandle as A, CreateDomainEventFromFactsOptions as At, withIdempotentCommit as B, PendingDomainEvent as Bt, IdentityMap as C, ExecutionContext as Ct, ProjectionPosition as D, EventCommitCandidatePosition as Dt, ProjectionCheckpointStore as E, EventCommitCandidate as Et, IdempotencyStore as F, DomainEventFactory as Ft, CommitEnrollment as G, createDomainEventFactory as Gt, DeadlineStore as H, UncommittedDomainEventOf as Ht, IdempotentCommitRequest as I, DomainEventFactoryOptions as It, WithCommitWorkResult as J, defaultDomainEventFactory as Jt, CommitEnrollmentOptions as K, createDomainEventFromFacts as Kt, IdempotentCommitResult as L, DomainEventStamp as Lt, IdempotencyOperationErrorContext as M, CreateDomainEventStampOptions as Mt, IdempotencyReconciliation as N, CreateUncommittedDomainEventOptions as Nt, isPositionAfter as O, AnyDomainEvent as Ot, IdempotencyReconciliationDecision as P, DomainEvent as Pt, EventBus as Q, AggregateIdentity as Qt, IdempotentExecution as R, EventIdFactory as Rt, AggregateClass as S, OutboxWriter as St, ProjectionCheckpoint as T, CommittedDomainEvent as Tt, DueDeadline as U, copyMetadata as Ut, DeadLetterDeadline as V, UncommittedDomainEvent as Vt, AggregateCommitToken as W, createDomainEvent as Wt, TransactionScope as X, recordDomainEvent as Xt, withCommit as Y, mergeMetadata as Yt, TransactionalOptions as Z, ClockFactory as Zt, capturePersistenceBaseline as _, routeEventsToCommandOutbox as _t, EventStoreAppendOptions as a, Version as at, persistenceProjectionDrifted as b, Outbox as bt, StreamReadResult as c, Id as ct, RepositoryTracking as d, CommandMessageContent as dt, CommandHandler as en, OnceOptions as et, UnitOfWorkIdentityMap as f, CommandMessageRelationships as ft, PersistenceModel as g, DurableCommandMessage as gt, PersistenceLifecycle as h, CommandOutboxWriter as ht, EventStore as i, JsonValue as in, ReplayableAggregate as it, IdempotencyLease as j, CreateDomainEventOptions as jt, IdempotencyClaim as k, AnyUncommittedDomainEvent as kt, AggregatePersistenceWrite as l, IdGenerator as lt, PersistenceChanges as m, CommandOutboxMapper as mt, ReplayableStreamPages as n, JsonObject as nn, Aggregate as nt, EventStreamReader as o, sameVersion as ot, PersistenceBaseline as p, CommandOutboxCommitCandidate as pt, WithCommitDeps as q, createUncommittedDomainEvent as qt, reconstituteAggregateFromStreamPages as r, JsonPrimitive as rn, AggregateSnapshot as rt, ReadStreamOptions as s, toVersion as st, SnapshotStore as t, PublishedCommand as tn, PublishOptions as tt, AggregateWriteIntent as u, CommandCommitOriginCandidate as ut, derivePersistenceChanges as v, DeadLetterRecord as vt, Projection as w, CommitPosition as wt, recapturePersistenceBaseline as x, OutboxRecord as xt, insertPersistenceBaseline as y, DispatchTrackingOutbox as yt, WithIdempotentCommitDeps as z, EventMetadata as zt };
|
|
2397
2824
|
//# sourceMappingURL=snapshot-store.d.ts.map
|