@shirudo/ddd-kit 3.0.0-rc.3 → 3.0.0-rc.4

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
@@ -153,7 +153,7 @@ var Entity = class {
153
153
  * today's decision validator.
154
154
  */
155
155
  _state;
156
- _deepFreezeState;
156
+ _stateFreezeMode;
157
157
  validateState;
158
158
  /**
159
159
  * **State ownership.** Plain-object and array states are shallow-copied
@@ -172,9 +172,9 @@ var Entity = class {
172
172
  constructor(id, initialState, config) {
173
173
  if (id === null || id === void 0) throw new Error("Entity ID cannot be null or undefined");
174
174
  this.id = id;
175
- this._deepFreezeState = config?.deepFreezeState ?? false;
175
+ this._stateFreezeMode = config?.deepFreezeState ?? false ? "deep" : "shallow";
176
176
  this.validateState = config?.validateState ?? noStateValidation;
177
- this._state = freezeStateByMode(shallowCopyOwned(initialState), this._deepFreezeState);
177
+ this._state = freezeStateByMode(shallowCopyOwned(initialState), this._stateFreezeMode);
178
178
  this.validateState(this._state);
179
179
  }
180
180
  /**
@@ -187,7 +187,7 @@ var Entity = class {
187
187
  * path. Ordinary domain behavior should use {@link setState} instead.
188
188
  */
189
189
  freezeState(value) {
190
- return freezeStateByMode(value, this._deepFreezeState);
190
+ return freezeStateByMode(value, this._stateFreezeMode);
191
191
  }
192
192
  /**
193
193
  * Sets the state of the entity.
@@ -210,8 +210,8 @@ var Entity = class {
210
210
  }
211
211
  };
212
212
  const noStateValidation = () => {};
213
- function freezeStateByMode(value, deep) {
214
- return deep ? deepFreeze(value) : freezeShallow(value);
213
+ function freezeStateByMode(value, mode) {
214
+ return mode === "deep" ? deepFreeze(value) : freezeShallow(value);
215
215
  }
216
216
  /**
217
217
  * Shallow-freezes `value` when it's a non-null object or array, so that
@@ -1025,7 +1025,7 @@ function mapHandlerFailure(error, mapExpectedError, busKind) {
1025
1025
  * Supports an optional type map (`TMap`) for automatic return type inference.
1026
1026
  * When `TMap` is concrete, `execute()` infers the result type from the command type.
1027
1027
  * An explicit competing result generic cannot override that map.
1028
- * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
1028
+ * Without `TMap`, the return type defaults to `unknown` or is specified per call.
1029
1029
  *
1030
1030
  * **Note:** This is a basic implementation suitable for development and simple use cases.
1031
1031
  * For production environments, consider implementing or using a more feature-rich bus that includes:
@@ -1043,13 +1043,13 @@ function mapHandlerFailure(error, mapExpectedError, busKind) {
1043
1043
  *
1044
1044
  * @example
1045
1045
  * ```typescript
1046
- * // With type map full inference
1046
+ * // With a type map: full inference
1047
1047
  * type Commands = { CreateOrder: OrderId; CancelOrder: void };
1048
1048
  * const bus = new CommandBus<Commands>();
1049
1049
  * const result = await bus.execute({ type: "CreateOrder", ... });
1050
1050
  * // result: Result<OrderId, string>
1051
1051
  *
1052
- * // Without type map same as before
1052
+ * // Without a type map: specify the return type per call
1053
1053
  * const bus = new CommandBus();
1054
1054
  * bus.register("CreateOrder", async (cmd) => ok(orderId));
1055
1055
  * const result = await bus.execute({ type: "CreateOrder", ... });
@@ -1788,42 +1788,62 @@ async function withIdempotentCommit(deps, request, fn) {
1788
1788
  claim: void 0,
1789
1789
  heartbeat: void 0
1790
1790
  };
1791
- const scope = { transactional: (work, options) => deps.scope.transactional(async (ctx) => {
1792
- attempt.claim = void 0;
1793
- attempt.heartbeat = void 0;
1791
+ const scope = { transactional: async (work, options) => {
1794
1792
  try {
1795
- const result = await work(ctx);
1796
- const currentHeartbeat = attempt.heartbeat;
1797
- await currentHeartbeat?.stop();
1798
- const heartbeatFailure = currentHeartbeat?.failure();
1799
- if (heartbeatFailure !== void 0) throw heartbeatFailure;
1800
- return result;
1793
+ return await deps.scope.transactional(async (ctx) => {
1794
+ attempt.claim = void 0;
1795
+ attempt.heartbeat = void 0;
1796
+ try {
1797
+ const result = await work(ctx);
1798
+ const currentHeartbeat = attempt.heartbeat;
1799
+ await currentHeartbeat?.stop();
1800
+ const heartbeatFailure = currentHeartbeat?.failure();
1801
+ if (heartbeatFailure !== void 0) throw heartbeatFailure;
1802
+ return result;
1803
+ } catch (error) {
1804
+ const currentHeartbeat = attempt.heartbeat;
1805
+ await currentHeartbeat?.stop();
1806
+ const heartbeatFailure = currentHeartbeat?.failure();
1807
+ const currentClaim = attempt.claim;
1808
+ if (heartbeatFailure !== void 0 && heartbeatFailure !== error && currentClaim) reportToObserver(() => deps.onIdempotencyError?.(heartbeatFailure, {
1809
+ operation: "renew",
1810
+ key: currentClaim.key,
1811
+ token: currentClaim.token
1812
+ }));
1813
+ const abandoned = attempt.claim;
1814
+ if (abandoned) {
1815
+ attempt.claim = void 0;
1816
+ try {
1817
+ await store.abandon(abandoned);
1818
+ } catch (abandonError) {
1819
+ reportToObserver(() => deps.onIdempotencyError?.(abandonError, {
1820
+ operation: "abandon",
1821
+ key: abandoned.key,
1822
+ token: abandoned.token
1823
+ }));
1824
+ }
1825
+ }
1826
+ throw error;
1827
+ }
1828
+ }, options);
1801
1829
  } catch (error) {
1802
- const currentHeartbeat = attempt.heartbeat;
1803
- await currentHeartbeat?.stop();
1804
- const heartbeatFailure = currentHeartbeat?.failure();
1805
- const currentClaim = attempt.claim;
1806
- if (heartbeatFailure !== void 0 && heartbeatFailure !== error && currentClaim) reportToObserver(() => deps.onIdempotencyError?.(heartbeatFailure, {
1807
- operation: "renew",
1808
- key: currentClaim.key,
1809
- token: currentClaim.token
1810
- }));
1811
- const abandoned = attempt.claim;
1812
- if (abandoned) {
1830
+ const staged = attempt.claim;
1831
+ if (staged) {
1813
1832
  attempt.claim = void 0;
1833
+ attempt.heartbeat = void 0;
1814
1834
  try {
1815
- await store.abandon(abandoned);
1835
+ await store.abandon(staged);
1816
1836
  } catch (abandonError) {
1817
1837
  reportToObserver(() => deps.onIdempotencyError?.(abandonError, {
1818
1838
  operation: "abandon",
1819
- key: abandoned.key,
1820
- token: abandoned.token
1839
+ key: staged.key,
1840
+ token: staged.token
1821
1841
  }));
1822
1842
  }
1823
1843
  }
1824
1844
  throw error;
1825
1845
  }
1826
- }, options) };
1846
+ } };
1827
1847
  const outcome = await withCommit({
1828
1848
  ...deps,
1829
1849
  scope
@@ -2073,7 +2093,7 @@ var InMemoryIdempotencyStore = class {
2073
2093
  * Supports an optional type map (`TMap`) for automatic return type inference.
2074
2094
  * When `TMap` is concrete, `execute()` and `executeUnsafe()` infer the result type from the query type.
2075
2095
  * Explicit competing result generics cannot override that map.
2076
- * Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
2096
+ * Without `TMap`, the return type defaults to `unknown` or is specified per call.
2077
2097
  *
2078
2098
  * **Note:** This is a basic implementation suitable for development and simple use cases.
2079
2099
  * For production environments, consider implementing or using a more feature-rich bus that includes:
@@ -2091,13 +2111,13 @@ var InMemoryIdempotencyStore = class {
2091
2111
  *
2092
2112
  * @example
2093
2113
  * ```typescript
2094
- * // With type map full inference
2114
+ * // With a type map: full inference
2095
2115
  * type Queries = { GetOrder: Order | null; ListOrders: Order[] };
2096
2116
  * const bus = new QueryBus<Queries>();
2097
2117
  * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
2098
2118
  * // result: Result<Order | null, string>
2099
2119
  *
2100
- * // Without type map same as before
2120
+ * // Without a type map: specify the return type per call
2101
2121
  * const bus = new QueryBus();
2102
2122
  * bus.register("GetOrder", async (query) => repository.findById(query.orderId));
2103
2123
  * const result = await bus.execute({ type: "GetOrder", orderId: "123" });
@@ -2695,7 +2715,8 @@ var UnitOfWork = class {
2695
2715
  throw classifyRunError(error, {
2696
2716
  workThrew,
2697
2717
  workCompleted,
2698
- workError
2718
+ workError,
2719
+ signal: options?.signal
2699
2720
  });
2700
2721
  } finally {
2701
2722
  session?.close();
@@ -3152,6 +3173,7 @@ function makeContext(repositories, session, signal) {
3152
3173
  * scope failed to even open a transaction); pass the error through.
3153
3174
  */
3154
3175
  function classifyRunError(error, state) {
3176
+ if (state.signal?.aborted && state.signal.reason !== void 0 && (error === state.signal.reason || causeChainContains(error, state.signal.reason))) return error;
3155
3177
  if (state.workThrew) {
3156
3178
  if (error === state.workError || causeChainContains(error, state.workError)) return error;
3157
3179
  return new RollbackError(state.workError, error);
@@ -3545,18 +3567,23 @@ var DeadlineProcessor = class extends PollLoop {
3545
3567
  const delivered = [];
3546
3568
  for (const deadline of batch) {
3547
3569
  if (signal?.aborted) break;
3570
+ let boundedContext;
3548
3571
  try {
3549
3572
  await runBoundedExecution("DeadlineProcessor.handler", {
3550
3573
  signal,
3551
3574
  timeoutMs: this.deliveryTimeoutMs
3552
- }, (context) => this.handler(deadline, context));
3575
+ }, (context) => {
3576
+ boundedContext = context;
3577
+ return this.handler(deadline, context);
3578
+ });
3553
3579
  delivered.push(deadline);
3554
3580
  } catch (error) {
3555
3581
  if (signal?.aborted) break;
3556
3582
  handlerFailed = true;
3557
3583
  const assessment = assessDeliveryFailure(error, this.classifyFailure);
3558
3584
  reportToObserver(() => this.observers.onDeliveryError(error, deadline, assessment));
3559
- if (assessment.kind !== "transient") try {
3585
+ const ownBudgetExpired = !signal?.aborted && boundedContext?.signal.aborted === true;
3586
+ if (assessment.kind !== "transient" || ownBudgetExpired) try {
3560
3587
  const deadLetter = await runBoundedExecution("DeadlineProcessor.markFailed", {
3561
3588
  signal,
3562
3589
  timeoutMs: this.storageTimeoutMs
@@ -4471,24 +4498,42 @@ var EventBusImpl = class {
4471
4498
  * `AggregateError` for multiple failures).
4472
4499
  */
4473
4500
  async publish(events, options = {}) {
4474
- return runBoundedExecution("EventBus.publish", {
4475
- signal: options.signal,
4476
- timeoutMs: options.timeoutMs ?? 3e4
4477
- }, (context) => this.publishWithinContext(events, context));
4478
- }
4479
- async publishWithinContext(events, context) {
4480
4501
  const errors = [];
4502
+ try {
4503
+ await runBoundedExecution("EventBus.publish", {
4504
+ signal: options.signal,
4505
+ timeoutMs: options.timeoutMs ?? 3e4
4506
+ }, (context) => this.publishWithinContext(events, context, errors));
4507
+ } catch (boundedError) {
4508
+ if (errors.length === 0) throw boundedError;
4509
+ throw new AggregateError([boundedError instanceof Error ? boundedError : new Error(String(boundedError), { cause: boundedError }), ...errors], "EventBus.publish aborted after handler failures");
4510
+ }
4511
+ if (errors.length === 1) throw errors[0];
4512
+ if (errors.length > 1) throw new AggregateError(errors, "Multiple event handlers failed");
4513
+ }
4514
+ async publishWithinContext(events, context, errors) {
4481
4515
  for (const event of events) {
4482
4516
  if (context.signal.aborted) throw abortReason(context.signal, "EventBus.publish aborted");
4483
4517
  const batch = [...this.handlers.get(event.type) ?? [], ...this.catchAllHandlers];
4484
4518
  if (batch.length > 0) {
4485
- const results = await Promise.allSettled(batch.map(async (handler) => handler(event, context)));
4486
- for (const result of results) if (result.status === "rejected") errors.push(result.reason instanceof Error ? result.reason : new Error(String(result.reason), { cause: result.reason }));
4519
+ const batchStart = errors.length;
4520
+ const failedIndices = [];
4521
+ await Promise.allSettled(batch.map(async (handler, index) => {
4522
+ try {
4523
+ await handler(event, context);
4524
+ } catch (reason) {
4525
+ failedIndices.push(index);
4526
+ errors.push(reason instanceof Error ? reason : new Error(String(reason), { cause: reason }));
4527
+ }
4528
+ }));
4529
+ const settled = errors.splice(batchStart);
4530
+ errors.push(...failedIndices.map((index, i) => ({
4531
+ index,
4532
+ error: settled[i]
4533
+ })).sort((a, b) => a.index - b.index).map((entry) => entry.error));
4487
4534
  }
4488
4535
  if (context.signal.aborted) throw abortReason(context.signal, "EventBus.publish aborted");
4489
4536
  }
4490
- if (errors.length === 1) throw errors[0];
4491
- if (errors.length > 1) throw new AggregateError(errors, "Multiple event handlers failed");
4492
4537
  }
4493
4538
  };
4494
4539
 
@@ -4772,7 +4817,7 @@ var InMemoryOutbox = class {
4772
4817
  const dispatchedReceipt = this.dispatchedEventIds.get(event.eventId);
4773
4818
  if (dispatchedReceipt !== void 0) {
4774
4819
  assertSameEventSource(event, source, dispatchedReceipt.source);
4775
- assertSameCandidateReceipt(event, position, dispatchedReceipt.position, false);
4820
+ assertSameCandidateReceipt(event, position, dispatchedReceipt.position);
4776
4821
  this.rememberDispatched(event.eventId, dispatchedReceipt.source, dispatchedReceipt.position);
4777
4822
  continue;
4778
4823
  }
@@ -4780,11 +4825,11 @@ var InMemoryOutbox = class {
4780
4825
  const deadLetter = this.dead.get(event.eventId);
4781
4826
  if (existing !== void 0) {
4782
4827
  assertSameEventSource(event, source, existing.source);
4783
- assertSameCandidateReceipt(event, position, existing.position, true);
4828
+ assertSameCandidateReceiptAllowingVersionRefresh(event, position, existing.position);
4784
4829
  }
4785
4830
  if (deadLetter) {
4786
4831
  assertSameEventSource(event, source, deadLetter.source);
4787
- assertSameCandidateReceipt(event, position, deadLetter.position, false);
4832
+ assertSameCandidateReceipt(event, position, deadLetter.position);
4788
4833
  this.dead.delete(event.eventId);
4789
4834
  this.pending.set(event.eventId, {
4790
4835
  dispatchId: deadLetter.dispatchId,
@@ -4801,7 +4846,7 @@ var InMemoryOutbox = class {
4801
4846
  let staleHeadVersion;
4802
4847
  if (existing !== void 0 && position.aggregateVersion < existing.position.aggregateVersion) staleHeadVersion = existing.position.aggregateVersion;
4803
4848
  else if (existing === void 0 && sourceCursor !== void 0 && position.aggregateVersion < sourceCursor.aggregateVersion) staleHeadVersion = sourceCursor.aggregateVersion;
4804
- if (staleHeadVersion !== void 0) throw 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);
4849
+ if (staleHeadVersion !== void 0) throw staleHeadError(event, source, position, staleHeadVersion);
4805
4850
  if (sourceCursor?.aggregateVersion === position.aggregateVersion) {
4806
4851
  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);
4807
4852
  const positionOwner = sourceCursor.eventIdsBySequence.get(position.commitSequence);
@@ -4880,7 +4925,7 @@ var InMemoryOutbox = class {
4880
4925
  const batchReceipt = receiptsInBatch.get(event.eventId);
4881
4926
  if (batchReceipt !== void 0) {
4882
4927
  assertSameEventSource(event, source, batchReceipt.source);
4883
- assertSameCandidateReceipt(event, position, batchReceipt.position, false);
4928
+ assertSameCandidateReceipt(event, position, batchReceipt.position);
4884
4929
  } else receiptsInBatch.set(event.eventId, {
4885
4930
  source,
4886
4931
  position
@@ -4888,18 +4933,18 @@ var InMemoryOutbox = class {
4888
4933
  const dispatchedReceipt = this.dispatchedEventIds.get(event.eventId);
4889
4934
  if (dispatchedReceipt !== void 0) {
4890
4935
  assertSameEventSource(event, source, dispatchedReceipt.source);
4891
- assertSameCandidateReceipt(event, position, dispatchedReceipt.position, false);
4936
+ assertSameCandidateReceipt(event, position, dispatchedReceipt.position);
4892
4937
  continue;
4893
4938
  }
4894
4939
  const existing = this.pending.get(event.eventId);
4895
4940
  if (existing !== void 0) {
4896
4941
  assertSameEventSource(event, source, existing.source);
4897
- assertSameCandidateReceipt(event, position, existing.position, true);
4942
+ assertSameCandidateReceiptAllowingVersionRefresh(event, position, existing.position);
4898
4943
  }
4899
4944
  const deadLetter = this.dead.get(event.eventId);
4900
4945
  if (deadLetter !== void 0) {
4901
4946
  assertSameEventSource(event, source, deadLetter.source);
4902
- assertSameCandidateReceipt(event, position, deadLetter.position, false);
4947
+ assertSameCandidateReceipt(event, position, deadLetter.position);
4903
4948
  }
4904
4949
  }
4905
4950
  }
@@ -4917,7 +4962,12 @@ var InMemoryOutbox = class {
4917
4962
  });
4918
4963
  continue;
4919
4964
  }
4920
- if (position.aggregateVersion < cursor.aggregateVersion) continue;
4965
+ if (position.aggregateVersion < cursor.aggregateVersion) {
4966
+ const dedupes = this.dispatchedEventIds.has(event.eventId) || this.dead.has(event.eventId);
4967
+ const pendingRecord = this.pending.get(event.eventId);
4968
+ if (pendingRecord !== void 0 && position.aggregateVersion < pendingRecord.position.aggregateVersion || pendingRecord === void 0 && !dedupes) throw staleHeadError(event, source, position, pendingRecord?.position.aggregateVersion ?? cursor.aggregateVersion);
4969
+ continue;
4970
+ }
4921
4971
  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);
4922
4972
  const positionOwner = cursor.eventIdsBySequence.get(position.commitSequence);
4923
4973
  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);
@@ -4992,10 +5042,24 @@ function cursorWithEvent(cursor, commitSequence, eventId) {
4992
5042
  eventIdsBySequence: new Map(cursor.eventIdsBySequence).set(commitSequence, eventId)
4993
5043
  };
4994
5044
  }
4995
- function assertSameCandidateReceipt(event, received, recorded, allowAggregateVersionRefresh) {
5045
+ function assertSameCandidateReceipt(event, received, recorded) {
5046
+ assertReceiptShape(event, received, recorded, false);
5047
+ }
5048
+ /**
5049
+ * The lenient variant for PENDING records only: this in-memory adapter
5050
+ * cannot observe rollback, so a re-harvested event may legitimately arrive
5051
+ * at a new aggregateVersion. Index and commit cardinality stay immutable.
5052
+ */
5053
+ function assertSameCandidateReceiptAllowingVersionRefresh(event, received, recorded) {
5054
+ assertReceiptShape(event, received, recorded, true);
5055
+ }
5056
+ function assertReceiptShape(event, received, recorded, allowAggregateVersionRefresh) {
4996
5057
  if ((allowAggregateVersionRefresh || received.aggregateVersion === recorded.aggregateVersion) && received.commitSequence === recorded.commitSequence && received.commitSize === recorded.commitSize) return;
4997
5058
  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);
4998
5059
  }
5060
+ function staleHeadError(event, source, position, staleHeadVersion) {
5061
+ 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);
5062
+ }
4999
5063
  function assertSameEventSource(event, received, recorded) {
5000
5064
  if (received.aggregateType === recorded.aggregateType && received.aggregateId === recorded.aggregateId) return;
5001
5065
  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);
@@ -5740,7 +5804,7 @@ var InMemoryEventStore = class {
5740
5804
  storedEvents = [];
5741
5805
  this.streams.set(key, storedEvents);
5742
5806
  }
5743
- for (const event of events) storedEvents.push(event);
5807
+ for (const event of events) storedEvents.push(structuredClone(event));
5744
5808
  this.totalEvents += events.length;
5745
5809
  }
5746
5810
  async readStream(stream, options) {
@@ -5759,7 +5823,7 @@ var InMemoryEventStore = class {
5759
5823
  return {
5760
5824
  exists: true,
5761
5825
  lastVersion: events.length,
5762
- events: events.slice(fromVersion, pageEnd)
5826
+ events: structuredClone(events.slice(fromVersion, pageEnd))
5763
5827
  };
5764
5828
  }
5765
5829
  };
@@ -5893,7 +5957,7 @@ var RetryingTransactionScope = class {
5893
5957
  assertNonNegativeFinite("RetryingTransactionScope", "maxDelayMs", this.maxDelayMs);
5894
5958
  this.isRetryable = policy.isRetryable ?? someChainRetryable;
5895
5959
  this.sleep = policy.sleep ?? defaultSleep;
5896
- this.random = policy.random ?? Math.random;
5960
+ this.random = neutralJitterSource(policy.random ?? Math.random);
5897
5961
  this.onRetry = policy.onRetry;
5898
5962
  }
5899
5963
  async transactional(fn, options) {
@@ -5933,8 +5997,9 @@ var RetryingTransactionScope = class {
5933
5997
  //#region src/repo/snapshot-model.ts
5934
5998
  /** Type-inference helper for declaring an adapter-owned snapshot model. */
5935
5999
  function defineSnapshotModel(model) {
5936
- assertSnapshotModel(model);
5937
- return Object.freeze({ ...model });
6000
+ const detached = Object.freeze({ ...model });
6001
+ assertSnapshotModel(detached);
6002
+ return detached;
5938
6003
  }
5939
6004
  /**
5940
6005
  * Captures a detached persistence envelope at an application-supplied time.
@@ -5989,6 +6054,8 @@ function reconstituteAggregateFromSnapshot(model, id, snapshot) {
5989
6054
  function assertSnapshotModel(model) {
5990
6055
  if (typeof model.aggregateType !== "string" || model.aggregateType.trim().length === 0) throw new TypeError("SnapshotModel.aggregateType must be a non-empty string");
5991
6056
  assertPositiveSafeInteger("SnapshotModel", "schemaVersion", model.schemaVersion);
6057
+ for (const key of ["capture", "reconstitute"]) if (typeof model[key] !== "function") throw new TypeError(`SnapshotModel.${key} is missing or not a function. defineSnapshotModel copies own enumerable properties only; prototype methods are not carried. Pass a plain object literal.`);
6058
+ if (model.migrate !== void 0 && typeof model.migrate !== "function") throw new TypeError("SnapshotModel.migrate must be a function when set");
5992
6059
  }
5993
6060
  function copySnapshotAt(snapshotAt) {
5994
6061
  if (!(snapshotAt instanceof Date) || !Number.isFinite(snapshotAt.getTime())) throw new SnapshotTimeValidationError();