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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as NonProgressingEventStreamPageError, B as SnapshotSchemaMismatchError, C as InvalidIntegrationMessageError, D as MissingEntityIdError, E as MisaddressedEventError, F as ProjectionOrderViolationError, G as UnprojectableEventError, H as UnenrolledChangesError, I as ProjectionReceiptViolationError, J as assertNoHostileOwnProtoKey, K as UnregisteredHandlerError, L as ReentrantEventRecordingError, M as PendingEventLimitExceededError, N as ProjectionGapError, O as MissingFoldError, P as ProjectionIdentityViolationError, R as ReplayHeadMismatchError, S as InvalidCommandMessageError, T as KitWiringError, U as UnmanagedInstanceError, V as SnapshotVersionNotRestoredError, W as UnmintedEventError, X as isInfrastructureErrorLike, Y as isDomainErrorLike, Z as isWiringErrorLike, _ as IdempotencyInFlightError, a as DirectStateMutationError, b as InMemoryCapacityExceededError, c as DuplicateEventIdError, d as EventHarvestError, f as FoldReturnedNoStateError, g as IdempotencyCompletionWithoutClaimError, h as IdempotencyClaimLostError, i as ConcurrencyConflictError, j as PendingEventBatchMismatchError, k as MissingHandlerError, l as DuplicateHandlerRegistrationError, m as HostileStateKeyError, n as AggregateNotFoundError, o as DomainError, p as ForeignEventError, q as UnreplayableAggregateError, r as CapabilityRegistryConflictError, s as DuplicateAggregateError, t as AggregateDeletedError, u as ErrorMapperFailedError, v as IdempotencyKeyReuseError, w as InvalidVersionError, x as InfrastructureError, y as IdempotencyReconciliationRequiredError, z as SnapshotCorruptedError } from "./chunks/kit-errors.js";
2
- import { A as voWithValidation, C as hasCooperativeBrand, D as vo, E as deepFreeze, F as findPropertyDescriptor, I as hasIntrinsicPrototypeChain, L as isBuiltInObject, M as deepOmit, N as deepEqual, O as voEquals, P as builtInTagWithoutInvokingAccessors, R as isIntrinsicConstructorPrototype, S as SnapshotTimeValidationError, T as ValueObject, _ as isRecordedDomainEvent, a as assertNonNegativeFinite, b as recordDomainEvent, c as abortReason, d as copyMetadata, f as createDomainEvent, g as defaultDomainEventFactory, h as createUncommittedDomainEvent, i as runBoundedExecution, j as deepEqualExcept, k as voEqualsExcept, l as adoptRecordedDomainEvent, m as createDomainEventFromFacts, n as DEFAULT_EXECUTION_TIMEOUT_MS, o as assertPositiveInteger, p as createDomainEventFactory, r as ownerSignalOf, s as assertPositiveSafeInteger, t as isDispatchTrackingOutbox, u as adoptUncommittedDomainEvent, v as isUncommittedDomainEvent, w as stampCooperativeBrand, x as DomainEventValidationError, y as mergeMetadata, z as isWeakMap } from "./chunks/ports.js";
1
+ import { $ as isDomainErrorLike, A as MissingHandlerError, B as SnapshotCorruptedError, C as InvalidEventStreamPageError, D as MisattributedEventError, E as KitWiringError, F as ProjectionOrderViolationError, G as UnmintedEventError, H as SnapshotVersionNotRestoredError, I as ProjectionReceiptViolationError, J as UnreplayableAggregateError, K as UnprojectableEventError, L as ReentrantEventRecordingError, M as PendingEventLimitExceededError, N as ProjectionGapError, O as MissingEntityIdError, P as ProjectionIdentityViolationError, Q as detachAggregateIdentity, R as ReplayRejectedError, S as InvalidCommandMessageError, T as InvalidVersionError, U as UnenrolledChangesError, V as SnapshotSchemaMismatchError, W as UnmanagedInstanceError, X as attributeConflictIntent, Y as assertNoHostileOwnProtoKey, Z as describeAggregateIdentity, _ as IdempotencyInFlightError, a as DirectStateMutationError, b as InMemoryCapacityExceededError, c as DuplicateEventIdError, d as EventHarvestError, et as isInfrastructureErrorLike, f as FoldReturnedNoStateError, g as IdempotencyCompletionWithoutClaimError, h as IdempotencyClaimLostError, i as ConcurrencyConflictError, j as PendingEventBatchMismatchError, k as MissingFoldError, l as DuplicateHandlerRegistrationError, m as HostileStateKeyError, n as AggregateNotFoundError, nt as rewriteErrorMessage, o as DomainError, p as ForeignEventError, q as UnregisteredHandlerError, r as CapabilityRegistryConflictError, s as DuplicateAggregateError, t as AggregateDeletedError, tt as isWiringErrorLike, u as ErrorMapperFailedError, v as IdempotencyKeyReuseError, w as InvalidIntegrationMessageError, x as InfrastructureError, y as IdempotencyReconciliationRequiredError, z as ReplayTargetMismatchError } from "./chunks/kit-errors.js";
2
+ import { A as SnapshotTimeValidationError, B as voWithValidation, C as defaultDomainEventFactory, D as mintUncommittedDomainEvent, E as mergeMetadata, F as ValueObject, G as isBuiltInObject, H as deepOmit, I as deepFreeze, K as isIntrinsicConstructorPrototype, L as vo, M as detachState, N as hasCooperativeBrand, O as recordDomainEvent, P as stampCooperativeBrand, R as voEquals, S as createUncommittedDomainEvent, T as isUncommittedDomainEvent, U as deepEqual, V as deepEqualExcept, W as findPropertyDescriptor, _ as adoptUncommittedDomainEvent, a as isDispatchTrackingOutbox, b as createDomainEventFactory, c as runBoundedExecution, d as assertPositiveInteger, f as assertPositiveSafeInteger, g as adoptRecordedDomainEvent, h as waitRejectingOnAbort, i as assertPageWithinWindow, j as detachFreezableState, k as DomainEventValidationError, l as MAX_TIMER_DELAY_MS, m as abortReason, n as readStreamPages, o as DEFAULT_EXECUTION_TIMEOUT_MS, p as assertTimerDelay, q as isWeakMap, r as assertPageNotEmpty, s as ownerSignalOf, t as pinTargetVersion, u as assertNonNegativeSafeInteger, v as copyMetadata, w as isRecordedDomainEvent, x as createDomainEventFromFacts, y as createDomainEvent, z as voEqualsExcept } from "./chunks/read-stream-pages.js";
3
3
  import { err, ok } from "@shirudo/result";
4
4
  import { ValidationError, someChainRetryable } from "@shirudo/base-error";
5
5
 
@@ -200,10 +200,13 @@ function assertJsonValue(value, path, invalid, active = /* @__PURE__ */ new Weak
200
200
  * destinations, while event-stream replay only rebuilds process state.
201
201
  */
202
202
  function routeEventsToCommandOutbox(outbox, mapper) {
203
- return { add: async (events) => {
204
- const commits = events.map((candidate) => toCommandCommit(candidate, mapper));
205
- await outbox.add(commits);
206
- } };
203
+ return {
204
+ add: async (events) => {
205
+ const commits = events.map((candidate) => toCommandCommit(candidate, mapper));
206
+ await outbox.add(commits);
207
+ },
208
+ endEventSources: (sources) => outbox.endEventSources(sources)
209
+ };
207
210
  }
208
211
  function toCommandCommit(candidate, mapper) {
209
212
  const mapped = mapper(candidate.event);
@@ -283,7 +286,7 @@ function invalid$1(path, reason) {
283
286
  }
284
287
 
285
288
  //#endregion
286
- //#region src/domain/aggregate/internal/global-capability-registry.ts
289
+ //#region src/internal/global-capability-registry.ts
287
290
  /**
288
291
  * The sentence a capability lookup appends to its rejection when the
289
292
  * registry is private to this copy, so the reader learns why an instance
@@ -343,7 +346,7 @@ function withRequire(registry, shared) {
343
346
 
344
347
  //#endregion
345
348
  //#region src/domain/aggregate/pending-event-lifecycle.ts
346
- const { registry: capabilities$2, require: require$1 } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/pending-event-lifecycle-registry/v6"));
349
+ const { registry: capabilities$2, require: require$1 } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/pending-event-lifecycle-registry/v7"));
347
350
  /** Resolves the lifecycle authority or throws `UnmanagedInstanceError`. */
348
351
  function requirePendingEventLifecycleCapability(aggregate, operation) {
349
352
  return require$1(aggregate, operation, "aggregate");
@@ -424,9 +427,9 @@ function createCommitTokenScope() {
424
427
  if (existing) {
425
428
  const record = recordsByToken.get(existing);
426
429
  if (!record) throw new EventHarvestError("withCommit: internal commit-token registry is inconsistent.");
427
- if (record.disposition === "deleted" && disposition === "saved") throw new EventHarvestError(`withCommit: aggregate ${String(aggregate.id)} was enrolled as saved after it was enrolled as deleted in the same transaction.`);
428
- if (options?.expectedVersion !== void 0 && options.expectedVersion !== record.expectedVersion) throw new EventHarvestError(`withCommit: aggregate ${String(aggregate.id)} was re-enrolled with expectedVersion ${String(options.expectedVersion)}, but its enrollment recorded ${String(record.expectedVersion)}. Duplicate enrollment must assert the same OCC baseline or none.`);
429
- if (enrollmentDiverged(record)) throw new EventHarvestError(`withCommit: aggregate ${String(aggregate.id)} changed after its commit batch was enrolled. Register persistence intent last.`);
430
+ if (record.disposition === "deleted" && disposition === "saved") throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} was enrolled as saved after it was enrolled as deleted in the same transaction.`);
431
+ if (options?.expectedVersion !== void 0 && options.expectedVersion !== record.expectedVersion) throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} was re-enrolled with expectedVersion ${String(options.expectedVersion)}, but its enrollment recorded ${String(record.expectedVersion)}. Duplicate enrollment must assert the same OCC baseline or none.`);
432
+ if (enrollmentDiverged(record)) throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} changed after its commit batch was enrolled. Register persistence intent last.`);
430
433
  if (disposition === "deleted") record.disposition = "deleted";
431
434
  return existing;
432
435
  }
@@ -467,7 +470,7 @@ function createCommitTokenScope() {
467
470
  if (!record) throw new EventHarvestError("withCommit: a commit token was not minted by this callback's enrollment capability. Forged and stale tokens are rejected.");
468
471
  if (seen.has(tokenObject)) continue;
469
472
  seen.add(tokenObject);
470
- if (enrollmentDiverged(record)) throw new EventHarvestError(`withCommit: aggregate ${String(record.aggregate.id)} changed after its commit batch was enrolled; events recorded after enrollment are not part of the attested write and would be silently dropped. Make domain decisions first, write, and enroll last.`);
473
+ if (enrollmentDiverged(record)) throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(record.aggregate.aggregateIdentity)} changed after its commit batch was enrolled; events recorded after enrollment are not part of the attested write and would be silently dropped. Make domain decisions first, write, and enroll last.`);
471
474
  records.push(record);
472
475
  }
473
476
  if (seen.size !== mintedTokenCount) throw new EventHarvestError("withCommit: every token minted by the current enrollment capability must be returned in `commits`. If an enrolled write must not commit, throw so the transaction rolls back.");
@@ -595,8 +598,21 @@ function createCommitTokenScope() {
595
598
  * ```
596
599
  */
597
600
  async function withCommit(deps, fn) {
601
+ return withCheckedCommit(deps, async (ctx, enrollment) => {
602
+ const { result, commits } = await fn(ctx, enrollment);
603
+ return {
604
+ result,
605
+ commits
606
+ };
607
+ }, void 0);
608
+ }
609
+ /**
610
+ * {@link withCommit} with a check per attempt before the commit. The scope
611
+ * receives `onAttemptStart` in its transactional options.
612
+ */
613
+ async function withCheckedCommit(deps, fn, onAttemptStart) {
598
614
  const postCommitTimeoutMs = deps.postCommitTimeoutMs ?? 3e4;
599
- assertNonNegativeFinite("withCommit", "postCommitTimeoutMs", postCommitTimeoutMs);
615
+ assertTimerDelay("withCommit", "postCommitTimeoutMs", postCommitTimeoutMs);
600
616
  if (deps.signal?.aborted) throw abortReason(deps.signal, "withCommit aborted before opening a transaction");
601
617
  const { result, commitRecords, events } = await deps.scope.transactional(async (ctx) => {
602
618
  const tokenScope = createCommitTokenScope();
@@ -606,44 +622,64 @@ async function withCommit(deps, fn) {
606
622
  } finally {
607
623
  tokenScope.close();
608
624
  }
609
- const commitRecords = tokenScope.resolve(fnResult.commits);
610
- const candidates = commitRecords.flatMap((record) => {
611
- const agg = record.aggregate;
612
- if (record.events.length > 0 && record.persistedVersion !== void 0 && record.version <= record.persistedVersion) throw new EventHarvestError(`withCommit: aggregate ${String(agg.id)} recorded events but did not advance its version beyond the persisted version (${String(record.persistedVersion)}). An eventful commit needs a unique cursor; use StateStoredAggregate.setState(currentState, event) instead of addDomainEvent(event) alone.`);
613
- const enrolledId = String(agg.id);
614
- const enrolledType = record.eventLifecycle.aggregateType();
615
- return record.events.map((event, index) => {
616
- if (!isRecordedDomainEvent(event)) throw new EventHarvestError(`withCommit: event "${event.type}" has not been recorded. Call recordPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox harvest.`, event.type);
617
- const recordedEvent = event;
618
- const commitSize = record.events.length;
619
- const aggregateId = recordedEvent.aggregateId;
620
- const aggregateType = recordedEvent.aggregateType;
621
- const missing = [];
622
- if (!aggregateId) missing.push("aggregateId");
623
- if (!aggregateType) missing.push("aggregateType");
624
- if (!aggregateId || !aggregateType) throw new EventHarvestError(`withCommit: event "${recordedEvent.type}" is missing ${missing.join(" and ")}. Use this.createEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); createEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on the envelope source.`, recordedEvent.type);
625
- if (aggregateId !== enrolledId || aggregateType !== enrolledType) throw new EventHarvestError(`withCommit: event "${recordedEvent.type}" is addressed to ${aggregateType} ${aggregateId} but was enrolled under ${enrolledType} ${enrolledId}. The aggregate base classes stamp the address on every recording path; an instance from another package copy must stamp it the same way.`, recordedEvent.type);
626
- return Object.freeze({
627
- event: recordedEvent,
628
- source: Object.freeze({
629
- aggregateId,
630
- aggregateType
631
- }),
632
- position: Object.freeze({
633
- aggregateVersion: record.version,
634
- commitSequence: index,
635
- commitSize
636
- })
625
+ try {
626
+ const commitRecords = tokenScope.resolve(fnResult.commits);
627
+ const candidates = commitRecords.flatMap((record) => {
628
+ const enrolled = record.aggregate.aggregateIdentity;
629
+ if (record.events.length > 0 && record.persistedVersion !== void 0 && record.version <= record.persistedVersion) throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(enrolled)} recorded events but did not advance its version beyond the persisted version (${String(record.persistedVersion)}). An eventful commit needs a unique cursor; use StateStoredAggregate.setState(currentState, event) instead of addDomainEvent(event) alone.`);
630
+ const enrolledId = enrolled.aggregateId;
631
+ const enrolledType = enrolled.aggregateType;
632
+ return record.events.map((event, index) => {
633
+ if (!isRecordedDomainEvent(event)) throw new EventHarvestError(`withCommit: event "${event.type}" has not been recorded. Call recordPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox harvest.`, event.type);
634
+ const recordedEvent = event;
635
+ const commitSize = record.events.length;
636
+ const aggregateId = recordedEvent.aggregateId;
637
+ const aggregateType = recordedEvent.aggregateType;
638
+ const missing = [];
639
+ if (!aggregateId) missing.push("aggregateId");
640
+ if (!aggregateType) missing.push("aggregateType");
641
+ if (!aggregateId || !aggregateType) throw new EventHarvestError(`withCommit: event "${recordedEvent.type}" is missing ${missing.join(" and ")}. Use this.createEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); createEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on the envelope source.`, recordedEvent.type);
642
+ if (aggregateId !== enrolledId || aggregateType !== enrolledType) throw new EventHarvestError(`withCommit: event "${recordedEvent.type}" belongs to ${describeAggregateIdentity({
643
+ aggregateType,
644
+ aggregateId
645
+ })} but was enrolled under ${describeAggregateIdentity({
646
+ aggregateType: enrolledType,
647
+ aggregateId: enrolledId
648
+ })}. The aggregate base classes stamp the aggregate identity on every recording path; an instance from another package copy must stamp it the same way.`, recordedEvent.type);
649
+ return Object.freeze({
650
+ event: recordedEvent,
651
+ source: Object.freeze({
652
+ aggregateId,
653
+ aggregateType
654
+ }),
655
+ position: Object.freeze({
656
+ aggregateVersion: record.version,
657
+ commitSequence: index,
658
+ commitSize
659
+ })
660
+ });
637
661
  });
638
662
  });
639
- });
640
- if (candidates.length > 0) await deps.outbox.add(candidates);
641
- return {
642
- result: fnResult.result,
643
- commitRecords,
644
- events: candidates.map(({ event }) => event)
645
- };
646
- }, { signal: deps.signal });
663
+ const endedSources = commitRecords.filter((record) => record.disposition === "deleted").map((record) => detachAggregateIdentity(record.aggregate.aggregateIdentity));
664
+ if (candidates.length > 0) await deps.outbox.add(Object.freeze(candidates));
665
+ if (endedSources.length > 0) await deps.outbox.endEventSources(Object.freeze(endedSources));
666
+ fnResult.checkBeforeCommit?.();
667
+ if (candidates.length > 0 || endedSources.length > 0) {
668
+ for (const record of commitRecords) if (enrollmentDiverged(record)) throw new EventHarvestError(`withCommit: aggregate ${describeAggregateIdentity(record.aggregate.aggregateIdentity)} changed while its outbox write ran. Await every change inside the work callback.`);
669
+ }
670
+ return {
671
+ result: fnResult.result,
672
+ commitRecords,
673
+ events: candidates.map(({ event }) => event)
674
+ };
675
+ } catch (error) {
676
+ fnResult.onFailureBeforeCommit?.(error);
677
+ throw error;
678
+ }
679
+ }, {
680
+ signal: deps.signal,
681
+ onAttemptStart
682
+ });
647
683
  const persistedObservations = [];
