@shirudo/ddd-kit 3.0.0-rc.10 → 3.0.0-rc.12

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
@@ -441,16 +464,71 @@ declare function createDomainEventFactory(options?: DomainEventFactoryOptions):
441
464
  * with {@link createDomainEventFactory} for custom policy.
442
465
  */
443
466
  declare const defaultDomainEventFactory: DomainEventFactory;
467
+ /**
468
+ * Creates an uncommitted domain event: the event that an aggregate method
469
+ * returns before the application shell records it. The payload and the
470
+ * metadata follow the input ownership rule of {@link createDomainEvent}.
471
+ */
444
472
  declare function createUncommittedDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateUncommittedDomainEventOptions): UncommittedDomainEvent<T, void>;
445
473
  declare function createUncommittedDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateUncommittedDomainEventOptions): UncommittedDomainEvent<T, P>;
446
474
  /**
447
475
  * Attaches shell-owned recording data to an accepted aggregate decision.
448
476
  *
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.
477
+ * The decision supplies the domain type, payload, source aggregate
478
+ * identity, and payload schema version. The stamp supplies only event
479
+ * identity, recording time, and trace metadata.
452
480
  */
453
481
  declare function recordDomainEvent<T extends string, P>(event: UncommittedDomainEvent<T, P>, stamp: DomainEventStamp): DomainEvent<T, P>;
482
+ /**
483
+ * Creates a domain event with default values.
484
+ * Sets occurredAt to current date and schemaVersion to 1 if not provided.
485
+ *
486
+ * **Input ownership.** The event is deeply frozen, and `payload` and
487
+ * `metadata` are deep-cloned first, so the caller's own objects are never
488
+ * frozen in place and later mutation of them does not bleed into the event
489
+ * (same contract as `vo()`). The event data rule checks the data before the
490
+ * copy and the copy after it. So the copy loses no data without an error,
491
+ * and no getter of the data runs. The in-memory event store applies the
492
+ * same rule on append. The checks catch accidents, not a hostile `Proxy`: a
493
+ * trap that changes the data during the check can hide a loss. An error
494
+ * that a `Proxy` trap throws passes unchanged, so a coded error of the
495
+ * caller keeps its code. These values throw a `TypeError`: a function, a
496
+ * Promise, a WeakMap or a WeakSet, an `Error`, a host object such as a
497
+ * `File`, a binary buffer, a `RegExp`, and shared memory. The same holds
498
+ * for what the copy would lose or change: a class instance, an accessor, an
499
+ * enumerable symbol-keyed property, a non-enumerable data property, and an
500
+ * expando on a built-in. A non-enumerable symbol key is metadata by
501
+ * convention, and the event does not get it. Map a value object to plain
502
+ * data before it goes into an event. `toJSON()` of a `ValueObject` is
503
+ * shallow, so map a nested value object too.
504
+ *
505
+ * **For aggregate-internal events, prefer `this.createEvent(...)` on
506
+ * `StateStoredAggregate` / `EventSourcedAggregate`.** That helper auto-injects
507
+ * `aggregateId` (from `this.id`) and `aggregateType` (from the
508
+ * aggregate's declared `aggregateType` property), which downstream
509
+ * consumers (outbox dispatchers, projection handlers, audit logs)
510
+ * route by. The commit boundary validates that both fields are present
511
+ * and throws if they are missing, so a direct `createDomainEvent(...)`
512
+ * call inside an aggregate that forgets the options is caught at
513
+ * runtime. Record pending decisions in the application
514
+ * shell before repository persistence or outbox harvest.
515
+ *
516
+ * Use `createDomainEvent(...)` directly for events that don't belong to
517
+ * an aggregate: system events, integration events, configuration events,
518
+ * test fixtures. For those, set `aggregateId` / `aggregateType` in
519
+ * `options` if downstream consumers expect routing metadata.
520
+ *
521
+ * @param type - The event type
522
+ * @param payload - The event payload
523
+ * @param options - Optional event configuration (including `aggregateId`
524
+ * and `aggregateType` for routing)
525
+ * @returns A domain event
526
+ *
527
+ * @example
528
+ * ```typescript
529
+ * const event = createDomainEvent("OrderCreated", { orderId: "123" });
530
+ * ```
531
+ */
454
532
  declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
455
533
  declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
456
534
  /**
@@ -531,7 +609,7 @@ type EventCommitCandidatePosition = Omit<CommitPosition, "previousEventfulAggreg
531
609
  */
532
610
  interface EventCommitCandidate<Evt extends AnyDomainEvent> {
533
611
  readonly event: Evt;
534
- readonly source: AggregateAddress;
612
+ readonly source: AggregateIdentity;
535
613
  readonly position: EventCommitCandidatePosition;
536
614
  }
537
615
  /**
@@ -541,7 +619,7 @@ interface EventCommitCandidate<Evt extends AnyDomainEvent> {
541
619
  */
542
620
  interface CommittedDomainEvent<Evt extends AnyDomainEvent> {
543
621
  readonly event: Evt;
544
- readonly source: AggregateAddress;
622
+ readonly source: AggregateIdentity;
545
623
  readonly position: CommitPosition;
546
624
  }
547
625
  //#endregion
@@ -625,6 +703,22 @@ interface OutboxWriter<Evt extends AnyDomainEvent> {
625
703
  * should reject it rather than replace or silently reinterpret it as a retry.
626
704
  */
627
705
  add: (events: ReadonlyArray<EventCommitCandidate<Evt>>) => Promise<void>;
706
+ /**
707
+ * Ends the event sources of removed aggregates. Called from inside
708
+ * `withCommit`'s transactional callback, after `add()`, atomically with
709
+ * the removal.
710
+ *
711
+ * For every source that has a head, the adapter marks the head as ended
712
+ * in the SAME transaction. The mark must outlive any retention of heads:
713
+ * a pruned ended head opens the source again. A source without a head
714
+ * has no events, so it has no continuity to protect: it stays open, and
715
+ * its first event starts a new head. After the mark, `add()` must reject
716
+ * a new event of that source: its aggregate was created again under a
717
+ * removed identity, which the kit does not support. An exact retry of a
718
+ * stored event stays idempotent, and ending a source twice is not an
719
+ * error.
720
+ */
721
+ endEventSources: (sources: ReadonlyArray<AggregateIdentity>) => Promise<void>;
628
722
  }
