@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/README.md +3 -1
- package/dist/chunks/deep-equal-except.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +131 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -153,7 +153,7 @@ var Entity = class {
|
|
|
153
153
|
* today's decision validator.
|
|
154
154
|
*/
|
|
155
155
|
_state;
|
|
156
|
-
|
|
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.
|
|
175
|
+
this._stateFreezeMode = config?.deepFreezeState ?? false ? "deep" : "shallow";
|
|
176
176
|
this.validateState = config?.validateState ?? noStateValidation;
|
|
177
|
-
this._state = freezeStateByMode(shallowCopyOwned(initialState), this.
|
|
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.
|
|
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,
|
|
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`,
|
|
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
|
|
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
|
|
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) =>
|
|
1792
|
-
attempt.claim = void 0;
|
|
1793
|
-
attempt.heartbeat = void 0;
|
|
1791
|
+
const scope = { transactional: async (work, options) => {
|
|
1794
1792
|
try {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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
|
|
1803
|
-
|
|
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(
|
|
1835
|
+
await store.abandon(staged);
|
|
1816
1836
|
} catch (abandonError) {
|
|
1817
1837
|
reportToObserver(() => deps.onIdempotencyError?.(abandonError, {
|
|
1818
1838
|
operation: "abandon",
|
|
1819
|
-
key:
|
|
1820
|
-
token:
|
|
1839
|
+
key: staged.key,
|
|
1840
|
+
token: staged.token
|
|
1821
1841
|
}));
|
|
1822
1842
|
}
|
|
1823
1843
|
}
|
|
1824
1844
|
throw error;
|
|
1825
1845
|
}
|
|
1826
|
-
}
|
|
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`,
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
4486
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
5937
|
-
|
|
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();
|