648
684
  for (const { aggregate, eventLifecycle, disposition, version, events: committedEvents } of commitRecords) try {
649
685
  if (disposition === "deleted") eventLifecycle.discardPendingEvents(committedEvents);
@@ -753,11 +789,21 @@ var QueryBus = class {
753
789
  * limit rejects a new address before mutation; delivery state is never
754
790
  * silently evicted.
755
791
  *
756
- * **Not transaction-aware**, the same documented limitation as the
757
- * other in-memory references: a rolled-back `schedule` or `cancel`
758
- * stays applied here. The transactional half of the contract is the
759
- * SQL adapter's job; prove it with `createDeadlineStoreContractTests`
760
- * and its rollback capability.
792
+ * On its own, the store knows nothing about your `TransactionScope`
793
+ * rollbacks: a rolled-back `schedule` or `cancel` stays applied. Register
794
+ * the store with an `InMemoryTransactionScope` for tests that roll back or
795
+ * retry.
796
+ *
797
+ * A processor reads only committed deadlines. While a transaction is open,
798
+ * `due` skips a deadline that the transaction scheduled. For an address that
799
+ * the transaction rescheduled or cancelled, it returns the committed
800
+ * deadline as it was at the first write of the transaction. A failure report
801
+ * against that deadline counts its attempts. At the ceiling, `due` stops
802
+ * returning it until the transaction ends. A rollback also undoes an acknowledgement or a failure report
803
+ * that a processor made while the transaction was open. The processor then
804
+ * delivers that deadline again. For production, prove the transactional half
805
+ * of the contract with `createDeadlineStoreContractTests` and its rollback
806
+ * capability.
761
807
  *
762
808
  * Payloads are deep-copied on schedule and on delivery
763
809
  * (`structuredClone`), so neither side can mutate the other's copy.
@@ -769,6 +815,11 @@ var InMemoryDeadlineStore = class {
769
815
  maxDeliveryAttempts;
770
816
  maxRecords;
771
817
  nextSequence = 0;
818
+ /**
819
+ * The committed deadline of each address that the open transaction wrote,
820
+ * captured at its first write. `undefined` marks an address without one.
821
+ */
822
+ committedVersions;
772
823
  constructor(options = {}) {
773
824
  const max = options.maxDeliveryAttempts ?? 5;
774
825
  assertPositiveInteger("InMemoryDeadlineStore", "maxDeliveryAttempts", max);
@@ -776,8 +827,38 @@ var InMemoryDeadlineStore = class {
776
827
  if (options.maxRecords !== void 0) assertPositiveSafeInteger("InMemoryDeadlineStore", "maxRecords", options.maxRecords);
777
828
  this.maxRecords = options.maxRecords;
778
829
  }
830
+ /**
831
+ * Records the state that a rollback of an `InMemoryTransactionScope`
832
+ * returns to: the pending and the dead-letter records. The rollback
833
+ * keeps the sequence counter, as a database sequence does, so a
834
+ * delivery id stays unique. Until the transaction ends, `due` returns the
835
+ * committed deadline of each address that the transaction writes.
836
+ */
837
+ beginTransaction() {
838
+ const pending = [...this.pending].map(([key, deadline]) => [key, { ...deadline }]);
839
+ const dead = [...this.dead].map(([deliveryId, deadline]) => [deliveryId, { ...deadline }]);
840
+ const versions = /* @__PURE__ */ new Map();
841
+ this.committedVersions = versions;
842
+ const end = () => {
843
+ if (this.committedVersions === versions) this.committedVersions = void 0;
844
+ };
845
+ return {
846
+ commit: end,
847
+ rollback: () => {
848
+ this.pending.clear();
849
+ for (const [key, deadline] of pending) this.pending.set(key, { ...deadline });
850
+ this.dead.clear();
851
+ for (const [deliveryId, deadline] of dead) this.dead.set(deliveryId, { ...deadline });
852
+ end();
853
+ }
854
+ };
855
+ }
779
856
  async schedule(deadline) {
780
- const deadlineAddress = address(deadline.scope, deadline.key);
857
+ const scope = deadline.scope;
858
+ const key = deadline.key;
859
+ const dueAt = new Date(deadline.dueAt);
860
+ const payload = structuredClone(deadline.payload);
861
+ const deadlineAddress = address(scope, key);
781
862
  if (!this.pending.has(deadlineAddress) && this.maxRecords !== void 0 && this.pending.size + this.dead.size >= this.maxRecords) throw new InMemoryCapacityExceededError({
782
863
  store: "InMemoryDeadlineStore",
783
864
  resource: "records",
@@ -786,26 +867,54 @@ var InMemoryDeadlineStore = class {
786
867
  attempted: 1
787
868
  });
788
869
  const sequence = this.nextSequence++;
789
- this.pending.set(deadlineAddress, {
870
+ const incarnation = {
790
871
  deliveryId: `deadline-${sequence}`,
791
- scope: deadline.scope,
792
- key: deadline.key,
793
- dueAt: new Date(deadline.dueAt),
794
- payload: structuredClone(deadline.payload),
872
+ scope,
873
+ key,
874
+ dueAt,
875
+ payload,
795
876
  attempts: 0,
796
877
  sequence
797
- });
878
+ };
879
+ this.recordCommittedVersion(deadlineAddress);
880
+ this.pending.set(deadlineAddress, incarnation);
798
881
  }
799
882
  async cancel(scope, key) {
800
- this.pending.delete(address(scope, key));
883
+ const deadlineAddress = address(scope, key);
884
+ this.recordCommittedVersion(deadlineAddress);
885
+ this.pending.delete(deadlineAddress);
801
886
  }
802
887
  async due(now, limit) {
803
888
  if (!Number.isInteger(limit) || limit < 0) throw new Error(`InMemoryDeadlineStore: limit must be an integer >= 0, got ${limit}`);
804
889
  if (limit === 0) return [];
805
- return [...this.pending.values()].filter((deadline) => deadline.dueAt.getTime() <= now.getTime()).sort((a, b) => a.dueAt.getTime() - b.dueAt.getTime() || a.sequence - b.sequence).slice(0, limit).map((deadline) => toRecord(deadline));
890
+ return this.committedPending().filter((deadline) => deadline.dueAt.getTime() <= now.getTime()).sort((a, b) => a.dueAt.getTime() - b.dueAt.getTime() || a.sequence - b.sequence).slice(0, limit).map((deadline) => toRecord(deadline));
891
+ }
892
+ recordCommittedVersion(deadlineAddress) {
893
+ const versions = this.committedVersions;
894
+ if (versions === void 0 || versions.has(deadlineAddress)) return;
895
+ const committed = this.pending.get(deadlineAddress);
896
+ versions.set(deadlineAddress, committed === void 0 ? void 0 : { ...committed });
897
+ }
898
+ /**
899
+ * The pending deadlines that a processor may see. For an address that the
900
+ * open transaction wrote, it is the committed deadline, if any, and not
901
+ * the live record.
902
+ */
903
+ committedPending() {
904
+ const versions = this.committedVersions;
905
+ if (versions === void 0) return [...this.pending.values()];
906
+ const deadlines = [...this.pending].filter(([deadlineAddress]) => !versions.has(deadlineAddress)).map(([, deadline]) => deadline);
907
+ for (const committed of versions.values()) if (committed !== void 0) deadlines.push(committed);
908
+ return deadlines;
909
+ }
910
+ /** The address of the committed deadline with this delivery id, if any. */
911
+ committedVersionAddress(deliveryId) {
912
+ for (const [deadlineAddress, committed] of this.committedVersions ?? []) if (committed?.deliveryId === deliveryId) return deadlineAddress;
806
913
  }
807
914
  async markDelivered(deliveryIds) {
808
915
  for (const deliveryId of deliveryIds) {
916
+ const committedAddress = this.committedVersionAddress(deliveryId);
917
+ if (committedAddress !== void 0) this.committedVersions?.set(committedAddress, void 0);
809
918
  this.dead.delete(deliveryId);
810
919
  for (const [key, deadline] of this.pending) if (deadline.deliveryId === deliveryId) {
811
920
  this.pending.delete(key);
@@ -825,6 +934,14 @@ var InMemoryDeadlineStore = class {
825
934
  }
826
935
  return;
827
936
  }
937
+ const committedAddress = this.committedVersionAddress(deliveryId);
938
+ const committed = committedAddress === void 0 ? void 0 : this.committedVersions?.get(committedAddress);
939
+ if (committedAddress !== void 0 && committed !== void 0) {
940
+ committed.attempts += 1;
941
+ if (error !== void 0) committed.lastError = String(error);
942
+ if (committed.attempts >= this.maxDeliveryAttempts) this.committedVersions?.set(committedAddress, void 0);
943
+ return;
944
+ }
828
945
  }
829
946
  async deadLetters() {
830
947
  return [...this.dead.values()].sort((a, b) => a.sequence - b.sequence).map(toDeadLetter);
@@ -995,9 +1112,9 @@ var PollLoop = class {
995
1112
  this.pollIntervalMs = options.pollIntervalMs ?? 250;
996
1113
  this.baseDelayMs = options.baseDelayMs ?? 50;
997
1114
  this.maxDelayMs = options.maxDelayMs ?? 5e3;
998
- assertNonNegativeFinite(context, "pollIntervalMs", this.pollIntervalMs);
999
- assertNonNegativeFinite(context, "baseDelayMs", this.baseDelayMs);
1000
- assertNonNegativeFinite(context, "maxDelayMs", this.maxDelayMs);
1115
+ assertTimerDelay(context, "pollIntervalMs", this.pollIntervalMs);
1116
+ assertTimerDelay(context, "baseDelayMs", this.baseDelayMs);
1117
+ assertTimerDelay(context, "maxDelayMs", this.maxDelayMs);
1001
1118
  this.jitter = neutralJitterSource(options.random ?? Math.random);
1002
1119
  }
1003
1120
  /**
@@ -1158,8 +1275,8 @@ var DeadlineProcessor = class extends PollLoop {
1158
1275
  this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
1159
1276
  this.deliveryTimeoutMs = options.deliveryTimeoutMs ?? 3e4;
1160
1277
  this.storageTimeoutMs = options.storageTimeoutMs ?? 3e4;
1161
- assertNonNegativeFinite("DeadlineProcessor", "deliveryTimeoutMs", this.deliveryTimeoutMs);
1162
- assertNonNegativeFinite("DeadlineProcessor", "storageTimeoutMs", this.storageTimeoutMs);
1278
+ assertTimerDelay("DeadlineProcessor", "deliveryTimeoutMs", this.deliveryTimeoutMs);
1279
+ assertTimerDelay("DeadlineProcessor", "storageTimeoutMs", this.storageTimeoutMs);
1163
1280
  }
1164
1281
  /**
1165
1282
  * One full delivery pass (the `run`/`drainOnce` shell lives on
@@ -1287,14 +1404,26 @@ function positiveSafeInteger(value) {
1287
1404
  * process must configure the limit or use a durable adapter; exhaustion
1288
1405
  * rejects new keys before mutation and never forgets an idempotency decision.
1289
1406
  *
1290
- * It is deliberately not transaction-aware. Claims and staged outcomes carry
1291
- * bounded leases, while every mutation compares the store-minted token. An
1292
- * expired pending claim may be replaced; an expired staged outcome cannot be
1293
- * guessed away and instead returns `reconciliation-required`. Only an
1294
- * authoritative `committed` / `not-committed` decision can settle it.
1407
+ * On its own, the store is the reference for the leased family: it cannot
1408
+ * see commits or rollbacks. Claims and staged outcomes carry bounded leases,
1409
+ * while every mutation compares the store-minted token. An expired pending
1410
+ * claim may be replaced; an expired staged outcome cannot be guessed away and
1411
+ * instead returns `reconciliation-required`. Only an authoritative
1412
+ * `committed` / `not-committed` decision can settle it.
1413
+ *
1414
+ * Register the store with an `InMemoryTransactionScope` for tests that roll
1415
+ * back or retry. A rollback then undoes the writes of `claim` and `complete`,
1416
+ * which the port runs inside the transaction. It returns each key that they
1417
+ * wrote to its earlier entry. The lease operations `renew`, `confirm`,
1418
+ * `abandon`, and `reconcile` stay out of band, as the port defines them. A
1419
+ * rollback keeps their writes on every other key. A confirmation that
1420
+ * arrives after the next transaction began therefore survives the rollback
1421
+ * of that transaction.
1295
1422
  */
1296
1423
  var InMemoryIdempotencyStore = class {
1297
1424
  entries = /* @__PURE__ */ new Map();
1425
+ /** The entries that `claim` and `complete` replaced in the open transaction. */
1426
+ replacedInTransaction;
1298
1427
  clock;
1299
1428
  claimTokenFactory;
1300
1429
  leaseDurationMs;
@@ -1308,73 +1437,76 @@ var InMemoryIdempotencyStore = class {
1308
1437
  this.renewAfterMs = options.renewAfterMs ?? Math.floor(this.leaseDurationMs / 2);
1309
1438
  if (options.maxEntries !== void 0) assertPositiveSafeInteger("InMemoryIdempotencyStore", "maxEntries", options.maxEntries);
1310
1439
  this.maxEntries = options.maxEntries;
1311
- if (!positiveSafeInteger(this.leaseDurationMs) || this.leaseDurationMs > 2147483647) throw new RangeError("leaseDurationMs must be a positive safe integer no greater than 2147483647");
1312
- if (!positiveSafeInteger(this.renewAfterMs) || this.renewAfterMs >= this.leaseDurationMs || this.renewAfterMs > 2147483647) throw new RangeError("renewAfterMs must be a positive safe integer below leaseDurationMs and no greater than 2147483647");
1440
+ if (!positiveSafeInteger(this.leaseDurationMs) || this.leaseDurationMs > 2147483647) throw new RangeError(`leaseDurationMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS}`);
1441
+ if (!positiveSafeInteger(this.renewAfterMs) || this.renewAfterMs >= this.leaseDurationMs || this.renewAfterMs > 2147483647) throw new RangeError(`renewAfterMs must be a positive safe integer below leaseDurationMs and no greater than ${MAX_TIMER_DELAY_MS}`);
1313
1442
  }
1314
- async claim(_ctx, key, fingerprint) {
1315
- const now = this.nowMs();
1316
- const existing = this.entries.get(key);
1317
- if (existing === void 0) {
1318
- if (this.maxEntries !== void 0 && this.entries.size >= this.maxEntries) throw new InMemoryCapacityExceededError({
1319
- store: "InMemoryIdempotencyStore",
1320
- resource: "entries",
1321
- limit: this.maxEntries,
1322
- current: this.entries.size,
1323
- attempted: 1
1324
- });
1325
- return this.createPending(key, fingerprint, now);
1326
- }
1327
- if (existing.fingerprint !== fingerprint) throw new IdempotencyKeyReuseError({
1328
- key,
1329
- storedFingerprint: existing.fingerprint,
1330
- receivedFingerprint: fingerprint
1331
- });
1332
- if (existing.status === "confirmed") return {
1333
- status: "completed",
1334
- outcome: structuredClone(existing.outcome)
1443
+ /**
1444
+ * Starts to record the entries that `claim` and `complete` replace. The
1445
+ * rollback restores them in reverse order. It keeps the token generation,
1446
+ * so a token from a rolled-back claim never names a later claim.
1447
+ */
1448
+ beginTransaction() {
1449
+ const replaced = [];
1450
+ this.replacedInTransaction = replaced;
1451
+ const end = () => {
1452
+ if (this.replacedInTransaction === replaced) this.replacedInTransaction = void 0;
1335
1453
  };
1336
- if (now < existing.expiresAtMs) throw new IdempotencyInFlightError({ key });
1337
- if (existing.status === "staged") return {
1338
- status: "reconciliation-required",
1339
- reconciliation: Object.freeze({
1340
- key,
1341
- fingerprint,
1342
- token: existing.token,
1343
- expiredAt: new Date(existing.expiresAtMs).toISOString()
1344
- })
1454
+ return {
1455
+ commit: end,
1456
+ rollback: () => {
1457
+ for (const [key, entry] of [...replaced].reverse()) if (entry === void 0) this.entries.delete(key);
1458
+ else this.entries.set(key, entry);
1459
+ end();
1460
+ }
1345
1461
  };
1346
- return this.createPending(key, fingerprint, now);
1462
+ }
1463
+ async claim(_ctx, key, fingerprint) {
1464
+ const now = this.nowMs();
1465
+ const decided = this.decideFromStoredEntry(key, fingerprint, now);
1466
+ if (decided !== void 0) return decided;
1467
+ const tokenPart = this.newTokenPart();
1468
+ return this.decideFromStoredEntry(key, fingerprint, now) ?? this.createPending(key, fingerprint, now, tokenPart);
1347
1469
  }
1348
1470
  async complete(_ctx, claim, outcome) {
1471
+ const { key, token } = claim;
1472
+ const owned = structuredClone(outcome);
1349
1473
  const now = this.nowMs();
1350
- const existing = this.entries.get(claim.key);
1351
- if (existing === void 0) throw new IdempotencyCompletionWithoutClaimError(claim.key);
1352
- if (existing.status !== "pending" || existing.token !== claim.token || now >= existing.expiresAtMs) throw this.claimLost(claim);
1474
+ const existing = this.entries.get(key);
1475
+ if (existing === void 0) throw new IdempotencyCompletionWithoutClaimError(key);
1476
+ if (existing.status !== "pending" || existing.token !== token || now >= existing.expiresAtMs) throw this.claimLost({
1477
+ key,
1478
+ token
1479
+ });
1353
1480
  const expiresAtMs = now + this.leaseDurationMs;
1354
1481
  this.lease(expiresAtMs);
1355
- this.entries.set(claim.key, {
1482
+ this.writeInTransaction(key, {
1356
1483
  fingerprint: existing.fingerprint,
1357
1484
  status: "staged",
1358
1485
  token: existing.token,
1359
1486
  expiresAtMs,
1360
- outcome: structuredClone(outcome)
1487
+ outcome: owned
1361
1488
  });
1362
1489
  }
1363
1490
  async renew(claim) {
1491
+ const { key, token } = claim;
1364
1492
  const now = this.nowMs();
1365
- const existing = this.entries.get(claim.key);
1366
- if (existing === void 0 || existing.status === "confirmed" || existing.token !== claim.token || now >= existing.expiresAtMs) throw this.claimLost(claim);
1493
+ const existing = this.entries.get(key);
1494
+ if (existing === void 0 || existing.status === "confirmed" || existing.token !== token || now >= existing.expiresAtMs) throw this.claimLost({
1495
+ key,
1496
+ token
1497
+ });
1367
1498
  const expiresAtMs = now + this.leaseDurationMs;
1368
1499
  const lease = this.lease(expiresAtMs);
1369
- this.entries.set(claim.key, {
1500
+ this.entries.set(key, {
1370
1501
  ...existing,
1371
1502
  expiresAtMs
1372
1503
  });
1373
1504
  return lease;
1374
1505
  }
1375
1506
  async confirm(claim) {
1376
- const existing = this.entries.get(claim.key);
1377
- if (existing?.status === "staged" && existing.token === claim.token) this.entries.set(claim.key, {
1507
+ const { key, token } = claim;
1508
+ const existing = this.entries.get(key);
1509
+ if (existing?.status === "staged" && existing.token === token) this.entries.set(key, {
1378
1510
  fingerprint: existing.fingerprint,
1379
1511
  status: "confirmed",
1380
1512
  token: existing.token,
@@ -1382,18 +1514,21 @@ var InMemoryIdempotencyStore = class {
1382
1514
  });
1383
1515
  }
1384
1516
  async abandon(claim) {
1385
- const existing = this.entries.get(claim.key);
1386
- if (existing !== void 0 && existing.status !== "confirmed" && existing.token === claim.token) this.entries.delete(claim.key);
1517
+ const { key, token } = claim;
1518
+ const existing = this.entries.get(key);
1519
+ if (existing !== void 0 && existing.status !== "confirmed" && existing.token === token) this.entries.delete(key);
1387
1520
  }
1388
1521
  async reconcile(reconciliation, decision) {
1522
+ const { key, token, fingerprint, expiredAt } = reconciliation;
1389
1523
  if (decision !== "committed" && decision !== "not-committed") throw new TypeError("reconcile decision must be committed or not-committed; uncertainty must leave the record untouched");
1390
- const existing = this.entries.get(reconciliation.key);
1391
- if (existing === void 0 || existing.status !== "staged" || existing.token !== reconciliation.token || existing.fingerprint !== reconciliation.fingerprint || new Date(existing.expiresAtMs).toISOString() !== reconciliation.expiredAt || this.nowMs() < existing.expiresAtMs) throw new IdempotencyClaimLostError({
1392
- key: reconciliation.key,
1393
- token: reconciliation.token
1524
+ const now = this.nowMs();
1525
+ const existing = this.entries.get(key);
1526
+ if (existing === void 0 || existing.status !== "staged" || existing.token !== token || existing.fingerprint !== fingerprint || new Date(existing.expiresAtMs).toISOString() !== expiredAt || now < existing.expiresAtMs) throw new IdempotencyClaimLostError({
1527
+ key,
1528
+ token
1394
1529
  });
1395
1530
  if (decision === "committed") {
1396
- this.entries.set(reconciliation.key, {
1531
+ this.entries.set(key, {
1397
1532
  fingerprint: existing.fingerprint,
1398
1533
  status: "confirmed",
1399
1534
  token: existing.token,
@@ -1401,7 +1536,7 @@ var InMemoryIdempotencyStore = class {
1401
1536
  });
1402
1537
  return;
1403
1538
  }
1404
- this.entries.delete(reconciliation.key);
1539
+ this.entries.delete(key);
1405
1540
  }
1406
1541
  /** Test hook: number of stored records in any state. */
1407
1542
  get size() {
@@ -1411,15 +1546,55 @@ var InMemoryIdempotencyStore = class {
1411
1546
  clear() {
1412
1547
  this.entries.clear();
1413
1548
  }
1414
- createPending(key, fingerprint, now) {
1549
+ /**
1550
+ * Returns the claim that the stored entry of the key decides, or
1551
+ * `undefined` when the store can create a pending claim. Throws for a
1552
+ * reused key, a claim in flight, and a full store.
1553
+ */
1554
+ decideFromStoredEntry(key, fingerprint, now) {
1555
+ const existing = this.entries.get(key);
1556
+ if (existing === void 0) {
1557
+ if (this.maxEntries !== void 0 && this.entries.size >= this.maxEntries) throw new InMemoryCapacityExceededError({
1558
+ store: "InMemoryIdempotencyStore",
1559
+ resource: "entries",
1560
+ limit: this.maxEntries,
1561
+ current: this.entries.size,
1562
+ attempted: 1
1563
+ });
1564
+ return;
1565
+ }
1566
+ if (existing.fingerprint !== fingerprint) throw new IdempotencyKeyReuseError({
1567
+ key,
1568
+ storedFingerprint: existing.fingerprint,
1569
+ receivedFingerprint: fingerprint
1570
+ });
1571
+ if (existing.status === "confirmed") return {
1572
+ status: "completed",
1573
+ outcome: structuredClone(existing.outcome)
1574
+ };
1575
+ if (now < existing.expiresAtMs) throw new IdempotencyInFlightError({ key });
1576
+ if (existing.status === "staged") return {
1577
+ status: "reconciliation-required",
1578
+ reconciliation: Object.freeze({
1579
+ key,
1580
+ fingerprint,
1581
+ token: existing.token,
1582
+ expiredAt: new Date(existing.expiresAtMs).toISOString()
1583
+ })
1584
+ };
1585
+ }
1586
+ newTokenPart() {
1415
1587
  const tokenPart = this.claimTokenFactory();
1416
1588
  if (typeof tokenPart !== "string" || tokenPart.length === 0) throw new TypeError("claimTokenFactory must return a non-empty string");
1589
+ return tokenPart;
1590
+ }
1591
+ createPending(key, fingerprint, now, tokenPart) {
1417
1592
  this.tokenGeneration += 1;
1418
1593
  if (!Number.isSafeInteger(this.tokenGeneration)) throw new RangeError("idempotency claim-token generation exhausted");
1419
1594
  const token = `${this.tokenGeneration}:${tokenPart}`;
1420
1595
  const expiresAtMs = now + this.leaseDurationMs;
1421
1596
  const lease = this.lease(expiresAtMs);
1422
- this.entries.set(key, {
1597
+ this.writeInTransaction(key, {
1423
1598
  fingerprint,
1424
1599
  status: "pending",
1425
1600
  token,
@@ -1434,6 +1609,10 @@ var InMemoryIdempotencyStore = class {
1434
1609
  })
1435
1610
  };
1436
1611
  }
1612
+ writeInTransaction(key, entry) {
1613
+ this.replacedInTransaction?.push([key, this.entries.get(key)]);
1614
+ this.entries.set(key, entry);
1615
+ }
1437
1616
  lease(expiresAtMs) {
1438
1617
  return Object.freeze({
1439
1618
  expiresAt: new Date(expiresAtMs).toISOString(),
@@ -1467,7 +1646,7 @@ function startLeaseHeartbeat(store, claim) {
1467
1646
  let heartbeatFailure;
1468
1647
  const schedule = (delayMs) => {
1469
1648
  if (!validRenewAfterMs(delayMs)) {
1470
- heartbeatFailure = /* @__PURE__ */ new TypeError("IdempotencyStore returned an invalid lease renewAfterMs; expected a positive safe integer no greater than 2147483647");
1649
+ heartbeatFailure = /* @__PURE__ */ new TypeError(`IdempotencyStore returned an invalid lease renewAfterMs; expected a positive safe integer no greater than ${MAX_TIMER_DELAY_MS}`);
1471
1650
  return;
1472
1651
  }
1473
1652
  timer = setTimeout(() => {
@@ -1677,13 +1856,13 @@ async function withIdempotentCommit(deps, request, fn) {
1677
1856
  }
1678
1857
 
1679
1858
  //#endregion
1680
- //#region src/domain/aggregate/aggregate-address.ts
1859
+ //#region src/domain/aggregate/aggregate-identity.ts
1681
1860
  /**
1682
1861
  * Collision-safe map-key encoding shared by the in-memory adapters.
1683
1862
  * Internal: durable adapters key on the two storage columns themselves.
1684
1863
  */
1685
- function encodeAggregateAddress(address) {
1686
- return JSON.stringify([address.aggregateType, address.aggregateId]);
1864
+ function encodeAggregateIdentity(identity) {
1865
+ return JSON.stringify([identity.aggregateType, identity.aggregateId]);
1687
1866
  }
1688
1867
 
1689
1868
  //#endregion
@@ -1707,16 +1886,19 @@ function isPositionAfter(candidate, reference) {
1707
1886
  * serves tests and in-memory read models.
1708
1887
  *
1709
1888
  * Its checkpoint-key locks serialize competing projectors only inside one
1710
- * process and only when they share this store instance. It is **not
1711
- * transaction-aware** (the `ctx` parameter is ignored): a rolled-back
1712
- * projector batch does not roll back its checkpoints. Use it for tests and
1713
- * disposable in-memory read models; production atomicity is the durable
1714
- * adapter's contract, proved with `createProjectionCheckpointStoreContractTests`
1715
- * and its rollback capability.
1889
+ * process and only when they share this store instance. The store ignores
1890
+ * the `ctx` parameter. On its own, it knows nothing about your
1891
+ * `TransactionScope` rollbacks: a rolled-back projector batch keeps its
1892
+ * checkpoints. Register the store with an `InMemoryTransactionScope` for
1893
+ * tests that roll back or retry. While a transaction is open, `hasReached`
1894
+ * answers from the checkpoints at the start of the transaction. Use it for
1895
+ * tests and disposable in-memory read models; production atomicity is the
1896
+ * durable adapter's contract, proved with
1897
+ * `createProjectionCheckpointStoreContractTests` and its rollback capability.
1716
1898
  *
1717
1899
  * Without `maxCheckpoints`, checkpoint retention is unbounded and supported
1718
1900
  * only for finite-lifetime tests and demos. A configured limit rejects a new
1719
- * address before mutation; existing watermarks remain updatable and are never
1901
+ * identity before mutation; existing watermarks remain updatable and are never
1720
1902
  * evicted because forgetting one would change projection correctness.
1721
1903
  *
1722
1904
  * Do not nest `withCheckpointLocks` calls whose key sets overlap. This
@@ -1731,15 +1913,40 @@ var InMemoryProjectionCheckpointStore = class {
1731
1913
  lockTails = /* @__PURE__ */ new Map();
1732
1914
  maxCheckpoints;
1733
1915
  checkpointCount = 0;
1916
+ /** The checkpoints at the start of the open transaction. */
1917
+ committedCheckpoints;
1734
1918
  constructor(options = {}) {
1735
1919
  if (options.maxCheckpoints !== void 0) assertPositiveSafeInteger("InMemoryProjectionCheckpointStore", "maxCheckpoints", options.maxCheckpoints);
1736
1920
  this.maxCheckpoints = options.maxCheckpoints;
1737
1921
  }
1738
- async withCheckpointLocks(_ctx, projection, addresses, work) {
1739
- const keys = [...new Set(addresses.map((address) => JSON.stringify([
1922
+ /**
1923
+ * Records the state that a rollback of an `InMemoryTransactionScope`
1924
+ * returns to: the checkpoints and their count. Until the transaction
1925
+ * ends, `hasReached` reads these recorded checkpoints. The rollback
1926
+ * keeps the checkpoint locks, because a caller still holds them.
1927
+ */
1928
+ beginTransaction() {
1929
+ const committed = new Map([...this.checkpoints].map(([projection, perAggregate]) => [projection, new Map(perAggregate)]));
1930
+ const checkpointCount = this.checkpointCount;
1931
+ this.committedCheckpoints = committed;
1932
+ const end = () => {
1933
+ if (this.committedCheckpoints === committed) this.committedCheckpoints = void 0;
1934
+ };
1935
+ return {
1936
+ commit: end,
1937
+ rollback: () => {
1938
+ this.checkpoints.clear();
1939
+ for (const [projection, perAggregate] of committed) this.checkpoints.set(projection, new Map(perAggregate));
1940
+ this.checkpointCount = checkpointCount;
1941
+ end();
1942
+ }
1943
+ };
1944
+ }
1945
+ async withCheckpointLocks(_ctx, projection, identities, work) {
1946
+ const keys = [...new Set(identities.map((identity) => JSON.stringify([
1740
1947
  projection,
1741
- address.aggregateType,
1742
- address.aggregateId
1948
+ identity.aggregateType,
1949
+ identity.aggregateId
1743
1950
  ])))].sort();
1744
1951
  const releases = [];
1745
1952
  for (const key of keys) {
@@ -1762,17 +1969,22 @@ var InMemoryProjectionCheckpointStore = class {
1762
1969
  for (let index = releases.length - 1; index >= 0; index -= 1) releases[index]?.();
1763
1970
  }
1764
1971
  }
1765
- async load(_ctx, projection, address) {
1766
- const stored = this.checkpoints.get(projection)?.get(encodeAggregateAddress(address));
1972
+ async load(_ctx, projection, identity) {
1973
+ const stored = this.checkpoints.get(projection)?.get(encodeAggregateIdentity(identity));
1767
1974
  return stored === void 0 ? void 0 : {
1768
1975
  ...stored,
1769
1976
  position: { ...stored.position }
1770
1977
  };
1771
1978
  }
1772
- async save(_ctx, projection, address, checkpoint) {
1773
- const addressKey = encodeAggregateAddress(address);
1979
+ async save(_ctx, projection, identity, checkpoint) {
1980
+ const identityKey = encodeAggregateIdentity(identity);
1981
+ const { position, ...rest } = checkpoint;
1982
+ const owned = {
1983
+ ...rest,
1984
+ position: { ...position }
1985
+ };
1774
1986
  let perAggregate = this.checkpoints.get(projection);
1775
- const isNewCheckpoint = perAggregate?.has(addressKey) !== true;
1987
+ const isNewCheckpoint = perAggregate?.has(identityKey) !== true;
1776
1988
  if (isNewCheckpoint && this.maxCheckpoints !== void 0 && this.checkpointCount >= this.maxCheckpoints) throw new InMemoryCapacityExceededError({
1777
1989
  store: "InMemoryProjectionCheckpointStore",
1778
1990
  resource: "checkpoints",
@@ -1784,14 +1996,11 @@ var InMemoryProjectionCheckpointStore = class {
1784
1996
  perAggregate = /* @__PURE__ */ new Map();
1785
1997
  this.checkpoints.set(projection, perAggregate);
1786
1998
  }
1787
- perAggregate.set(addressKey, {
1788
- ...checkpoint,
1789
- position: { ...checkpoint.position }
1790
- });
1999
+ perAggregate.set(identityKey, owned);
1791
2000
  if (isNewCheckpoint) this.checkpointCount += 1;
1792
2001
  }
1793
- async hasReached(projection, address, position) {
1794
- const stored = this.checkpoints.get(projection)?.get(encodeAggregateAddress(address));
2002
+ async hasReached(projection, identity, position) {
2003
+ const stored = (this.committedCheckpoints ?? this.checkpoints).get(projection)?.get(encodeAggregateIdentity(identity));
1795
2004
  if (stored === void 0) return false;
1796
2005
  return !isPositionAfter(position, stored.position);
1797
2006
  }
@@ -1870,8 +2079,8 @@ function projectionFromHandlers(options) {
1870
2079
  * Because checkpoints advance only across a verified chain, a position
1871
2080
  * at or behind the watermark is already traversed and can be skipped under
1872
2081
  * the source's one-logical-event-per-position contract.
1873
- * - **Feeds are complete per aggregate address.** Once a feed supplies one
1874
- * address, it must supply every committed envelope in that address's cursor
2082
+ * - **Feeds are complete per aggregate identity.** Once a feed supplies one
2083
+ * identity, it must supply every committed envelope in that aggregate's cursor
1875
2084
  * chain. Do not event-type-filter a projector subscription. Irrelevant event
1876
2085
  * types are explicit no-ops in `Projection.apply`; invoking the handler and
1877
2086
  * checkpointing their positions preserves continuity.
@@ -1890,13 +2099,13 @@ function projectionFromHandlers(options) {
1890
2099
  * redeliveries.
1891
2100
  * - **Malformed envelopes reject loudly.** A missing/empty `eventId`, a
1892
2101
  * missing/invalid `position`, missing `source.aggregateId` /
1893
- * `source.aggregateType`, or an optional event address contradicting its
2102
+ * `source.aggregateType`, or an optional event identity contradicting its
1894
2103
  * authoritative envelope source fails the batch BEFORE anything is applied.
1895
2104
  * The domain event remains persistence-agnostic.
1896
2105
  * - **Competing instances serialize by checkpoint key.** The required
1897
2106
  * {@link ProjectionCheckpointStore.withCheckpointLocks} callback covers the
1898
- * complete load / apply / save critical section for every addressed
1899
- * aggregate. The adapter must lock a key even when its checkpoint row does
2107
+ * complete load / apply / save critical section for every aggregate of
2108
+ * the batch. The adapter must lock a key even when its checkpoint row does
1900
2109
  * not exist yet; a plain row lock is insufficient at genesis. Without that
1901
2110
  * adapter guarantee, only a hard single-projector deployment is safe.
1902
2111
  *
@@ -1924,7 +2133,7 @@ var Projector = class {
1924
2133
  * per advanced aggregate. Rejects
1925
2134
  * (after rollback) when a handler throws or an envelope carries no
1926
2135
  * valid cursor; the caller's at-least-once redelivery retries the batch.
1927
- * The input must be a complete, ordered feed per aggregate address; an
2136
+ * The input must be a complete, ordered feed per aggregate identity; an
1928
2137
  * event-type-filtered subscription cannot satisfy the cursor contract.
1929
2138
  * An already-aborted signal rejects before validation or transaction setup.
1930
2139
  * In-flight cancellation is forwarded to the transaction scope rather than
@@ -1955,36 +2164,36 @@ var Projector = class {
1955
2164
  return {
1956
2165
  event,
1957
2166
  position,
1958
- address: {
2167
+ identity: {
1959
2168
  aggregateType,
1960
2169
  aggregateId
1961
2170
  }
1962
2171
  };
1963
2172
  });
1964
- const lockAddresses = [...new Map(cursored.map(({ address }) => [encodeAggregateAddress(address), address])).entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, address]) => address);
2173
+ const lockIdentities = [...new Map(cursored.map(({ identity }) => [encodeAggregateIdentity(identity), identity])).entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, identity]) => identity);
1965
2174
  const projectWithLocks = async (ctx) => {
1966
2175
  let applied = 0;
1967
2176
  let skipped = 0;
1968
2177
  const checkpointsAtBatchStart = /* @__PURE__ */ new Map();
1969
2178
  const watermarks = /* @__PURE__ */ new Map();
1970
- for (const { event, address } of cursored) {
1971
- const key = encodeAggregateAddress(address);
2179
+ for (const { event, identity } of cursored) {
2180
+ const key = encodeAggregateIdentity(identity);
1972
2181
  if (watermarks.has(key)) continue;
1973
- const stored = await this.checkpoints.load(ctx, this.projection.name, address);
2182
+ const stored = await this.checkpoints.load(ctx, this.projection.name, identity);
1974
2183
  if (stored !== void 0 && !isValidCheckpoint(stored)) throw new UnprojectableEventError(this.projection.name, event.eventId, "found a stored checkpoint with an invalid or legacy cursor. Migrate the commitSize/previousEventfulAggregateVersion/lastAppliedEventId columns or reset and rebuild this projection.");
1975
2184
  checkpointsAtBatchStart.set(key, stored);
1976
2185
  watermarks.set(key, stored?.position);
1977
2186
  }
1978
- for (const { event, position, address } of cursored) {
1979
- const stored = checkpointsAtBatchStart.get(encodeAggregateAddress(address));
2187
+ for (const { event, position, identity } of cursored) {
2188
+ const stored = checkpointsAtBatchStart.get(encodeAggregateIdentity(identity));
1980
2189
  if (stored !== void 0 && isSameOrderedPosition(position, stored.position)) {
1981
2190
  if (event.eventId !== stored.lastAppliedEventId) throw new ProjectionIdentityViolationError(this.projection.name, event.eventId, stored.lastAppliedEventId, formatPosition(position));
1982
2191
  if (!isSamePositionReceipt(position, stored.position)) throw new ProjectionReceiptViolationError(this.projection.name, event.eventId, formatReceipt(stored.position), formatReceipt(position));
1983
2192
  }
1984
2193
  }
1985
2194
  const batchReceiptsByPosition = /* @__PURE__ */ new Map();
1986
- for (const { event, position, address } of cursored) {
1987
- const key = addressedPositionKey(address, position);
2195
+ for (const { event, position, identity } of cursored) {
2196
+ const key = aggregatePositionKey(identity, position);
1988
2197
  const recorded = batchReceiptsByPosition.get(key);
1989
2198
  if (recorded !== void 0 && recorded.eventId !== event.eventId) throw new ProjectionIdentityViolationError(this.projection.name, event.eventId, recorded.eventId, formatPosition(position));
1990
2199
  if (recorded !== void 0 && !isSamePositionReceipt(position, recorded.position)) throw new ProjectionReceiptViolationError(this.projection.name, event.eventId, formatReceipt(recorded.position), formatReceipt(position));
@@ -1995,11 +2204,11 @@ var Projector = class {
1995
2204
  }
1996
2205
  const newestUnprocessed = /* @__PURE__ */ new Map();
1997
2206
  const positionsSeenInBatch = /* @__PURE__ */ new Set();
1998
- for (const { event, position, address } of cursored) {
1999
- const key = encodeAggregateAddress(address);
2207
+ for (const { event, position, identity } of cursored) {
2208
+ const key = encodeAggregateIdentity(identity);
2000
2209
  const stored = watermarks.get(key);
2001
2210
  if (stored !== void 0 && !isPositionAfter(position, stored)) continue;
2002
- const positionKey = addressedPositionKey(address, position);
2211
+ const positionKey = aggregatePositionKey(identity, position);
2003
2212
  if (positionsSeenInBatch.has(positionKey)) continue;
2004
2213
  positionsSeenInBatch.add(positionKey);
2005
2214
  const newest = newestUnprocessed.get(key);
@@ -2008,8 +2217,8 @@ var Projector = class {
2008
2217
  }
2009
2218
  const advanced = /* @__PURE__ */ new Map();
2010
2219
  const toApply = [];
2011
- for (const { event, position, address } of cursored) {
2012
- const key = encodeAggregateAddress(address);
2220
+ for (const { event, position, identity } of cursored) {
2221
+ const key = encodeAggregateIdentity(identity);
2013
2222
  const watermark = watermarks.get(key);
2014
2223
  if (watermark !== void 0 && !isPositionAfter(position, watermark)) {
2015
2224
  skipped += 1;
@@ -2018,7 +2227,7 @@ var Projector = class {
2018
2227
  if (!isContiguousPosition(position, watermark)) throw new ProjectionGapError(this.projection.name, event.eventId, formatPosition(watermark), formatPosition(position));
2019
2228
  watermarks.set(key, position);
2020
2229
  advanced.set(key, {
2021
- address,
2230
+ identity,
2022
2231
  checkpoint: {
2023
2232
  position,
2024
2233
  lastAppliedEventId: event.eventId
@@ -2028,23 +2237,23 @@ var Projector = class {
2028
2237
  applied += 1;
2029
2238
  }
2030
2239
  for (const { event } of toApply) await this.projection.apply(ctx, event);
2031
- for (const { address, checkpoint } of advanced.values()) await this.checkpoints.save(ctx, this.projection.name, address, checkpoint);
2240
+ for (const { identity, checkpoint } of advanced.values()) await this.checkpoints.save(ctx, this.projection.name, identity, checkpoint);
2032
2241
  return {
2033
2242
  applied,
2034
2243
  skipped
2035
2244
  };
2036
2245
  };
2037
- return this.scope.transactional((ctx) => this.checkpoints.withCheckpointLocks(ctx, this.projection.name, lockAddresses, () => projectWithLocks(ctx)), { signal: options.signal });
2246
+ return this.scope.transactional((ctx) => this.checkpoints.withCheckpointLocks(ctx, this.projection.name, lockIdentities, () => projectWithLocks(ctx)), { signal: options.signal });
2038
2247
  }
2039
2248
  /**
2040
2249
  * The wait-for-version query: `true` when this projection has
2041
- * processed the addressed aggregate at least up to `position`. Pass the
2250
+ * processed the given aggregate at least up to `position`. Pass the
2042
2251
  * position of the last event the awaited commit emitted (see
2043
2252
  * {@link ProjectionCheckpointStore.hasReached} for why the full
2044
2253
  * cursor, not just the version).
2045
2254
  */
2046
- hasProcessed(address, position) {
2047
- return this.checkpoints.hasReached(this.projection.name, address, position);
2255
+ hasProcessed(identity, position) {
2256
+ return this.checkpoints.hasReached(this.projection.name, identity, position);
2048
2257
  }
2049
2258
  /**
2050
2259
  * Rebuild entry point: one transaction that clears this
@@ -2052,7 +2261,7 @@ var Projector = class {
2052
2261
  * `truncate`, the read model with them. Replay the source through
2053
2262
  * {@link Projector.project} afterwards. Stop all live consumers for this
2054
2263
  * projection before reset and keep them stopped through catch-up replay;
2055
- * rebuild is not coordinated by the per-address delivery locks.
2264
+ * rebuild is not coordinated by the per-aggregate delivery locks.
2056
2265
  */
2057
2266
  async reset() {
2058
2267
  await this.scope.transactional(async (ctx) => {
@@ -2098,10 +2307,10 @@ function isSameOrderedPosition(left, right) {
2098
2307
  function isSamePositionReceipt(left, right) {
2099
2308
  return isSameOrderedPosition(left, right) && left.commitSize === right.commitSize && left.previousEventfulAggregateVersion === right.previousEventfulAggregateVersion;
2100
2309
  }
2101
- function addressedPositionKey(address, position) {
2310
+ function aggregatePositionKey(identity, position) {
2102
2311
  return JSON.stringify([
2103
- address.aggregateType,
2104
- address.aggregateId,
2312
+ identity.aggregateType,
2313
+ identity.aggregateId,
2105
2314
  position.aggregateVersion,
2106
2315
  position.commitSequence
2107
2316
  ]);
@@ -2121,6 +2330,33 @@ function formatReceipt(position) {
2121
2330
  return `(${position.aggregateVersion}, ${position.commitSequence}; commitSize=${position.commitSize}, previousEventfulAggregateVersion=${String(position.previousEventfulAggregateVersion)})`;
2122
2331
  }
2123
2332
 
2333
+ //#endregion
2334
+ //#region src/internal/cause-chain.ts
2335
+ /**
2336
+ * Cycle-safe, getter-throw-safe walk over `error`'s standard `cause`
2337
+ * chain. `visit` runs for every object link (the top error included) and
2338
+ * receives the link plus its lazily read `cause`; a non-undefined return
2339
+ * stops the walk. A throwing `cause` getter (lazy deserialization, revoked
2340
+ * Proxy) ends the walk as no-match instead of replacing the real failure
2341
+ * with the getter's exception.
2342
+ */
2343
+ function findInCauseChain(error, visit) {
2344
+ const seen = /* @__PURE__ */ new Set();
2345
+ let current = error;
2346
+ while (current !== null && typeof current === "object" && !seen.has(current)) {
2347
+ seen.add(current);
2348
+ let cause;
2349
+ try {
2350
+ cause = current.cause;
2351
+ } catch {
2352
+ return;
2353
+ }
2354
+ const found = visit(current, cause);
2355
+ if (found !== void 0) return found;
2356
+ current = cause;
2357
+ }
2358
+ }
2359
+
2124
2360
  //#endregion
2125
2361
  //#region src/application/unit-of-work/errors.ts
2126
2362
  /**
@@ -2147,10 +2383,13 @@ var NestedUnitOfWorkError = class extends KitWiringError {
2147
2383
  }
2148
2384
  };
2149
2385
  /**
2150
- * Thrown when the unit-of-work context is used after `run()` has
2151
- * settled: reading `context.repositories`, calling an adapter-held
2152
- * `tracking.trackLoaded`, or using a repository facade after the transaction
2153
- * has committed or rolled back.
2386
+ * Thrown when the unit-of-work context is used after the unit of work closed
2387
+ * it: reading `context.repositories`, calling an adapter-held
2388
+ * `tracking.trackLoaded`, or using a repository facade after the flush of its
2389
+ * attempt, while the transaction commits, or after it committed or rolled
2390
+ * back. An `add`, `update`, or `remove` between the start of the flush and the
2391
+ * last check before the commit fails the attempt with `AggregateTrackingError`
2392
+ * instead.
2154
2393
  *
2155
2394
  * Use-after-close is a programming bug (typically a leaked context
2156
2395
  * reference or a fire-and-forget promise outliving the callback), so
@@ -2166,18 +2405,22 @@ var NestedUnitOfWorkError = class extends KitWiringError {
2166
2405
  var TransactionClosedError = class extends KitWiringError {
2167
2406
  operation;
2168
2407
  constructor(operation) {
2169
- super("TRANSACTION_CLOSED", `Unit of work is closed: ${operation} was called after the transaction committed or rolled back. Do not use the context or repository facade or tracking capability outside the run() callback.`);
2408
+ super("TRANSACTION_CLOSED", `Unit of work is closed: ${operation} was called after its attempt ended. Do not use the context, the repository facade, or the tracking capability outside the run() callback, and await all work inside it.`);
2170
2409
  this.operation = operation;
2171
2410
  }
2172
2411
  };
2173
2412
  /** A repository factory returned a value that cannot be wrapped as a facade. */
2174
2413
  var InvalidRepositoryAdapterError = class extends KitWiringError {
2175
2414
  repository;
2415
+ reason;
2176
2416
  receivedType;
2177
- constructor(repository, receivedType) {
2178
- super("INVALID_REPOSITORY_ADAPTER", `Repository factory "${repository}" returned ${receivedType}; it must return an adapter object.`);
2179
- this.repository = repository;
2180
- this.receivedType = receivedType;
2417
+ operation;
2418
+ constructor(options) {
2419
+ super("INVALID_REPOSITORY_ADAPTER", options.reason === "not_an_object" ? `Repository factory "${options.repository}" returned ${options.receivedType}; it must return an adapter object.` : `Repository factory "${options.repository}" returned an adapter that defines ${options.operation}. The unit of work installs add, update, and remove on the facade, so an adapter must not define them. Remove ${options.operation} from the adapter.`);
2420
+ this.repository = options.repository;
2421
+ this.reason = options.reason;
2422
+ this.receivedType = options.reason === "not_an_object" ? options.receivedType : void 0;
2423
+ this.operation = options.reason === "defines_lifecycle_operation" ? options.operation : void 0;
2181
2424
  }
2182
2425
  };
2183
2426
  /** A Unit of Work received repository wiring that bypassed {@link defineRepository}. */
@@ -2190,12 +2433,12 @@ var InvalidRepositoryDefinitionError = class extends KitWiringError {
2190
2433
  };
2191
2434
  /** A repository's persistence-error policy threw or returned a non-kit error. */
2192
2435
  var RepositoryErrorMappingFailedError = class extends KitWiringError {
2193
- aggregateId;
2436
+ identity;
2194
2437
  intent;
2195
2438
  mapperCause;
2196
2439
  constructor(options) {
2197
- super("REPOSITORY_ERROR_MAPPING_FAILED", `The repository error mapper failed for ${options.intent} of aggregate ${options.aggregateId}. The original persistence failure is preserved as cause; the mapper failure is available as mapperCause.`, options.persistenceError);
2198
- this.aggregateId = options.aggregateId;
2440
+ super("REPOSITORY_ERROR_MAPPING_FAILED", `The repository error mapper failed for ${options.intent} of aggregate ${describeAggregateIdentity(options.identity)}. The original persistence failure is preserved as cause; the mapper failure is available as mapperCause.`, options.persistenceError);
2441
+ this.identity = detachAggregateIdentity(options.identity);
2199
2442
  this.intent = options.intent;
2200
2443
  this.mapperCause = options.mapperError;
2201
2444
  }
@@ -2205,36 +2448,51 @@ var RepositoryErrorMappingFailedError = class extends KitWiringError {
2205
2448
  *
2206
2449
  * This is a wiring error, not a persistence failure: the statements cannot
2207
2450
  * run the write the Unit of Work registered, and a retry repeats the same
2208
- * outcome. {@link versionedFlush} raises it during the commit phase, so the
2209
- * repository's `mapError` receives it like a store failure. A mapper must
2210
- * return an `InfrastructureError`. So the use case receives the mapper's own
2211
- * error, and the reason stays in the cause chain.
2451
+ * outcome. The commit phase hands it to the caller unchanged, never to the
2452
+ * repository's `mapError`, because a mapper returns an `InfrastructureError`
2453
+ * and would relabel the defect as a store failure.
2212
2454
  */
2213
2455
  var InvalidFlushStatementError = class extends KitWiringError {
2214
- aggregateType;
2215
- aggregateId;
2456
+ identity;
2457
+ /** The write, or `null` when the code that found the defect does not know it. */
2216
2458
  intent;
2217
2459
  reason;
2218
- /** What the statement returned instead of a row count. */
2460
+ /** The value that the flush received in place of a valid one. */
2219
2461
  received;
2220
2462
  /** The failure of `isDuplicate`, when the classifier itself threw. */
2221
2463
  classifierCause;
2222
2464
  constructor(options) {
2223
2465
  super("INVALID_FLUSH_STATEMENT", flushStatementReasonMessage(options), options.cause);
2224
- this.aggregateType = options.aggregateType;
2225
- this.aggregateId = options.aggregateId;
2226
- this.intent = options.intent;
2466
+ this.identity = detachAggregateIdentity(options.identity);
2467
+ this.intent = options.intent ?? null;
2227
2468
  this.reason = options.reason;
2228
2469
  this.received = options.received;
2229
2470
  this.classifierCause = options.classifierCause;
2230
2471
  }
2231
2472
  };
2473
+ /**
2474
+ * Sets the write intent on a statement defect that its raising code could not
2475
+ * name, and renders the message again.
2476
+ */
2477
+ function attributeFlushStatementIntent(defect, intent) {
2478
+ if (defect.intent === intent) return;
2479
+ Reflect.set(defect, "intent", intent);
2480
+ rewriteErrorMessage(defect, flushStatementReasonMessage({
2481
+ identity: defect.identity,
2482
+ intent,
2483
+ reason: defect.reason,
2484
+ received: defect.received
2485
+ }));
2486
+ }
2232
2487
  function flushStatementReasonMessage(options) {
2233
- const site = `${options.intent} of ${options.aggregateType}(${options.aggregateId})`;
2488
+ const aggregate = describeAggregateIdentity(options.identity);
2489
+ const site = options.intent == null ? aggregate : `${options.intent} of ${aggregate}`;
2490
+ const statement = options.intent ?? "write";
2234
2491
  switch (options.reason) {
2235
- case "statement_absent": return `The Unit of Work registered the ${site}, but the statements carry no ${options.intent}. ` + (options.intent === "remove" ? "A definition with physicalRemoval: true needs a remove statement." : "A definition without appendOnly: true needs an update statement.");
2236
- case "no_row_count": return `The ${options.intent} statement of the ${site} returned ${options.received ?? "no value"}. It must return the count of rows its predicate matched, so the flush can tell a conflict from a write.`;
2237
- case "no_expected_version": return `The ${site} carries no expectedVersion. The Unit of Work captures it when the aggregate is loaded, so this write did not come from a loaded aggregate.`;
2492
+ case "statement_absent": return `The Unit of Work registered the ${site}, but the statements carry no ${statement} statement. ` + (options.intent === "remove" ? "A definition with physicalRemoval: true needs a remove statement." : "A definition without appendOnly: true needs an update statement.");
2493
+ case "no_row_count": return `The ${statement} statement of the ${site} returned ${options.received ?? "no value"}. It must return the count of rows its predicate matched, so the flush can tell a conflict from a write.`;
2494
+ case "no_expected_version": return `The ${site} carries no valid expectedVersion` + (options.received === void 0 ? "" : ` (${options.received})`) + ". The Unit of Work captures it when the aggregate is loaded, so this write did not come from a loaded aggregate.";
2495
+ case "no_version": return `The currentVersion statement of ${site} returned ${options.received ?? "no value"}. It must return the stored version, a safe integer of at least 0, or undefined when the aggregate does not exist. Convert a bigint or a numeric string with Number().`;
2238
2496
  case "duplicate_check_failed": return `The isDuplicate check of the ${site} threw. It must classify the error of insert, never fail. The insert failure is the cause; the failure of the check is in classifierCause.`;
2239
2497
  }
2240
2498
  }
@@ -2246,25 +2504,29 @@ function flushStatementReasonMessage(options) {
2246
2504
  * cannot execute truthfully. Retrying the same callback cannot repair it.
2247
2505
  */
2248
2506
  var AggregateTrackingError = class extends KitWiringError {
2249
- aggregateId;
2507
+ identity;
2250
2508
  operation;
2251
2509
  reason;
2252
2510
  registeredIntent;
2253
- constructor(aggregateId, operation, reason, registeredIntent, options = {}) {
2254
- super("AGGREGATE_TRACKING", trackingReasonMessage(aggregateId, operation, reason, registeredIntent, options));
2255
- this.aggregateId = aggregateId;
2256
- this.operation = operation;
2257
- this.reason = reason;
2258
- this.registeredIntent = registeredIntent;
2511
+ constructor(options) {
2512
+ super("AGGREGATE_TRACKING", trackingReasonMessage(options));
2513
+ this.identity = detachAggregateIdentity(options.identity);
2514
+ this.operation = options.operation;
2515
+ this.reason = options.reason;
2516
+ this.registeredIntent = options.registeredIntent;
2259
2517
  }
2260
2518
  };
2261
- function trackingReasonMessage(aggregateId, operation, reason, registeredIntent, options) {
2262
- switch (reason) {
2263
- case "not_loaded": return `Aggregate ${aggregateId} cannot be registered for ${operation}: it was not loaded into this unit of work. Load it through the repository before updating or removing it.`;
2264
- case "loaded_as_new": return `Aggregate ${aggregateId} cannot be added as new because it was loaded by this unit of work. ` + (options.appendOnly ? "Its repository is append-only, so a loaded aggregate is already persisted and must not change." : "Use update for a loaded aggregate.");
2265
- case "different_repository": return `Aggregate ${aggregateId} cannot be registered for ${operation} through a different repository in the same unit of work. One aggregate instance must remain owned by the repository definition that first tracked it.`;
2266
- case "conflicting_intent": return `Aggregate ${aggregateId} is already registered for ${registeredIntent ?? "another write"}; ${operation} would create conflicting persistence intent in one unit of work. Decide the final lifecycle outcome before registering it.`;
2267
- case "mutated_after_registration": return `Aggregate ${aggregateId} changed after ${registeredIntent ?? "write"} was registered. Make domain decisions first and call add, update, or remove last so persisted state and recorded events cannot diverge.`;
2519
+ function trackingReasonMessage(options) {
2520
+ const { operation, registeredIntent } = options;
2521
+ const aggregate = describeAggregateIdentity(options.identity);
2522
+ switch (options.reason) {
2523
+ case "not_loaded": return `Aggregate ${aggregate} cannot be registered for ${operation}: it was not loaded into this unit of work. Load it through the repository before updating or removing it.`;
2524
+ case "loaded_as_new": return `Aggregate ${aggregate} cannot be added as new because it was loaded by this unit of work. ` + (options.appendOnly ? "Its repository is append-only, so a loaded aggregate is already persisted and must not change." : "Use update for a loaded aggregate.");
2525
+ case "different_repository": return `Aggregate ${aggregate} cannot be registered for ${operation} through a different repository in the same unit of work. One aggregate instance must remain owned by the repository definition that first tracked it.`;
2526
+ case "conflicting_intent": return `Aggregate ${aggregate} is already registered for ${registeredIntent ?? "another write"}; ${operation} would create conflicting persistence intent in one unit of work. Decide the final lifecycle outcome before registering it.`;
2527
+ case "mutated_after_registration": return `Aggregate ${aggregate} changed after ${registeredIntent ?? "write"} was registered. Make domain decisions first and call add, update, or remove last so persisted state and recorded events cannot diverge.`;
2528
+ case "registered_during_flush": return `Aggregate ${aggregate} cannot be registered for ${operation}: the unit of work already flushes its writes, so this write cannot join the transaction. Await every repository call inside the run() callback.`;
2529
+ case "identity_already_tracked": return `Aggregate ${aggregate} cannot be registered for ${operation}: this unit of work already tracks another instance with the same identity. Change the tracked instance, or give the new aggregate a new id.`;
2268
2530
  }
2269
2531
  }
2270
2532
  /**
@@ -2297,29 +2559,44 @@ var CommitError = class extends InfrastructureError {
2297
2559
  }
2298
2560
  };
2299
2561
  /**
2300
- * The work callback threw AND the transaction scope rejected with a
2301
- * DIFFERENT error that does not wrap the callback's error in its cause
2302
- * chain - the strongest available signal that the rollback itself
2303
- * failed. The callback's (primary) error is preserved as `cause`, so
2304
- * cause-chain helpers (`someChainRetryable`, `findInCauseChain`) still
2305
- * see a wrapped `ConcurrencyConflictError` & co.; the scope's error is
2306
- * carried in {@link rollbackCause}.
2307
- *
2308
- * Scopes that rethrow the original error (Drizzle, Prisma do) never
2309
- * produce this; scopes that WRAP the original are detected via the
2310
- * cause chain and passed through unchanged instead.
2562
+ * A step inside the transaction threw, and the transaction scope rejected
2563
+ * with a different error that does not wrap that failure in its cause chain.
2564
+ * This is the strongest available signal that the rollback itself failed.
2565
+ * The step is the work callback, or a step after it and before the commit,
2566
+ * for example the outbox write. The failure inside the transaction is the
2567
+ * `cause`, so cause-chain helpers (`someChainRetryable`, `findInCauseChain`)
2568
+ * still see a wrapped `ConcurrencyConflictError` & co. The scope's error is
2569
+ * in {@link rollbackCause}. A wiring error as that failure is the cause too,
2570
+ * so both facts stay visible.
2571
+ *
2572
+ * A scope that rethrows the failure (Drizzle and Prisma do) never produces
2573
+ * this. A scope that wraps the failure is detected through the cause chain.
2574
+ * A wrapped failure of the work passes through unchanged. A wrapped failure
2575
+ * after the work becomes a `CommitError`, or passes through when it is a
2576
+ * wiring error.
2311
2577
  */
2312
2578
  var RollbackError = class extends InfrastructureError {
2313
2579
  rollbackCause;
2314
2580
  constructor(cause, rollbackCause) {
2315
2581
  super({
2316
2582
  code: "ROLLBACK_FAILED",
2317
- message: "The work callback failed and the transaction scope rejected with a different error (possible rollback failure). The callback's error is the cause; the scope's error is in rollbackCause.",
2583
+ message: "A step inside the transaction failed and the transaction scope rejected with a different error (possible rollback failure). The failure inside the transaction is the cause; the scope's error is in rollbackCause.",
2318
2584
  cause
2319
2585
  });
2320
2586
  this.rollbackCause = rollbackCause;
2321
2587
  }
2322
2588
  };
2589
+ /**
2590
+ * The first kit wiring error in the cause chain of `error`, also from another
2591
+ * copy of the kit. A wiring error states a defect of the wiring, so the unit
2592
+ * of work passes it to the caller and never lets a mapper or a retry
2593
+ * relabel it, even when an adapter or a scope wrapped it. If the rollback
2594
+ * failed as well, the wiring error becomes the cause of a `RollbackError`,
2595
+ * so the rollback failure stays visible.
2596
+ */
2597
+ function wiringErrorInCauseChain(error) {
2598
+ return findInCauseChain(error, (link) => isWiringErrorLike(link) ? link : void 0);
2599
+ }
2323
2600
 
2324
2601
  //#endregion
2325
2602
  //#region src/domain/aggregate/pending-event-recording.ts
@@ -2347,17 +2624,30 @@ function recordPendingEvents(aggregate, source, stampOptions) {
2347
2624
  //#endregion
2348
2625
  //#region src/application/unit-of-work/repository-facade.ts
2349
2626
  /**
2350
- * Builds the application-facing repository facade. Standard lifecycle writes
2351
- * are always supplied by the Unit of Work; similarly named adapter methods are
2352
- * never invoked. Other methods are bound to the adapter so classes with private
2353
- * fields keep their normal receiver.
2627
+ * Builds the application-facing repository facade. The Unit of Work supplies
2628
+ * the lifecycle writes, and an adapter that defines its own add, update, or
2629
+ * remove is rejected. Other methods are bound to the adapter so classes with private
2630
+ * fields keep their normal receiver. A member that returns the adapter itself
2631
+ * (a fluent `this`) returns the facade instead. A promise passes unchanged: a
2632
+ * raw adapter that it resolves to can only read, because an adapter defines
2633
+ * no add, update, or remove.
2354
2634
  */
2355
2635
  function bindRepositoryWrites(adapter, session, definition, repository) {
2356
- if (adapter === null || typeof adapter !== "object") throw new InvalidRepositoryAdapterError(repository, adapter === null ? "null" : typeof adapter);
2636
+ if (adapter === null || typeof adapter !== "object") throw new InvalidRepositoryAdapterError({
2637
+ repository,
2638
+ reason: "not_an_object",
2639
+ receivedType: adapter === null ? "null" : typeof adapter
2640
+ });
2641
+ for (const operation of REPOSITORY_LIFECYCLE_OPERATIONS) if (hasMemberBelowObjectPrototype(adapter, operation)) throw new InvalidRepositoryAdapterError({
2642
+ repository,
2643
+ reason: "defines_lifecycle_operation",
2644
+ operation
2645
+ });
2357
2646
  const state = createRepositoryFacadeState(adapter, session, definition);
2358
2647
  installRepositoryLifecycleOperations(state);
2359
2648
  forwardAdapterOwnProperties(state);
2360
- return new Proxy(state.target, createRepositoryFacadeHandler(state));
2649
+ state.facade = new Proxy(state.target, createRepositoryFacadeHandler(state));
2650
+ return state.facade;
2361
2651
  }
2362
2652
  const REPOSITORY_LIFECYCLE_OPERATIONS = [
2363
2653
  "add",
@@ -2368,6 +2658,7 @@ function createRepositoryFacadeState(source, session, definition) {
2368
2658
  return {
2369
2659
  source,
2370
2660
  target: Object.create(Reflect.getPrototypeOf(source)),
2661
+ facade: void 0,
2371
2662
  session,
2372
2663
  definition,
2373
2664
  methodCache: /* @__PURE__ */ new Map(),
@@ -2382,10 +2673,20 @@ function isRepositoryLifecycleOperation(property) {
2382
2673
  return REPOSITORY_LIFECYCLE_OPERATIONS.includes(property);
2383
2674
  }
2384
2675
  /**
2385
- * Own-or-inherited presence that stops BEFORE `Object.prototype`: members
2386
- * every object inherits (`toString`, `valueOf`, `constructor`) are language
2387
- * plumbing, not repository surface, and must not trip the facade's
2388
- * session-open assertion.
2676
+ * Whether a property read or `in` probe is language plumbing, not a
2677
+ * repository member: promise resolution probes `then`, JSON.stringify probes
2678
+ * `toJSON`, string interpolation reads `toString`, and inspection utilities
2679
+ * read well-known symbols. Only a property present BELOW Object.prototype is
2680
+ * repository surface, except `constructor`, which every class prototype
2681
+ * carries. Plumbing answers without the session-open assertion, so logging a
2682
+ * leaked facade after close cannot mask the original failure.
2683
+ */
2684
+ function isLanguagePlumbing(state, property) {
2685
+ return property === "constructor" || !hasMemberBelowObjectPrototype(state.target, property) && !hasMemberBelowObjectPrototype(state.source, property);
2686
+ }
2687
+ /**
2688
+ * Own-or-inherited presence that stops BEFORE `Object.prototype`, where the
2689
+ * members that every object inherits live.
2389
2690
  */
2390
2691
  function hasMemberBelowObjectPrototype(object, property) {
2391
2692
  let current = object;
@@ -2398,20 +2699,29 @@ function hasMemberBelowObjectPrototype(object, property) {
2398
2699
  function readRepositorySource(state, property) {
2399
2700
  state.session.assertOpen(repositoryOperationName(property));
2400
2701
  const value = Reflect.get(state.source, property, state.source);
2401
- if (typeof value !== "function") return value;
2702
+ if (typeof value !== "function") return facadeInPlaceOfSource(state, value);
2402
2703
  const cached = state.methodCache.get(property);
2403
2704
  if (cached && cached.sourceMethod === value) return cached.guarded;
2404
2705
  const sourceMethod = value;
2405
- const guarded = (...args) => {
2406
- state.session.assertOpen(repositoryOperationName(property));
2407
- return Reflect.apply(sourceMethod, state.source, args);
2408
- };
2706
+ const guarded = new Proxy(sourceMethod, {
2707
+ apply: (method, _receiver, args) => {
2708
+ state.session.assertOpen(repositoryOperationName(property));
2709
+ return facadeInPlaceOfSource(state, Reflect.apply(method, state.source, args));
2710
+ },
2711
+ construct: (method, args, newTarget) => {
2712
+ state.session.assertOpen(repositoryOperationName(property));
2713
+ return Reflect.construct(method, args, newTarget);
2714
+ }
2715
+ });
2409
2716
  state.methodCache.set(property, {
2410
2717
  sourceMethod,
2411
2718
  guarded
2412
2719
  });
2413
2720
  return guarded;
2414
2721
  }
2722
+ function facadeInPlaceOfSource(state, value) {
2723
+ return value === state.source ? state.facade : value;
2724
+ }
2415
2725
  function defineForwardedRepositoryProperty(state, property, descriptor) {
2416
2726
  Object.defineProperty(state.target, property, {
2417
2727
  configurable: true,
@@ -2426,8 +2736,8 @@ function defineForwardedRepositoryProperty(state, property, descriptor) {
2426
2736
  }
2427
2737
  function installedLifecycleOperations(definition) {
2428
2738
  const operations = ["add"];
2429
- if (!definition.appendOnly) operations.push("update");
2430
- if (definition.physicalRemoval) operations.push("remove");
2739
+ if (definition.appendOnly !== true) operations.push("update");
2740
+ if (definition.physicalRemoval === true) operations.push("remove");
2431
2741
  return operations;
2432
2742
  }
2433
2743
  function installRepositoryLifecycleOperations(state) {
@@ -2438,7 +2748,6 @@ function installRepositoryLifecycleOperations(state) {
2438
2748
  enumerable: false,
2439
2749
  writable: false,
2440
2750
  value: (aggregate) => {
2441
- state.session.assertOpen(repositoryOperationName(operation));
2442
2751
  state.session[operation](aggregate, state.definition);
2443
2752
  }
2444
2753
  });
@@ -2454,7 +2763,11 @@ function forwardAdapterOwnProperties(state) {
2454
2763
  function createRepositoryFacadeHandler(state) {
2455
2764
  return {
2456
2765
  get: (target, property, receiver) => {
2457
- if (!hasMemberBelowObjectPrototype(target, property) && !hasMemberBelowObjectPrototype(state.source, property)) return Reflect.get(target, property, receiver);
2766
+ if (isLanguagePlumbing(state, property)) return Reflect.get(target, property, receiver);
2767
+ if (state.writes.has(property)) {
2768
+ state.session.assertOpenForRegistration(repositoryOperationName(property));
2769
+ return Reflect.get(target, property, receiver);
2770
+ }
2458
2771
  state.session.assertOpen(repositoryOperationName(property));
2459
2772
  if (Reflect.getOwnPropertyDescriptor(target, property)) return Reflect.get(target, property, receiver);
2460
2773
  if (isRepositoryLifecycleOperation(property)) return void 0;
@@ -2462,6 +2775,11 @@ function createRepositoryFacadeHandler(state) {
2462
2775
  },
2463
2776
  set: (target, property, value, receiver) => setRepositoryFacadeProperty(state, target, property, value, receiver),
2464
2777
  has: (target, property) => {
2778
+ if (isLanguagePlumbing(state, property)) return Reflect.has(target, property);
2779
+ if (state.writes.has(property)) {
2780
+ state.session.assertOpenForRegistration(repositoryOperationName(property));
2781
+ return true;
2782
+ }
2465
2783
  state.session.assertOpen(repositoryOperationName(property));
2466
2784
  return state.writes.has(property) || !isRepositoryLifecycleOperation(property) && (Reflect.has(target, property) || Reflect.has(state.source, property));
2467
2785
  },
@@ -2578,7 +2896,8 @@ var IdentityMap = class {
2578
2896
  return this._deleted.get(type)?.has(id) ?? false;
2579
2897
  }
2580
2898
  /**
2581
- * Registers the hydrated instance for type+id.
2899
+ * Registers the hydrated instance under its class and the id of its
2900
+ * `aggregateIdentity`.
2582
2901
  *
2583
2902
  * - Re-registering the SAME instance is a no-op (idempotent).
2584
2903
  * - Registering a DIFFERENT instance for an occupied type+id throws:
@@ -2590,15 +2909,16 @@ var IdentityMap = class {
2590
2909
  * work throws `AggregateDeletedError`: deletion is final within
2591
2910
  * the operation.
2592
2911
  */
2593
- set(type, id, aggregate) {
2594
- if (this._deleted.get(type)?.has(id)) throw new AggregateDeletedError(String(id));
2912
+ set(type, aggregate) {
2913
+ const id = aggregate.aggregateIdentity.aggregateId;
2914
+ if (this._deleted.get(type)?.has(id)) throw new AggregateDeletedError({ identity: aggregate.aggregateIdentity });
2595
2915
  let store = this._stores.get(type);
2596
2916
  if (store === void 0) {
2597
2917
  store = /* @__PURE__ */ new Map();
2598
2918
  this._stores.set(type, store);
2599
2919
  }
2600
2920
  const existing = store.get(id);
2601
- if (existing !== void 0 && existing !== aggregate) throw new Error(`IdentityMap: a different instance is already registered for ${type.name}(${String(id)}). Check get() before hydrating - two live instances of one aggregate break the one-instance-per-unit-of-work contract that exactly-once event harvest relies on.`);
2921
+ if (existing !== void 0 && existing !== aggregate) throw new Error(`IdentityMap: a different instance is already registered for ${describeAggregateIdentity(aggregate.aggregateIdentity)}. Check get() before hydrating: two live instances of one aggregate break the one-instance-per-unit-of-work contract that exactly-once event harvest relies on.`);
2602
2922
  store.set(id, aggregate);
2603
2923
  if (aggregate !== null && typeof aggregate === "object" && !this._pendingAtRegistration.has(aggregate)) {
2604
2924
  const pending = pendingEventCountOf(aggregate);
@@ -2761,13 +3081,24 @@ var Session = class {
2761
3081
  _commitTokens = /* @__PURE__ */ new Set();
2762
3082
  _identityMap = new IdentityMap();
2763
3083
  _identityMapView = Object.freeze({
2764
- get: this._identityMap.get.bind(this._identityMap),
2765
- has: this._identityMap.has.bind(this._identityMap),
2766
- isDeleted: this._identityMap.isDeleted.bind(this._identityMap)
3084
+ get: (type, id) => this._phase === "open" ? this._identityMap.get(type, id) : void 0,
3085
+ has: (type, id) => this._phase === "open" && this._identityMap.has(type, id),
3086
+ isDeleted: (type, id) => this._phase === "open" && this._identityMap.isDeleted(type, id)
2767
3087
  });
2768
3088
  _trackingByAggregate = /* @__PURE__ */ new WeakMap();
2769
3089
  _trackedAggregates = /* @__PURE__ */ new Set();
2770
- _closed = false;
3090
+ /** Whether the context, facades, and tracking may still be used. */
3091
+ _phase = "open";
3092
+ /**
3093
+ * True from the start of the flush until the last check before the
3094
+ * commit ran or the run ended. A write registered in that window would
3095
+ * be committed and harvested without its flush, so it is rejected, and
3096
+ * the first rejection also fails the attempt, even when the caller
3097
+ * swallows it. After the window, a registration reports a closed unit of
3098
+ * work: nothing can fail the attempt any more.
3099
+ */
3100
+ _lateRegistrationsFailAttempt = false;
3101
+ _registrationDuringFlush;
2771
3102
  constructor(commitEnrollment) {
2772
3103
  this.commitEnrollment = commitEnrollment;
2773
3104
  }
@@ -2794,31 +3125,55 @@ var Session = class {
2794
3125
  }
2795
3126
  trackLoaded(aggregate, definition) {
2796
3127
  this.assertOpen("tracking.trackLoaded");
3128
+ requirePendingEventLifecycleReadView(aggregate, "tracking.trackLoaded");
3129
+ const tracked = this._identityMap.get(definition.aggregate, aggregate.id);
3130
+ if (tracked !== void 0 && tracked !== aggregate) return this.trackLoaded(tracked, definition);
2797
3131
  const existing = this._trackingByAggregate.get(aggregate);
2798
- if (existing && existing.definition !== definition) throw new AggregateTrackingError(String(aggregate.id), "load", "different_repository", existing.registration?.intent);
2799
- this._identityMap.set(definition.aggregate, aggregate.id, aggregate);
2800
- if (existing) return aggregate;
3132
+ if (existing && existing.definition !== definition) throw new AggregateTrackingError({
3133
+ identity: aggregate.aggregateIdentity,
3134
+ operation: "load",
3135
+ reason: "different_repository",
3136
+ registeredIntent: existing.registration?.intent
3137
+ });
3138
+ if (existing) {
3139
+ this._identityMap.set(definition.aggregate, aggregate);
3140
+ return aggregate;
3141
+ }
3142
+ const baseline = capturePersistenceBaseline(definition.persistence, aggregate);
3143
+ this._identityMap.set(definition.aggregate, aggregate);
2801
3144
  const entry = {
2802
3145
  aggregate,
2803
3146
  lifecycle: "loaded",
2804
3147
  expectedVersion: aggregate.version,
2805
3148
  definition,
2806
- baseline: capturePersistenceBaseline(definition.persistence, aggregate)
3149
+ baseline
2807
3150
  };
2808
3151
  this._trackingByAggregate.set(aggregate, entry);
2809
3152
  this._trackedAggregates.add(entry);
2810
3153
  return aggregate;
2811
3154
  }
2812
3155
  add(aggregate, definition) {
2813
- this.assertOpen("repository.add");
3156
+ if (this.isRepeatAdmitted(aggregate, definition, "add")) return;
2814
3157
  this.assertNotRemoved(aggregate, definition);
2815
3158
  const existing = this._trackingByAggregate.get(aggregate);
2816
- if (existing && existing.definition !== definition) throw new AggregateTrackingError(String(aggregate.id), "add", "different_repository", existing.registration?.intent);
2817
- if (existing?.lifecycle === "loaded") throw new AggregateTrackingError(String(aggregate.id), "add", "loaded_as_new", existing.registration?.intent, { appendOnly: definition.appendOnly === true });
3159
+ if (existing && existing.definition !== definition) throw new AggregateTrackingError({
3160
+ identity: aggregate.aggregateIdentity,
3161
+ operation: "add",
3162
+ reason: "different_repository",
3163
+ registeredIntent: existing.registration?.intent
3164
+ });
3165
+ if (existing?.lifecycle === "loaded") throw new AggregateTrackingError({
3166
+ identity: aggregate.aggregateIdentity,
3167
+ operation: "add",
3168
+ reason: "loaded_as_new",
3169
+ registeredIntent: existing.registration?.intent,
3170
+ appendOnly: definition.appendOnly === true
3171
+ });
2818
3172
  let entry = existing;
2819
3173
  const newlyTracked = !entry;
2820
3174
  if (!entry) {
2821
- this._identityMap.set(definition.aggregate, aggregate.id, aggregate);
3175
+ this.assertIdentityNotTaken(aggregate, definition);
3176
+ this._identityMap.set(definition.aggregate, aggregate);
2822
3177
  entry = {
2823
3178
  aggregate,
2824
3179
  lifecycle: "new",
@@ -2840,15 +3195,29 @@ var Session = class {
2840
3195
  throw error;
2841
3196
  }
2842
3197
  }
3198
+ /** Two live instances of one identity would each flush and harvest. */
3199
+ assertIdentityNotTaken(aggregate, definition) {
3200
+ const tracked = this._identityMap.get(definition.aggregate, aggregate.id);
3201
+ if (tracked === void 0 || tracked === aggregate) return;
3202
+ throw new AggregateTrackingError({
3203
+ identity: aggregate.aggregateIdentity,
3204
+ operation: "add",
3205
+ reason: "identity_already_tracked",
3206
+ registeredIntent: this.registrationOf(tracked)?.intent
3207
+ });
3208
+ }
2843
3209
  update(aggregate, definition) {
2844
- this.assertOpen("repository.update");
3210
+ if (this.isRepeatAdmitted(aggregate, definition, "update")) return;
2845
3211
  const entry = this.loadedEntryFor(aggregate, "update", definition);
2846
3212
  this.registerWrite(entry, "update", definition);
2847
3213
  }
2848
3214
  remove(aggregate, definition) {
2849
- this.assertOpen("repository.remove");
3215
+ if (this.isRepeatAdmitted(aggregate, definition, "remove")) return;
2850
3216
  const entry = this._trackingByAggregate.get(aggregate);
2851
- if (this.isRemovedInstance(aggregate) && entry?.definition === definition) return;
3217
+ if (this.isRemovedInstance(aggregate) && entry?.definition === definition) {
3218
+ this.assertUnchangedAfterRegistration(entry, "remove");
3219
+ return;
3220
+ }
2852
3221
  const loaded = this.loadedEntryFor(aggregate, "remove", definition);
2853
3222
  this.registerWrite(loaded, "remove", definition);
2854
3223
  }
@@ -2866,18 +3235,38 @@ var Session = class {
2866
3235
  loadedEntryFor(aggregate, operation, definition) {
2867
3236
  this.assertNotRemoved(aggregate, definition);
2868
3237
  const entry = this._trackingByAggregate.get(aggregate);
2869
- if (entry && entry.definition !== definition) throw new AggregateTrackingError(String(aggregate.id), operation, "different_repository", entry.registration?.intent);
2870
- if (entry && entry.lifecycle === "new" && entry.definition === definition) throw new AggregateTrackingError(String(aggregate.id), operation, "conflicting_intent", entry.registration?.intent);
2871
- if (entry?.lifecycle !== "loaded" || entry.definition !== definition) throw new AggregateTrackingError(String(aggregate.id), operation, "not_loaded", entry?.registration?.intent);
3238
+ if (entry && entry.definition !== definition) throw new AggregateTrackingError({
3239
+ identity: aggregate.aggregateIdentity,
3240
+ operation,
3241
+ reason: "different_repository",
3242
+ registeredIntent: entry.registration?.intent
3243
+ });
3244
+ if (entry && entry.lifecycle === "new" && entry.definition === definition) throw new AggregateTrackingError({
3245
+ identity: aggregate.aggregateIdentity,
3246
+ operation,
3247
+ reason: "conflicting_intent",
3248
+ registeredIntent: entry.registration?.intent
3249
+ });
3250
+ if (entry?.lifecycle !== "loaded" || entry.definition !== definition) throw new AggregateTrackingError({
3251
+ identity: aggregate.aggregateIdentity,
3252
+ operation,
3253
+ reason: "not_loaded",
3254
+ registeredIntent: entry?.registration?.intent
3255
+ });
2872
3256
  return entry;
2873
3257
  }
2874
3258
  assertNotRemoved(aggregate, definition) {
2875
- if (this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError(String(aggregate.id));
3259
+ if (this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError({ identity: aggregate.aggregateIdentity });
2876
3260
  }
2877
3261
  registerIntent(entry, intent) {
2878
3262
  if (entry.registration !== void 0) {
2879
- if (entry.registration.intent !== intent) throw new AggregateTrackingError(String(entry.aggregate.id), intent, "conflicting_intent", entry.registration.intent);
2880
- this.assertUnchangedAfterRegistration(entry);
3263
+ if (entry.registration.intent !== intent) throw new AggregateTrackingError({
3264
+ identity: entry.aggregate.aggregateIdentity,
3265
+ operation: intent,
3266
+ reason: "conflicting_intent",
3267
+ registeredIntent: entry.registration.intent
3268
+ });
3269
+ this.assertUnchangedAfterRegistration(entry, intent);
2881
3270
  return false;
2882
3271
  }
2883
3272
  entry.registration = Object.freeze({
@@ -2896,17 +3285,22 @@ var Session = class {
2896
3285
  if (index >= 0) this._registeredWrites.splice(index, 1);
2897
3286
  delete entry.registration;
2898
3287
  }
2899
- assertUnchangedAfterRegistration(entry) {
3288
+ assertUnchangedAfterRegistration(entry, operation) {
2900
3289
  const registration = entry.registration;
2901
3290
  if (registration === void 0) return;
2902
3291
  const currentEvents = entry.aggregate.pendingEvents;
2903
3292
  const persistenceChanged = persistenceProjectionDrifted(registration.baseline, entry.aggregate);
2904
3293
  const sameEvents = currentEvents.length === registration.events.length && currentEvents.every((event, index) => event === registration.events[index]);
2905
- if (registration.version !== entry.aggregate.version || !sameEvents || persistenceChanged) throw new AggregateTrackingError(String(entry.aggregate.id), "commit", "mutated_after_registration", registration.intent);
3294
+ if (registration.version !== entry.aggregate.version || !sameEvents || persistenceChanged) throw new AggregateTrackingError({
3295
+ identity: entry.aggregate.aggregateIdentity,
3296
+ operation,
3297
+ reason: "mutated_after_registration",
3298
+ registeredIntent: registration.intent
3299
+ });
2906
3300
  }
2907
3301
  registerSavedCommit(aggregate, definition, expectedVersion) {
2908
3302
  this.assertOpen("repository.add/update");
2909
- if (this.isRemovedInstance(aggregate) || this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError(String(aggregate.id));
3303
+ if (this.isRemovedInstance(aggregate) || this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError({ identity: aggregate.aggregateIdentity });
2910
3304
  const token = this.commitEnrollment.enrollSaved(aggregate, { expectedVersion });
2911
3305
  this._commitTokens.add(token);
2912
3306
  return token;
@@ -2926,28 +3320,40 @@ var Session = class {
2926
3320
  * transaction.
2927
3321
  */
2928
3322
  assertReadyToCommit() {
3323
+ if (this._registrationDuringFlush !== void 0) throw this._registrationDuringFlush;
2929
3324
  for (const entry of this._trackedAggregates) {
2930
3325
  if (entry.registration !== void 0) {
2931
- this.assertUnchangedAfterRegistration(entry);
3326
+ this.assertUnchangedAfterRegistration(entry, "commit");
2932
3327
  continue;
2933
3328
  }
2934
- if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion || persistenceProjectionDrifted(entry.baseline, entry.aggregate))) throw new UnenrolledChangesError(String(entry.aggregate.id), { appendOnly: entry.definition.appendOnly === true });
3329
+ if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion || persistenceProjectionDrifted(entry.baseline, entry.aggregate))) throw new UnenrolledChangesError({
3330
+ identity: entry.aggregate.aggregateIdentity,
3331
+ appendOnly: entry.definition.appendOnly === true
3332
+ });
2935
3333
  }
2936
3334
  for (const instance of this._identityMap.instancesWithNewPendingEvents()) {
2937
3335
  if (instance !== null && typeof instance === "object" && this.registrationOf(instance) !== void 0) continue;
2938
- const id = instance.id;
2939
- throw new UnenrolledChangesError(String(id), { appendOnly: this._trackingByAggregate.get(instance)?.definition.appendOnly === true });
3336
+ const aggregate = instance;
3337
+ throw new UnenrolledChangesError({
3338
+ identity: aggregate.aggregateIdentity,
3339
+ appendOnly: this._trackingByAggregate.get(aggregate)?.definition.appendOnly === true
3340
+ });
2940
3341
  }
2941
3342
  }
2942
3343
  /** Flushes every registered receipt in deterministic registration order. */
2943
3344
  async flush(transaction) {
2944
3345
  this.assertOpen("unitOfWork.flush");
3346
+ this._lateRegistrationsFailAttempt = true;
2945
3347
  for (const entry of this._registeredWrites) {
2946
3348
  const registration = entry.registration;
2947
- if (registration === void 0) throw new AggregateTrackingError(String(entry.aggregate.id), "commit", "mutated_after_registration");
3349
+ if (registration === void 0) throw new AggregateTrackingError({
3350
+ identity: entry.aggregate.aggregateIdentity,
3351
+ operation: "commit",
3352
+ reason: "mutated_after_registration"
3353
+ });
2948
3354
  const write = Object.freeze({
2949
3355
  intent: registration.intent,
2950
- aggregateId: entry.aggregate.id,
3356
+ aggregateIdentity: entry.aggregate.aggregateIdentity,
2951
3357
  expectedVersion: entry.expectedVersion,
2952
3358
  version: registration.version,
2953
3359
  changes: registration.changes,
@@ -2963,42 +3369,119 @@ var Session = class {
2963
3369
  get commitTokens() {
2964
3370
  return [...this._commitTokens];
2965
3371
  }
3372
+ /**
3373
+ * Closes the session for use. The tracked state stays, and late
3374
+ * registrations still fail the attempt until its last check before the
3375
+ * commit: a scope can abandon an attempt and still commit it, and that
3376
+ * attempt's check must see its own state. Leaked views answer empty once
3377
+ * the session is closed, so the state cannot serve a later operation.
3378
+ */
2966
3379
  close() {
2967
- this._closed = true;
2968
- this._identityMap.clear();
2969
- this._trackedAggregates.clear();
2970
- this._registeredWrites.length = 0;
2971
- this._commitTokens.clear();
3380
+ this._phase = "closed";
3381
+ }
3382
+ /** Runs {@link assertReadyToCommit} for the last time before the commit. */
3383
+ assertReadyToCommitLastTime() {
3384
+ this.assertReadyToCommit();
3385
+ this._lateRegistrationsFailAttempt = false;
3386
+ }
3387
+ /** Closes the session for good, when its attempt or the run ended. */
3388
+ end() {
3389
+ this._phase = "closed";
3390
+ this._lateRegistrationsFailAttempt = false;
2972
3391
  }
2973
3392
  assertOpen(operation) {
2974
- if (this._closed) throw new TransactionClosedError(operation);
3393
+ if (this._phase !== "open") throw new TransactionClosedError(operation);
3394
+ }
3395
+ /** Like {@link assertOpen}, but open while a late registration still counts. */
3396
+ assertOpenForRegistration(operation) {
3397
+ if (!this._lateRegistrationsFailAttempt) this.assertOpen(operation);
3398
+ }
3399
+ /**
3400
+ * Admits an add, update, or remove. Before the flush, the call proceeds.
3401
+ * In the late-registration window, a repeat of the same registered write
3402
+ * stays a no-op (returns true), and any other registration is rejected
3403
+ * and fails the attempt. After the window, the call reports a closed unit
3404
+ * of work before it looks at its argument.
3405
+ */
3406
+ isRepeatAdmitted(aggregate, definition, operation) {
3407
+ const name = repositoryOperation(operation);
3408
+ if (!this._lateRegistrationsFailAttempt) {
3409
+ this.assertOpen(name);
3410
+ requirePendingEventLifecycleReadView(aggregate, name);
3411
+ return false;
3412
+ }
3413
+ requirePendingEventLifecycleReadView(aggregate, name);
3414
+ const entry = this._trackingByAggregate.get(aggregate);
3415
+ if (entry?.definition === definition && entry.registration?.intent === operation) {
3416
+ this.assertUnchangedAfterRegistration(entry, operation);
3417
+ return true;
3418
+ }
3419
+ const rejection = new AggregateTrackingError({
3420
+ identity: aggregate.aggregateIdentity,
3421
+ operation,
3422
+ reason: "registered_during_flush"
3423
+ });
3424
+ this._registrationDuringFlush ??= rejection;
3425
+ throw rejection;
2975
3426
  }
2976
3427
  };
3428
+ /**
3429
+ * A flush and an event store report what they observed; the unit of work
3430
+ * knows which write it asked for. So the intent of a conflict and of a
3431
+ * statement defect comes from here. It is set on the error object itself,
3432
+ * found by its code anywhere in the cause chain, so a wrapped error and an
3433
+ * error from another copy of the kit get it too.
3434
+ */
3435
+ function attributeWriteIntent(error, intent) {
3436
+ const conflict = firstInCauseChainWithCode(error, "CONCURRENCY_CONFLICT");
3437
+ if (conflict instanceof ConcurrencyConflictError) attributeConflictIntent(conflict, intent);
3438
+ else if (conflict !== void 0) Reflect.set(conflict, "intent", intent);
3439
+ const defect = firstInCauseChainWithCode(error, "INVALID_FLUSH_STATEMENT");
3440
+ if (defect instanceof InvalidFlushStatementError) attributeFlushStatementIntent(defect, intent);
3441
+ else if (defect !== void 0) Reflect.set(defect, "intent", intent);
3442
+ }
3443
+ function firstInCauseChainWithCode(error, code) {
3444
+ return findInCauseChain(error, (link) => {
3445
+ try {
3446
+ return link.code === code ? link : void 0;
3447
+ } catch {
3448
+ return;
3449
+ }
3450
+ });
3451
+ }
2977
3452
  function mapRepositoryPersistenceError(definition, error, write) {
2978
- if (isWiringErrorLike(error)) throw error;
3453
+ attributeWriteIntent(error, write.intent);
3454
+ const wiringError = wiringErrorInCauseChain(error);
3455
+ if (wiringError !== void 0) throw wiringError;
2979
3456
  let mapped;
2980
3457
  try {
2981
3458
  mapped = definition.mapError(error, write);
2982
3459
  } catch (mapperError) {
2983
3460
  throw new RepositoryErrorMappingFailedError({
2984
- aggregateId: String(write.aggregateId),
3461
+ identity: write.aggregateIdentity,
2985
3462
  intent: write.intent,
2986
3463
  persistenceError: error,
2987
3464
  mapperError
2988
3465
  });
2989
3466
  }
2990
- if (isInfrastructureErrorLike(mapped)) return mapped;
3467
+ if (isInfrastructureErrorLike(mapped)) {
3468
+ attributeWriteIntent(mapped, write.intent);
3469
+ return mapped;
3470
+ }
2991
3471
  throw new RepositoryErrorMappingFailedError({
2992
- aggregateId: String(write.aggregateId),
3472
+ identity: write.aggregateIdentity,
2993
3473
  intent: write.intent,
2994
3474
  persistenceError: error,
2995
3475
  mapperError: /* @__PURE__ */ new TypeError("Repository mapError must return an InfrastructureError instance")
2996
3476
  });
2997
3477
  }
3478
+ function repositoryOperation(operation) {
3479
+ return `repository.${operation}`;
3480
+ }
2998
3481
 
2999
3482
  //#endregion
3000
3483
  //#region src/application/unit-of-work/unit-of-work.ts
3001
- const repositoryDefinitionBrand = Symbol.for("@shirudo/ddd-kit/repository-definition/v2");
3484
+ const repositoryDefinitionBrand = Symbol.for("@shirudo/ddd-kit/repository-definition/v3");
3002
3485
  function assertRepositoryDefinitionMembers(definition) {
3003
3486
  for (const key of [
3004
3487
  "create",
@@ -3007,6 +3490,19 @@ function assertRepositoryDefinitionMembers(definition) {
3007
3490
  ]) if (typeof definition[key] !== "function") throw new TypeError(`defineRepository: "${key}" is missing or not a function on the definition. The builder copies own enumerable properties only; prototype methods and non-enumerable members are not carried. Pass a plain object literal.`);
3008
3491
  if (typeof definition.aggregate !== "function") throw new TypeError("defineRepository: \"aggregate\" is missing or not a class reference on the definition. Pass a plain object literal with own enumerable properties.");
3009
3492
  if (definition.persistence === null || typeof definition.persistence !== "object") throw new TypeError("defineRepository: \"persistence\" is missing or not a PersistenceModel on the definition. Pass a plain object literal with own enumerable properties.");
3493
+ assertPersistenceModelMembers(definition.persistence);
3494
+ for (const key of ["appendOnly", "physicalRemoval"]) {
3495
+ const flag = definition[key];
3496
+ if (flag !== void 0 && typeof flag !== "boolean") throw new TypeError(`defineRepository: "${key}" must be true or false, got ${String(flag)}.`);
3497
+ }
3498
+ }
3499
+ function assertPersistenceModelMembers(persistence) {
3500
+ for (const key of [
3501
+ "capture",
3502
+ "changes",
3503
+ "isEmpty"
3504
+ ]) if (typeof persistence[key] !== "function") throw new TypeError(`defineRepository: "persistence.${key}" is not a function. A PersistenceModel needs capture, changes, and isEmpty.`);
3505
+ if (persistence.captureEquals !== void 0 && typeof persistence.captureEquals !== "function") throw new TypeError("defineRepository: \"persistence.captureEquals\" is not a function. Omit it, or pass a function that compares two captures.");
3010
3506
  }
3011
3507
  /**
3012
3508
  * Defines repository wiring for an application-owned driven port.
@@ -3015,7 +3511,8 @@ function assertRepositoryDefinitionMembers(definition) {
3015
3511
  * aggregate, persistence, event, and lifecycle types from the adapter wiring.
3016
3512
  * The port must declare `add` for the aggregate. It declares `update` unless
3017
3513
  * the definition sets `appendOnly: true`. If it declares `remove`, the
3018
- * definition must set `physicalRemoval: true`. A violated constraint fails
3514
+ * definition must set `physicalRemoval: true`. The adapter that `create`
3515
+ * returns does not define `add`, `update`, or `remove`. A violated constraint fails
3019
3516
  * the call with a compiler error that names the constraint. The adapter
3020
3517
  * created by the definition implements only the remaining methods because
3021
3518
  * lifecycle writes are installed by the Unit of Work.
@@ -3113,11 +3610,10 @@ var UnitOfWork = class {
3113
3610
  if (this._active) throw new NestedUnitOfWorkError();
3114
3611
  this._active = true;
3115
3612
  let session;
3116
- let workCompleted = false;
3117
- let workThrew = false;
3118
- let workError;
3613
+ const sessions = [];
3614
+ let attempt = startAttempt();
3119
3615
  try {
3120
- return await withCommit({
3616
+ return await withCheckedCommit({
3121
3617
  outbox: this.deps.outbox,
3122
3618
  bus: this.deps.bus,
3123
3619
  scope: this.deps.scope,
@@ -3130,37 +3626,40 @@ var UnitOfWork = class {
3130
3626
  session?.close();
3131
3627
  const s = new Session(enrollment);
3132
3628
  session = s;
3133
- workCompleted = false;
3134
- workThrew = false;
3135
- workError = void 0;
3136
- const context = makeContext(this.buildRepositories(tx, s), s, options?.signal);
3629
+ sessions.push(s);
3630
+ const current = startAttempt();
3631
+ attempt = current;
3137
3632
  try {
3138
- const result = await work(context);
3633
+ const result = await work(makeContext(this.buildRepositories(tx, s), s, options?.signal));
3139
3634
  s.assertReadyToCommit();
3140
3635
  await s.flush(tx);
3141
3636
  s.assertReadyToCommit();
3142
- workCompleted = true;
3637
+ current.workCompleted = true;
3143
3638
  const commits = s.commitTokens;
3144
3639
  s.close();
3145
3640
  return {
3146
3641
  result,
3147
- commits
3642
+ commits,
3643
+ checkBeforeCommit: () => s.assertReadyToCommitLastTime(),
3644
+ onFailureBeforeCommit: (error) => {
3645
+ current.failedBeforeCommit = true;
3646
+ current.failureBeforeCommit = error;
3647
+ s.end();
3648
+ }
3148
3649
  };
3149
3650
  } catch (error) {
3150
- workThrew = true;
3151
- workError = error;
3651
+ current.workThrew = true;
3652
+ current.workError = error;
3653
+ s.end();
3152
3654
  throw error;
3153
3655
  }
3656
+ }, () => {
3657
+ attempt = startAttempt();
3154
3658
  });
3155
3659
  } catch (error) {
3156
- throw classifyRunError(error, {
3157
- workThrew,
3158
- workCompleted,
3159
- workError,
3160
- signal: options?.signal
3161
- });
3660
+ throw classifyRunError(error, attempt, options?.signal);
3162
3661
  } finally {
3163
- session?.close();
3662
+ for (const ended of sessions) ended.end();
3164
3663
  this._active = false;
3165
3664
  }
3166
3665
  }
@@ -3188,13 +3687,23 @@ function makeContext(repositories, session, signal) {
3188
3687
  signal
3189
3688
  };
3190
3689
  }
3690
+ function startAttempt() {
3691
+ return {
3692
+ workCompleted: false,
3693
+ workThrew: false,
3694
+ workError: void 0,
3695
+ failedBeforeCommit: false,
3696
+ failureBeforeCommit: void 0
3697
+ };
3698
+ }
3191
3699
  /**
3192
3700
  * Classifies a `withCommit` rejection into the error `run()` should throw,
3193
- * using the flags captured inside the work wrapper. Pure and total: it
3701
+ * using the record of the attempt that the scope ran last. Pure and total: it
3194
3702
  * returns the error to throw rather than throwing itself, so `run()` reads
3195
3703
  * as orchestration and this decision is unit-testable in isolation.
3196
3704
  *
3197
- * - `workThrew`: the work callback (or `assertReadyToCommit`) threw.
3705
+ * - `workThrew`: the attempt threw before it completed: the repository
3706
+ * factories, the work callback, or `assertReadyToCommit`.
3198
3707
  * The scope normally rethrows that error unchanged (rolled back, pass
3199
3708
  * through so a `ConcurrencyConflictError` & co. stay catchable as-is); a
3200
3709
  * scope that WRAPS the original is detected via the cause chain and also
@@ -3202,66 +3711,37 @@ function makeContext(repositories, session, signal) {
3202
3711
  * callback's error indicates the rollback itself failed, which becomes a
3203
3712
  * {@link RollbackError}.
3204
3713
  * - `workCompleted`: the callback finished; the failure is post-completion.
3205
- * A harvest-guard violation (an event missing aggregateId / aggregateType,
3206
- * or an eventful persisted aggregate that did not advance its version) is a deterministic
3207
- * programming bug, surfaced as its {@link EventHarvestError} (which does
3208
- * NOT extend `InfrastructureError`, so a retry-on-Infrastructure handler
3209
- * skips it). It is thrown inside `scope.transactional()`, so a wrapping
3210
- * scope can nest it: walk the chain rather than a bare `instanceof`. Only
3211
- * genuinely unforeseeable post-completion failures (outbox write, the
3212
- * commit itself) become {@link CommitError}.
3714
+ * The checks run in this order. First, a wiring error in the rejection (a
3715
+ * harvest-guard violation, or an aggregate that changed after its
3716
+ * registration) is a deterministic programming bug and passes through; it
3717
+ * does NOT extend `InfrastructureError`, so a retry-on-Infrastructure
3718
+ * handler skips it. It is thrown inside `scope.transactional()`, so a
3719
+ * wrapping scope can nest it: walk the chain rather than a bare
3720
+ * `instanceof`. Second, a step that failed inside the transaction after
3721
+ * the callback (`failedBeforeCommit`) and a rejection that neither is nor
3722
+ * wraps that failure indicate a possible rollback failure: a
3723
+ * {@link RollbackError} with that failure as its cause, also when it is a
3724
+ * wiring error, as in the work phase. Every other post-completion failure
3725
+ * (a failed step whose rollback succeeded, the commit itself) becomes a
3726
+ * {@link CommitError}.
3213
3727
  * - Neither flag set: `withCommit` rejected before the callback ran (the
3214
3728
  * scope failed to even open a transaction); pass the error through.
3215
3729
  */
3216
- function classifyRunError(error, state) {
3217
- if (state.signal?.aborted && state.signal.reason !== void 0 && (error === state.signal.reason || causeChainContains(error, state.signal.reason))) return error;
3730
+ function classifyRunError(error, state, signal) {
3731
+ if (signal?.aborted && signal.reason !== void 0 && (error === signal.reason || causeChainContains(error, signal.reason))) return error;
3218
3732
  if (state.workThrew) {
3219
3733
  if (error === state.workError || causeChainContains(error, state.workError)) return error;
3220
3734
  return new RollbackError(state.workError, error);
3221
3735
  }
3222
3736
  if (state.workCompleted) {
3223
- const harvestError = findHarvestErrorInChain(error);
3224
- if (harvestError) return harvestError;
3737
+ const wiringError = wiringErrorInCauseChain(error);
3738
+ if (wiringError) return wiringError;
3739
+ if (state.failedBeforeCommit && error !== state.failureBeforeCommit && !causeChainContains(error, state.failureBeforeCommit)) return new RollbackError(state.failureBeforeCommit, error);
3225
3740
  return new CommitError(error);
3226
3741
  }
3227
3742
  return error;
3228
3743
  }
3229
3744
  /**
3230
- * Cycle-safe, getter-throw-safe walk over `error`'s standard `cause`
3231
- * chain. `visit` runs for every object link (the top error included) and
3232
- * receives the link plus its lazily read `cause`; a non-undefined return
3233
- * stops the walk. A throwing `cause` getter (lazy deserialization, revoked
3234
- * Proxy) ends the walk as no-match instead of replacing the real failure
3235
- * with the getter's exception.
3236
- */
3237
- function findInCauseChain(error, visit) {
3238
- const seen = /* @__PURE__ */ new Set();
3239
- let current = error;
3240
- while (current !== null && typeof current === "object" && !seen.has(current)) {
3241
- seen.add(current);
3242
- let cause;
3243
- try {
3244
- cause = current.cause;
3245
- } catch {
3246
- return;
3247
- }
3248
- const found = visit(current, cause);
3249
- if (found !== void 0) return found;
3250
- current = cause;
3251
- }
3252
- }
3253
- /**
3254
- * Walks `error`'s `cause` chain and returns the first `EventHarvestError`,
3255
- * or `undefined`. `withCommit` throws the harvest-guard error INSIDE
3256
- * `scope.transactional`, so a wrapping scope can nest it; matching
3257
- * only the top-level error would let the wrapper mask the non-retryable
3258
- * type. `withCommit` and `run()` share this module, so the local
3259
- * `instanceof` is reliable for the un-wrapped link.
3260
- */
3261
- function findHarvestErrorInChain(error) {
3262
- return findInCauseChain(error, (link) => link instanceof EventHarvestError ? link : void 0);
3263
- }
3264
- /**
3265
3745
  * Whether `error`'s `cause` chain contains `target` by reference. A
3266
3746
  * `target` of `undefined`/`null` never matches: every error without a
3267
3747
  * `cause` property would otherwise "contain" a thrown `undefined`.
@@ -3290,9 +3770,8 @@ function causeChainContains(error, target) {
3290
3770
  */
3291
3771
  function versionedFlush(statements) {
3292
3772
  const versionedWrites = versionedWritesOf(statements);
3293
- const aggregateType = statements.aggregateType;
3294
3773
  const runInsert = insertWriter(statements);
3295
- const { update: runUpdate, remove: runRemove } = versionedWriters(aggregateType, versionedWrites);
3774
+ const { update: runUpdate, remove: runRemove } = versionedWriters(versionedWrites);
3296
3775
  return async (transaction, write) => {
3297
3776
  switch (write.intent) {
3298
3777
  case "add": return runInsert(transaction, write);
@@ -3307,7 +3786,7 @@ function versionedFlush(statements) {
3307
3786
  }
3308
3787
  /** Reads its statements here, like its versioned peers. */
3309
3788
  function insertWriter(statements) {
3310
- const { aggregateType, insert, isDuplicate } = statements;
3789
+ const { insert, isDuplicate } = statements;
3311
3790
  return async (transaction, write) => {
3312
3791
  try {
3313
3792
  await insert(transaction, write);
@@ -3317,8 +3796,7 @@ function insertWriter(statements) {
3317
3796
  duplicate = isDuplicate(error);
3318
3797
  } catch (classifierCause) {
3319
3798
  throw new InvalidFlushStatementError({
3320
- aggregateType,
3321
- aggregateId: write.aggregateId,
3799
+ identity: write.aggregateIdentity,
3322
3800
  intent: "add",
3323
3801
  reason: "duplicate_check_failed",
3324
3802
  cause: error,
@@ -3327,8 +3805,7 @@ function insertWriter(statements) {
3327
3805
  }
3328
3806
  if (!duplicate) throw error;
3329
3807
  throw new DuplicateAggregateError({
3330
- aggregateType,
3331
- aggregateId: write.aggregateId,
3808
+ identity: write.aggregateIdentity,
3332
3809
  cause: error
3333
3810
  });
3334
3811
  }
@@ -3343,42 +3820,72 @@ function versionedWritesOf(statements) {
3343
3820
  if ((statements.update ?? statements.remove) === void 0) return void 0;
3344
3821
  throw new TypeError("versionedFlush: the statements carry update or remove but no currentVersion. A failed version check reports the stored version through currentVersion.");
3345
3822
  }
3346
- function versionedWriters(aggregateType, versionedWrites) {
3823
+ function versionedWriters(versionedWrites) {
3347
3824
  return {
3348
- update: versionedWriter(aggregateType, versionedWrites, "update"),
3349
- remove: versionedWriter(aggregateType, versionedWrites, "remove")
3825
+ update: versionedWriter(versionedWrites, "update"),
3826
+ remove: versionedWriter(versionedWrites, "remove")
3350
3827
  };
3351
3828
  }
3352
3829
  /**
3353
3830
  * Resolves the statement of one intent here, so a statements object that
3354
3831
  * changes after the flush is built has no effect.
3355
3832
  */
3356
- function versionedWriter(aggregateType, versionedWrites, intent) {
3833
+ function versionedWriter(versionedWrites, intent) {
3357
3834
  const statement = versionedWrites?.[intent];
3358
3835
  const statementDefect = (write, reason, received) => new InvalidFlushStatementError({
3359
- aggregateType,
3360
- aggregateId: write.aggregateId,
3836
+ identity: write.aggregateIdentity,
3361
3837
  intent,
3362
3838
  reason,
3363
3839
  received
3364
3840
  });
3365
3841
  return async (transaction, write) => {
3366
3842
  if (versionedWrites === void 0 || statement === void 0) throw statementDefect(write, "statement_absent");
3367
- if (write.expectedVersion === void 0) throw statementDefect(write, "no_expected_version");
3843
+ if (!isStoredVersion(write.expectedVersion)) throw statementDefect(write, "no_expected_version", describeReceived(write.expectedVersion));
3368
3844
  const matchedRows = await statement(transaction, write);
3369
- if (!Number.isInteger(matchedRows) || matchedRows < 0) throw statementDefect(write, "no_row_count", describeMatchedRows(matchedRows));
3845
+ if (!Number.isInteger(matchedRows) || matchedRows < 0) throw statementDefect(write, "no_row_count", describeReceived(matchedRows));
3370
3846
  if (matchedRows > 0) return;
3371
- const stored = await readCurrentVersion(versionedWrites, transaction, write.aggregateId);
3372
- throw new ConcurrencyConflictError({
3373
- aggregateType,
3374
- aggregateId: write.aggregateId,
3847
+ throw await classifyConcurrencyConflict({
3848
+ identity: write.aggregateIdentity,
3375
3849
  expectedVersion: write.expectedVersion,
3376
- cause: stored.read ? void 0 : stored.readFailure,
3377
- ...storedVersionOf(stored, write.expectedVersion)
3850
+ transaction,
3851
+ currentVersion: versionedWrites.currentVersion
3378
3852
  });
3379
3853
  };
3380
3854
  }
3381
3855
  /**
3856
+ * Builds the conflict of a compare-and-set that matched no row. It reads the
3857
+ * stored version and names the reason of the conflict. The zero row count
3858
+ * already proves the conflict, so the read is diagnostic: a read that fails
3859
+ * becomes `version_unknown`, with the failure as the cause.
3860
+ *
3861
+ * A version read that returns neither a stored version nor `undefined`, and
3862
+ * an expected version that is no version, are defects of the statements. They
3863
+ * throw {@link InvalidFlushStatementError}.
3864
+ */
3865
+ async function classifyConcurrencyConflict(options) {
3866
+ const { identity, expectedVersion } = options;
3867
+ if (!isStoredVersion(expectedVersion)) throw new InvalidFlushStatementError({
3868
+ identity,
3869
+ reason: "no_expected_version",
3870
+ received: describeReceived(expectedVersion)
3871
+ });
3872
+ const stored = await readStoredVersion(() => options.currentVersion(options.transaction, identity.aggregateId));
3873
+ if (stored.read && stored.currentVersion !== void 0 && !isStoredVersion(stored.currentVersion)) throw new InvalidFlushStatementError({
3874
+ identity,
3875
+ reason: "no_version",
3876
+ received: describeReceived(stored.currentVersion)
3877
+ });
3878
+ return new ConcurrencyConflictError({
3879
+ identity,
3880
+ expectedVersion,
3881
+ cause: stored.read ? void 0 : stored.readFailure,
3882
+ ...storedVersionOf(stored, expectedVersion)
3883
+ });
3884
+ }
3885
+ function isStoredVersion(value) {
3886
+ return Number.isSafeInteger(value) && value >= 0;
3887
+ }
3888
+ /**
3382
3889
  * Names which version the conflict reports, and why. A read that failed leaves
3383
3890
  * the stored version unknown; a store that lost the aggregate has none to
3384
3891
  * name; a version that still equals the expected one means the statement
@@ -3395,21 +3902,23 @@ function storedVersionOf(stored, expectedVersion) {
3395
3902
  actualVersion: stored.currentVersion
3396
3903
  };
3397
3904
  }
3398
- /** Names what a statement returned instead of a row count. */
3399
- function describeMatchedRows(value) {
3905
+ /** Names a value that the flush received in place of a valid one. */
3906
+ function describeReceived(value) {
3400
3907
  if (value === void 0) return void 0;
3401
- return typeof value === "number" ? String(value) : `${String(value)} (${typeof value})`;
3908
+ if (typeof value === "number") return String(value);
3909
+ let text;
3910
+ try {
3911
+ text = String(value);
3912
+ } catch {
3913
+ text = Object.prototype.toString.call(value);
3914
+ }
3915
+ return `${text} (${typeof value})`;
3402
3916
  }
3403
- /**
3404
- * Reads the stored version for the `actualVersion` of a conflict. The zero
3405
- * row count already proves the conflict, so a failed read must not replace
3406
- * it. Such a read reports no version and travels as the conflict's cause.
3407
- */
3408
- async function readCurrentVersion(versionedWrites, transaction, aggregateId) {
3917
+ async function readStoredVersion(currentVersion) {
3409
3918
  try {
3410
3919
  return {
3411
3920
  read: true,
3412
- currentVersion: await versionedWrites.currentVersion(transaction, aggregateId)
3921
+ currentVersion: await currentVersion()
3413
3922
  };
3414
3923
  } catch (readFailure) {
3415
3924
  return {
@@ -3956,6 +4465,21 @@ function entityIds(entities) {
3956
4465
  * (emitting any event becomes a compile error).
3957
4466
  */
3958
4467
  var BaseAggregate = class extends Entity {
4468
+ #aggregateIdentity;
4469
+ /**
4470
+ * The full identity of the aggregate: its declared `aggregateType` and
4471
+ * its `id`, as one frozen value. The id alone is unique only within the
4472
+ * type, so this value names the aggregate unambiguously.
4473
+ */
4474
+ get aggregateIdentity() {
4475
+ if (this.#aggregateIdentity !== void 0) return this.#aggregateIdentity;
4476
+ const identity = Object.freeze({
4477
+ aggregateType: this.aggregateType,
4478
+ aggregateId: this.id
4479
+ });
4480
+ if (identity.aggregateType !== void 0) this.#aggregateIdentity = identity;
4481
+ return identity;
4482
+ }
3959
4483
  _version = 0;
3960
4484
  /**
3961
4485
  * Version the persistence layer last confirmed for this instance:
@@ -3980,8 +4504,7 @@ var BaseAggregate = class extends Entity {
3980
4504
  this.discardPendingEventsAfterDeletion(events);
3981
4505
  },
3982
4506
  persistedVersion: () => this._persistedVersion,
3983
- pendingEventCount: () => this._pendingEvents.length,
3984
- aggregateType: () => this.aggregateType
4507
+ pendingEventCount: () => this._pendingEvents.length
3985
4508
  });
3986
4509
  registerPendingEventRecordingCapability(this, { record: (createStamp) => this.recordPendingDecisions(createStamp) });
3987
4510
  }
@@ -4005,11 +4528,14 @@ var BaseAggregate = class extends Entity {
4005
4528
  if (!isUncommittedDomainEvent(candidate)) throw new UnmintedEventError(event.type);
4006
4529
  return recordDomainEvent(candidate, createStamp(candidate, index));
4007
4530
  });
4008
- if (this._pendingEvents !== stamped || this._pendingEvents.length !== stampedCount) throw new ReentrantEventRecordingError(String(this.id));
4531
+ if (this._pendingEvents !== stamped || this._pendingEvents.length !== stampedCount) throw new ReentrantEventRecordingError({ identity: this.aggregateIdentity });
4009
4532
  const seenEventIds = /* @__PURE__ */ new Set();
4010
4533
  for (const event of recorded) {
4011
4534
  const eventId = event.eventId;
4012
- if (seenEventIds.has(eventId)) throw new DuplicateEventIdError(String(this.id), eventId);
4535
+ if (seenEventIds.has(eventId)) throw new DuplicateEventIdError({
4536
+ identity: this.aggregateIdentity,
4537
+ eventId
4538
+ });
4013
4539
  seenEventIds.add(eventId);
4014
4540
  }
4015
4541
  this._pendingEvents = recorded;
@@ -4030,7 +4556,11 @@ var BaseAggregate = class extends Entity {
4030
4556
  this.stripAcknowledgedPrefix(events);
4031
4557
  }
4032
4558
  stripAcknowledgedPrefix(events) {
4033
- if (events.length > this._pendingEvents.length || events.some((event, index) => event !== this._pendingEvents[index])) throw new PendingEventBatchMismatchError(String(this.id), events.length, this._pendingEvents.length);
4559
+ if (events.length > this._pendingEvents.length || events.some((event, index) => event !== this._pendingEvents[index])) throw new PendingEventBatchMismatchError({
4560
+ identity: this.aggregateIdentity,
4561
+ batchLength: events.length,
4562
+ pendingLength: this._pendingEvents.length
4563
+ });
4034
4564
  this._pendingEvents = this._pendingEvents.slice(events.length);
4035
4565
  }
4036
4566
  get version() {
@@ -4099,7 +4629,7 @@ var BaseAggregate = class extends Entity {
4099
4629
  * ```
4100
4630
  */
4101
4631
  markReconstituted(version) {
4102
- assertReplayTargetHasNoPendingEvents(this.id, this._pendingEvents.length);
4632
+ assertReplayTargetHasNoPendingEvents(this.aggregateIdentity, this._pendingEvents.length);
4103
4633
  const restored = toVersion(version);
4104
4634
  if (restored < this._version) throw new InvalidVersionError(version, `is below the current version ${this._version}`);
4105
4635
  this.setVersion(restored);
@@ -4108,8 +4638,8 @@ var BaseAggregate = class extends Entity {
4108
4638
  /**
4109
4639
  * Appends a domain event to the pending list. The event must be minted
4110
4640
  * by a kit constructor; a missing `aggregateId` or `aggregateType` is
4111
- * stamped from this aggregate, and an address that names another
4112
- * aggregate throws {@link MisaddressedEventError} before anything is
4641
+ * stamped from this aggregate, and an aggregate identity that names another
4642
+ * aggregate throws {@link MisattributedEventError} before anything is
4113
4643
  * recorded. Each append is one fact. A decision appended twice becomes
4114
4644
  * two facts with distinct ids. A recorded event that is already pending
4115
4645
  * throws {@link DuplicateEventIdError}, and a list at `maxPendingEvents`
@@ -4124,7 +4654,7 @@ var BaseAggregate = class extends Entity {
4124
4654
  * application shell rejects it; use `setState(currentState, event)`.
4125
4655
  */
4126
4656
  addDomainEvent(event) {
4127
- const stamped = this.addressNewEvent(event);
4657
+ const stamped = this.stampNewEventIdentity(event);
4128
4658
  this.assertEventIdsNotPending([stamped]);
4129
4659
  this.assertPendingEventLimit(1);
4130
4660
  this.appendStampedEvent(stamped);
@@ -4141,7 +4671,10 @@ var BaseAggregate = class extends Entity {
4141
4671
  for (const pending of this._pendingEvents) if (isRecordedDomainEvent(pending)) pendingIds.add(pending.eventId);
4142
4672
  for (const event of batch) {
4143
4673
  if (!isRecordedDomainEvent(event)) continue;
4144
- if (pendingIds.has(event.eventId)) throw new DuplicateEventIdError(String(this.id), event.eventId);
4674
+ if (pendingIds.has(event.eventId)) throw new DuplicateEventIdError({
4675
+ identity: this.aggregateIdentity,
4676
+ eventId: event.eventId
4677
+ });
4145
4678
  pendingIds.add(event.eventId);
4146
4679
  }
4147
4680
  }
@@ -4157,8 +4690,7 @@ var BaseAggregate = class extends Entity {
4157
4690
  const pending = this._pendingEvents.length;
4158
4691
  if (pending + added <= limit) return;
4159
4692
  throw new PendingEventLimitExceededError({
4160
- aggregateType: this.aggregateType,
4161
- aggregateId: String(this.id),
4693
+ identity: this.aggregateIdentity,
4162
4694
  limit,
4163
4695
  pending,
4164
4696
  added
@@ -4166,7 +4698,7 @@ var BaseAggregate = class extends Entity {
4166
4698
  }
4167
4699
  /**
4168
4700
  * Appends an event that the caller already passed through
4169
- * {@link addressNewEvent}, {@link assertEventIdsNotPending} and
4701
+ * {@link stampNewEventIdentity}, {@link assertEventIdsNotPending} and
4170
4702
  * {@link assertPendingEventLimit}. `setState()` and `apply()` run the
4171
4703
  * three before the state moves and append afterwards, so each gate
4172
4704
  * runs once per event.
@@ -4183,22 +4715,23 @@ var BaseAggregate = class extends Entity {
4183
4715
  this._pendingEvents = [];
4184
4716
  }
4185
4717
  /**
4186
- * Address discipline for NEW facts, shared by both flavours: a
4718
+ * Aggregate identity discipline for NEW facts, shared by both flavours: a
4187
4719
  * present-but-foreign `aggregateId` / `aggregateType` is a wiring bug and
4188
- * throws {@link MisaddressedEventError}; missing fields are filled in
4189
- * from the aggregate, so a recorded event is always fully addressed and
4190
- * can never fail the harvest or the replay guard later. The mint gate
4720
+ * throws {@link MisattributedEventError}; missing fields are filled in
4721
+ * from the aggregate, so a recorded event always carries its full
4722
+ * aggregate identity and can never fail the harvest or the replay guard
4723
+ * later. The mint gate
4191
4724
  * runs first, so an unminted event fails before anything else. The
4192
4725
  * stamped copy is frozen like the original (payload and metadata are
4193
- * shared, already deep-frozen by the constructors); a fully addressed
4726
+ * shared, already deep-frozen by the constructors); a fully stamped
4194
4727
  * event is returned as is.
4195
4728
  */
4196
- addressNewEvent(event) {
4729
+ stampNewEventIdentity(event) {
4197
4730
  this.assertMintedEvent(event);
4198
4731
  const { aggregateId, aggregateType } = event;
4199
4732
  const idForeign = aggregateId !== void 0 && aggregateId !== this.id;
4200
4733
  const typeForeign = aggregateType !== void 0 && aggregateType !== this.aggregateType;
4201
- if (idForeign || typeForeign) throw new MisaddressedEventError({
4734
+ if (idForeign || typeForeign) throw new MisattributedEventError({
4202
4735
  expected: {
4203
4736
  aggregateType: this.aggregateType,
4204
4737
  aggregateId: this.id
@@ -4240,11 +4773,11 @@ var BaseAggregate = class extends Entity {
4240
4773
  * concrete event producer, rather than in shell-owned recording data.
4241
4774
  */
4242
4775
  createEvent(type, payload, options) {
4243
- return createUncommittedDomainEvent(type, payload, {
4776
+ return mintUncommittedDomainEvent(type, payload, {
4244
4777
  ...options,
4245
4778
  aggregateId: this.id,
4246
4779
  aggregateType: this.aggregateType
4247
- });
4780
+ }, "createEvent");
4248
4781
  }
4249
4782
  };
4250
4783
  /**
@@ -4267,8 +4800,11 @@ var BaseAggregate = class extends Entity {
4267
4800
  * @internal Shared by the aggregate flavours in this package; not part of
4268
4801
  * the public API.
4269
4802
  */
4270
- function assertReplayTargetHasNoPendingEvents(id, pending) {
4271
- if (pending > 0) throw new UnreplayableAggregateError(String(id), `it carries ${pending} unflushed pending event(s) that are not part of the persisted stream; discard this dirty instance and reconstitute a fresh aggregate before restoring persisted history`);
4803
+ function assertReplayTargetHasNoPendingEvents(identity, pending) {
4804
+ if (pending > 0) throw new UnreplayableAggregateError({
4805
+ identity,
4806
+ reason: `it carries ${pending} unflushed pending event(s) that are not part of the persisted stream; discard this dirty instance and reconstitute a fresh aggregate before restoring persisted history`
4807
+ });
4272
4808
  }
4273
4809
 
4274
4810
  //#endregion
@@ -4358,7 +4894,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
4358
4894
  * recorded and the version advances with it.
4359
4895
  */
4360
4896
  setState(_newState) {
4361
- throw new DirectStateMutationError(String(this.id));
4897
+ throw new DirectStateMutationError({ identity: this.aggregateIdentity });
4362
4898
  }
4363
4899
  /**
4364
4900
  * Applies an event: validates the decision, locates the fold, computes
@@ -4371,9 +4907,9 @@ var EventSourcedAggregate = class extends BaseAggregate {
4371
4907
  * Throws `MissingFoldError` if no fold is declared for `event.type`.
4372
4908
  * Throws `FoldReturnedNoStateError` (wiring) when the fold returns
4373
4909
  * `undefined`, the signature of a fold without a `return`.
4374
- * Throws `MisaddressedEventError` (wiring) when the event carries an
4910
+ * Throws `MisattributedEventError` (wiring) when the event carries an
4375
4911
  * `aggregateId` or `aggregateType` naming a different aggregate;
4376
- * missing address fields are stamped from the aggregate instead.
4912
+ * missing aggregate identity fields are stamped from the aggregate instead.
4377
4913
  *
4378
4914
  * State is not mutated if any step throws: the fold is invoked into
4379
4915
  * a local and only stored once all checks pass.
@@ -4390,7 +4926,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
4390
4926
  * @param event - The domain event to apply
4391
4927
  */
4392
4928
  apply(event) {
4393
- const stamped = this.addressNewEvent(event);
4929
+ const stamped = this.stampNewEventIdentity(event);
4394
4930
  this.assertEventIdsNotPending([stamped]);
4395
4931
  this.assertPendingEventLimit(1);
4396
4932
  this.validateEvent(stamped);
@@ -4414,16 +4950,16 @@ var EventSourcedAggregate = class extends BaseAggregate {
4414
4950
  * and the discriminator is resolved via the (statically-sound)
4415
4951
  * `folds` map.
4416
4952
  *
4417
- * Replay address check: a history event that names a DIFFERENT
4953
+ * Replay aggregate identity check: a history event that names a DIFFERENT
4418
4954
  * aggregate id or type is a persisted row that belongs to someone
4419
4955
  * else (a miswired stream read, colliding ids across types, a
4420
4956
  * corrupted store). Throws `ForeignEventError`, an
4421
4957
  * `InfrastructureError`, which PROPAGATES through the replay
4422
4958
  * methods (their `Result` channel is reserved for `DomainError`
4423
4959
  * stream corruption) after the all-or-nothing rollback. History
4424
- * events without the optional address fields pass unchecked (the
4960
+ * events without the optional aggregate identity fields pass unchecked (the
4425
4961
  * fields are optional on the event shape); NEW events are covered
4426
- * by the stricter `addressNewEvent` on the apply path.
4962
+ * by the stricter `stampNewEventIdentity` on the apply path.
4427
4963
  */
4428
4964
  assertReplayedEventBelongsHere(event) {
4429
4965
  const idMismatch = event.aggregateId !== void 0 && event.aggregateId !== this.id;
@@ -4467,15 +5003,15 @@ var EventSourcedAggregate = class extends BaseAggregate {
4467
5003
  * a reconstituted aggregate at v=P catching up on M newer events ends at
4468
5004
  * v=P+M. Events carry no stream position, so an overlap with the current
4469
5005
  * version is invisible here: the caller passes only the events after
4470
- * that version and checks the final version against the pinned stream
4471
- * head ({@link ReplayHeadMismatchError}).
5006
+ * that version, and it compares the final version with the version it
5007
+ * expects.
4472
5008
  *
4473
5009
  * The replay target must not carry pending decisions. Factory-vs-load
4474
5010
  * lifecycle is owned by the Unit of Work rather than inferred from an
4475
5011
  * aggregate persistence flag.
4476
5012
  */
4477
5013
  replayHistory(history) {
4478
- assertReplayTargetHasNoPendingEvents(this.id, requirePendingEventLifecycleReadView(this, "replayHistory").pendingEventCount());
5014
+ assertReplayTargetHasNoPendingEvents(this.aggregateIdentity, requirePendingEventLifecycleReadView(this, "replayHistory").pendingEventCount());
4479
5015
  if (history.length === 0) return ok();
4480
5016
  const previousState = this.state;
4481
5017
  const startVersion = this.version;
@@ -4499,10 +5035,10 @@ var EventSourcedAggregate = class extends BaseAggregate {
4499
5035
  /**
4500
5036
  * Reconstitutes an event-sourced aggregate from one page of history and
4501
5037
  * yields it only on success. `createReplayTarget` builds the instance: a
4502
- * fresh one, or one restored from a snapshot. The instance exists only
5038
+ * fresh one, or one reconstituted from a snapshot. The instance exists only
4503
5039
  * inside this call. A rejected replay therefore leaves the caller with
4504
- * nothing to return by mistake. Later catch-up pages go through
4505
- * `replayHistory` on the value. A `DomainError` from a fold rides the
5040
+ * nothing to return by mistake. A stream read in pages goes through
5041
+ * `reconstituteAggregateFromStreamPages` instead. A `DomainError` from a fold rides the
4506
5042
  * `Result`; wiring errors and a foreign row throw, as in `replayHistory`.
4507
5043
  * The creator runs outside the `Result`: what it throws propagates.
4508
5044
  */
@@ -4528,13 +5064,13 @@ var StateStoredAggregate = class extends BaseAggregate {
4528
5064
  /**
4529
5065
  * Replaces the state, advances the OCC version, and records the events
4530
5066
  * of the change, in that order. State validation, the event mint gate,
4531
- * the event address check, the pending-identity check, and the pending
4532
- * event limit check run before the change becomes observable, so a
4533
- * rejected decision records nothing and moves nothing. Without events the call is a plain versioned state
4534
- * change.
5067
+ * the aggregate identity check of the event, the pending event-id check,
5068
+ * and the pending event limit check run before the change becomes
5069
+ * observable, so a rejected decision records nothing and moves nothing.
5070
+ * Without events the call is a plain versioned state change.
4535
5071
  */
4536
5072
  setState(newState, events = []) {
4537
- const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.addressNewEvent(event));
5073
+ const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.stampNewEventIdentity(event));
4538
5074
  this.assertEventIdsNotPending(stamped);
4539
5075
  this.assertPendingEventLimit(stamped.length);
4540
5076
  const next = this.nextVersion();
@@ -5399,117 +5935,6 @@ function voValidated(t, validate, message = "Validation failed") {
5399
5935
  return issues.hasIssues() ? err(issues) : ok(vo(t));
5400
5936
  }
5401
5937
 
5402
- //#endregion
5403
- //#region src/internal/structural/detach-state.ts
5404
- /**
5405
- * Returns a copy of `state` that shares no object with the original.
5406
- * Throws a `TypeError` that names the path when the graph carries a value
5407
- * a structured clone would lose or silently degrade. A class instance, a
5408
- * subclass of a built-in included, loses the methods on its prototype. A
5409
- * symbol-keyed, non-enumerable, or accessor property and an expando on a
5410
- * built-in are dropped. A function or a symbol value throws a raw
5411
- * `DataCloneError`. An Error, a Promise, a WeakMap, or a WeakSet cannot be
5412
- * detached at all. A SharedArrayBuffer and a view over one keep sharing
5413
- * their memory. A Proxy is invisible to the walk and fails inside the
5414
- * clone; that failure is rethrown as a `TypeError` with the cause.
5415
- *
5416
- * Plain objects (from any realm), arrays, Dates, Maps, Sets, bigints, and
5417
- * typed arrays pass. A RegExp passes: pattern and flags survive the clone,
5418
- * and `lastIndex` restores as 0. The scan state of a global or sticky
5419
- * pattern is not domain data. A non-enumerable property on a built-in
5420
- * passes: it is the built-in's own machinery (`lastIndex`), not data. A
5421
- * non-enumerable symbol key passes anywhere: it is metadata by convention.
5422
- *
5423
- * The concrete entity uses it for a detached read DTO of a plain-data
5424
- * state. The snapshot model uses it for the captured DTO and the restored
5425
- * state. A state that carries a class-based child is mapped to plain data
5426
- * first, in the entity or in the model.
5427
- */
5428
- function detachState(state) {
5429
- assertDetachable(state, "", /* @__PURE__ */ new WeakSet());
5430
- try {
5431
- return structuredClone(state);
5432
- } catch (cause) {
5433
- throw new TypeError("detachState: state holds a Proxy or a host object that cannot be cloned; map it to plain data", { cause });
5434
- }
5435
- }
5436
- const INDEX_KEY = /^(0|[1-9]\d*)$/;
5437
- function assertDetachable(value, path, seen) {
5438
- if (typeof value === "function") throw new TypeError(`detachState: state${path} is a function; map it to plain data`);
5439
- if (typeof value === "symbol") throw new TypeError(`detachState: state${path} is a symbol; map it to plain data`);
5440
- if (value === null || typeof value !== "object") return;
5441
- const object = value;
5442
- if (seen.has(object)) return;
5443
- seen.add(object);
5444
- if (Array.isArray(object)) {
5445
- if (!hasIntrinsicPrototypeChain(object, "Array")) throwClassInstance(object, path);
5446
- assertOwnPropertiesDetachable(object, path, seen, "array");
5447
- return;
5448
- }
5449
- const tag = builtInTagWithoutInvokingAccessors(object);
5450
- if (tag !== void 0) {
5451
- if (!hasIntrinsicPrototypeChain(object)) throwClassInstance(object, path);
5452
- if (tag === "[object Map]") {
5453
- let index = 0;
5454
- for (const [key, entry] of object) {
5455
- assertDetachable(key, `${path}<map key #${index}>`, seen);
5456
- assertDetachable(entry, `${path}<map value #${index}>`, seen);
5457
- index++;
5458
- }
5459
- } else if (tag === "[object Set]") {
5460
- let index = 0;
5461
- for (const member of object) {
5462
- assertDetachable(member, `${path}<set member #${index}>`, seen);
5463
- index++;
5464
- }
5465
- } else if (tag === "[object Promise]" || tag === "[object WeakMap]" || tag === "[object WeakSet]") throw new TypeError(`detachState: state${path} is a ${tag.slice(8, -1)} and cannot be detached`);
5466
- else if (tag === "[object Error]") throw new TypeError(`detachState: state${path} is an Error; map it to plain data`);
5467
- else if (sharesMemory(object, tag)) throw new TypeError(`detachState: state${path} is backed by a SharedArrayBuffer and the copy would share its memory; map it to plain data`);
5468
- assertOwnPropertiesDetachable(object, path, seen, "built-in");
5469
- return;
5470
- }
5471
- const prototype = Object.getPrototypeOf(object);
5472
- if (!(prototype === null || isIntrinsicConstructorPrototype(prototype, "Object") && Object.getPrototypeOf(prototype) === null)) throwClassInstance(object, path);
5473
- assertOwnPropertiesDetachable(object, path, seen, "record");
5474
- }
5475
- /**
5476
- * Audits the own properties the clone would copy or drop. The clone keeps
5477
- * the enumerable own keys of a record and of an array, expandos included,
5478
- * and drops every own key of another built-in. `length` on an array and
5479
- * the non-enumerable keys of a built-in (`lastIndex`) are its own
5480
- * machinery, not data. Index keys of a typed array or a boxed String are
5481
- * its content and pass as such.
5482
- */
5483
- function assertOwnPropertiesDetachable(object, path, seen, kind) {
5484
- for (const key of Reflect.ownKeys(object)) {
5485
- const descriptor = Object.getOwnPropertyDescriptor(object, key);
5486
- if (descriptor === void 0) continue;
5487
- if (typeof key === "symbol") {
5488
- if (!descriptor.enumerable) continue;
5489
- throw new TypeError(`detachState: state${path} has a symbol-keyed property; map it to plain data`);
5490
- }
5491
- if (kind === "array" && key === "length") continue;
5492
- const isIndex = INDEX_KEY.test(key);
5493
- if (!descriptor.enumerable) {
5494
- if (kind === "built-in") continue;
5495
- throw new TypeError(`detachState: state${path}.${key} is not enumerable and the clone would drop it; map it to plain data`);
5496
- }
5497
- if (kind === "built-in" && isIndex) continue;
5498
- const memberPath = isIndex ? `${path}[${key}]` : `${path}.${key}`;
5499
- if (!("value" in descriptor)) throw new TypeError(`detachState: state${memberPath} is an accessor property; map it to plain data`);
5500
- if (kind === "built-in") throw new TypeError(`detachState: state${memberPath} is an expando on a ${object.constructor?.name ?? "built-in"} and the clone would drop it; map it to plain data`);
5501
- assertDetachable(descriptor.value, memberPath, seen);
5502
- }
5503
- }
5504
- function sharesMemory(object, tag) {
5505
- if (tag === "[object SharedArrayBuffer]") return true;
5506
- return ArrayBuffer.isView(object) && Object.prototype.toString.call(object.buffer) === "[object SharedArrayBuffer]";
5507
- }
5508
- function throwClassInstance(object, path) {
5509
- const name = Object.getPrototypeOf(object)?.constructor?.name || "anonymous class";
5510
- throw new TypeError(`detachState: state${path} is a class instance (${name}); map it to plain data`);
5511
- }
5512
-
5513
5938
  //#endregion
5514
5939
  //#region src/messaging/event-bus/errors.ts
5515
5940
  /** Keeps a deep chain readable in the message without losing the recent path. */
@@ -5912,6 +6337,8 @@ var EventBusImpl = class {
5912
6337
  }
5913
6338
  once(eventType, options) {
5914
6339
  return new Promise((resolve, reject) => {
6340
+ const timeoutMs = options?.timeoutMs;
6341
+ if (timeoutMs !== void 0) assertTimerDelay("EventBus.once", "timeoutMs", timeoutMs);
5915
6342
  if (this.closed) {
5916
6343
  reject(new EventBusClosedError("once"));
5917
6344
  return;
@@ -5953,10 +6380,10 @@ var EventBusImpl = class {
5953
6380
  };
5954
6381
  signal.addEventListener("abort", abortListener);
5955
6382
  }
5956
- if (typeof options?.timeoutMs === "number") timer = setTimeout(() => {
6383
+ if (timeoutMs !== void 0) timer = setTimeout(() => {
5957
6384
  cleanup();
5958
- reject(/* @__PURE__ */ new Error(`EventBus.once timed out after ${options.timeoutMs}ms waiting for "${eventType}"`));
5959
- }, options.timeoutMs);
6385
+ reject(/* @__PURE__ */ new Error(`EventBus.once timed out after ${timeoutMs}ms waiting for "${eventType}"`));
6386
+ }, timeoutMs);
5960
6387
  });
5961
6388
  }
5962
6389
  /**
@@ -6218,7 +6645,10 @@ function invalid(path, reason) {
6218
6645
  * name.
6219
6646
  */
6220
6647
  function outboxWriterAcceptingEventLoss() {
6221
- return { add: async () => {} };
6648
+ return {
6649
+ add: async () => {},
6650
+ endEventSources: async () => {}
6651
+ };
6222
6652
  }
6223
6653
  /**
6224
6654
  * In-memory reference implementation of `DispatchTrackingOutbox<Evt>`
@@ -6234,12 +6664,16 @@ function outboxWriterAcceptingEventLoss() {
6234
6664
  * recent-dispatch receipt cache keeps retries idempotent after acknowledgement.
6235
6665
  * Re-adding a pending event refreshes the stored commit envelope while the
6236
6666
  * delivery attempt count survives. Its commit sequence and size remain
6237
- * immutable; only this transaction-unaware adapter may move a still-pending
6238
- * event to another aggregate version after an outer rollback leaked the first
6239
- * add. Dead-lettered and acknowledged retries must match the complete original
6240
- * candidate receipt. Reusing an `eventId` for another source or commit position
6667
+ * immutable. Only this in-memory adapter may move a still-pending event to
6668
+ * another aggregate version. Without an `InMemoryTransactionScope`, it keeps
6669
+ * the first add of a rolled-back attempt. Dead-lettered and acknowledged
6670
+ * retries must match the complete original candidate receipt. Reusing an `eventId` for another source or commit position
6241
6671
  * throws {@link EventHarvestError} while the pending, dead-letter, or bounded
6242
- * dispatched receipt still proves the collision. Insertion order is preserved:
6672
+ * dispatched receipt still proves the collision. `add` also throws
6673
+ * {@link EventHarvestError} for a malformed candidate: a part that is not an
6674
+ * object, an `eventId` or `type` that is not an own string data property, a
6675
+ * source field that is not a string, or a position that is not a valid
6676
+ * commit position. Insertion order is preserved:
6243
6677
  * `getPending` returns records in commit order, as the port contract requires.
6244
6678
  *
6245
6679
  * Dispatch tracking: `markFailed` increments the record's attempt count
@@ -6249,7 +6683,9 @@ function outboxWriterAcceptingEventLoss() {
6249
6683
  * inverse of `deadLetters()`); `markDispatched` acks pending AND
6250
6684
  * dead-lettered records (manual redelivery then ack).
6251
6685
  * To link future eventful commits, the implementation also retains one
6252
- * source cursor per qualified aggregate after dispatch. Consequently a
6686
+ * source cursor per qualified aggregate after dispatch. `endEventSources`
6687
+ * marks the cursor of a removed aggregate as ended, and a new event of that
6688
+ * source then throws {@link EventHarvestError}. Consequently a
6253
6689
  * long-lived instance is bounded only when `maxRecords` and `maxSources` are
6254
6690
  * configured, plus `maxRetainedDispatchedEventIds`; use a durable adapter with
6255
6691
  * an explicit source-head lifecycle and an event-id unique key for unbounded
@@ -6263,12 +6699,21 @@ function outboxWriterAcceptingEventLoss() {
6263
6699
  * that are never `markDispatched` accumulate until `maxRecords` rejects a new
6264
6700
  * add, or without that option grow unbounded. For a deliberate no-delivery
6265
6701
  * setup use {@link outboxWriterAcceptingEventLoss} instead. Sharper still:
6266
- * events `add()`ed inside a transaction that later rolls back are NOT
6267
- * removed (the Map knows nothing about your scope's rollback). Tests
6268
- * that assert rollback purity need an outbox that participates in the
6269
- * test store's transactional semantics; see the reference adapter at
6270
- * https://github.com/shi-rudo/ddd-kit-ts/blob/main/src/testing/repository-contract.test.ts
6271
- * (repo-only, not shipped to npm).
6702
+ * on its own, the outbox knows nothing about your scope's rollback. Events
6703
+ * `add()`ed inside a transaction that later rolls back stay, and a source
6704
+ * that `endEventSources` ended inside such a transaction stays ended: the
6705
+ * outbox then rejects every later event of that aggregate, also on the retry
6706
+ * of a `RetryingTransactionScope`. For tests that roll back or retry,
6707
+ * register the outbox with an `InMemoryTransactionScope`. A rollback then
6708
+ * returns the outbox to its state at the start of the attempt.
6709
+ *
6710
+ * A relay reads only committed records. While a transaction is open,
6711
+ * `getPending` returns a record that the transaction wrote as it was
6712
+ * committed at the first write, with its current attempts. It skips a record
6713
+ * that was not pending at that write, such as a new or a requeued record. A
6714
+ * rollback also undoes an acknowledgement or a failure report that a
6715
+ * dispatcher made while the transaction was open. The dispatcher then
6716
+ * delivers that record again.
6272
6717
  *
6273
6718
  * @example
6274
6719
  * ```ts
@@ -6291,8 +6736,15 @@ var InMemoryOutbox = class {
6291
6736
  dead = /* @__PURE__ */ new Map();
6292
6737
  /** Latest eventful commit and its predecessor per qualified source. */
6293
6738
  sourceCursors = /* @__PURE__ */ new Map();
6739
+ /** Keys of source cursors whose aggregate was removed. */
6740
+ endedSourceKeys = /* @__PURE__ */ new Set();
6294
6741
  /** Bounded insertion-ordered receipts for exact retries after acknowledgement. */
6295
6742
  dispatchedEventIds = /* @__PURE__ */ new Map();
6743
+ /**
6744
+ * The committed pending record of each event id that the open transaction
6745
+ * wrote, captured at its first write. `undefined` marks an id without one.
6746
+ */
6747
+ committedVersions;
6296
6748
  maxDeliveryAttempts;
6297
6749
  maxRetainedDispatchedEventIds;
6298
6750
  maxRecords;
@@ -6310,11 +6762,14 @@ var InMemoryOutbox = class {
6310
6762
  this.maxSources = options?.maxSources;
6311
6763
  }
6312
6764
  async add(events) {
6313
- this.assertBatchEventReceiptIntegrity(events);
6314
- this.assertBatchPositionIntegrity(events);
6315
- this.assertCapacity(events);
6316
- for (const message of events) {
6765
+ const batch = Array.from(events, ownCandidate);
6766
+ const headRetries = this.retriesAtSourceHead(batch);
6767
+ this.assertBatchEventReceiptIntegrity(batch);
6768
+ this.assertBatchPositionIntegrity(batch, headRetries);
6769
+ this.assertCapacity(batch, headRetries);
6770
+ for (const message of batch) {
6317
6771
  const { event, source, position } = message;
6772
+ if (headRetries.has(event.eventId)) continue;
6318
6773
  const dispatchedReceipt = this.dispatchedEventIds.get(event.eventId);
6319
6774
  if (dispatchedReceipt !== void 0) {
6320
6775
  assertSameEventSource(event, source, dispatchedReceipt.source);
@@ -6332,6 +6787,7 @@ var InMemoryOutbox = class {
6332
6787
  assertSameEventSource(event, source, deadLetter.source);
6333
6788
  assertSameCandidateReceipt(event, position, deadLetter.position);
6334
6789
  this.dead.delete(event.eventId);
6790
+ this.recordCommittedVersion(event.eventId);
6335
6791
  this.pending.set(event.eventId, {
6336
6792
  dispatchId: deadLetter.dispatchId,
6337
6793
  event: deadLetter.event,
@@ -6341,17 +6797,16 @@ var InMemoryOutbox = class {
6341
6797
  });
6342
6798
  continue;
6343
6799
  }
6344
- const ownedSource = Object.freeze({ ...source });
6345
- const sourceKey = encodeAggregateAddress(source);
6800
+ const sourceKey = encodeAggregateIdentity(source);
6346
6801
  const sourceCursor = this.sourceCursors.get(sourceKey);
6347
6802
  let staleHeadVersion;
6348
6803
  if (existing !== void 0 && position.aggregateVersion < existing.position.aggregateVersion) staleHeadVersion = existing.position.aggregateVersion;
6349
6804
  else if (existing === void 0 && sourceCursor !== void 0 && position.aggregateVersion < sourceCursor.aggregateVersion) staleHeadVersion = sourceCursor.aggregateVersion;
6350
6805
  if (staleHeadVersion !== void 0) throw staleHeadError(event, source, position, staleHeadVersion);
6351
6806
  if (sourceCursor?.aggregateVersion === position.aggregateVersion) {
6352
- if (sourceCursor.commitSize !== position.commitSize) throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${source.aggregateType} ${source.aggregateId}: aggregate version ${position.aggregateVersion} was already recorded with commitSize ${sourceCursor.commitSize}, not ${position.commitSize}.`, event.type);
6807
+ if (sourceCursor.commitSize !== position.commitSize) throw commitSizeMismatchError(event, source, position, sourceCursor.commitSize);
6353
6808
  const positionOwner = sourceCursor.eventIdsBySequence.get(position.commitSequence);
6354
- if (positionOwner !== void 0 && positionOwner !== event.eventId) throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${source.aggregateType} ${source.aggregateId}: source position (${position.aggregateVersion}, ${position.commitSequence}) is already owned by event "${positionOwner}". One qualified source position must identify exactly one immutable event.`, event.type);
6809
+ if (positionOwner !== void 0 && positionOwner !== event.eventId) throw positionOwnedError(event, source, position, positionOwner);
6355
6810
  }
6356
6811
  let previousEventfulAggregateVersion;
6357
6812
  if (existing !== void 0 && existing.position.aggregateVersion !== position.aggregateVersion) {
@@ -6381,27 +6836,30 @@ var InMemoryOutbox = class {
6381
6836
  previousEventfulAggregateVersion
6382
6837
  });
6383
6838
  if (existing) {
6839
+ this.recordCommittedVersion(event.eventId);
6384
6840
  existing.event = event;
6385
- existing.source = ownedSource;
6841
+ existing.source = source;
6386
6842
  existing.position = ownedPosition;
6387
6843
  continue;
6388
6844
  }
6845
+ this.recordCommittedVersion(event.eventId);
6389
6846
  this.pending.set(event.eventId, {
6390
6847
  dispatchId: event.eventId,
6391
6848
  event,
6392
- source: ownedSource,
6849
+ source,
6393
6850
  position: ownedPosition,
6394
6851
  attempts: 0
6395
6852
  });
6396
6853
  }
6854
+ for (const { event, source, position } of batch) if (headRetries.has(event.eventId)) this.rememberDispatched(event.eventId, source, position);
6397
6855
  }
6398
- assertCapacity(events) {
6856
+ assertCapacity(events, headRetries) {
6399
6857
  const newRecordIds = /* @__PURE__ */ new Set();
6400
6858
  const newSourceKeys = /* @__PURE__ */ new Set();
6401
6859
  for (const { event, source } of events) {
6402
- if (this.pending.has(event.eventId) || this.dead.has(event.eventId) || this.dispatchedEventIds.has(event.eventId)) continue;
6860
+ if (this.pending.has(event.eventId) || this.dead.has(event.eventId) || this.dispatchedEventIds.has(event.eventId) || headRetries.has(event.eventId)) continue;
6403
6861
  newRecordIds.add(event.eventId);
6404
- const sourceKey = encodeAggregateAddress(source);
6862
+ const sourceKey = encodeAggregateIdentity(source);
6405
6863
  if (!this.sourceCursors.has(sourceKey)) newSourceKeys.add(sourceKey);
6406
6864
  }
6407
6865
  const currentRecords = this.pending.size + this.dead.size;
@@ -6449,10 +6907,12 @@ var InMemoryOutbox = class {
6449
6907
  }
6450
6908
  }
6451
6909
  }
6452
- assertBatchPositionIntegrity(events) {
6910
+ assertBatchPositionIntegrity(events, headRetries) {
6453
6911
  const simulatedCursors = /* @__PURE__ */ new Map();
6454
6912
  for (const { event, source, position } of events) {
6455
- const sourceKey = encodeAggregateAddress(source);
6913
+ if (headRetries.has(event.eventId)) continue;
6914
+ const sourceKey = encodeAggregateIdentity(source);
6915
+ this.assertSourceNotEnded(event, source, position, sourceKey);
6456
6916
  const cursor = simulatedCursors.get(sourceKey) ?? this.sourceCursors.get(sourceKey);
6457
6917
  if (cursor === void 0 || position.aggregateVersion > cursor.aggregateVersion) {
6458
6918
  simulatedCursors.set(sourceKey, {
@@ -6469,23 +6929,26 @@ var InMemoryOutbox = class {
6469
6929
  if (pendingRecord !== void 0 && position.aggregateVersion < pendingRecord.position.aggregateVersion || pendingRecord === void 0 && !dedupes) throw staleHeadError(event, source, position, pendingRecord?.position.aggregateVersion ?? cursor.aggregateVersion);
6470
6930
  continue;
6471
6931
  }
6472
- if (cursor.commitSize !== position.commitSize) throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${source.aggregateType} ${source.aggregateId}: aggregate version ${position.aggregateVersion} was already recorded with commitSize ${cursor.commitSize}, not ${position.commitSize}.`, event.type);
6932
+ if (cursor.commitSize !== position.commitSize) throw commitSizeMismatchError(event, source, position, cursor.commitSize);
6473
6933
  const positionOwner = cursor.eventIdsBySequence.get(position.commitSequence);
6474
- if (positionOwner !== void 0 && positionOwner !== event.eventId) throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${source.aggregateType} ${source.aggregateId}: source position (${position.aggregateVersion}, ${position.commitSequence}) is already owned by event "${positionOwner}". One qualified source position must identify exactly one immutable event.`, event.type);
6934
+ if (positionOwner !== void 0 && positionOwner !== event.eventId) throw positionOwnedError(event, source, position, positionOwner);
6475
6935
  if (positionOwner === void 0) simulatedCursors.set(sourceKey, cursorWithEvent(cursor, position.commitSequence, event.eventId));
6476
6936
  }
6477
6937
  }
6478
6938
  async getPending(limit) {
6479
6939
  const max = typeof limit === "number" ? Math.max(0, Number.isNaN(limit) ? 0 : limit) : Number.POSITIVE_INFINITY;
6480
6940
  const batch = [];
6481
- for (const record of this.pending.values()) {
6941
+ const versions = this.committedVersions;
6942
+ for (const [eventId, live] of this.pending) {
6482
6943
  if (batch.length >= max) break;
6944
+ const committed = versions?.has(eventId) ? versions.get(eventId) : live;
6945
+ if (committed === void 0) continue;
6483
6946
  batch.push({
6484
- dispatchId: record.dispatchId,
6485
- event: record.event,
6486
- source: record.source,
6487
- position: record.position,
6488
- attempts: record.attempts
6947
+ dispatchId: committed.dispatchId,
6948
+ event: committed.event,
6949
+ source: committed.source,
6950
+ position: committed.position,
6951
+ attempts: live.attempts
6489
6952
  });
6490
6953
  }
6491
6954
  return batch;
@@ -6495,9 +6958,88 @@ var InMemoryOutbox = class {
6495
6958
  const record = this.pending.get(id) ?? this.dead.get(id);
6496
6959
  if (record !== void 0) this.rememberDispatched(id, record.source, record.position);
6497
6960
  this.pending.delete(id);
6961
+ this.forgetCommittedVersion(id);
6498
6962
  this.dead.delete(id);
6499
6963
  }
6500
6964
  }
6965
+ /**
6966
+ * Records the state that a rollback of an `InMemoryTransactionScope`
6967
+ * returns to: pending and dead-letter records, receipts, source cursors,
6968
+ * and ended sources, in their order. Until the transaction ends,
6969
+ * `getPending` returns the committed record of each event id that the
6970
+ * transaction writes.
6971
+ */
6972
+ beginTransaction() {
6973
+ const pending = [...this.pending].map(([id, record]) => [id, { ...record }]);
6974
+ const dead = [...this.dead];
6975
+ const receipts = [...this.dispatchedEventIds];
6976
+ const cursors = [...this.sourceCursors];
6977
+ const ended = [...this.endedSourceKeys];
6978
+ const versions = /* @__PURE__ */ new Map();
6979
+ this.committedVersions = versions;
6980
+ const end = () => {
6981
+ if (this.committedVersions === versions) this.committedVersions = void 0;
6982
+ };
6983
+ return {
6984
+ commit: end,
6985
+ rollback: () => {
6986
+ refill(this.pending, pending);
6987
+ refill(this.dead, dead);
6988
+ refill(this.dispatchedEventIds, receipts);
6989
+ refill(this.sourceCursors, cursors);
6990
+ this.endedSourceKeys.clear();
6991
+ for (const key of ended) this.endedSourceKeys.add(key);
6992
+ end();
6993
+ }
6994
+ };
6995
+ }
6996
+ /** A record that a dispatcher removed is no longer pending in the committed state. */
6997
+ forgetCommittedVersion(eventId) {
6998
+ if (this.committedVersions?.has(eventId)) this.committedVersions.set(eventId, void 0);
6999
+ }
7000
+ recordCommittedVersion(eventId) {
7001
+ const versions = this.committedVersions;
7002
+ if (versions === void 0 || versions.has(eventId)) return;
7003
+ const committed = this.pending.get(eventId);
7004
+ versions.set(eventId, committed === void 0 ? void 0 : { ...committed });
7005
+ }
7006
+ async endEventSources(sources) {
7007
+ for (const source of sources) {
7008
+ const sourceKey = encodeAggregateIdentity(source);
7009
+ if (this.sourceCursors.has(sourceKey)) this.endedSourceKeys.add(sourceKey);
7010
+ }
7011
+ }
7012
+ /**
7013
+ * Rejects a new event of an ended source above its head. An exact retry of
7014
+ * a stored event of an ended source stays a retry; the caller skips
7015
+ * retries at the source head before this check. The stale-head, commit
7016
+ * size, and position checks reject a candidate below the head or at a
7017
+ * position that another event owns, and their messages name the
7018
+ * re-created identity as a cause.
7019
+ */
7020
+ assertSourceNotEnded(event, source, position, sourceKey) {
7021
+ if (!this.endedSourceKeys.has(sourceKey)) return;
7022
+ if (this.pending.has(event.eventId) || this.dead.has(event.eventId) || this.dispatchedEventIds.has(event.eventId)) return;
7023
+ const cursor = this.sourceCursors.get(sourceKey);
7024
+ if (cursor !== void 0 && (position.aggregateVersion < cursor.aggregateVersion || position.aggregateVersion === cursor.aggregateVersion && cursor.eventIdsBySequence.has(position.commitSequence))) return;
7025
+ throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: the aggregate was removed, so its event source ended. The kit does not support an aggregate that is created again under a removed identity. Give the new aggregate a new id.`, event.type);
7026
+ }
7027
+ /**
7028
+ * Event ids of the batch that the source head names at their position
7029
+ * although no receipt, pending record, or dead letter remains: events
7030
+ * that were dispatched and whose receipt was evicted. They are decided
7031
+ * against the heads before the batch, so their place in the batch does
7032
+ * not matter. Their receipts are restored after the batch.
7033
+ */
7034
+ retriesAtSourceHead(events) {
7035
+ const retries = /* @__PURE__ */ new Set();
7036
+ for (const { event, source, position } of events) if (!this.pending.has(event.eventId) && !this.dead.has(event.eventId) && !this.dispatchedEventIds.has(event.eventId) && this.recordedAtSourceHead(event, source, position)) retries.add(event.eventId);
7037
+ return retries;
7038
+ }
7039
+ recordedAtSourceHead(event, source, position) {
7040
+ const cursor = this.sourceCursors.get(encodeAggregateIdentity(source));
7041
+ return cursor?.aggregateVersion === position.aggregateVersion && cursor.commitSize === position.commitSize && cursor.eventIdsBySequence.get(position.commitSequence) === event.eventId;
7042
+ }
6501
7043
  rememberDispatched(eventId, source, position) {
6502
7044
  this.dispatchedEventIds.delete(eventId);
6503
7045
  this.dispatchedEventIds.set(eventId, {
@@ -6521,6 +7063,7 @@ var InMemoryOutbox = class {
6521
7063
  record.lastError = error instanceof Error ? error.message : String(error ?? "unknown");
6522
7064
  if (record.attempts >= this.maxDeliveryAttempts) {
6523
7065
  this.pending.delete(dispatchId);
7066
+ this.forgetCommittedVersion(dispatchId);
6524
7067
  const deadLetter = {
6525
7068
  dispatchId: record.dispatchId,
6526
7069
  event: record.event,
@@ -6547,9 +7090,10 @@ function assertSameCandidateReceipt(event, received, recorded) {
6547
7090
  assertReceiptShape(event, received, recorded, false);
6548
7091
  }
6549
7092
  /**
6550
- * The lenient variant for PENDING records only: this in-memory adapter
6551
- * cannot observe rollback, so a re-harvested event may legitimately arrive
6552
- * at a new aggregateVersion. Index and commit cardinality stay immutable.
7093
+ * The lenient variant for PENDING records only: an outbox without an
7094
+ * `InMemoryTransactionScope` cannot observe a rollback, so a re-harvested
7095
+ * event may legitimately arrive at a new aggregateVersion. Index and commit
7096
+ * cardinality stay immutable.
6553
7097
  */
6554
7098
  function assertSameCandidateReceiptAllowingVersionRefresh(event, received, recorded) {
6555
7099
  assertReceiptShape(event, received, recorded, true);
@@ -6558,12 +7102,77 @@ function assertReceiptShape(event, received, recorded, allowAggregateVersionRefr
6558
7102
  if ((allowAggregateVersionRefresh || received.aggregateVersion === recorded.aggregateVersion) && received.commitSequence === recorded.commitSequence && received.commitSize === recorded.commitSize) return;
6559
7103
  throw new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}": its commit candidate changed from (${recorded.aggregateVersion}, ${recorded.commitSequence}; commitSize=${recorded.commitSize}) to (${received.aggregateVersion}, ${received.commitSequence}; commitSize=${received.commitSize}). An exact redelivery must keep its source position immutable.`, event.type);
6560
7104
  }
7105
+ /**
7106
+ * The fields of a candidate that the outbox reads, read once and checked.
7107
+ * The function copies the source and the position. The event stays the
7108
+ * caller's object, because a dispatcher gets the event that the commit
7109
+ * recorded. The outbox reads only `eventId` and `type` from it, so these
7110
+ * must be own data properties: a read of them then runs no getter.
7111
+ */
7112
+ function ownCandidate(candidate, index) {
7113
+ let subject = `the candidate at index ${index}`;
7114
+ let eventType;
7115
+ const rejection = (field, expected, observed) => new EventHarvestError(`InMemoryOutbox rejected ${subject}: ${field} must be ${expected}, but it is ${observed}. Pass the candidates that withCommit builds.`, eventType);
7116
+ if (typeof candidate !== "object" || candidate === null) throw rejection("the candidate", "an object", describeRejected(candidate));
7117
+ const { event, source, position } = candidate;
7118
+ if (typeof event !== "object" || event === null) throw rejection("event", "an object", describeRejected(event));
7119
+ const eventId = Object.getOwnPropertyDescriptor(event, "eventId");
7120
+ const type = Object.getOwnPropertyDescriptor(event, "type");
7121
+ if (typeof type?.value === "string") eventType = type.value;
7122
+ if (typeof eventId?.value === "string") subject = `event "${eventId.value}" (the candidate at index ${index})`;
7123
+ for (const [field, descriptor] of [["event.eventId", eventId], ["event.type", type]]) {
7124
+ if (descriptor === void 0) throw rejection(field, "an own data property", "missing");
7125
+ if (!("value" in descriptor)) throw rejection(field, "an own data property", "an accessor");
7126
+ if (typeof descriptor.value !== "string") throw rejection(field, "a string", describeRejected(descriptor.value));
7127
+ }
7128
+ if (typeof source !== "object" || source === null) throw rejection("source", "an object", describeRejected(source));
7129
+ const { aggregateId, aggregateType } = source;
7130
+ if (typeof aggregateType !== "string") throw rejection("source.aggregateType", "a string", describeRejected(aggregateType));
7131
+ if (typeof aggregateId !== "string") throw rejection("source.aggregateId", "a string", describeRejected(aggregateId));
7132
+ if (typeof position !== "object" || position === null) throw rejection("position", "an object", describeRejected(position));
7133
+ const { aggregateVersion, commitSequence, commitSize } = position;
7134
+ if (!Number.isSafeInteger(aggregateVersion) || aggregateVersion < 0) throw rejection("position.aggregateVersion", "an integer >= 0", describeRejected(aggregateVersion));
7135
+ if (!Number.isSafeInteger(commitSize) || commitSize < 1) throw rejection("position.commitSize", "an integer >= 1", describeRejected(commitSize));
7136
+ if (!Number.isSafeInteger(commitSequence) || commitSequence < 0 || commitSequence >= commitSize) throw rejection("position.commitSequence", `an integer from 0 to ${commitSize - 1}`, describeRejected(commitSequence));
7137
+ return {
7138
+ event,
7139
+ source: Object.freeze({
7140
+ aggregateId,
7141
+ aggregateType
7142
+ }),
7143
+ position: Object.freeze({
7144
+ aggregateVersion,
7145
+ commitSequence,
7146
+ commitSize
7147
+ })
7148
+ };
7149
+ }
7150
+ /** Describes a rejected value. It does not call `toString` of an object. */
7151
+ function describeRejected(value) {
7152
+ if (typeof value === "string") return JSON.stringify(value);
7153
+ if (value === null || value === void 0) return String(value);
7154
+ if (typeof value === "object") return "an object";
7155
+ if (typeof value === "function") return "a function";
7156
+ if (typeof value === "symbol") return "a symbol";
7157
+ return `${typeof value} ${String(value)}`;
7158
+ }
7159
+ /** Replaces the entries of a map with recorded ones, in their order. */
7160
+ function refill(map, entries) {
7161
+ map.clear();
7162
+ for (const [key, value] of entries) map.set(key, value);
7163
+ }
7164
+ function commitSizeMismatchError(event, source, position, recordedCommitSize) {
7165
+ return new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: aggregate version ${position.aggregateVersion} was already recorded with commitSize ${recordedCommitSize}, not ${position.commitSize}. An aggregate that was removed and created again under the same identity causes this too; the kit does not support that, so give the new aggregate a new id.`, event.type);
7166
+ }
7167
+ function positionOwnedError(event, source, position, positionOwner) {
7168
+ return new EventHarvestError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: source position (${position.aggregateVersion}, ${position.commitSequence}) is already owned by event "${positionOwner}". One qualified source position must identify exactly one immutable event. An aggregate that was removed and created again under the same identity causes this too; the kit does not support that, so give the new aggregate a new id.`, event.type);
7169
+ }
6561
7170
  function staleHeadError(event, source, position, staleHeadVersion) {
6562
- return new EventHarvestError(`InMemoryOutbox rejected stale event "${event.eventId}" for ${source.aggregateType} ${source.aggregateId} at aggregate version ${position.aggregateVersion}: the event-source head is already ${staleHeadVersion}. The dispatched-id receipt may have expired; use a durable outbox with a transactional eventId unique key for unbounded idempotency.`, event.type);
7171
+ return new EventHarvestError(`InMemoryOutbox rejected stale event "${event.eventId}" for ${describeAggregateIdentity(source)} at aggregate version ${position.aggregateVersion}: the event-source head is already ${staleHeadVersion}. There are two possible causes. First, the aggregate was removed and created again under the same identity. The kit does not support this, because the versions of the new aggregate restart below the head. Give the new aggregate a new id. Second, this is a retry of an event whose dispatched-id receipt expired. Use a durable outbox with a transactional eventId unique key for unbounded idempotency.`, event.type);
6563
7172
  }
6564
7173
  function assertSameEventSource(event, received, recorded) {
6565
7174
  if (received.aggregateType === recorded.aggregateType && received.aggregateId === recorded.aggregateId) return;
6566
- throw new EventHarvestError(`InMemoryOutbox rejected eventId collision for "${event.eventId}": it already belongs to ${recorded.aggregateType} ${recorded.aggregateId}, but was received for ${received.aggregateType} ${received.aggregateId}. An eventId must identify one immutable event across all aggregate sources.`, event.type);
7175
+ throw new EventHarvestError(`InMemoryOutbox rejected eventId collision for "${event.eventId}": it already belongs to ${describeAggregateIdentity(recorded)}, but was received for ${describeAggregateIdentity(received)}. An eventId must identify one immutable event across all aggregate sources.`, event.type);
6567
7176
  }
6568
7177
 
6569
7178
  //#endregion
@@ -6718,8 +7327,8 @@ var OutboxDispatcher = class extends PollLoop {
6718
7327
  this.classifyFailure = options.classifyFailure;
6719
7328
  this.deliveryTimeoutMs = options.deliveryTimeoutMs ?? 3e4;
6720
7329
  this.storageTimeoutMs = options.storageTimeoutMs ?? 3e4;
6721
- assertNonNegativeFinite("OutboxDispatcher", "deliveryTimeoutMs", this.deliveryTimeoutMs);
6722
- assertNonNegativeFinite("OutboxDispatcher", "storageTimeoutMs", this.storageTimeoutMs);
7330
+ assertTimerDelay("OutboxDispatcher", "deliveryTimeoutMs", this.deliveryTimeoutMs);
7331
+ assertTimerDelay("OutboxDispatcher", "storageTimeoutMs", this.storageTimeoutMs);
6723
7332
  }
6724
7333
  /**
6725
7334
  * One full dispatch pass (the `run`/`drainOnce` shell lives on
@@ -6813,6 +7422,19 @@ var OutboxDispatcher = class extends PollLoop {
6813
7422
 
6814
7423
  //#endregion
6815
7424
  //#region src/persistence/event-store/adapters/in-memory-event-store.ts
7425
+ /**
7426
+ * A copy of `event` that shares nothing with the caller. The walk rejects a
7427
+ * value that a copy would lose or change, for example a function, a class
7428
+ * instance, or an accessor, and what a freeze cannot make immutable (binary
7429
+ * data and a RegExp), as the event constructors do.
7430
+ * It runs no accessor unless a Proxy trap adds one. Its rejection names the
7431
+ * event, and any other error passes unchanged.
7432
+ */
7433
+ function copyForStorage(event, index, stream) {
7434
+ const subject = `InMemoryEventStore.append (stream ${describeAggregateIdentity(stream)}): events[${index}]`;
7435
+ if (event === null || typeof event !== "object") throw new TypeError(`${subject} is not an event object`);
7436
+ return detachFreezableState(event, subject);
7437
+ }
6816
7438
  function assertStreamPosition(name, value) {
6817
7439
  if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new RangeError(`InMemoryEventStore: ${name} must be a non-negative safe integer, got ${String(value)}`);
6818
7440
  }
@@ -6826,20 +7448,19 @@ function assertStreamPosition(name, value) {
6826
7448
  * `InMemoryCapacityExceededError`; histories are never silently evicted.
6827
7449
  * Implements the full port contract: expectedVersion-guarded appends
6828
7450
  * (throwing `ConcurrencyConflictError` on mismatch), atomic rejected
6829
- * appends, explicit missing/existing stream state with the actual head,
7451
+ * appends, explicit absent/existing stream state with the actual head,
6830
7452
  * append-order reads, mandatory page bounds, and `(fromVersion, toVersion]`
6831
- * slicing. Invalid limits or positions reject with `RangeError`.
7453
+ * slicing. Invalid limits or positions reject with `RangeError`. A read
7454
+ * with an aborted `signal` rejects with its `reason`.
6832
7455
  *
6833
7456
  * For production, back the port with a durable store whose append and
6834
7457
  * the aggregate transaction share atomicity (a table with a
6835
7458
  * `(aggregate_type, aggregate_id, position)` unique key inside the same
6836
7459
  * transaction, or a dedicated event store). Same caveat as
6837
- * `InMemoryOutbox`: this class
6838
- * lives in memory only and knows nothing about your `TransactionScope`
6839
- * rollbacks; events appended inside a transaction that later rolls back
6840
- * are NOT removed. The event-sourced repository contract suite's
6841
- * reference environment shows the snapshot/restore pattern for
6842
- * rollback-pure in-memory testing.
7460
+ * `InMemoryOutbox`: this class lives in memory only. On its own, it knows
7461
+ * nothing about your `TransactionScope` rollbacks, so events appended inside
7462
+ * a transaction that later rolls back stay. Register the store with an
7463
+ * `InMemoryTransactionScope` for tests that roll back or retry.
6843
7464
  */
6844
7465
  var InMemoryEventStore = class {
6845
7466
  streams = /* @__PURE__ */ new Map();
@@ -6852,14 +7473,48 @@ var InMemoryEventStore = class {
6852
7473
  this.maxStreams = options.maxStreams;
6853
7474
  this.maxEvents = options.maxEvents;
6854
7475
  }
7476
+ /**
7477
+ * Records the state that a rollback of an `InMemoryTransactionScope`
7478
+ * returns to: every stream and the event count.
7479
+ */
7480
+ beginTransaction() {
7481
+ const streams = [...this.streams].map(([key, events]) => [key, [...events]]);
7482
+ const totalEvents = this.totalEvents;
7483
+ return {
7484
+ commit: () => {},
7485
+ rollback: () => {
7486
+ this.streams.clear();
7487
+ for (const [key, events] of streams) this.streams.set(key, [...events]);
7488
+ this.totalEvents = totalEvents;
7489
+ }
7490
+ };
7491
+ }
6855
7492
  async append(stream, events, options) {
6856
- if (events.length === 0) return;
6857
- const key = encodeAggregateAddress(stream);
7493
+ const batch = Array.from(events);
7494
+ if (batch.length === 0) return;
7495
+ const identity = detachAggregateIdentity(stream);
7496
+ const expectedVersion = options.expectedVersion;
7497
+ const key = encodeAggregateIdentity(identity);
7498
+ this.existingStreamForAppend(identity, key, expectedVersion, batch.length);
7499
+ const owned = batch.map((event, index) => copyForStorage(event, index, identity));
7500
+ let storedEvents = this.existingStreamForAppend(identity, key, expectedVersion, owned.length);
7501
+ if (storedEvents === void 0) {
7502
+ storedEvents = [];
7503
+ this.streams.set(key, storedEvents);
7504
+ }
7505
+ for (const event of owned) storedEvents.push(event);
7506
+ this.totalEvents += owned.length;
7507
+ }
7508
+ /**
7509
+ * The stream that an append of `count` events at `expectedVersion`
7510
+ * writes to, or `undefined` for a new stream. Throws when the version
7511
+ * is stale or a capacity would overflow.
7512
+ */
7513
+ existingStreamForAppend(stream, key, expectedVersion, count) {
6858
7514
  const existing = this.streams.get(key);
6859
- if ((existing?.length ?? 0) !== options.expectedVersion) throw new ConcurrencyConflictError({
6860
- aggregateType: stream.aggregateType,
6861
- aggregateId: stream.aggregateId,
6862
- expectedVersion: options.expectedVersion,
7515
+ if ((existing?.length ?? 0) !== expectedVersion) throw new ConcurrencyConflictError({
7516
+ identity: stream,
7517
+ expectedVersion,
6863
7518
  reason: "stale_version",
6864
7519
  actualVersion: existing?.length ?? 0
6865
7520
  });
@@ -6870,26 +7525,21 @@ var InMemoryEventStore = class {
6870
7525
  current: this.streams.size,
6871
7526
  attempted: 1
6872
7527
  });
6873
- if (this.maxEvents !== void 0 && this.totalEvents + events.length > this.maxEvents) throw new InMemoryCapacityExceededError({
7528
+ if (this.maxEvents !== void 0 && this.totalEvents + count > this.maxEvents) throw new InMemoryCapacityExceededError({
6874
7529
  store: "InMemoryEventStore",
6875
7530
  resource: "events",
6876
7531
  limit: this.maxEvents,
6877
7532
  current: this.totalEvents,
6878
- attempted: events.length
7533
+ attempted: count
6879
7534
  });
6880
- let storedEvents = existing;
6881
- if (storedEvents === void 0) {
6882
- storedEvents = [];
6883
- this.streams.set(key, storedEvents);
6884
- }
6885
- for (const event of events) storedEvents.push(structuredClone(event));
6886
- this.totalEvents += events.length;
7535
+ return existing;
6887
7536
  }
6888
7537
  async readStream(stream, options) {
6889
7538
  if (!Number.isSafeInteger(options?.limit) || options.limit < 1) throw new RangeError(`InMemoryEventStore: limit must be a positive safe integer, got ${String(options?.limit)}`);
6890
7539
  assertStreamPosition("fromVersion", options.fromVersion);
6891
7540
  assertStreamPosition("toVersion", options.toVersion);
6892
- const events = this.streams.get(encodeAggregateAddress(stream));
7541
+ if (options.signal?.aborted) throw abortReason(options.signal, "InMemoryEventStore.readStream aborted");
7542
+ const events = this.streams.get(encodeAggregateIdentity(stream));
6893
7543
  if (events === void 0) return {
6894
7544
  exists: false,
6895
7545
  lastVersion: 0,
@@ -6906,6 +7556,191 @@ var InMemoryEventStore = class {
6906
7556
  }
6907
7557
  };
6908
7558
 
7559
+ //#endregion
7560
+ //#region src/persistence/event-store/reconstitute-from-stream-pages.ts
7561
+ /**
7562
+ * Reconstitutes an event-sourced aggregate from the pages of a stream read
7563
+ * and yields it only when the replay ends at the target version.
7564
+ *
7565
+ * This is the paged form of `reconstituteAggregateFromHistory`.
7566
+ * `createReplayTarget` builds the replay target: a fresh instance for a
7567
+ * full replay, or an instance reconstituted from a snapshot for a read that
7568
+ * starts at `snapshot.version`. `read` is the reachable branch of a kit
7569
+ * read, or the value that an adapter that pages on its own built.
7570
+ *
7571
+ * The call checks the window first. `targetVersion` must be a positive
7572
+ * safe integer, and `fromVersion` a non-negative one at or below it. A bad
7573
+ * window rejects with `RangeError` before the replay target is built.
7574
+ *
7575
+ * The call then primes the replay target with an empty history, so the
7576
+ * aggregate runs its own guard even when the read holds no page: a replay
7577
+ * target with pending decisions throws `UnreplayableAggregateError`. The
7578
+ * replay target must stand at `read.fromVersion`. A replay target at
7579
+ * another version throws {@link ReplayTargetMismatchError} before the
7580
+ * first page, even when it also rejected the empty history: a wiring
7581
+ * defect wins over a rejection that the data cannot explain.
7582
+ *
7583
+ * Each page goes through `replayHistory` on the replay target, so
7584
+ * allocation stays bounded by the size of a page; `readStreamPages` rejects
7585
+ * a page over its `limit`. An empty page, and a page that would run past
7586
+ * the target version, throw {@link InvalidEventStreamPageError} before any
7587
+ * row of them reaches the aggregate. Events carry no stream position, so
7588
+ * only the call can find pages that end short of the target version. After
7589
+ * the last page it compares the final version with `read.targetVersion` and
7590
+ * throws {@link ReplayTargetMismatchError} on a difference.
7591
+ *
7592
+ * When the aggregate rejects a stored event with a `DomainError`, the call
7593
+ * stops reading and returns {@link ReplayRejectedError} as `Err`. The
7594
+ * error names the stream and the window of the rejected page, and it holds
7595
+ * the `DomainError` as `cause`. The replay target never escapes on that
7596
+ * path. Wiring errors and a foreign row throw, as in `replayHistory`. What
7597
+ * `createReplayTarget` throws propagates.
7598
+ */
7599
+ async function reconstituteAggregateFromStreamPages(createReplayTarget, read) {
7600
+ assertNonNegativeSafeInteger("reconstituteAggregateFromStreamPages", "fromVersion", read.fromVersion);
7601
+ assertPositiveSafeInteger("reconstituteAggregateFromStreamPages", "targetVersion", read.targetVersion);
7602
+ if (read.fromVersion > read.targetVersion) throw new RangeError(`reconstituteAggregateFromStreamPages: fromVersion must not exceed targetVersion, got ${read.fromVersion} > ${read.targetVersion}`);
7603
+ const aggregate = createReplayTarget();
7604
+ const primed = aggregate.replayHistory([]);
7605
+ if (aggregate.version !== read.fromVersion) throw new ReplayTargetMismatchError({
7606
+ identity: read.stream,
7607
+ reason: "target_not_at_cursor",
7608
+ fromVersion: read.fromVersion,
7609
+ targetVersion: read.targetVersion,
7610
+ actualVersion: aggregate.version
7611
+ });
7612
+ if (primed.isErr()) return err(new ReplayRejectedError({
7613
+ identity: read.stream,
7614
+ fromVersion: aggregate.version,
7615
+ toVersion: aggregate.version,
7616
+ cause: primed.error
7617
+ }));
7618
+ for await (const page of read.pages) {
7619
+ assertPageNotEmpty(read.stream, page.length, aggregate.version, read.targetVersion);
7620
+ assertPageWithinWindow(read.stream, page.length, aggregate.version, read.targetVersion);
7621
+ const versionBeforePage = aggregate.version;
7622
+ const replayed = aggregate.replayHistory(page);
7623
+ if (replayed.isErr()) return err(new ReplayRejectedError({
7624
+ identity: read.stream,
7625
+ fromVersion: versionBeforePage,
7626
+ toVersion: versionBeforePage + page.length,
7627
+ cause: replayed.error
7628
+ }));
7629
+ }
7630
+ if (aggregate.version !== read.targetVersion) throw new ReplayTargetMismatchError({
7631
+ identity: read.stream,
7632
+ reason: "pages_short_of_target",
7633
+ fromVersion: read.fromVersion,
7634
+ targetVersion: read.targetVersion,
7635
+ actualVersion: aggregate.version
7636
+ });
7637
+ return ok(aggregate);
7638
+ }
7639
+
7640
+ //#endregion
7641
+ //#region src/persistence/repository/adapters/in-memory-transaction-scope.ts
7642
+ const ABORT_MESSAGE$1 = "InMemoryTransactionScope aborted";
7643
+ const { registry: registeredParticipants } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/in-memory-transaction-participants/v1"));
7644
+ /**
7645
+ * A {@link TransactionScope} for tests and demos that gives in-memory stores
7646
+ * a rollback. Register every store that the work writes inside the
7647
+ * transaction. When the work fails, each registered store undoes the writes
7648
+ * of the attempt, so the retry of a `RetryingTransactionScope` does not see
7649
+ * them. Each store decides what its rollback undoes, and its documentation
7650
+ * says so.
7651
+ *
7652
+ * **One transaction at a time.** A store cannot tell which of two open
7653
+ * transactions a write belongs to. The scope therefore runs its transactions
7654
+ * one after the other, in call order. A queued transaction stops its wait
7655
+ * when its abort signal fires. Work that waits inside a transaction for
7656
+ * another transaction of the same scope never finishes, because the inner
7657
+ * transaction waits for the outer one. Only the abort signal of the inner
7658
+ * transaction ends that wait. A database with only one connection behaves
7659
+ * the same way. Use a database with real isolation to test concurrent
7660
+ * transactions.
7661
+ *
7662
+ * **Code outside a transaction.** A dispatcher, a deadline processor, and a
7663
+ * `hasReached` check run outside the transactions. Outbox `getPending`,
7664
+ * deadline `due`, and checkpoint `hasReached` read only committed writes.
7665
+ * Other reads also see the writes of an open transaction. A store that
7666
+ * restores its whole state on rollback also undoes the writes that code
7667
+ * outside the transaction made meanwhile. An example is an acknowledgement
7668
+ * of an outbox dispatcher. The dispatcher then delivers the record again,
7669
+ * which the at-least-once contract allows.
7670
+ *
7671
+ * **A store belongs to one scope.** A scope works like one in-memory
7672
+ * database, so register all stores of a test with one scope. A second scope
7673
+ * with a registered store throws at construction, because the rollback of
7674
+ * one scope would undo a commit of the other.
7675
+ *
7676
+ * A store that is not registered keeps its own behavior: it does not roll
7677
+ * back.
7678
+ */
7679
+ var InMemoryTransactionScope = class {
7680
+ participants;
7681
+ lastTransaction = Promise.resolve();
7682
+ constructor(participants) {
7683
+ const listed = /* @__PURE__ */ new Map();
7684
+ for (const [index, participant] of participants.entries()) {
7685
+ if (typeof participant?.beginTransaction !== "function") throw new TypeError(`InMemoryTransactionScope: participant ${index} has no beginTransaction method.`);
7686
+ const firstIndex = listed.get(participant);
7687
+ if (firstIndex !== void 0) throw new TypeError(`InMemoryTransactionScope: participant ${index} repeats participant ${firstIndex}.`);
7688
+ if (registeredParticipants.has(participant)) throw new TypeError(`InMemoryTransactionScope: participant ${index} already belongs to an InMemoryTransactionScope. Register all in-memory stores of a test with one scope.`);
7689
+ listed.set(participant, index);
7690
+ }
7691
+ for (const participant of listed.keys()) registeredParticipants.set(participant, this);
7692
+ this.participants = [...listed.keys()];
7693
+ }
7694
+ async transactional(fn, options) {
7695
+ const previous = this.lastTransaction;
7696
+ let finish;
7697
+ this.lastTransaction = new Promise((resolve) => {
7698
+ finish = resolve;
7699
+ });
7700
+ const signal = options?.signal;
7701
+ try {
7702
+ await waitRejectingOnAbort(previous, signal, ABORT_MESSAGE$1);
7703
+ if (signal?.aborted) throw abortReason(signal, ABORT_MESSAGE$1);
7704
+ const transactions = [];
7705
+ let result;
7706
+ try {
7707
+ for (const participant of this.participants) transactions.push(participant.beginTransaction());
7708
+ result = await fn(void 0);
7709
+ } catch (error) {
7710
+ const rollback = endAll([...transactions].reverse(), (transaction) => transaction.rollback());
7711
+ throw rollback.failed ? rollback.failure : error;
7712
+ }
7713
+ const commit = endAll(transactions, (transaction) => transaction.commit());
7714
+ if (commit.failed) throw commit.failure;
7715
+ return result;
7716
+ } finally {
7717
+ previous.then(finish);
7718
+ }
7719
+ }
7720
+ };
7721
+ /**
7722
+ * Ends every transaction, also after a failed end, and returns the first
7723
+ * failure. For a rollback, a scope that rejects with a different error than
7724
+ * the failure inside the transaction tells `UnitOfWork.run` that the
7725
+ * rollback failed.
7726
+ */
7727
+ function endAll(transactions, end) {
7728
+ let failure;
7729
+ let failed = false;
7730
+ for (const transaction of transactions) try {
7731
+ end(transaction);
7732
+ } catch (error) {
7733
+ if (!failed) {
7734
+ failure = error;
7735
+ failed = true;
7736
+ }
7737
+ }
7738
+ return {
7739
+ failed,
7740
+ failure
7741
+ };
7742
+ }
7743
+
6909
7744
  //#endregion
6910
7745
  //#region src/persistence/repository/retrying-scope.ts
6911
7746
  const DEFAULT_MAX_ATTEMPTS = 3;
@@ -6940,6 +7775,10 @@ function defaultSleep(ms, signal) {
6940
7775
  * After `maxAttempts` the last error is rethrown unchanged, so a caller
6941
7776
  * can still match `ConcurrencyConflictError` and map it to HTTP 409.
6942
7777
  *
7778
+ * **Attempts.** Before each attempt, the scope calls `onAttemptStart` from
7779
+ * the `transactional` options, so the caller can attribute a failure to the
7780
+ * attempt that raised it.
7781
+ *
6943
7782
  * **Cancellation.** The `AbortSignal` from `transactional` options is
6944
7783
  * checked before each attempt and aborts the backoff wait, so an
6945
7784
  * `AbortSignal.timeout(ms)` bounds total elapsed time (there is
@@ -6960,8 +7799,8 @@ var RetryingTransactionScope = class {
6960
7799
  this.baseDelayMs = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
6961
7800
  this.maxDelayMs = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
6962
7801
  assertPositiveInteger("RetryingTransactionScope", "maxAttempts", this.maxAttempts);
6963
- assertNonNegativeFinite("RetryingTransactionScope", "baseDelayMs", this.baseDelayMs);
6964
- assertNonNegativeFinite("RetryingTransactionScope", "maxDelayMs", this.maxDelayMs);
7802
+ assertTimerDelay("RetryingTransactionScope", "baseDelayMs", this.baseDelayMs);
7803
+ assertTimerDelay("RetryingTransactionScope", "maxDelayMs", this.maxDelayMs);
6965
7804
  this.isRetryable = policy.isRetryable ?? someChainRetryable;
6966
7805
  this.sleep = policy.sleep ?? defaultSleep;
6967
7806
  this.random = neutralJitterSource(policy.random ?? Math.random);
@@ -6979,6 +7818,7 @@ var RetryingTransactionScope = class {
6979
7818
  };
6980
7819
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
6981
7820
  if (signal?.aborted) throw abortReason(signal, ABORT_MESSAGE);
7821
+ options?.onAttemptStart?.();
6982
7822
  try {
6983
7823
  return await this.inner.transactional(fn, options);
6984
7824
  } catch (error) {
@@ -7014,6 +7854,13 @@ var RetryingTransactionScope = class {
7014
7854
  * rebuildable derived data, so `maxEntries` may evict the least recently used
7015
7855
  * entry and `ttlMs` may expire it safely. A load updates LRU recency but does
7016
7856
  * not extend TTL; only another save does.
7857
+ *
7858
+ * The port keeps snapshot writes out of the write transaction, so the store
7859
+ * does not roll back on its own. If you register it with an
7860
+ * `InMemoryTransactionScope`, a rollback restores the snapshots, their LRU
7861
+ * order, and their expiry from the start of the transaction. It also undoes
7862
+ * a save or a delete that code made outside the transaction meanwhile. This
7863
+ * is safe, because a snapshot is derived data.
7017
7864
  */
7018
7865
  var InMemorySnapshotStore = class {
7019
7866
  snapshots = /* @__PURE__ */ new Map();
@@ -7027,8 +7874,22 @@ var InMemorySnapshotStore = class {
7027
7874
  this.ttlMs = options.ttlMs;
7028
7875
  this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
7029
7876
  }
7030
- async load(address) {
7031
- const key = encodeAggregateAddress(address);
7877
+ /**
7878
+ * Records the state that a rollback of an `InMemoryTransactionScope`
7879
+ * returns to: every snapshot in LRU order.
7880
+ */
7881
+ beginTransaction() {
7882
+ const snapshots = [...this.snapshots];
7883
+ return {
7884
+ commit: () => {},
7885
+ rollback: () => {
7886
+ this.snapshots.clear();
7887
+ for (const [key, stored] of snapshots) this.snapshots.set(key, stored);
7888
+ }
7889
+ };
7890
+ }
7891
+ async load(identity) {
7892
+ const key = encodeAggregateIdentity(identity);
7032
7893
  const stored = this.snapshots.get(key);
7033
7894
  if (stored === void 0) return void 0;
7034
7895
  if (stored.expiresAtMs !== void 0 && this.readClock() >= stored.expiresAtMs) {
@@ -7039,9 +7900,9 @@ var InMemorySnapshotStore = class {
7039
7900
  this.snapshots.set(key, stored);
7040
7901
  return structuredClone(stored.snapshot);
7041
7902
  }
7042
- async save(address, snapshot) {
7903
+ async save(identity, snapshot) {
7043
7904
  const ownedSnapshot = structuredClone(snapshot);
7044
- const key = encodeAggregateAddress(address);
7905
+ const key = encodeAggregateIdentity(identity);
7045
7906
  let expiresAtMs;
7046
7907
  if (this.ttlMs !== void 0) {
7047
7908
  const nowMs = this.readClock();
@@ -7058,8 +7919,8 @@ var InMemorySnapshotStore = class {
7058
7919
  expiresAtMs
7059
7920
  });
7060
7921
  }
7061
- async delete(address) {
7062
- this.snapshots.delete(encodeAggregateAddress(address));
7922
+ async delete(identity) {
7923
+ this.snapshots.delete(encodeAggregateIdentity(identity));
7063
7924
  }
7064
7925
  readClock() {
7065
7926
  const now = this.clock();
@@ -7122,7 +7983,10 @@ function reconstituteAggregateFromSnapshot(model, id, snapshot) {
7122
7983
  try {
7123
7984
  version = toVersion(snapshot.version);
7124
7985
  } catch (error) {
7125
- throw new SnapshotCorruptedError(`Snapshot of ${model.aggregateType} ${String(id)} carries the invalid version ${String(snapshot.version)}. Discard the derived snapshot and refold from the stream.`, error);
7986
+ throw new SnapshotCorruptedError(`Snapshot of ${describeAggregateIdentity({
7987
+ aggregateType: model.aggregateType,
7988
+ aggregateId: String(id)
7989
+ })} carries the invalid version ${String(snapshot.version)}. Discard the derived snapshot and refold from the stream.`, error);
7126
7990
  }
7127
7991
  let aggregate;
7128
7992
  try {
@@ -7130,19 +7994,26 @@ function reconstituteAggregateFromSnapshot(model, id, snapshot) {
7130
7994
  if (storedSchemaVersion === model.schemaVersion) state = detachState(snapshot.state);
7131
7995
  else if (model.migrate) state = detachState(model.migrate(detachState(snapshot.state), storedSchemaVersion));
7132
7996
  else throw new SnapshotSchemaMismatchError({
7133
- aggregateType: model.aggregateType,
7134
- aggregateId: String(id),
7997
+ identity: {
7998
+ aggregateType: model.aggregateType,
7999
+ aggregateId: String(id)
8000
+ },
7135
8001
  expectedSchemaVersion: model.schemaVersion,
7136
8002
  actualSchemaVersion: storedSchemaVersion
7137
8003
  });
7138
8004
  aggregate = model.reconstitute(id, state, version);
7139
8005
  } catch (error) {
7140
- if (isDomainErrorLike(error)) throw new SnapshotCorruptedError(`Snapshot of ${model.aggregateType} ${String(id)} (schema ${storedSchemaVersion}, version ${String(snapshot.version)}) was rejected during reconstitution. Discard the derived snapshot and refold from the stream.`, error);
8006
+ if (isDomainErrorLike(error)) throw new SnapshotCorruptedError(`Snapshot of ${describeAggregateIdentity({
8007
+ aggregateType: model.aggregateType,
8008
+ aggregateId: String(id)
8009
+ })} (schema ${storedSchemaVersion}, version ${String(snapshot.version)}) was rejected during reconstitution. Discard the derived snapshot and refold from the stream.`, error);
7141
8010
  throw error;
7142
8011
  }
7143
8012
  if (aggregate.version !== snapshot.version) throw new SnapshotVersionNotRestoredError({
7144
- aggregateType: model.aggregateType,
7145
- aggregateId: String(id),
8013
+ identity: {
8014
+ aggregateType: model.aggregateType,
8015
+ aggregateId: String(id)
8016
+ },
7146
8017
  snapshotVersion: snapshot.version,
7147
8018
  restoredVersion: aggregate.version
7148
8019
  });
@@ -7160,5 +8031,5 @@ function copySnapshotAt(snapshotAt) {
7160
8031
  }
7161
8032
 
7162
8033
  //#endregion
7163
- export { AggregateDeletedError, AggregateNotFoundError, AggregateTrackingError, CapabilityRegistryConflictError, CommandBus, CommitError, ConcurrencyConflictError, DeadlineProcessor, DirectStateMutationError, DomainError, DomainEventValidationError, DomainStateMachine, DomainTransitionGuardRejectedError, DuplicateAggregateError, DuplicateEventIdError, DuplicateHandlerRegistrationError, Entity, ErrorMapperFailedError, EventBusClosedError, EventBusImpl, EventHarvestError, EventSourcedAggregate, FoldReturnedNoStateError, ForeignEventError, HostileStateKeyError, IdempotencyClaimLostError, IdempotencyCompletionWithoutClaimError, IdempotencyInFlightError, IdempotencyKeyReuseError, IdempotencyReconciliationRequiredError, IdentityMap, InMemoryCapacityExceededError, InMemoryDeadlineStore, InMemoryEventStore, InMemoryIdempotencyStore, InMemoryOutbox, InMemoryProjectionCheckpointStore, InMemorySnapshotStore, InfrastructureError, InvalidCommandMessageError, InvalidDomainMachineContextError, InvalidDomainMachineDefinitionError, InvalidDomainMachineInputError, InvalidDomainMachineSnapshotError, InvalidDomainTransitionError, InvalidDomainTransitionGuardResultError, InvalidDomainTransitionResultError, InvalidFlushStatementError, InvalidIntegrationMessageError, InvalidRepositoryAdapterError, InvalidRepositoryDefinitionError, InvalidVersionError, MisaddressedEventError, MissingEntityIdError, MissingFoldError, MissingHandlerError, NestedUnitOfWorkError, NonProgressingEventStreamPageError, OutboxDispatcher, PendingEventBatchMismatchError, PendingEventLimitExceededError, ProjectionGapError, ProjectionIdentityViolationError, ProjectionOrderViolationError, ProjectionReceiptViolationError, Projector, PublishDepthExceededError, QueryBus, ReentrantDomainStateMachineEvaluationError, ReentrantEventRecordingError, ReplayHeadMismatchError, RepositoryErrorMappingFailedError, RetryingTransactionScope, RollbackError, SnapshotCorruptedError, SnapshotSchemaMismatchError, SnapshotTimeValidationError, SnapshotVersionNotRestoredError, Specification, StateStoredAggregate, TransactionClosedError, UnenrolledChangesError, UnitOfWork, UnmanagedInstanceError, UnmintedEventError, UnprojectableEventError, UnregisteredHandlerError, UnreplayableAggregateError, ValueObject, analyzeDomainMachineDefinition, canTransitionDomainState, captureAggregateSnapshot, capturePersistenceBaseline, copyMetadata, createDomainEvent, createDomainEventFactory, createDomainEventFromFacts, createInitialDomainMachineSnapshot, createIntegrationMessage, createUncommittedDomainEvent, decodeIntegrationMessage, deepEqual, deepEqualExcept, deepFreeze, deepOmit, defaultDomainEventFactory, defineRepository, defineSnapshotModel, derivePersistenceChanges, detachState, domainErrorToResult, encodeIntegrationMessage, entityIds, eventBusSink, findEntityById, freezeShallow, hasEntityId, ignoreProjectionEvent, insertPersistenceBaseline, integrationMessageToCommittedEvent, isDomainErrorLike, isInfrastructureErrorLike, isPositionAfter, isWiringErrorLike, mergeMetadata, outboxWriterAcceptingEventLoss, persistenceProjectionDrifted, prepareDomainMachineDefinition, projectionFromHandlers, recapturePersistenceBaseline, reconstituteAggregateFromHistory, reconstituteAggregateFromSnapshot, recordDomainEvent, recordPendingEvents, removeEntityById, replaceEntityById, routeEventsToCommandOutbox, sameEntity, sameVersion, specification, toVersion, transitionDomainState, updateEntityById, versionedFlush, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withCommit, withIdempotentCommit };
8034
+ export { AggregateDeletedError, AggregateNotFoundError, AggregateTrackingError, CapabilityRegistryConflictError, CommandBus, CommitError, ConcurrencyConflictError, DeadlineProcessor, DirectStateMutationError, DomainError, DomainEventValidationError, DomainStateMachine, DomainTransitionGuardRejectedError, DuplicateAggregateError, DuplicateEventIdError, DuplicateHandlerRegistrationError, Entity, ErrorMapperFailedError, EventBusClosedError, EventBusImpl, EventHarvestError, EventSourcedAggregate, FoldReturnedNoStateError, ForeignEventError, HostileStateKeyError, IdempotencyClaimLostError, IdempotencyCompletionWithoutClaimError, IdempotencyInFlightError, IdempotencyKeyReuseError, IdempotencyReconciliationRequiredError, IdentityMap, InMemoryCapacityExceededError, InMemoryDeadlineStore, InMemoryEventStore, InMemoryIdempotencyStore, InMemoryOutbox, InMemoryProjectionCheckpointStore, InMemorySnapshotStore, InMemoryTransactionScope, InfrastructureError, InvalidCommandMessageError, InvalidDomainMachineContextError, InvalidDomainMachineDefinitionError, InvalidDomainMachineInputError, InvalidDomainMachineSnapshotError, InvalidDomainTransitionError, InvalidDomainTransitionGuardResultError, InvalidDomainTransitionResultError, InvalidEventStreamPageError, InvalidFlushStatementError, InvalidIntegrationMessageError, InvalidRepositoryAdapterError, InvalidRepositoryDefinitionError, InvalidVersionError, MisattributedEventError, MissingEntityIdError, MissingFoldError, MissingHandlerError, NestedUnitOfWorkError, OutboxDispatcher, PendingEventBatchMismatchError, PendingEventLimitExceededError, ProjectionGapError, ProjectionIdentityViolationError, ProjectionOrderViolationError, ProjectionReceiptViolationError, Projector, PublishDepthExceededError, QueryBus, ReentrantDomainStateMachineEvaluationError, ReentrantEventRecordingError, ReplayRejectedError, ReplayTargetMismatchError, RepositoryErrorMappingFailedError, RetryingTransactionScope, RollbackError, SnapshotCorruptedError, SnapshotSchemaMismatchError, SnapshotTimeValidationError, SnapshotVersionNotRestoredError, Specification, StateStoredAggregate, TransactionClosedError, UnenrolledChangesError, UnitOfWork, UnmanagedInstanceError, UnmintedEventError, UnprojectableEventError, UnregisteredHandlerError, UnreplayableAggregateError, ValueObject, analyzeDomainMachineDefinition, canTransitionDomainState, captureAggregateSnapshot, capturePersistenceBaseline, classifyConcurrencyConflict, copyMetadata, createDomainEvent, createDomainEventFactory, createDomainEventFromFacts, createInitialDomainMachineSnapshot, createIntegrationMessage, createUncommittedDomainEvent, decodeIntegrationMessage, deepEqual, deepEqualExcept, deepFreeze, deepOmit, defaultDomainEventFactory, defineRepository, defineSnapshotModel, derivePersistenceChanges, detachState, domainErrorToResult, encodeIntegrationMessage, entityIds, eventBusSink, findEntityById, freezeShallow, hasEntityId, ignoreProjectionEvent, insertPersistenceBaseline, integrationMessageToCommittedEvent, isDomainErrorLike, isInfrastructureErrorLike, isPositionAfter, isWiringErrorLike, mergeMetadata, outboxWriterAcceptingEventLoss, persistenceProjectionDrifted, pinTargetVersion, prepareDomainMachineDefinition, projectionFromHandlers, readStreamPages, recapturePersistenceBaseline, reconstituteAggregateFromHistory, reconstituteAggregateFromSnapshot, reconstituteAggregateFromStreamPages, recordDomainEvent, recordPendingEvents, removeEntityById, replaceEntityById, routeEventsToCommandOutbox, sameEntity, sameVersion, specification, toVersion, transitionDomainState, updateEntityById, versionedFlush, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withCommit, withIdempotentCommit };
7164
8035
  //# sourceMappingURL=index.js.map