@shirudo/ddd-kit 1.2.0 → 1.3.0
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 +2 -2
- package/dist/{aggregate-DclYgG_D.d.ts → aggregate-BGdgvqKh.d.ts} +61 -7
- package/dist/index.d.ts +179 -36
- package/dist/index.js +240 -26
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +24 -23
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ok, err } from '@shirudo/result';
|
|
2
|
-
import { BaseError, ValidationError } from '@shirudo/base-error';
|
|
2
|
+
import { BaseError, someChainRetryable, ValidationError } from '@shirudo/base-error';
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -944,7 +944,7 @@ var BaseAggregate = class extends Entity {
|
|
|
944
944
|
* `changedKeys`/`hasChanges`. An override that skips `super` leaves
|
|
945
945
|
* that baseline uncaptured: `changedKeys` permanently reports ALL
|
|
946
946
|
* keys and `hasChanges` never returns `false`, so a partial-write
|
|
947
|
-
* repository silently degrades to full writes on every save
|
|
947
|
+
* repository silently degrades to full writes on every save, on top
|
|
948
948
|
* of the broken version sync.
|
|
949
949
|
*
|
|
950
950
|
* @param version - The version the row currently holds in the DB
|
|
@@ -1237,7 +1237,7 @@ var AggregateRoot = class extends BaseAggregate {
|
|
|
1237
1237
|
* If you override this, call `super.markRestored(version)` FIRST:
|
|
1238
1238
|
* skipping it leaves the baseline uncaptured, so `changedKeys`
|
|
1239
1239
|
* permanently reports ALL keys and `hasChanges` never returns `false`
|
|
1240
|
-
*
|
|
1240
|
+
* (partial-write repositories silently degrade to full writes), on
|
|
1241
1241
|
* top of breaking version sync.
|
|
1242
1242
|
*/
|
|
1243
1243
|
markRestored(version) {
|
|
@@ -1260,7 +1260,7 @@ var AggregateRoot = class extends BaseAggregate {
|
|
|
1260
1260
|
* **How it works.** `setState()` replaces state immutably and the
|
|
1261
1261
|
* state object is shallow-frozen, so unchanged top-level sub-objects
|
|
1262
1262
|
* keep reference identity across mutations. The diff is therefore a
|
|
1263
|
-
* shallow per-key `!==` against the baseline reference
|
|
1263
|
+
* shallow per-key `!==` against the baseline reference: O(top-level
|
|
1264
1264
|
* keys), no proxies, no deep diff. A key also counts as dirty when its
|
|
1265
1265
|
* *presence* differs (added or removed, even with an `undefined`
|
|
1266
1266
|
* value). Computed fresh on every access (a new `Set` each time), so
|
|
@@ -1273,12 +1273,12 @@ var AggregateRoot = class extends BaseAggregate {
|
|
|
1273
1273
|
* class-instance `TState` mutated through its own methods defeats
|
|
1274
1274
|
* tracking entirely (the reference never changes). A keyless `TState`
|
|
1275
1275
|
* (primitive, bare `Date`) has no keys to report, so `changedKeys`
|
|
1276
|
-
* stays empty for it
|
|
1276
|
+
* stays empty for it; use {@link hasChanges}, whose reference
|
|
1277
1277
|
* fallback covers keyless states. A deep-equal but newly-referenced
|
|
1278
1278
|
* value reports a false POSITIVE (harmless extra write); under the
|
|
1279
1279
|
* contract above there are no false negatives.
|
|
1280
1280
|
*
|
|
1281
|
-
* Granularity is per top-level key
|
|
1281
|
+
* Granularity is per top-level key, table-granular, not row-granular:
|
|
1282
1282
|
* a dirty collection key means "this child table changed", not which
|
|
1283
1283
|
* rows. `EventSourcedAggregate` deliberately has no `changedKeys`;
|
|
1284
1284
|
* its `pendingEvents` are the change record.
|
|
@@ -1293,16 +1293,16 @@ var AggregateRoot = class extends BaseAggregate {
|
|
|
1293
1293
|
* Safe skip signal: `false` only when there is genuinely nothing to
|
|
1294
1294
|
* persist or flush. `true` when the aggregate has never been
|
|
1295
1295
|
* persisted, the version moved past `persistedVersion`, there are
|
|
1296
|
-
* unflushed {@link pendingEvents}, any state key is dirty, or
|
|
1296
|
+
* unflushed {@link pendingEvents}, any state key is dirty, or, for
|
|
1297
1297
|
* keyless states the per-key diff cannot see (primitive `TState`,
|
|
1298
|
-
* zero-own-key objects like a bare `Date`)
|
|
1298
|
+
* zero-own-key objects like a bare `Date`), the state reference
|
|
1299
1299
|
* changed since the baseline.
|
|
1300
1300
|
*
|
|
1301
1301
|
* The version clause is deliberate: `setState({...state}, true)` with
|
|
1302
1302
|
* identical per-key values yields empty {@link changedKeys} but a
|
|
1303
1303
|
* bumped version. If a repository skipped `save()` on a state-only
|
|
1304
1304
|
* check, `withCommit` would still call `markPersisted(version)` after
|
|
1305
|
-
* commit, desyncing `persistedVersion` from the DB row
|
|
1305
|
+
* commit, desyncing `persistedVersion` from the DB row; and the next
|
|
1306
1306
|
* uncontended save would throw a false `ConcurrencyConflictError`.
|
|
1307
1307
|
*
|
|
1308
1308
|
* The pending-events clause covers the sanctioned decoupled
|
|
@@ -1447,6 +1447,28 @@ var MissingHandlerError = class extends BaseError {
|
|
|
1447
1447
|
__name(this, "MissingHandlerError");
|
|
1448
1448
|
}
|
|
1449
1449
|
};
|
|
1450
|
+
var EventHarvestError = class extends BaseError {
|
|
1451
|
+
constructor(message, eventType) {
|
|
1452
|
+
super(message, void 0, { name: "EventHarvestError" });
|
|
1453
|
+
this.eventType = eventType;
|
|
1454
|
+
}
|
|
1455
|
+
static {
|
|
1456
|
+
__name(this, "EventHarvestError");
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
var UnenrolledChangesError = class extends BaseError {
|
|
1460
|
+
constructor(aggregateId) {
|
|
1461
|
+
super(
|
|
1462
|
+
`Aggregate ${aggregateId} was loaded in this unit of work and has pending events, but was never enrolled (no save), so its events would be silently dropped. Call repository.save(aggregate), and ensure save() calls session.enrollSaved before the row write.`,
|
|
1463
|
+
void 0,
|
|
1464
|
+
{ name: "UnenrolledChangesError" }
|
|
1465
|
+
);
|
|
1466
|
+
this.aggregateId = aggregateId;
|
|
1467
|
+
}
|
|
1468
|
+
static {
|
|
1469
|
+
__name(this, "UnenrolledChangesError");
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1450
1472
|
var AggregateDeletedError = class extends BaseError {
|
|
1451
1473
|
constructor(aggregateId) {
|
|
1452
1474
|
super(
|
|
@@ -1675,8 +1697,17 @@ var CommandBus = class {
|
|
|
1675
1697
|
}
|
|
1676
1698
|
};
|
|
1677
1699
|
|
|
1700
|
+
// src/utils/abort.ts
|
|
1701
|
+
function abortReason(signal, fallbackMessage) {
|
|
1702
|
+
return signal.reason ?? new Error(fallbackMessage);
|
|
1703
|
+
}
|
|
1704
|
+
__name(abortReason, "abortReason");
|
|
1705
|
+
|
|
1678
1706
|
// src/app/handler.ts
|
|
1679
1707
|
async function withCommit(deps, fn) {
|
|
1708
|
+
if (deps.signal?.aborted) {
|
|
1709
|
+
throw abortReason(deps.signal, "withCommit aborted before opening a transaction");
|
|
1710
|
+
}
|
|
1680
1711
|
const { result, aggregates, deleted, events } = await deps.scope.transactional(
|
|
1681
1712
|
async (ctx) => {
|
|
1682
1713
|
const fnResult = await fn(ctx);
|
|
@@ -1690,8 +1721,9 @@ async function withCommit(deps, fn) {
|
|
|
1690
1721
|
});
|
|
1691
1722
|
}
|
|
1692
1723
|
if (event.aggregateVersion > agg.version) {
|
|
1693
|
-
throw new
|
|
1694
|
-
`withCommit: event "${event.type}" carries a pre-set aggregateVersion (${event.aggregateVersion}) AHEAD of its aggregate's commit version (${agg.version}). A stale-or-copied pre-set would advance consumer idempotency watermarks past real history; remove the manual aggregateVersion or correct it
|
|
1724
|
+
throw new EventHarvestError(
|
|
1725
|
+
`withCommit: event "${event.type}" carries a pre-set aggregateVersion (${event.aggregateVersion}) AHEAD of its aggregate's commit version (${agg.version}). A stale-or-copied pre-set would advance consumer idempotency watermarks past real history; remove the manual aggregateVersion or correct it.`,
|
|
1726
|
+
event.type
|
|
1695
1727
|
);
|
|
1696
1728
|
}
|
|
1697
1729
|
return event;
|
|
@@ -1702,10 +1734,11 @@ async function withCommit(deps, fn) {
|
|
|
1702
1734
|
if (!event.aggregateId) missing.push("aggregateId");
|
|
1703
1735
|
if (!event.aggregateType) missing.push("aggregateType");
|
|
1704
1736
|
if (missing.length > 0) {
|
|
1705
|
-
throw new
|
|
1737
|
+
throw new EventHarvestError(
|
|
1706
1738
|
`withCommit: event "${event.type}" is missing ${missing.join(
|
|
1707
1739
|
" and "
|
|
1708
|
-
)}. Use this.recordEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); recordEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on these fields for routing
|
|
1740
|
+
)}. Use this.recordEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); recordEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on these fields for routing.`,
|
|
1741
|
+
event.type
|
|
1709
1742
|
);
|
|
1710
1743
|
}
|
|
1711
1744
|
}
|
|
@@ -1718,7 +1751,8 @@ async function withCommit(deps, fn) {
|
|
|
1718
1751
|
deleted: new Set(fnResult.deleted ?? []),
|
|
1719
1752
|
events: harvested
|
|
1720
1753
|
};
|
|
1721
|
-
}
|
|
1754
|
+
},
|
|
1755
|
+
{ signal: deps.signal }
|
|
1722
1756
|
);
|
|
1723
1757
|
for (const agg of aggregates) {
|
|
1724
1758
|
try {
|
|
@@ -1751,6 +1785,10 @@ var IdentityMap = class {
|
|
|
1751
1785
|
}
|
|
1752
1786
|
_stores = /* @__PURE__ */ new Map();
|
|
1753
1787
|
_deleted = /* @__PURE__ */ new Map();
|
|
1788
|
+
// pendingEvents length captured when an instance was first registered
|
|
1789
|
+
// (load time), so the unit of work can tell events RECORDED AFTER load
|
|
1790
|
+
// apart from a "dirty" reconstitution that already carried events.
|
|
1791
|
+
_pendingAtRegistration = /* @__PURE__ */ new WeakMap();
|
|
1754
1792
|
/** The cached instance for type+id, or `undefined` (also after {@link delete}). */
|
|
1755
1793
|
get(type, id) {
|
|
1756
1794
|
return this._stores.get(type)?.get(id);
|
|
@@ -1762,7 +1800,7 @@ var IdentityMap = class {
|
|
|
1762
1800
|
/**
|
|
1763
1801
|
* Whether type+id was {@link delete}d in this unit of work. The
|
|
1764
1802
|
* read path checks this BEFORE hydrating and returns `null`, so
|
|
1765
|
-
* "deleted in this operation" reads uniformly as not-found
|
|
1803
|
+
* "deleted in this operation" reads uniformly as not-found,
|
|
1766
1804
|
* regardless of whether the repository's physical delete already
|
|
1767
1805
|
* removed the row or is deferred within the transaction. Without
|
|
1768
1806
|
* the check, a read-only probe of a deleted aggregate would crash
|
|
@@ -1801,6 +1839,34 @@ var IdentityMap = class {
|
|
|
1801
1839
|
);
|
|
1802
1840
|
}
|
|
1803
1841
|
store.set(id, aggregate);
|
|
1842
|
+
if (aggregate !== null && typeof aggregate === "object" && !this._pendingAtRegistration.has(aggregate)) {
|
|
1843
|
+
const pending = pendingEventsOf(aggregate);
|
|
1844
|
+
if (pending) {
|
|
1845
|
+
this._pendingAtRegistration.set(aggregate, pending.length);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Registered instances that have recorded MORE pending events than they
|
|
1851
|
+
* carried when first registered (loaded). Used by the unit of work's
|
|
1852
|
+
* end-of-run guard: an aggregate that gained events after load but was
|
|
1853
|
+
* never enrolled would silently drop them. A read-only load, or a
|
|
1854
|
+
* reconstitution that already carried events, shows no increase and is
|
|
1855
|
+
* not reported.
|
|
1856
|
+
*/
|
|
1857
|
+
instancesWithNewPendingEvents() {
|
|
1858
|
+
const result = [];
|
|
1859
|
+
for (const store of this._stores.values()) {
|
|
1860
|
+
for (const instance of store.values()) {
|
|
1861
|
+
const pending = pendingEventsOf(instance);
|
|
1862
|
+
if (!pending) continue;
|
|
1863
|
+
const atRegistration = this._pendingAtRegistration.get(instance) ?? 0;
|
|
1864
|
+
if (pending.length > atRegistration) {
|
|
1865
|
+
result.push(instance);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return result;
|
|
1804
1870
|
}
|
|
1805
1871
|
/**
|
|
1806
1872
|
* Removes the entry for type+id and records a tombstone: subsequent
|
|
@@ -1824,6 +1890,12 @@ var IdentityMap = class {
|
|
|
1824
1890
|
this._deleted.clear();
|
|
1825
1891
|
}
|
|
1826
1892
|
};
|
|
1893
|
+
function pendingEventsOf(value) {
|
|
1894
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
1895
|
+
const pending = value.pendingEvents;
|
|
1896
|
+
return Array.isArray(pending) ? pending : void 0;
|
|
1897
|
+
}
|
|
1898
|
+
__name(pendingEventsOf, "pendingEventsOf");
|
|
1827
1899
|
|
|
1828
1900
|
// src/app/unit-of-work.ts
|
|
1829
1901
|
var NestedUnitOfWorkError = class extends BaseError {
|
|
@@ -1857,7 +1929,7 @@ var CommitError = class extends InfrastructureError {
|
|
|
1857
1929
|
}
|
|
1858
1930
|
constructor(cause) {
|
|
1859
1931
|
super(
|
|
1860
|
-
"Unit of work failed after the work callback completed: the
|
|
1932
|
+
"Unit of work failed after the work callback completed: the outbox write or the transaction commit rejected. The transaction did not commit; this failure may be transient, inspect the cause (e.g. someChainRetryable) before retrying.",
|
|
1861
1933
|
cause,
|
|
1862
1934
|
{ name: "CommitError" }
|
|
1863
1935
|
);
|
|
@@ -1890,7 +1962,10 @@ var UnitOfWork = class {
|
|
|
1890
1962
|
* run the post-commit lifecycle (markPersisted, publish) for every
|
|
1891
1963
|
* enrolled aggregate. Returns the callback's result.
|
|
1892
1964
|
*/
|
|
1893
|
-
async run(work) {
|
|
1965
|
+
async run(work, options) {
|
|
1966
|
+
if (options?.signal?.aborted) {
|
|
1967
|
+
throw abortReason(options.signal, "UnitOfWork.run aborted before opening a transaction");
|
|
1968
|
+
}
|
|
1894
1969
|
if (this._active) {
|
|
1895
1970
|
throw new NestedUnitOfWorkError();
|
|
1896
1971
|
}
|
|
@@ -1905,7 +1980,8 @@ var UnitOfWork = class {
|
|
|
1905
1980
|
outbox: this.deps.outbox,
|
|
1906
1981
|
bus: this.deps.bus,
|
|
1907
1982
|
scope: this.deps.scope,
|
|
1908
|
-
onPublishError: this.deps.onPublishError
|
|
1983
|
+
onPublishError: this.deps.onPublishError,
|
|
1984
|
+
signal: options?.signal
|
|
1909
1985
|
},
|
|
1910
1986
|
async (tx) => {
|
|
1911
1987
|
session?.close();
|
|
@@ -1915,9 +1991,10 @@ var UnitOfWork = class {
|
|
|
1915
1991
|
workThrew = false;
|
|
1916
1992
|
workError = void 0;
|
|
1917
1993
|
const repositories = this.buildRepositories(tx, s);
|
|
1918
|
-
const context = makeContext(repositories, tx, s);
|
|
1994
|
+
const context = makeContext(repositories, tx, s, options?.signal);
|
|
1919
1995
|
try {
|
|
1920
1996
|
const result = await work(context);
|
|
1997
|
+
s.assertAllChangesEnrolled();
|
|
1921
1998
|
workCompleted = true;
|
|
1922
1999
|
const aggregates = s.enrolledAggregates;
|
|
1923
2000
|
const deleted = s.deletedAggregates;
|
|
@@ -1938,6 +2015,10 @@ var UnitOfWork = class {
|
|
|
1938
2015
|
throw new RollbackError(workError, error);
|
|
1939
2016
|
}
|
|
1940
2017
|
if (workCompleted) {
|
|
2018
|
+
const harvestError = findHarvestErrorInChain(error);
|
|
2019
|
+
if (harvestError) {
|
|
2020
|
+
throw harvestError;
|
|
2021
|
+
}
|
|
1941
2022
|
throw new CommitError(error);
|
|
1942
2023
|
}
|
|
1943
2024
|
throw error;
|
|
@@ -1987,6 +2068,24 @@ var Session = class {
|
|
|
1987
2068
|
);
|
|
1988
2069
|
this._enrolled.add(aggregate);
|
|
1989
2070
|
}
|
|
2071
|
+
/**
|
|
2072
|
+
* End-of-run safety net: a loaded aggregate (registered in the identity
|
|
2073
|
+
* map via `getById`) that carries pending events but was never enrolled
|
|
2074
|
+
* is almost certainly a forgotten `save()` / `enrollSaved`, whose events
|
|
2075
|
+
* would otherwise be silently dropped. Convert that silent loss into a
|
|
2076
|
+
* loud, rolling-back {@link UnenrolledChangesError}. Only sees loaded
|
|
2077
|
+
* aggregates; a freshly created one that was never enrolled is invisible
|
|
2078
|
+
* to the kit (the contract test suite remains the full mitigation).
|
|
2079
|
+
*/
|
|
2080
|
+
assertAllChangesEnrolled() {
|
|
2081
|
+
for (const instance of this._identityMap.instancesWithNewPendingEvents()) {
|
|
2082
|
+
if (this._enrolled.has(instance) || this._deleted.has(instance)) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const id = instance.id;
|
|
2086
|
+
throw new UnenrolledChangesError(String(id));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1990
2089
|
get enrolledAggregates() {
|
|
1991
2090
|
return [...this._enrolled];
|
|
1992
2091
|
}
|
|
@@ -2003,7 +2102,7 @@ var Session = class {
|
|
|
2003
2102
|
}
|
|
2004
2103
|
}
|
|
2005
2104
|
};
|
|
2006
|
-
function makeContext(repositories, transaction, session) {
|
|
2105
|
+
function makeContext(repositories, transaction, session, signal) {
|
|
2007
2106
|
return {
|
|
2008
2107
|
get repositories() {
|
|
2009
2108
|
session.assertOpen("context.repositories");
|
|
@@ -2013,10 +2112,32 @@ function makeContext(repositories, transaction, session) {
|
|
|
2013
2112
|
session.assertOpen("context.rawTransaction");
|
|
2014
2113
|
return transaction;
|
|
2015
2114
|
},
|
|
2016
|
-
session
|
|
2115
|
+
session,
|
|
2116
|
+
// The caller's own signal: exposed directly, not gated by
|
|
2117
|
+
// assertOpen, so polling `aborted` after close stays harmless.
|
|
2118
|
+
signal
|
|
2017
2119
|
};
|
|
2018
2120
|
}
|
|
2019
2121
|
__name(makeContext, "makeContext");
|
|
2122
|
+
function findHarvestErrorInChain(error) {
|
|
2123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2124
|
+
let current = error;
|
|
2125
|
+
while (current !== null && typeof current === "object" && !seen.has(current)) {
|
|
2126
|
+
seen.add(current);
|
|
2127
|
+
if (current instanceof EventHarvestError) {
|
|
2128
|
+
return current;
|
|
2129
|
+
}
|
|
2130
|
+
let next;
|
|
2131
|
+
try {
|
|
2132
|
+
next = current.cause;
|
|
2133
|
+
} catch {
|
|
2134
|
+
return void 0;
|
|
2135
|
+
}
|
|
2136
|
+
current = next;
|
|
2137
|
+
}
|
|
2138
|
+
return void 0;
|
|
2139
|
+
}
|
|
2140
|
+
__name(findHarvestErrorInChain, "findHarvestErrorInChain");
|
|
2020
2141
|
function causeChainContains(error, target) {
|
|
2021
2142
|
if (target === void 0 || target === null) {
|
|
2022
2143
|
return false;
|
|
@@ -2103,7 +2224,7 @@ var EventBusImpl = class {
|
|
|
2103
2224
|
once(eventType, options) {
|
|
2104
2225
|
return new Promise((resolve, reject) => {
|
|
2105
2226
|
if (options?.signal?.aborted) {
|
|
2106
|
-
reject(options.signal
|
|
2227
|
+
reject(abortReason(options.signal, "EventBus.once aborted"));
|
|
2107
2228
|
return;
|
|
2108
2229
|
}
|
|
2109
2230
|
let timer;
|
|
@@ -2125,9 +2246,7 @@ var EventBusImpl = class {
|
|
|
2125
2246
|
if (options?.signal) {
|
|
2126
2247
|
abortListener = /* @__PURE__ */ __name(() => {
|
|
2127
2248
|
cleanup();
|
|
2128
|
-
reject(
|
|
2129
|
-
options.signal.reason ?? new Error("EventBus.once aborted")
|
|
2130
|
-
);
|
|
2249
|
+
reject(abortReason(options.signal, "EventBus.once aborted"));
|
|
2131
2250
|
}, "abortListener");
|
|
2132
2251
|
options.signal.addEventListener("abort", abortListener);
|
|
2133
2252
|
}
|
|
@@ -2207,6 +2326,101 @@ var InMemoryOutbox = class {
|
|
|
2207
2326
|
for (const id of dispatchIds) this.pending.delete(id);
|
|
2208
2327
|
}
|
|
2209
2328
|
};
|
|
2329
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
2330
|
+
var DEFAULT_BASE_DELAY_MS = 50;
|
|
2331
|
+
var DEFAULT_MAX_DELAY_MS = 1e3;
|
|
2332
|
+
function computeBackoffDelay(attempt, opts) {
|
|
2333
|
+
const exponential = opts.baseDelayMs * 2 ** (attempt - 1);
|
|
2334
|
+
const capped = Math.min(opts.maxDelayMs, exponential);
|
|
2335
|
+
const jitter = 0.8 + opts.random() * 0.4;
|
|
2336
|
+
return Math.max(0, Math.min(opts.maxDelayMs, Math.round(capped * jitter)));
|
|
2337
|
+
}
|
|
2338
|
+
__name(computeBackoffDelay, "computeBackoffDelay");
|
|
2339
|
+
function assertNonNegativeFinite(field, value) {
|
|
2340
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2341
|
+
throw new Error(
|
|
2342
|
+
`RetryingTransactionScope: ${field} must be a non-negative finite number, got ${value}`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
__name(assertNonNegativeFinite, "assertNonNegativeFinite");
|
|
2347
|
+
var ABORT_MESSAGE = "RetryingTransactionScope aborted";
|
|
2348
|
+
function defaultSleep(ms, signal) {
|
|
2349
|
+
return new Promise((resolve, reject) => {
|
|
2350
|
+
if (signal?.aborted) {
|
|
2351
|
+
reject(abortReason(signal, ABORT_MESSAGE));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
let onAbort;
|
|
2355
|
+
const timer = setTimeout(() => {
|
|
2356
|
+
if (onAbort && signal) signal.removeEventListener("abort", onAbort);
|
|
2357
|
+
resolve();
|
|
2358
|
+
}, ms);
|
|
2359
|
+
if (signal) {
|
|
2360
|
+
onAbort = /* @__PURE__ */ __name(() => {
|
|
2361
|
+
clearTimeout(timer);
|
|
2362
|
+
reject(abortReason(signal, ABORT_MESSAGE));
|
|
2363
|
+
}, "onAbort");
|
|
2364
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
__name(defaultSleep, "defaultSleep");
|
|
2369
|
+
var RetryingTransactionScope = class {
|
|
2370
|
+
constructor(inner, policy = {}) {
|
|
2371
|
+
this.inner = inner;
|
|
2372
|
+
this.maxAttempts = policy.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2373
|
+
this.baseDelayMs = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
2374
|
+
this.maxDelayMs = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
2375
|
+
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`RetryingTransactionScope: maxAttempts must be an integer >= 1, got ${this.maxAttempts}`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
assertNonNegativeFinite("baseDelayMs", this.baseDelayMs);
|
|
2381
|
+
assertNonNegativeFinite("maxDelayMs", this.maxDelayMs);
|
|
2382
|
+
this.isRetryable = policy.isRetryable ?? someChainRetryable;
|
|
2383
|
+
this.sleep = policy.sleep ?? defaultSleep;
|
|
2384
|
+
this.random = policy.random ?? Math.random;
|
|
2385
|
+
this.onRetry = policy.onRetry;
|
|
2386
|
+
}
|
|
2387
|
+
static {
|
|
2388
|
+
__name(this, "RetryingTransactionScope");
|
|
2389
|
+
}
|
|
2390
|
+
// Policy resolved and validated once at construction (a misconfigured
|
|
2391
|
+
// policy is a wiring bug and fails fast, never at run time).
|
|
2392
|
+
maxAttempts;
|
|
2393
|
+
baseDelayMs;
|
|
2394
|
+
maxDelayMs;
|
|
2395
|
+
isRetryable;
|
|
2396
|
+
sleep;
|
|
2397
|
+
random;
|
|
2398
|
+
onRetry;
|
|
2399
|
+
async transactional(fn, options) {
|
|
2400
|
+
const { maxAttempts, isRetryable, sleep } = this;
|
|
2401
|
+
const signal = options?.signal;
|
|
2402
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2403
|
+
if (signal?.aborted) {
|
|
2404
|
+
throw abortReason(signal, ABORT_MESSAGE);
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
return await this.inner.transactional(fn, options);
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
if (attempt === maxAttempts || !isRetryable(error)) {
|
|
2410
|
+
throw error;
|
|
2411
|
+
}
|
|
2412
|
+
const delayMs = computeBackoffDelay(attempt, {
|
|
2413
|
+
baseDelayMs: this.baseDelayMs,
|
|
2414
|
+
maxDelayMs: this.maxDelayMs,
|
|
2415
|
+
random: this.random
|
|
2416
|
+
});
|
|
2417
|
+
this.onRetry?.({ attempt, error, delayMs });
|
|
2418
|
+
await sleep(delayMs, signal);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
throw new Error("RetryingTransactionScope: exhausted without result");
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2210
2424
|
function voValidated(t, validate, message = "Validation failed") {
|
|
2211
2425
|
const issues = new ValidationError(message);
|
|
2212
2426
|
validate(issues, t);
|
|
@@ -2214,6 +2428,6 @@ function voValidated(t, validate, message = "Validation failed") {
|
|
|
2214
2428
|
}
|
|
2215
2429
|
__name(voValidated, "voValidated");
|
|
2216
2430
|
|
|
2217
|
-
export { AggregateDeletedError, AggregateNotFoundError, AggregateRoot, CommandBus, CommitError, ConcurrencyConflictError, DomainError, DuplicateAggregateError, Entity, EventBusImpl, EventSourcedAggregate, IdentityMap, InMemoryOutbox, InfrastructureError, MissingHandlerError, NestedUnitOfWorkError, QueryBus, RollbackError, TransactionClosedError, UnitOfWork, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
|
|
2431
|
+
export { AggregateDeletedError, AggregateNotFoundError, AggregateRoot, CommandBus, CommitError, ConcurrencyConflictError, DomainError, DuplicateAggregateError, Entity, EventBusImpl, EventHarvestError, EventSourcedAggregate, IdentityMap, InMemoryOutbox, InfrastructureError, MissingHandlerError, NestedUnitOfWorkError, QueryBus, RetryingTransactionScope, RollbackError, TransactionClosedError, UnenrolledChangesError, UnitOfWork, ValueObject, computeBackoffDelay, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
|
|
2218
2432
|
//# sourceMappingURL=index.js.map
|
|
2219
2433
|
//# sourceMappingURL=index.js.map
|