629
723
  /**
630
724
  * Transactional outbox port: the bridge between the write-side
@@ -773,7 +867,7 @@ interface DurableCommandMessage<C extends PublishedCommand> extends CommandMessa
773
867
  */
774
868
  interface CommandCommitOriginCandidate {
775
869
  readonly eventId: string;
776
- readonly source: AggregateAddress;
870
+ readonly source: AggregateIdentity;
777
871
  readonly position: EventCommitCandidatePosition;
778
872
  }
779
873
  /**
@@ -801,6 +895,14 @@ interface CommandOutboxCommitCandidate<C extends PublishedCommand> {
801
895
  */
802
896
  interface CommandOutboxWriter<C extends PublishedCommand> {
803
897
  add(commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>): Promise<void>;
898
+ /**
899
+ * Ends the source cursors of removed aggregates, in the same transaction
900
+ * as the removal. After the mark, `add()` must reject a new commit whose
901
+ * origin comes from an ended source; an exact retry by `origin.eventId`
902
+ * stays idempotent. A source without a cursor stays open, and ending a
903
+ * source twice is not an error. See `OutboxWriter.endEventSources`.
904
+ */
905
+ endEventSources(sources: ReadonlyArray<AggregateIdentity>): Promise<void>;
804
906
  }
805
907
  /** Maps one private accepted event to zero or more addressed commands. */
806
908
  type CommandOutboxMapper<Evt extends AnyDomainEvent, C extends PublishedCommand> = (event: Evt) => ReadonlyArray<CommandMessageContent<C>>;
@@ -937,6 +1039,8 @@ interface AggregateSnapshot<TState> {
937
1039
  */
938
1040
  interface Aggregate<TId extends Id<string>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
939
1041
  readonly id: TId;
1042
+ /** The aggregate type and the id: the full identity of the aggregate. */
1043
+ readonly aggregateIdentity: AggregateIdentity<TId>;
940
1044
  readonly version: Version;
941
1045
  readonly pendingEvents: ReadonlyArray<PendingDomainEvent<TEvent>>;
942
1046
  }
@@ -953,7 +1057,8 @@ interface ReplayableAggregate<TId extends Id<string>, TEvent extends AnyDomainEv
953
1057
  * `Result` because event-stream corruption is an expected
954
1058
  * recoverable failure at the infrastructure boundary: a `DomainError`
955
1059
  * thrown by a fold arrives as `Err`. Every other failure propagates
956
- * after the all-or-nothing rollback.
1060
+ * after the all-or-nothing rollback. The pending-decisions guard runs
1061
+ * for an empty history as well; an empty history is otherwise a no-op.
957
1062
  *
958
1063
  * @throws ForeignEventError when a history event names another aggregate
959
1064
  * @throws UnreplayableAggregateError when the target carries pending
@@ -1238,6 +1343,15 @@ interface TransactionalOptions {
1238
1343
  * eagerly cancellable.
1239
1344
  */
1240
1345
  readonly signal?: AbortSignal;
1346
+ /**
1347
+ * Tells the caller that a new attempt starts. A scope that can open more
1348
+ * than one transaction for one call (a retrying scope) calls it before
1349
+ * each attempt opens its transaction. The caller can then tell a failure
1350
+ * to open the next attempt apart from a failure of the previous one. A
1351
+ * scope that opens one transaction per call can ignore it. A scope that
1352
+ * wraps another scope passes the options on unchanged.
1353
+ */
1354
+ readonly onAttemptStart?: () => void;
1241
1355
  }
1242
1356
  /**
1243
1357
  * Transaction-scope abstraction.
@@ -1759,15 +1873,15 @@ type IdempotencyClaim = {
1759
1873
  * production pattern and the only family that proves atomic command effect +
1760
1874
  * idempotency completion without reconciliation.
1761
1875
  *
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.
1876
+ * A NON-transactional store (the in-memory reference on its own, a separate
1877
+ * durable store) cannot see commits or rollbacks. Every fresh claim
1878
+ * therefore returns a store-minted token and bounded lease. The wrapper
1879
+ * renews it while the transaction runs; `complete`, `renew`, `confirm`,
1880
+ * `abandon`, and `reconcile` compare the token so a stale owner cannot
1881
+ * mutate a successor claim. An expired PENDING claim may be replaced. An
1882
+ * expired STAGED outcome is never replayed or released automatically:
1883
+ * `claim` returns `reconciliation-required`, and the application must
1884
+ * consult the source of truth. `unknown` keeps it blocked.
1771
1885
  *
1772
1886
  * A lease is coordination, not a security or exactly-once boundary. To return
1773
1887
  * `not-committed` safely, the source transaction must persist an idempotency
@@ -2026,23 +2140,23 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
2026
2140
  * Implementations may serialize more than the requested keys, but never
2027
2141
  * less. The callback is non-reentrant for an overlapping key set.
2028
2142
  */
2029
- withCheckpointLocks<R>(ctx: TCtx, projection: string, addresses: ReadonlyArray<AggregateAddress>, work: () => Promise<R>): Promise<R>;
2143
+ withCheckpointLocks<R>(ctx: TCtx, projection: string, identities: ReadonlyArray<AggregateIdentity>, work: () => Promise<R>): Promise<R>;
2030
2144
  /**
2031
- * The stored watermark receipt for `(projection, address)`, or `undefined`
2145
+ * The stored watermark receipt for `(projection, identity)`, or `undefined`
2032
2146
  * when this projection has never applied an event of that
2033
2147
  * aggregate. Called inside the projector's transaction.
2034
2148
  */
