@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.
@@ -1,4 +1,4 @@
1
- import { u as DomainError } from "./kit-errors.js";
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-address.d.ts
163
+ //#region src/domain/aggregate/aggregate-identity.d.ts
164
164
  /**
165
- * Stable value address of one aggregate instance.
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 addresses are
174
- * migrated. When bounded contexts share infrastructure and reuse a domain
175
- * name, qualify it at the source (`sales.order`, `fulfillment.order`). The kit
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 AggregateAddress<TAggregateId extends string = string> {
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 address, and payload
327
- * schema version because those values describe the business fact it produced.
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 address, and payload
450
- * schema version. The stamp supplies only event identity, recording time, and
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: AggregateAddress;
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: AggregateAddress;
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: AggregateAddress;
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 durable
1763
- * store) cannot see commits or rollbacks. Every fresh claim therefore returns
1764
- * a store-minted token and bounded lease. The wrapper renews it while the
1765
- * transaction runs; `complete`, `renew`, `confirm`, `abandon`, and `reconcile`
1766
- * compare the token so a stale owner cannot mutate a successor claim. An
1767
- * expired PENDING claim may be replaced. An expired STAGED outcome is never
1768
- * replayed or released automatically: `claim` returns
1769
- * `reconciliation-required`, and the application must consult the source of
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, addresses: ReadonlyArray<AggregateAddress>, work: () => Promise<R>): Promise<R>;
2088
+ withCheckpointLocks<R>(ctx: TCtx, projection: string, identities: ReadonlyArray<AggregateIdentity>, work: () => Promise<R>): Promise<R>;
2030
2089
  /**
2031
- * The stored watermark receipt for `(projection, address)`, or `undefined`
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, address: AggregateAddress): Promise<ProjectionCheckpoint | undefined>;
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, address: AggregateAddress, checkpoint: ProjectionCheckpoint): Promise<void>;
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, address)` is at or past
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, address: AggregateAddress, position: ProjectionPosition): Promise<boolean>;
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
- * address it carries, including event types this read model does not use.
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, but must return at least
2127
- * one while unread events remain inside the requested window.
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 catch-up read: `readStream(stream, { fromVersion:
2133
- * snapshot.version, limit: 256 })` yields the next page passed to
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`. A missing stream is therefore
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 addressed by the
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): AggregateAddress<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 address = this.stream(id);
2198
- * const first = await this.eventStore.readStream(address, { limit: 256 });
2199
- * if (!first.exists) return undefined;
2200
- * const targetVersion = first.lastVersion; // pin the first observed head
2201
- * const reconstituted = reconstituteAggregateFromHistory(
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
- * first.events,
2598
+ * read,
2204
2599
  * );
2205
- * if (reconstituted.isErr()) throw reconstituted.error; // corrupt stream
2206
- * const order = reconstituted.value;
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(this.stream(write.aggregateId), write.events, {
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: AggregateAddress, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
2301
- /**
2302
- * Reads one bounded page of the qualified stream in append order. An unknown stream returns
2303
- * `{ exists: false, lastVersion: 0, events: [] }`; an existing stream keeps
2304
- * `exists: true` even when its requested window is empty. An existing stream
2305
- * has at least one event, so `exists: true` implies `lastVersion >= 1`;
2306
- * metadata or tombstones without events must be reported as absent.
2307
- * `lastVersion` is always the actual stream head. `options.limit` is
2308
- * mandatory and caps the returned array. An adapter may return fewer than
2309
- * the requested limit, but if the requested window still contains unread
2310
- * events it must return a non-empty contiguous prefix so callers can make
2311
- * progress. `options.fromVersion`
2312
- * excludes positions at or below its 1-based event count;
2313
- * `options.toVersion` includes positions through its count, so both bounds
2314
- * describe `(fromVersion, toVersion]`. `toVersion: 0` and inverted ranges
2315
- * return an empty existing window, while a bound beyond the head clamps to
2316
- * the head. This distinction is load-bearing for snapshot catch-up and
2317
- * point-in-time reconstruction: a repository can verify the requested
2318
- * historical window against the authoritative head. `limit` must be a
2319
- * positive safe integer; present bounds must be non-negative safe integers.
2320
- * Invalid options reject with `RangeError` before querying storage.
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
- * Each page's `exists`, `lastVersion`, and `events` must describe one
2323
- * consistent view of the stream. Multiple page reads are not one database
2324
- * snapshot: pin the first page's `lastVersion` as `toVersion` on every
2325
- * continuation, then advance `fromVersion` by the number of events actually
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
- readStream(stream: AggregateAddress, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
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(address: AggregateAddress): Promise<AggregateSnapshot<TState> | undefined>;
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(address: AggregateAddress, snapshot: AggregateSnapshot<TState>): Promise<void>;
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(address: AggregateAddress): Promise<void>;
2820
+ delete(identity: AggregateIdentity): Promise<void>;
2394
2821
  }
2395
2822
  //#endregion
2396
- export { routeEventsToCommandOutbox as $, WithCommitWorkResult as A, defaultDomainEventFactory as At, ReplayableAggregate as B, JsonValue as Bt, DeadLetterDeadline as C, UncommittedDomainEvent as Ct, CommitEnrollment as D, createDomainEventFactory as Dt, AggregateCommitToken as E, createDomainEvent as Et, EventHandler as F, Command as Ft, IdGenerator as G, sameVersion as H, OnceOptions as I, CommandHandler as It, CommandMessageRelationships as J, CommandCommitOriginCandidate as K, PublishOptions as L, PublishedCommand as Lt, TransactionScope as M, recordDomainEvent as Mt, TransactionalOptions as N, ClockFactory as Nt, CommitEnrollmentOptions as O, createDomainEventFromFacts as Ot, EventBus as P, AggregateAddress as Pt, DurableCommandMessage as Q, Aggregate as R, JsonObject as Rt, withIdempotentCommit as S, PendingDomainEvent as St, DueDeadline as T, copyMetadata as Tt, toVersion as U, Version as V, Id as W, CommandOutboxMapper as X, CommandOutboxCommitCandidate as Y, CommandOutboxWriter as Z, IdempotencyStore as _, DomainEventFactory as _t, StreamReadResult as a, ExecutionContext as at, IdempotentExecution as b, EventIdFactory as bt, ProjectionCheckpointStore as c, EventCommitCandidate as ct, IdempotencyClaim as d, AnyUncommittedDomainEvent as dt, DeadLetterRecord as et, IdempotencyClaimHandle as f, CreateDomainEventFromFactsOptions as ft, IdempotencyReconciliationDecision as g, DomainEvent as gt, IdempotencyReconciliation as h, CreateUncommittedDomainEventOptions as ht, ReadStreamOptions as i, OutboxWriter as it, withCommit as j, mergeMetadata as jt, WithCommitDeps as k, createUncommittedDomainEvent as kt, ProjectionPosition as l, EventCommitCandidatePosition as lt, IdempotencyOperationErrorContext as m, CreateDomainEventStampOptions as mt, EventStore as n, Outbox as nt, Projection as o, CommitPosition as ot, IdempotencyLease as p, CreateDomainEventOptions as pt, CommandMessageContent as q, EventStoreAppendOptions as r, OutboxRecord as rt, ProjectionCheckpoint as s, CommittedDomainEvent as st, SnapshotStore as t, DispatchTrackingOutbox as tt, isPositionAfter as u, AnyDomainEvent as ut, IdempotentCommitRequest as v, DomainEventFactoryOptions as vt, DeadlineStore as w, UncommittedDomainEventOf as wt, WithIdempotentCommitDeps as x, EventMetadata as xt, IdempotentCommitResult as y, DomainEventStamp as yt, AggregateSnapshot as z, JsonPrimitive as zt };
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