2035
- load(ctx: TCtx, projection: string, address: AggregateAddress): Promise<ProjectionCheckpoint | undefined>;
2149
+ load(ctx: TCtx, projection: string, identity: AggregateIdentity): Promise<ProjectionCheckpoint | undefined>;
2036
2150
  /**
2037
2151
  * Persists the watermark receipt, overwriting a previous one (last write
2038
2152
  * wins; the projector only calls this with advancing checkpoints).
2039
2153
  * Called inside the projector's transaction, after the read-model
2040
2154
  * update it accounts for.
2041
2155
  */
2042
- save(ctx: TCtx, projection: string, address: AggregateAddress, checkpoint: ProjectionCheckpoint): Promise<void>;
2156
+ save(ctx: TCtx, projection: string, identity: AggregateIdentity, checkpoint: ProjectionCheckpoint): Promise<void>;
2043
2157
  /**
2044
2158
  * The wait-for-version building block: `true` when the stored
2045
- * watermark for `(projection, address)` is at or past
2159
+ * watermark for `(projection, identity)` is at or past
2046
2160
  * `position`. Runs OUTSIDE any transaction (a query-side poll).
2047
2161
  *
2048
2162
  * Pass the position of the LAST event your commit emitted: all
@@ -2050,7 +2164,7 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
2050
2164
  * on the version alone would report "reached" while later events
2051
2165
  * of the same commit are still unapplied.
2052
2166
  */
2053
- hasReached(projection: string, address: AggregateAddress, position: ProjectionPosition): Promise<boolean>;
2167
+ hasReached(projection: string, identity: AggregateIdentity, position: ProjectionPosition): Promise<boolean>;
2054
2168
  /**
2055
2169
  * Deletes every checkpoint of `projection` (other projections'
2056
2170
  * checkpoints are untouched): the rebuild entry point. Called
@@ -2067,7 +2181,7 @@ interface ProjectionCheckpointStore<TCtx = unknown> {
2067
2181
  * read-model writes.
2068
2182
  *
2069
2183
  * The projector feed MUST contain every committed envelope for each aggregate
2070
- * address it carries, including event types this read model does not use.
2184
+ * identity it carries, including event types this read model does not use.
2071
2185
  * Handle those events as explicit no-ops in `apply`: the projector still
2072
2186
  * advances their cursor. Filtering a broker subscription by event type drops
2073
2187
  * positions from the source chain and turns the next commit into a real gap.
@@ -2106,6 +2220,298 @@ interface Projection<Evt extends AnyDomainEvent, TCtx = unknown> {
2106
2220
  truncate?(ctx: TCtx): Promise<void>;
2107
2221
  }
2108
2222
  //#endregion
2223
+ //#region src/persistence/repository/identity-map.d.ts
2224
+ /**
2225
+ * A class reference used as the type key of the identity map. Keying
2226
+ * on the CLASS (not a name string) makes collisions impossible by
2227
+ * construction: `Restaurant` and `Booking` are different keys even if
2228
+ * someone names two aggregates identically across modules, and there
2229
+ * is no string-discipline to maintain.
2230
+ *
2231
+ * `Function & { prototype: TAgg }` carries this alone. The kit's aggregate
2232
+ * convention is a **protected constructor** plus static factories, and
2233
+ * TypeScript rejects assigning such a class to a construct-signature type, so
2234
+ * the prototype witness is what accepts them. It infers `TAgg` as well.
2235
+ * Measured: a construct signature beside it changes neither what the type
2236
+ * accepts nor what it infers, and it was the last `any` in the published
2237
+ * types.
2238
+ */
2239
+ type AggregateClass<TAgg> = Function & {
2240
+ prototype: TAgg;
2241
+ };
2242
+ /**
2243
+ * Per-unit-of-work Identity Map (Fowler, PoEAA): within one operation,
2244
+ * one aggregate type+id maps to exactly ONE in-memory instance.
2245
+ *
2246
+ * This is the shipped implementation of the contract the
2247
+ * Repository guide (`docs/guide/repository.md`) places on
2248
+ * `AggregatePersistence` implementations: two `findById(id)` calls in the same
2249
+ * unit of work MUST return the same instance, because commit-token
2250
+ * write registration dedupes by JavaScript object identity. Two instances for
2251
+ * one logical aggregate can otherwise produce two tokens, two harvests,
2252
+ * and two post-commit lifecycle calls.
2253
+ *
2254
+ * Storage is two-level (per-type stores created lazily), so
2255
+ * `Restaurant:123` and `Booking:123` can never collide: the type key
2256
+ * is the aggregate CLASS, not the id alone and not a name string.
2257
+ *
2258
+ * Repository read-path contract:
2259
+ *
2260
+ * ```ts
2261
+ * async findById(id: OrderId): Promise<Order | undefined> {
2262
+ * const cached = this.tracking.identityMap.get(Order, id);
2263
+ * if (cached) return cached;
2264
+ * // Deleted in this unit of work = gone, even if the physical
2265
+ * // delete is deferred and the row is still visible in the tx.
2266
+ * if (this.tracking.identityMap.isDeleted(Order, id)) return undefined;
2267
+ *
2268
+ * const row = await this.loadRow(id);
2269
+ * if (!row) return undefined;
2270
+ * const order = Order.reconstitute(row.id, row.state, row.version);
2271
+ * return this.tracking.trackLoaded(order);
2272
+ * }
2273
+ * ```
2274
+ *
2275
+ * Deletion is final within an operation: {@link delete} removes the
2276
+ * entry AND records a tombstone, so a later {@link set} of the same
2277
+ * type+id throws `AggregateDeletedError`: a second instance of a
2278
+ * deleted aggregate can never sneak back into the unit of work, even
2279
+ * through a repository whose row delete is deferred.
2280
+ *
2281
+ * Lifetime is ONE unit of work: the `UnitOfWork` creates a fresh map
2282
+ * per `run()` and clears it on close. Never cache across operations;
2283
+ * that would silently bypass optimistic concurrency control.
2284
+ */
2285
+ declare class IdentityMap {
2286
+ private readonly _stores;
2287
+ private readonly _deleted;
2288
+ private _pendingAtRegistration;
2289
+ /** The cached instance for type+id, or `undefined` (also after {@link delete}). */
2290
+ get<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): TAgg | undefined;
2291
+ /** Whether an instance is registered for type+id (false after {@link delete}). */
2292
+ has<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
2293
+ /**
2294
+ * Whether type+id was {@link delete}d in this unit of work. The
2295
+ * read path checks this BEFORE hydrating and returns `null`, so
2296
+ * "deleted in this operation" reads uniformly as not-found,
2297
+ * regardless of whether the repository's physical delete already
2298
+ * removed the row or is deferred within the transaction. Without
2299
+ * the check, a read-only probe of a deleted aggregate would crash
2300
+ * in {@link set} for deferred-write repositories and return `null`
2301
+ * for immediate-write ones.
2302
+ */
2303
+ isDeleted<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
2304
+ /**
2305
+ * Registers the hydrated instance under its class and the id of its
2306
+ * `aggregateIdentity`.
2307
+ *
2308
+ * - Re-registering the SAME instance is a no-op (idempotent).
2309
+ * - Registering a DIFFERENT instance for an occupied type+id throws:
2310
+ * that is precisely the identity-map violation this class exists
2311
+ * to prevent (the repository hydrated twice instead of checking
2312
+ * {@link get} first), and letting it pass would double-harvest
2313
+ * events downstream.
2314
+ * - Registering a type+id that was {@link delete}d in this unit of
2315
+ * work throws `AggregateDeletedError`: deletion is final within
2316
+ * the operation.
2317
+ */
2318
+ set<TAgg extends {
2319
+ readonly aggregateIdentity: AggregateIdentity;
2320
+ }>(type: AggregateClass<TAgg>, aggregate: TAgg): void;
2321
+ /**
2322
+ * Registered instances that have recorded MORE pending events than they
2323
+ * carried when first registered (loaded). Used by the unit of work's
2324
+ * end-of-run guard: an aggregate that gained events after load but was
2325
+ * never enrolled would silently drop them. A read-only load, or a
2326
+ * reconstitution that already carried events, shows no increase and is
2327
+ * not reported.
2328
+ */
2329
+ instancesWithNewPendingEvents(): unknown[];
2330
+ /**
2331
+ * Takes back a registration WITHOUT a tombstone: the entry is removed
2332
+ * only when the stored instance IS the given one, and a later
2333
+ * {@link set} of the same type+id stays legal. The Unit of Work calls
2334
+ * this when a registration step fails after {@link set} already ran, so
2335
+ * the failed instance cannot be served by `findById` as a phantom.
2336
+ * This is rollback, not deletion; deletion finality belongs to
2337
+ * {@link delete}.
2338
+ */
2339
+ discard<TAgg>(type: AggregateClass<TAgg>, id: Id<string>, aggregate: TAgg): void;
2340
+ /**
2341
+ * Removes the entry for type+id and records a tombstone: subsequent
2342
+ * {@link get} / {@link has} report absence, and a subsequent
2343
+ * {@link set} of the same type+id throws `AggregateDeletedError`.
2344
+ * The Unit of Work calls this as part of `repository.remove(aggregate)`;
2345
+ * repository adapters receive only the read-only
2346
+ * identity-map view.
2347
+ */
2348
+ delete<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): void;
2349
+ /** Empties all stores and tombstones. Called by the unit of work on close. */
2350
+ clear(): void;
2351
+ }
2352
+ //#endregion
2353
+ //#region src/persistence/repository/persistence-model.d.ts
2354
+ /** Whether a baseline represents an existing row or a pending insert. */
2355
+ type PersistenceLifecycle = "loaded" | "new";
2356
+ /**
2357
+ * Adapter-owned projection and change derivation for one aggregate type.
2358
+ *
2359
+ * The domain model does not implement this contract. A repository adapter
2360
+ * chooses what it persists, how that projection is captured at load, and
2361
+ * whether a change set is a partial diff or a full replacement.
2362
+ */
2363
+ interface PersistenceModel<TAggregate, TBaseline, TChangeSet> {
2364
+ /**
2365
+ * Captures the adapter's persistence projection at the current moment.
2366
+ * Return a detached value or an immutable value object: the Unit of Work
2367
+ * retains it as a baseline and cannot make an arbitrary adapter type safe.
2368
+ *
2369
+ * Capture must be deterministic for an unchanged aggregate: the Unit of
2370
+ * Work compares successive captures to detect mutation after write
2371
+ * registration. A capture that embeds ambient values (clock reads,
2372
+ * random ids) would make every commit look mutated. The default
2373
+ * comparison is the package's structural deep equality, which matches
2374
+ * `Set` members and `Map` keys by reference (JS `SameValueZero`
2375
+ * semantics): a capture that re-materializes object Set members or Map
2376
+ * keys on every call must supply {@link captureEquals}.
2377
+ *
2378
+ * A column that the store owns stays out of the capture, for example an
2379
+ * `updatedAt` that the adapter stamps; `flush` writes it. A captured store
2380
+ * column pushes a stale value back on the next write, or it marks every
2381
+ * row as changed.
2382
+ */
2383
+ capture(aggregate: TAggregate): TBaseline;
2384
+ /**
2385
+ * Adapter-owned equality for two captures of the persistence projection.
2386
+ * Optional: the default is the package's structural deep equality (Set
2387
+ * members and Map keys by reference). Supply it when the capture shape
2388
+ * needs domain-specific comparison, for example rebuilt value-object Set
2389
+ * members compared by value.
2390
+ */
2391
+ readonly captureEquals?: (a: TBaseline, b: TBaseline) => boolean;
2392
+ /**
2393
+ * Derives the adapter's write payload from its own baseline.
2394
+ *
2395
+ * `baseline` is absent for a new aggregate. `lifecycle` disambiguates that
2396
+ * case from an adapter whose loaded baseline type itself admits `undefined`.
2397
+ * The returned payload must not share mutable references with the aggregate;
2398
+ * it is the exact value later handed to `flush`.
2399
+ */
2400
+ changes(baseline: TBaseline | undefined, aggregate: TAggregate, lifecycle: PersistenceLifecycle): TChangeSet;
2401
+ /** Tells orchestration whether the derived state write is empty. */
2402
+ isEmpty(changes: TChangeSet): boolean;
2403
+ }
2404
+ declare const persistenceBaselineBrand: unique symbol;
2405
+ /**
2406
+ * Opaque, typed receipt for an adapter-owned persistence baseline.
2407
+ *
2408
+ * It intentionally exposes no data. The Unit of Work may retain the token and
2409
+ * ask the owning adapter capability to derive changes, but cannot branch on or
2410
+ * couple itself to the baseline's shape.
2411
+ */
2412
+ interface PersistenceBaseline<TAggregate, TChangeSet> {
2413
+ readonly [persistenceBaselineBrand]: (aggregate: TAggregate) => TChangeSet;
2414
+ }
2415
+ /** A derived adapter change set plus its adapter-defined emptiness result. */
2416
+ interface PersistenceChanges<TChangeSet> {
2417
+ readonly value: TChangeSet;
2418
+ readonly empty: boolean;
2419
+ }
2420
+ /** Captures a baseline for an aggregate restored by a repository adapter. */
2421
+ declare function capturePersistenceBaseline<TAggregate, TBaseline, TChangeSet>(model: PersistenceModel<TAggregate, TBaseline, TChangeSet>, aggregate: TAggregate): PersistenceBaseline<TAggregate, TChangeSet>;
2422
+ /** Creates the explicit no-row baseline for a newly added aggregate. */
2423
+ declare function insertPersistenceBaseline<TAggregate, TBaseline, TChangeSet>(model: PersistenceModel<TAggregate, TBaseline, TChangeSet>): PersistenceBaseline<TAggregate, TChangeSet>;
2424
+ /**
2425
+ * Captures the aggregate's current adapter projection using the capability
2426
+ * carried by an existing baseline. Used to seal persistence-last registration.
2427
+ */
2428
+ declare function recapturePersistenceBaseline<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): PersistenceBaseline<TAggregate, TChangeSet>;
2429
+ /**
2430
+ * Recaptures the adapter projection and reports whether it drifted from the
2431
+ * baseline's stored capture, using the model's `captureEquals` when supplied
2432
+ * and structural deep equality otherwise.
2433
+ *
2434
+ * This, not `changes()`/`isEmpty()`, is the mutation detector: the
2435
+ * `PersistenceModel` contract explicitly permits a full-replacement change
2436
+ * set whose `isEmpty` is never true, so a non-empty change set proves
2437
+ * nothing about mutation. Comparing capture to capture asks the honest
2438
+ * question independent of the model's diffing strategy. A `"new"` lifecycle
2439
+ * baseline has no stored capture and never reports drift.
2440
+ */
2441
+ declare function persistenceProjectionDrifted<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): boolean;
2442
+ /** Derives a typed adapter change set without exposing the stored baseline. */
2443
+ declare function derivePersistenceChanges<TAggregate, TChangeSet>(baseline: PersistenceBaseline<TAggregate, TChangeSet>, aggregate: TAggregate): PersistenceChanges<TChangeSet>;
2444
+ //#endregion
2445
+ //#region src/application/unit-of-work/persistence-contract.d.ts
2446
+ /** The explicit persistence intent registered for one tracked aggregate. */
2447
+ type AggregateWriteIntent = "add" | "update" | "remove";
2448
+ /** Read-only Identity Map operations available to repository adapters. */
2449
+ type UnitOfWorkIdentityMap = Pick<IdentityMap, "get" | "has" | "isDeleted">;
2450
+ /**
2451
+ * Repository-specific tracking capability handed only to its adapter.
2452
+ *
2453
+ * Read paths call {@link RepositoryTracking.trackLoaded} before returning a
2454
+ * restored aggregate.
2455
+ * Write methods exposed to application code are replaced by Unit-of-Work-owned
2456
+ * `add`, `update`, and `remove` registrations, so an adapter implementation of
2457
+ * those methods cannot perform durable I/O early or skip event harvesting.
2458
+ *
2459
+ * Contract for repository implementations:
2460
+ * - `findById(id)` checks `identityMap.get` BEFORE hydrating, treats
2461
+ * `identityMap.isDeleted` as not-found (`undefined`), and returns
2462
+ * `tracking.trackLoaded(aggregate)` after hydration. This captures the
2463
+ * expected version before application code can mutate the instance.
2464
+ * - Adapter objects have no lifecycle methods; the facade installs the
2465
+ * Unit-of-Work-owned `add`, `update` unless the definition is append-only,
2466
+ * and `remove` with `physicalRemoval`. An adapter that defines `add`,
2467
+ * `update`, or `remove` fails with `InvalidRepositoryAdapterError`.
2468
+ * - Other repository methods are reads. A custom method that performs a write
2469
+ * would bypass the Unit of Work and violates the adapter contract.
2470
+ */
2471
+ interface RepositoryTracking<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
2472
+ /**
2473
+ * Registers an aggregate restored by a repository before returning it to
2474
+ * application code. The Unit of Work identity-maps the instance and captures
2475
+ * its current version as the optimistic-concurrency expectation.
2476
+ *
2477
+ * Return the result, not the argument. If the Unit of Work already tracks
2478
+ * an instance with the same identity, the result is that instance: when
2479
+ * two loads of one id overlap, the first tracked instance wins.
2480
+ */
2481
+ trackLoaded(aggregate: TAggregate): TAggregate;
2482
+ /**
2483
+ * Read-only view of the per-operation Identity Map (Fowler): one aggregate type+id,
2484
+ * one in-memory instance. Created fresh per `run()`, cleared on
2485
+ * close; accessing it after close throws
2486
+ * {@link TransactionClosedError}.
2487
+ */
2488
+ readonly identityMap: UnitOfWorkIdentityMap;
2489
+ }
2490
+ /**
2491
+ * Immutable adapter input for one registered aggregate write.
2492
+ *
2493
+ * `expectedVersion` is captured when a loaded aggregate joins the Unit of
2494
+ * Work and is absent for `add`. `version`, `changes`, and `events` describe
2495
+ * the exact moment at which the application registered its write intent.
2496
+ * Adapters must use the expected/current version pair for their OCC predicate
2497
+ * and must not read mutable write state back from an aggregate reference.
2498
+ *
2499
+ * The predicate itself is adapter code. The compare-and-set must run in the
2500
+ * store's own write statement. The kit does not know the store, so it cannot
2501
+ * write that statement. The kit owns the version pair and
2502
+ * {@link ConcurrencyConflictError}; the repository contract suite proves the
2503
+ * predicate.
2504
+ */
2505
+ interface AggregatePersistenceWrite<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>, TChangeSet> {
2506
+ readonly intent: AggregateWriteIntent;
2507
+ /** The aggregate type and the id of the written aggregate. */
2508
+ readonly aggregateIdentity: AggregateIdentity<TAggregate["id"]>;
2509
+ readonly expectedVersion: Version | undefined;
2510
+ readonly version: Version;
2511
+ readonly changes: PersistenceChanges<TChangeSet>;
2512
+ readonly events: TAggregate["pendingEvents"];
2513
+ }
2514
+ //#endregion
2109
2515
  //#region src/persistence/event-store/event-store.d.ts
2110
2516
  /** Options for {@link EventStore.append}. */
2111
2517
  interface EventStoreAppendOptions {
@@ -2123,18 +2529,15 @@ interface ReadStreamOptions {
2123
2529
  /**
2124
2530
  * Maximum number of events returned by this page. Required so callers
2125
2531
  * 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.
2532
+ * safe integer. An adapter may return fewer events, for example when it
2533
+ * clamps `limit` to a maximum of its own. It must return at least one
2534
+ * event while unread events remain inside the requested window.
2128
2535
  */
2129
2536
  readonly limit: number;
2130
2537
  /**
2131
2538
  * 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.
2539
+ * for example the version of a snapshot. Defaults to `0` (the first
2540
+ * stream page). Must be a non-negative safe integer when present.
2138
2541
  */
2139
2542
  readonly fromVersion?: number;
2140
2543
  /**
@@ -2143,10 +2546,16 @@ interface ReadStreamOptions {
2143
2546
  * `(fromVersion, toVersion]`. Defaults to the actual stream head.
2144
2547
  * `0` therefore returns an empty window; a value beyond the head clamps
2145
2548
  * to the head; and `fromVersion >= toVersion` is an empty interval, not
2146
- * an error.
2147
- * Must be a non-negative safe integer when present.
2549
+ * an error. Must be a non-negative safe integer when present.
2148
2550
  */
2149
2551
  readonly toVersion?: number;
2552
+ /**
2553
+ * Cooperative-cancellation signal of the surrounding operation. An
2554
+ * adapter either rejects with the `reason` of an aborted signal, or
2555
+ * bounds each page read with the timeout of its driver. So a page read
2556
+ * that hangs cannot hang its caller.
2557
+ */
2558
+ readonly signal?: AbortSignal;
2150
2559
  }
2151
2560
  /**
2152
2561
  * State returned by {@link EventStore.readStream}.
@@ -2155,7 +2564,7 @@ interface ReadStreamOptions {
2155
2564
  * of the requested read window and page limit. `exists: true` implies
2156
2565
  * `lastVersion >= 1`: an
2157
2566
  * 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
2567
+ * events must be reported as `exists: false`. An absent stream is therefore
2159
2568
  * distinguishable from an existing stream whose requested window is empty.
2160
2569
  * Snapshot-backed repositories use that distinction to reject a snapshot whose
2161
2570
  * version lies beyond the current authoritative stream head.
@@ -2169,9 +2578,50 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
2169
2578
  readonly lastVersion: number;
2170
2579
  readonly events: ReadonlyArray<Evt>;
2171
2580
  };
2581
+ /**
2582
+ * The read half of the store: one bounded page of a stream at a time. A
2583
+ * store implements both halves; a read-only implementation offers this
2584
+ * role alone.
2585
+ */
2586
+ interface EventStreamReader<Evt extends AnyDomainEvent> {
2587
+ /**
2588
+ * Reads one bounded page of the qualified stream in append order. An absent stream returns
2589
+ * `{ exists: false, lastVersion: 0, events: [] }`; an existing stream keeps
2590
+ * `exists: true` even when its requested window is empty. An existing stream
2591
+ * has at least one event, so `exists: true` implies `lastVersion >= 1`;
2592
+ * metadata or tombstones without events must be reported as absent.
2593
+ * `lastVersion` is always the actual stream head. `options.limit` is
2594
+ * mandatory and caps the returned array. An adapter may return fewer than
2595
+ * the requested limit, but if the requested window still contains unread
2596
+ * events it must return a non-empty contiguous prefix so callers can make
2597
+ * progress. Each returned event occupies one stream position, so the
2598
+ * events of a page fill the positions after `fromVersion` without a gap.
2599
+ * `options.fromVersion`
2600
+ * excludes positions at or below its 1-based event count;
2601
+ * `options.toVersion` includes positions through its count, so both bounds
2602
+ * describe `(fromVersion, toVersion]`. `toVersion: 0` and inverted ranges
2603
+ * return an empty existing window, while a bound beyond the head clamps to
2604
+ * the head. This distinction is load-bearing for snapshot catch-up and
2605
+ * point-in-time reconstruction: a repository can verify the requested
2606
+ * historical window against the authoritative head. `limit` must be a
2607
+ * positive safe integer; present bounds must be non-negative safe integers.
2608
+ * Invalid options reject with `RangeError` before querying storage.
2609
+ *
2610
+ * Each page's `exists`, `lastVersion`, and `events` must describe one
2611
+ * consistent view of the stream. Multiple page reads are not one database
2612
+ * snapshot. A caller that reads a stream in several pages pins a target
2613
+ * version on the first page, passes it as `toVersion` on every
2614
+ * continuation, and advances `fromVersion` by the number of events each
2615
+ * page returned. Because streams are append-only, that yields a stable
2616
+ * prefix even if new events arrive while replay is in progress. The returned
2617
+ * event array is owned by the caller; implementations must not hand out
2618
+ * mutable live internal state.
2619
+ */
2620
+ readStream(stream: AggregateIdentity, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
2621
+ }
2172
2622
  /**
2173
2623
  * Driven port for event-sourced aggregate persistence: an append-only
2174
- * store with one stream per aggregate. Each stream is addressed by the
2624
+ * store with one stream per aggregate. Each stream is identified by the
2175
2625
  * qualified tuple `(aggregateType, aggregateId)`, because aggregate ids
2176
2626
  * are type-scoped rather than globally unique.
2177
2627
  *
@@ -2187,58 +2637,38 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
2187
2637
  * Repository usage (see the event-sourcing guide):
2188
2638
  *
2189
2639
  * ```ts
2190
- * private stream(id: OrderId): AggregateAddress<OrderId> {
2640
+ * private stream(id: OrderId): AggregateIdentity<OrderId> {
2191
2641
  * return { aggregateType: "Order", aggregateId: id };
2192
2642
  * }
2193
2643
  *
2194
2644
  * async findById(id: OrderId): Promise<Order | undefined> {
2195
2645
  * const cached = this.tracking.identityMap.get(Order, id);
2196
2646
  * 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(
2647
+ * const read = await readStreamPages(this.eventStore, this.stream(id), {
2648
+ * limit: 256,
2649
+ * });
2650
+ * if (!read.reachable) return undefined;
2651
+ * const loaded = await reconstituteAggregateFromStreamPages(
2202
2652
  * () => Order.reconstitute(id), // bare instance, no events
2203
- * first.events,
2653
+ * read,
2204
2654
  * );
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);
2655
+ * if (loaded.isErr()) throw loaded.error; // corrupt stream
2656
+ * return this.tracking.trackLoaded(loaded.value);
2233
2657
  * }
2234
2658
  *
2235
2659
  * flush(write: AggregatePersistenceWrite<Order, number | undefined>) {
2236
- * return this.eventStore.append(this.stream(write.aggregateId), write.events, {
2660
+ * return this.eventStore.append(write.aggregateIdentity, write.events, {
2237
2661
  * expectedVersion: write.expectedVersion ?? 0,
2238
2662
  * });
2239
2663
  * }
2240
2664
  * ```
2241
2665
  *
2666
+ * `readStreamPages` pins the head on the first page as the target version
2667
+ * and reads the rest in bounded pages up to it.
2668
+ * `reconstituteAggregateFromStreamPages` replays the pages and checks that
2669
+ * the replay ends at the target version. The long form, for an adapter that
2670
+ * pages on its own, is in the event-sourcing guide.
2671
+ *
2242
2672
  * `flush` appends the exact event batch registered by `add` or `update`;
2243
2673
  * `withCommit`
2244
2674
  * separately composes them into outbox envelopes. The event store's own
@@ -2247,7 +2677,7 @@ type StreamReadResult<Evt extends AnyDomainEvent> = {
2247
2677
  * The exact appended event batch is acknowledged only after the surrounding
2248
2678
  * transaction commits. Rollback leaves it pending.
2249
2679
  */
2250
- interface EventStore<Evt extends AnyDomainEvent> {
2680
+ interface EventStore<Evt extends AnyDomainEvent> extends EventStreamReader<Evt> {
2251
2681
  /**
2252
2682
  * Atomically appends `events` to the stream, guarded by optimistic
2253
2683
  * concurrency: the append succeeds only when the stream currently
@@ -2292,44 +2722,96 @@ interface EventStore<Evt extends AnyDomainEvent> {
2292
2722
  * touching the store (an ES repository skips `append` for aggregates
2293
2723
  * without pending events anyway).
2294
2724
  *
2725
+ * A load that read its pages through a replica still appends here with
2726
+ * the pinned target as `expectedVersion`. The primary rejects a stale
2727
+ * head, so a lagging replica cannot lose an update.
2728
+ *
2295
2729
  * Treat `aggregateType` as a stable technical stream category. If two
2296
2730
  * bounded contexts share one physical store and reuse a domain name,
2297
2731
  * qualify it at the source (`sales.order`, `fulfillment.order`). Renaming
2298
2732
  * it changes the stream key and therefore requires a data migration.
2299
2733
  */
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.
2734
+ append(stream: AggregateIdentity, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
2735
+ }
2736
+ //#endregion
2737
+ //#region src/persistence/event-store/reconstitute-from-stream-pages.d.ts
2738
+ /**
2739
+ * The shape the replay reads: the stream, the window, and the pages.
2740
+ * `readStreamPages` returns it as the reachable branch of a kit read. An
2741
+ * adapter that pages on its own builds it directly, after it pinned the
2742
+ * target version with `pinTargetVersion`.
2743
+ */
2744
+ interface ReplayableStreamPages<Evt extends AnyDomainEvent> {
2745
+ /** The qualified stream the pages come from. */
2746
+ readonly stream: AggregateIdentity;
2747
+ /**
2748
+ * The cursor the read started at: `fromVersion`, or `0`. The pages hold
2749
+ * the events after it, so the replay target must stand at this version.
2750
+ */
2751
+ readonly fromVersion: number;
2752
+ /**
2753
+ * The version the replay must end at: the head at read time, or a
2754
+ * `toVersion` at or below it. Never a version taken from a snapshot, and
2755
+ * never below `fromVersion`. Later appends do not move it.
2756
+ */
2757
+ readonly targetVersion: number;
2758
+ /**
2759
+ * The events after the cursor through the target version, in append
2760
+ * order, one bounded page per iteration. Every iteration starts again
2761
+ * from the first page, so a second replay of one read sees the same
2762
+ * prefix. A page holds at least one event, and the pages end at the
2763
+ * target version. The replay rejects an empty page, and a page that
2764
+ * would run past the target version, with
2765
+ * {@link InvalidEventStreamPageError} before any row of it reaches the
2766
+ * aggregate. `createReplayableStreamPagesContractTests` proves these
2767
+ * rules for an adapter.
2321
2768
  *
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.
2769
+ * The events of a page are stored facts: no consumer, upcaster, or fold
2770
+ * changes one in place. `readStreamPages` reads fresh events on every
2771
+ * later iteration, and `createReplayableStreamPages` hands out frozen
2772
+ * ones, so a consumer that breaks this rule fails in its own tests.
2330
2773
  */
2331
- readStream(stream: AggregateAddress, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
2774
+ readonly pages: AsyncIterable<ReadonlyArray<Evt>>;
2332
2775
  }
2776
+ /**
2777
+ * Reconstitutes an event-sourced aggregate from the pages of a stream read
2778
+ * and yields it only when the replay ends at the target version.
2779
+ *
2780
+ * This is the paged form of `reconstituteAggregateFromHistory`.
2781
+ * `createReplayTarget` builds the replay target: a fresh instance for a
2782
+ * full replay, or an instance reconstituted from a snapshot for a read that
2783
+ * starts at `snapshot.version`. `read` is the reachable branch of a kit
2784
+ * read, or the value that an adapter that pages on its own built.
2785
+ *
2786
+ * The call checks the window first. `targetVersion` must be a positive
2787
+ * safe integer, and `fromVersion` a non-negative one at or below it. A bad
2788
+ * window rejects with `RangeError` before the replay target is built.
2789
+ *
2790
+ * The call then primes the replay target with an empty history, so the
2791
+ * aggregate runs its own guard even when the read holds no page: a replay
2792
+ * target with pending decisions throws `UnreplayableAggregateError`. The
2793
+ * replay target must stand at `read.fromVersion`. A replay target at
2794
+ * another version throws {@link ReplayTargetMismatchError} before the
2795
+ * first page, even when it also rejected the empty history: a wiring
2796
+ * defect wins over a rejection that the data cannot explain.
2797
+ *
2798
+ * Each page goes through `replayHistory` on the replay target, so
2799
+ * allocation stays bounded by the size of a page; `readStreamPages` rejects
2800
+ * a page over its `limit`. An empty page, and a page that would run past
2801
+ * the target version, throw {@link InvalidEventStreamPageError} before any
2802
+ * row of them reaches the aggregate. Events carry no stream position, so
2803
+ * only the call can find pages that end short of the target version. After
2804
+ * the last page it compares the final version with `read.targetVersion` and
2805
+ * throws {@link ReplayTargetMismatchError} on a difference.
2806
+ *
2807
+ * When the aggregate rejects a stored event with a `DomainError`, the call
2808
+ * stops reading and returns {@link ReplayRejectedError} as `Err`. The
2809
+ * error names the stream and the window of the rejected page, and it holds
2810
+ * the `DomainError` as `cause`. The replay target never escapes on that
2811
+ * path. Wiring errors and a foreign row throw, as in `replayHistory`. What
2812
+ * `createReplayTarget` throws propagates.
2813
+ */
2814
+ declare function reconstituteAggregateFromStreamPages<TAggregate extends ReplayableAggregate<Id<string>, AnyDomainEvent>>(createReplayTarget: () => TAggregate, read: ReplayableStreamPages<Parameters<TAggregate["replayHistory"]>[0][number]>): Promise<Result<TAggregate, ReplayRejectedError>>;
2333
2815
  //#endregion
2334
2816
  //#region src/persistence/snapshot-store/snapshot-store.d.ts
2335
2817
  /**
@@ -2371,14 +2853,14 @@ interface SnapshotStore<TState = unknown> {
2371
2853
  * The latest snapshot for the aggregate, or `undefined` when none
2372
2854
  * exists. The repository falls back to a full replay then.
2373
2855
  */
2374
- load(address: AggregateAddress): Promise<AggregateSnapshot<TState> | undefined>;
2856
+ load(identity: AggregateIdentity): Promise<AggregateSnapshot<TState> | undefined>;
2375
2857
  /**
2376
2858
  * Persists `snapshot` as the new latest for the aggregate,
2377
2859
  * replacing any previous one. Called AFTER the write transaction
2378
2860
  * committed (see the port docs); a single-row upsert is the
2379
2861
  * standard implementation.
2380
2862
  */
2381
- save(address: AggregateAddress, snapshot: AggregateSnapshot<TState>): Promise<void>;
2863
+ save(identity: AggregateIdentity, snapshot: AggregateSnapshot<TState>): Promise<void>;
2382
2864
  /**
2383
2865
  * Removes the aggregate's snapshot; a no-op when none exists. The
2384
2866
  * two callers: the schema-migration fallback (a
@@ -2390,8 +2872,8 @@ interface SnapshotStore<TState = unknown> {
2390
2872
  * which a stale snapshot resurrects the erased aggregate on the
2391
2873
  * snapshot load path; snapshot-first degrades to a full replay.
2392
2874
  */
2393
- delete(address: AggregateAddress): Promise<void>;
2875
+ delete(identity: AggregateIdentity): Promise<void>;
2394
2876
  }
2395
2877
  //#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 };
2878
+ 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
2879
  //# sourceMappingURL=snapshot-store.d.ts.map