@fabricorg/platform 1.10.0 → 1.10.1
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/CHANGELOG.md +46 -0
- package/README.md +31 -2
- package/dist/actions/index.cjs +45 -26
- package/dist/actions/index.cjs.map +1 -1
- package/dist/actions/index.d.cts +225 -226
- package/dist/actions/index.d.ts +225 -226
- package/dist/actions/index.js +41 -2
- package/dist/actions/index.js.map +1 -1
- package/dist/adapters/index.cjs +696 -66
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +367 -365
- package/dist/adapters/index.d.ts +367 -365
- package/dist/adapters/index.js +682 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/agent-hitl/index.cjs +21 -10
- package/dist/agent-hitl/index.cjs.map +1 -1
- package/dist/agent-hitl/index.d.cts +30 -30
- package/dist/agent-hitl/index.d.ts +30 -30
- package/dist/agent-hitl/index.js +21 -2
- package/dist/agent-hitl/index.js.map +1 -1
- package/dist/certification/index.cjs +109 -24
- package/dist/certification/index.cjs.map +1 -1
- package/dist/certification/index.d.cts +33 -39
- package/dist/certification/index.d.ts +33 -39
- package/dist/certification/index.js +107 -8
- package/dist/certification/index.js.map +1 -1
- package/dist/discovery/index.cjs +12 -10
- package/dist/discovery/index.cjs.map +1 -1
- package/dist/discovery/index.d.cts +2 -10
- package/dist/discovery/index.d.ts +2 -10
- package/dist/discovery/index.js +12 -2
- package/dist/discovery/index.js.map +1 -1
- package/dist/engine-B4csEOEc.js +223 -0
- package/dist/engine-B4csEOEc.js.map +1 -0
- package/dist/engine-BqScuhrT.cjs +246 -0
- package/dist/engine-BqScuhrT.cjs.map +1 -0
- package/dist/events/index.cjs +39 -10
- package/dist/events/index.cjs.map +1 -1
- package/dist/events/index.d.cts +43 -43
- package/dist/events/index.d.ts +43 -43
- package/dist/events/index.js +39 -2
- package/dist/events/index.js.map +1 -1
- package/dist/evidence/index.cjs +12 -50
- package/dist/evidence/index.d.cts +2 -229
- package/dist/evidence/index.d.ts +2 -229
- package/dist/evidence/index.js +1 -5
- package/dist/evidence-BuCICb0c.js +516 -0
- package/dist/evidence-BuCICb0c.js.map +1 -0
- package/dist/evidence-Cq48eDvA.cjs +575 -0
- package/dist/evidence-Cq48eDvA.cjs.map +1 -0
- package/dist/governance/index.cjs +24 -18
- package/dist/governance/index.cjs.map +1 -1
- package/dist/governance/index.d.cts +80 -83
- package/dist/governance/index.d.ts +80 -83
- package/dist/governance/index.js +22 -2
- package/dist/governance/index.js.map +1 -1
- package/dist/ids/index.cjs +52 -22
- package/dist/ids/index.cjs.map +1 -1
- package/dist/ids/index.d.cts +17 -17
- package/dist/ids/index.d.ts +17 -17
- package/dist/ids/index.js +49 -2
- package/dist/ids/index.js.map +1 -1
- package/dist/index-9jasmw76.d.cts +124 -0
- package/dist/index-BXpeFdk2.d.cts +190 -0
- package/dist/index-BXpeFdk2.d.ts +190 -0
- package/dist/index-BnbsKZsT.d.cts +231 -0
- package/dist/index-CIl1rYRv.d.cts +11 -0
- package/dist/index-CIl1rYRv.d.ts +11 -0
- package/dist/index-CVHA1ypT.d.cts +262 -0
- package/dist/index-DZ4mnNb7.d.cts +78 -0
- package/dist/index-DZ4mnNb7.d.ts +78 -0
- package/dist/index-IqcDvl69.d.ts +124 -0
- package/dist/index-fmUiRBXu.d.cts +57 -0
- package/dist/index-fmUiRBXu.d.ts +57 -0
- package/dist/index-sPUPs8pr.d.ts +262 -0
- package/dist/index-z9-CYOIo.d.ts +231 -0
- package/dist/index.cjs +111 -393
- package/dist/index.d.cts +18 -17
- package/dist/index.d.ts +18 -17
- package/dist/index.js +19 -20
- package/dist/manifest/index.cjs +224 -35
- package/dist/manifest/index.cjs.map +1 -1
- package/dist/manifest/index.d.cts +130 -131
- package/dist/manifest/index.d.ts +130 -131
- package/dist/manifest/index.js +219 -7
- package/dist/manifest/index.js.map +1 -1
- package/dist/modules/index.cjs +281 -42
- package/dist/modules/index.cjs.map +1 -1
- package/dist/modules/index.d.cts +157 -159
- package/dist/modules/index.d.ts +157 -159
- package/dist/modules/index.js +274 -6
- package/dist/modules/index.js.map +1 -1
- package/dist/objects/index.cjs +355 -22
- package/dist/objects/index.cjs.map +1 -1
- package/dist/objects/index.d.cts +2 -77
- package/dist/objects/index.d.ts +2 -77
- package/dist/objects/index.js +352 -2
- package/dist/objects/index.js.map +1 -1
- package/dist/policies/index.cjs +14 -56
- package/dist/policies/index.d.cts +2 -261
- package/dist/policies/index.d.ts +2 -261
- package/dist/policies/index.js +1 -3
- package/dist/policies-DH_GCXQ-.js +627 -0
- package/dist/policies-DH_GCXQ-.js.map +1 -0
- package/dist/policies-Nxbp1CH8.cjs +698 -0
- package/dist/policies-Nxbp1CH8.cjs.map +1 -0
- package/dist/privacy/index.cjs +26 -43
- package/dist/privacy/index.cjs.map +1 -1
- package/dist/privacy/index.d.cts +2 -189
- package/dist/privacy/index.d.ts +2 -189
- package/dist/privacy/index.js +18 -3
- package/dist/privacy/index.js.map +1 -1
- package/dist/projections/index.cjs +241 -23
- package/dist/projections/index.cjs.map +1 -1
- package/dist/projections/index.d.cts +2 -125
- package/dist/projections/index.d.ts +2 -125
- package/dist/projections/index.js +238 -3
- package/dist/projections/index.js.map +1 -1
- package/dist/state-machines/index.cjs +76 -18
- package/dist/state-machines/index.cjs.map +1 -1
- package/dist/state-machines/index.d.cts +2 -55
- package/dist/state-machines/index.d.ts +2 -55
- package/dist/state-machines/index.js +74 -2
- package/dist/state-machines/index.js.map +1 -1
- package/dist/testing/index.cjs +27 -19
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +17 -19
- package/dist/testing/index.d.ts +17 -19
- package/dist/testing/index.js +25 -3
- package/dist/testing/index.js.map +1 -1
- package/package.json +6 -5
- package/dist/chunk-2AE6TMAX.cjs +0 -44
- package/dist/chunk-2AE6TMAX.cjs.map +0 -1
- package/dist/chunk-6HY35TKB.cjs +0 -491
- package/dist/chunk-6HY35TKB.cjs.map +0 -1
- package/dist/chunk-6UGEIDLC.cjs +0 -26
- package/dist/chunk-6UGEIDLC.cjs.map +0 -1
- package/dist/chunk-7GAWSUOG.js +0 -64
- package/dist/chunk-7GAWSUOG.js.map +0 -1
- package/dist/chunk-A42JJIPL.cjs +0 -699
- package/dist/chunk-A42JJIPL.cjs.map +0 -1
- package/dist/chunk-AFXKSIVI.cjs +0 -540
- package/dist/chunk-AFXKSIVI.cjs.map +0 -1
- package/dist/chunk-B5DOGDPG.js +0 -42
- package/dist/chunk-B5DOGDPG.js.map +0 -1
- package/dist/chunk-BJGZE3GE.js +0 -20
- package/dist/chunk-BJGZE3GE.js.map +0 -1
- package/dist/chunk-CRZXTZ36.cjs +0 -258
- package/dist/chunk-CRZXTZ36.cjs.map +0 -1
- package/dist/chunk-D4SWRBUH.cjs +0 -22
- package/dist/chunk-D4SWRBUH.cjs.map +0 -1
- package/dist/chunk-DBTAUI6C.cjs +0 -395
- package/dist/chunk-DBTAUI6C.cjs.map +0 -1
- package/dist/chunk-EMSAFNPX.js +0 -98
- package/dist/chunk-EMSAFNPX.js.map +0 -1
- package/dist/chunk-F3E5SXPN.cjs +0 -28
- package/dist/chunk-F3E5SXPN.cjs.map +0 -1
- package/dist/chunk-HEYMMVDP.js +0 -480
- package/dist/chunk-HEYMMVDP.js.map +0 -1
- package/dist/chunk-IFW43ELT.cjs +0 -331
- package/dist/chunk-IFW43ELT.cjs.map +0 -1
- package/dist/chunk-LFE74IJX.js +0 -20
- package/dist/chunk-LFE74IJX.js.map +0 -1
- package/dist/chunk-LZSE6QGQ.cjs +0 -60
- package/dist/chunk-LZSE6QGQ.cjs.map +0 -1
- package/dist/chunk-MCWSUGB3.cjs +0 -102
- package/dist/chunk-MCWSUGB3.cjs.map +0 -1
- package/dist/chunk-MLBALEUR.js +0 -290
- package/dist/chunk-MLBALEUR.js.map +0 -1
- package/dist/chunk-NNXQZPB3.cjs +0 -31
- package/dist/chunk-NNXQZPB3.cjs.map +0 -1
- package/dist/chunk-OFCCZ27Z.js +0 -26
- package/dist/chunk-OFCCZ27Z.js.map +0 -1
- package/dist/chunk-PDN5DOLQ.js +0 -242
- package/dist/chunk-PDN5DOLQ.js.map +0 -1
- package/dist/chunk-PIDZQAEL.js +0 -50
- package/dist/chunk-PIDZQAEL.js.map +0 -1
- package/dist/chunk-PXD7NG6X.js +0 -686
- package/dist/chunk-PXD7NG6X.js.map +0 -1
- package/dist/chunk-Q4Q4JPR3.js +0 -524
- package/dist/chunk-Q4Q4JPR3.js.map +0 -1
- package/dist/chunk-RALMUSOX.js +0 -55
- package/dist/chunk-RALMUSOX.js.map +0 -1
- package/dist/chunk-SNTV45LH.cjs +0 -295
- package/dist/chunk-SNTV45LH.cjs.map +0 -1
- package/dist/chunk-TBS2S5AX.cjs +0 -40
- package/dist/chunk-TBS2S5AX.cjs.map +0 -1
- package/dist/chunk-TYNIFC7J.js +0 -253
- package/dist/chunk-TYNIFC7J.js.map +0 -1
- package/dist/chunk-UGWGHTNC.cjs +0 -56
- package/dist/chunk-UGWGHTNC.cjs.map +0 -1
- package/dist/chunk-WI7CWNYI.cjs +0 -68
- package/dist/chunk-WI7CWNYI.cjs.map +0 -1
- package/dist/chunk-WNZP53GI.js +0 -386
- package/dist/chunk-WNZP53GI.js.map +0 -1
- package/dist/chunk-WQ3BYI7T.cjs +0 -249
- package/dist/chunk-WQ3BYI7T.cjs.map +0 -1
- package/dist/chunk-Y2PJLRIY.js +0 -36
- package/dist/chunk-Y2PJLRIY.js.map +0 -1
- package/dist/chunk-ZAEVQSB5.js +0 -326
- package/dist/chunk-ZAEVQSB5.js.map +0 -1
- package/dist/chunk-ZMQTV5OJ.js +0 -27
- package/dist/chunk-ZMQTV5OJ.js.map +0 -1
- package/dist/evidence/index.cjs.map +0 -1
- package/dist/evidence/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/policies/index.cjs.map +0 -1
- package/dist/policies/index.js.map +0 -1
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,68 +1,698 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region adapters/circuit-breaker.ts
|
|
3
|
+
/**
|
|
4
|
+
* Map an {@link AdapterOutcomeKind} to how it counts toward a circuit breaker.
|
|
5
|
+
*
|
|
6
|
+
* | Outcome kind | Counts as |
|
|
7
|
+
* |---------------------|-----------|
|
|
8
|
+
* | `success` | `success` |
|
|
9
|
+
* | `transient_failure` | `failure` |
|
|
10
|
+
* | `timeout` | `failure` |
|
|
11
|
+
* | `ambiguous` | `failure` |
|
|
12
|
+
* | `permanent_failure` | `ignored` |
|
|
13
|
+
*
|
|
14
|
+
* `ambiguous` counts as a failure: the dependency did not give a definite
|
|
15
|
+
* answer, which is a health signal even though the Host routes the invocation
|
|
16
|
+
* to reconciliation. `permanent_failure` (validation or rejection) is the
|
|
17
|
+
* dependency working correctly and saying no, so it does not trip the breaker.
|
|
18
|
+
* `executeWithAdapterRetry` classifies a thrown error with the caller's
|
|
19
|
+
* `classifyThrownError` (default `transient_failure`) and maps that kind here,
|
|
20
|
+
* so an unclassified thrown error counts as a failure.
|
|
21
|
+
*/
|
|
22
|
+
function classifyCircuitBreakerOutcome(kind) {
|
|
23
|
+
switch (kind) {
|
|
24
|
+
case "success": return "success";
|
|
25
|
+
case "permanent_failure": return "ignored";
|
|
26
|
+
case "transient_failure":
|
|
27
|
+
case "timeout":
|
|
28
|
+
case "ambiguous": return "failure";
|
|
29
|
+
default: return "failure";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const MANUAL_OVERRIDES = ["force_open", "force_closed"];
|
|
33
|
+
/**
|
|
34
|
+
* Validate a circuit-breaker config. Throws a `RangeError` naming the first
|
|
35
|
+
* invalid field: `mode` must be `monitor` or `enforce`; `failureThreshold` and
|
|
36
|
+
* `minimumSamples` positive integers; `windowMs` and `probeLeaseMs` positive
|
|
37
|
+
* finite numbers; `cooldownMs` a non-negative finite number; `manualOverride`
|
|
38
|
+
* `force_open` or `force_closed` when present.
|
|
39
|
+
*/
|
|
40
|
+
function assertAdapterCircuitBreakerConfig(config) {
|
|
41
|
+
if (!config || typeof config !== "object") throw new RangeError("Adapter circuit breaker config must be an object.");
|
|
42
|
+
if (config.mode !== "monitor" && config.mode !== "enforce") throw new RangeError(`Adapter circuit breaker mode must be "monitor" or "enforce"; got ${JSON.stringify(config.mode)}.`);
|
|
43
|
+
if (!Number.isInteger(config.failureThreshold) || config.failureThreshold < 1) throw new RangeError(`Adapter circuit breaker failureThreshold must be a positive integer; got ${String(config.failureThreshold)}.`);
|
|
44
|
+
if (typeof config.windowMs !== "number" || !Number.isFinite(config.windowMs) || config.windowMs <= 0) throw new RangeError(`Adapter circuit breaker windowMs must be a positive number of milliseconds; got ${String(config.windowMs)}.`);
|
|
45
|
+
if (typeof config.cooldownMs !== "number" || !Number.isFinite(config.cooldownMs) || config.cooldownMs < 0) throw new RangeError(`Adapter circuit breaker cooldownMs must be a non-negative number of milliseconds; got ${String(config.cooldownMs)}.`);
|
|
46
|
+
if (config.minimumSamples !== void 0 && (!Number.isInteger(config.minimumSamples) || config.minimumSamples < 1)) throw new RangeError(`Adapter circuit breaker minimumSamples must be a positive integer; got ${String(config.minimumSamples)}.`);
|
|
47
|
+
if (config.probeLeaseMs !== void 0 && (typeof config.probeLeaseMs !== "number" || !Number.isFinite(config.probeLeaseMs) || config.probeLeaseMs <= 0)) throw new RangeError(`Adapter circuit breaker probeLeaseMs must be a positive number of milliseconds; got ${String(config.probeLeaseMs)}.`);
|
|
48
|
+
if (config.manualOverride !== void 0 && !MANUAL_OVERRIDES.includes(config.manualOverride)) throw new RangeError(`Adapter circuit breaker manualOverride must be "force_open" or "force_closed"; got ${JSON.stringify(config.manualOverride)}.`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Dependency-free, in-process circuit breaker for one adapter operation.
|
|
52
|
+
*
|
|
53
|
+
* State lives in this instance only: it is not shared between processes,
|
|
54
|
+
* hosts, or replicas, and it is lost on restart. Each replica trips on the
|
|
55
|
+
* failures it observes itself.
|
|
56
|
+
*
|
|
57
|
+
* Tripping rule (parity with the .NET `InMemoryAdapterCircuitBreaker`): after
|
|
58
|
+
* a failure is recorded while `closed`, let `N` be the outcomes recorded within
|
|
59
|
+
* the last `windowMs` and `M = minimumSamples ?? failureThreshold`. The breaker
|
|
60
|
+
* opens when `N >= M` and either
|
|
61
|
+
* 1. the most recent `failureThreshold` outcomes in the window are all
|
|
62
|
+
* failures, or
|
|
63
|
+
* 2. the failure ratio in the window is at least `failureThreshold / M`.
|
|
64
|
+
*
|
|
65
|
+
* With `minimumSamples` omitted this reads "`failureThreshold` consecutive
|
|
66
|
+
* failures". With `minimumSamples: 20, failureThreshold: 10` it also opens at
|
|
67
|
+
* a 50% failure rate over at least 20 calls.
|
|
68
|
+
*
|
|
69
|
+
* After `cooldownMs` the breaker turns `half_open` and admits one probe. A
|
|
70
|
+
* probe success closes it and clears the window; a probe failure reopens it
|
|
71
|
+
* and restarts the cooldown. At most one probe is outstanding at a time: it
|
|
72
|
+
* holds a lease of `probeLeaseMs` (default `max(cooldownMs, windowMs)`, never
|
|
73
|
+
* derived from the cooldown alone, so `cooldownMs: 0` still admits one probe).
|
|
74
|
+
* A probe that has not recorded when its lease expires is considered lost and
|
|
75
|
+
* the next call becomes the probe.
|
|
76
|
+
*
|
|
77
|
+
* Epochs: the breaker keeps a generation counter that advances on every state
|
|
78
|
+
* change (including one committed by an elapsed cooldown), every manual
|
|
79
|
+
* override change, and every probe grant. Each admission carries the
|
|
80
|
+
* generation it was granted in. `record` first commits any elapsed cooldown,
|
|
81
|
+
* then compares generations. An outcome from an earlier epoch is stale: it is
|
|
82
|
+
* dropped entirely. It changes no state, does not resolve or free the probe
|
|
83
|
+
* slot, and is not added to the rolling window, so it cannot contribute to a
|
|
84
|
+
* later trip either. Because a probe grant starts a new epoch, the outstanding
|
|
85
|
+
* probe is the only `probe: true` admission of the current epoch, which is how
|
|
86
|
+
* a replaced probe's late outcome is told apart. An outcome recorded without an
|
|
87
|
+
* admission, or with one that has no `generation`, is treated as current: it is
|
|
88
|
+
* sampled, can trip a closed breaker and resolves an outstanding probe.
|
|
89
|
+
*
|
|
90
|
+
* Telemetry is buffered and delivered after each operation commits. An
|
|
91
|
+
* elapsed cooldown is committed, and its `cooldown_elapsed` event delivered,
|
|
92
|
+
* as a step of its own before `acquire`, `record`, `getState` or
|
|
93
|
+
* `setManualOverride` continues, so a callback reacting to it (for example by
|
|
94
|
+
* forcing the breaker open) governs the call that observed it. Every other
|
|
95
|
+
* event is delivered once the method's decision or record is final, and a
|
|
96
|
+
* callback never revokes an admission that is already committed. Two cases
|
|
97
|
+
* follow from that: a breaker call made from inside a callback queues its own
|
|
98
|
+
* `cooldown_elapsed` behind the current delivery and decides first; and with
|
|
99
|
+
* `cooldownMs: 0`, a cooldown that falls due again during the decision (because
|
|
100
|
+
* a callback reopened the breaker) is committed and admitted before its event
|
|
101
|
+
* is delivered.
|
|
102
|
+
*
|
|
103
|
+
* `monitor` mode runs the same state machine and reports the same telemetry
|
|
104
|
+
* but admits every call; calls enforce mode would have refused are reported
|
|
105
|
+
* as `would_reject`.
|
|
106
|
+
*
|
|
107
|
+
* A manual override pins the reported state (`force_open` → `open`,
|
|
108
|
+
* `force_closed` → `closed`) and suspends automatic transitions while set;
|
|
109
|
+
* outcomes of calls admitted under the override are still recorded. Like an
|
|
110
|
+
* automatic open, `force_open` blocks only in `enforce` mode. Clearing the
|
|
111
|
+
* override returns the breaker to `closed` with its window intact.
|
|
112
|
+
*/
|
|
113
|
+
var InMemoryAdapterCircuitBreaker = class {
|
|
114
|
+
key;
|
|
115
|
+
mode;
|
|
116
|
+
config;
|
|
117
|
+
now;
|
|
118
|
+
onEvent;
|
|
119
|
+
probeLeaseMs;
|
|
120
|
+
samples = [];
|
|
121
|
+
state = "closed";
|
|
122
|
+
openedAt = 0;
|
|
123
|
+
probeIssuedAt;
|
|
124
|
+
/** Advances on every state change, manual override change and probe grant. */
|
|
125
|
+
generation = 0;
|
|
126
|
+
manualOverride;
|
|
127
|
+
/** Telemetry waiting for the current operation to commit. */
|
|
128
|
+
outbox = [];
|
|
129
|
+
operationDepth = 0;
|
|
130
|
+
flushing = false;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
assertAdapterCircuitBreakerConfig(options.config);
|
|
133
|
+
this.key = options.key;
|
|
134
|
+
this.config = { ...options.config };
|
|
135
|
+
this.mode = options.config.mode;
|
|
136
|
+
this.now = options.now ?? Date.now;
|
|
137
|
+
this.onEvent = options.onEvent;
|
|
138
|
+
this.manualOverride = options.config.manualOverride;
|
|
139
|
+
this.probeLeaseMs = options.config.probeLeaseMs ?? Math.max(options.config.cooldownMs, options.config.windowMs);
|
|
140
|
+
}
|
|
141
|
+
getState() {
|
|
142
|
+
this.settle(this.now());
|
|
143
|
+
return this.reportedState();
|
|
144
|
+
}
|
|
145
|
+
getManualOverride() {
|
|
146
|
+
return this.manualOverride;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Set (`force_open` / `force_closed`) or clear (`undefined`) the manual
|
|
150
|
+
* override. Clearing returns the breaker to `closed` with its window intact.
|
|
151
|
+
*/
|
|
152
|
+
setManualOverride(override) {
|
|
153
|
+
if (override !== void 0 && !MANUAL_OVERRIDES.includes(override)) throw new RangeError(`Adapter circuit breaker manualOverride must be "force_open" or "force_closed"; got ${JSON.stringify(override)}.`);
|
|
154
|
+
const now = this.now();
|
|
155
|
+
this.settle(now);
|
|
156
|
+
if (override === this.manualOverride) return;
|
|
157
|
+
this.operation(() => {
|
|
158
|
+
const from = this.reportedState();
|
|
159
|
+
this.manualOverride = override;
|
|
160
|
+
this.generation++;
|
|
161
|
+
if (override === void 0) {
|
|
162
|
+
this.state = "closed";
|
|
163
|
+
this.releaseProbe();
|
|
164
|
+
}
|
|
165
|
+
this.emit({
|
|
166
|
+
type: "state_change",
|
|
167
|
+
key: this.key,
|
|
168
|
+
mode: this.mode,
|
|
169
|
+
at: now,
|
|
170
|
+
from,
|
|
171
|
+
to: this.reportedState(),
|
|
172
|
+
reason: override === void 0 ? "manual_override_cleared" : "manual_override",
|
|
173
|
+
...override ? { manualOverride: override } : {}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
check() {
|
|
178
|
+
return this.decide(false, this.now());
|
|
179
|
+
}
|
|
180
|
+
acquire() {
|
|
181
|
+
const now = this.now();
|
|
182
|
+
this.settle(now);
|
|
183
|
+
return this.operation(() => this.decide(true, now));
|
|
184
|
+
}
|
|
185
|
+
record(outcome, admission) {
|
|
186
|
+
const now = this.now();
|
|
187
|
+
this.settle(now);
|
|
188
|
+
this.operation(() => this.applyOutcome(outcome, admission, now));
|
|
189
|
+
}
|
|
190
|
+
applyOutcome(outcome, admission, now) {
|
|
191
|
+
const generation = admission?.generation;
|
|
192
|
+
if (generation !== void 0 && generation !== this.generation) return;
|
|
193
|
+
const resolvesProbe = this.state === "half_open" && this.probeIssuedAt !== void 0 && (generation === void 0 || admission?.probe === true);
|
|
194
|
+
if (outcome === "ignored") {
|
|
195
|
+
if (resolvesProbe) this.releaseProbe();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.prune(now);
|
|
199
|
+
this.samples.push({
|
|
200
|
+
at: now,
|
|
201
|
+
failure: outcome === "failure"
|
|
202
|
+
});
|
|
203
|
+
if (this.manualOverride) return;
|
|
204
|
+
if (resolvesProbe) {
|
|
205
|
+
this.releaseProbe();
|
|
206
|
+
if (outcome === "success") {
|
|
207
|
+
this.samples = [];
|
|
208
|
+
this.transition("closed", "probe_succeeded", now);
|
|
209
|
+
} else {
|
|
210
|
+
this.openedAt = now;
|
|
211
|
+
this.transition("open", "probe_failed", now);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (this.state === "closed" && outcome === "failure" && this.shouldTrip()) {
|
|
216
|
+
this.openedAt = now;
|
|
217
|
+
this.transition("open", "failure_threshold", now);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `take` is false for {@link check}: a pure preview that sees an elapsed
|
|
222
|
+
* cooldown without committing it, takes no probe and emits nothing.
|
|
223
|
+
*/
|
|
224
|
+
decide(take, now) {
|
|
225
|
+
if (this.manualOverride === "force_closed") return {
|
|
226
|
+
allowed: true,
|
|
227
|
+
state: "closed",
|
|
228
|
+
probe: false,
|
|
229
|
+
manualOverride: "force_closed",
|
|
230
|
+
generation: this.generation
|
|
231
|
+
};
|
|
232
|
+
if (this.manualOverride === "force_open") return this.refuse(take, {
|
|
233
|
+
state: "open",
|
|
234
|
+
manualOverride: "force_open"
|
|
235
|
+
});
|
|
236
|
+
const cooldownElapsed = this.state === "open" && now - this.openedAt >= this.config.cooldownMs;
|
|
237
|
+
if (take) this.advance(now);
|
|
238
|
+
const state = !take && cooldownElapsed ? "half_open" : this.state;
|
|
239
|
+
if (state === "closed") return {
|
|
240
|
+
allowed: true,
|
|
241
|
+
state: "closed",
|
|
242
|
+
probe: false,
|
|
243
|
+
generation: this.generation
|
|
244
|
+
};
|
|
245
|
+
if (state === "open") return this.refuse(take, {
|
|
246
|
+
state: "open",
|
|
247
|
+
retryAfterMs: Math.max(0, this.openedAt + this.config.cooldownMs - now)
|
|
248
|
+
});
|
|
249
|
+
if (!(!take && cooldownElapsed || this.probeIssuedAt === void 0 || now - this.probeIssuedAt >= this.probeLeaseMs)) return this.refuse(take, { state: "half_open" });
|
|
250
|
+
if (!take) return {
|
|
251
|
+
allowed: true,
|
|
252
|
+
state: "half_open",
|
|
253
|
+
probe: true,
|
|
254
|
+
generation: this.generation
|
|
255
|
+
};
|
|
256
|
+
this.probeIssuedAt = now;
|
|
257
|
+
this.generation++;
|
|
258
|
+
return {
|
|
259
|
+
allowed: true,
|
|
260
|
+
state: "half_open",
|
|
261
|
+
probe: true,
|
|
262
|
+
generation: this.generation
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
refuse(take, detail) {
|
|
266
|
+
const enforce = this.mode === "enforce";
|
|
267
|
+
if (take) this.emit({
|
|
268
|
+
type: enforce ? "rejected" : "would_reject",
|
|
269
|
+
key: this.key,
|
|
270
|
+
mode: this.mode,
|
|
271
|
+
at: this.now(),
|
|
272
|
+
...detail
|
|
273
|
+
});
|
|
274
|
+
const generation = this.generation;
|
|
275
|
+
return enforce ? {
|
|
276
|
+
allowed: false,
|
|
277
|
+
probe: false,
|
|
278
|
+
...detail,
|
|
279
|
+
generation
|
|
280
|
+
} : {
|
|
281
|
+
allowed: true,
|
|
282
|
+
probe: false,
|
|
283
|
+
wouldReject: true,
|
|
284
|
+
...detail,
|
|
285
|
+
generation
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
releaseProbe() {
|
|
289
|
+
this.probeIssuedAt = void 0;
|
|
290
|
+
}
|
|
291
|
+
/** Apply the time-based open → half_open transition. */
|
|
292
|
+
advance(now) {
|
|
293
|
+
if (this.state === "open" && now - this.openedAt >= this.config.cooldownMs) {
|
|
294
|
+
this.releaseProbe();
|
|
295
|
+
this.transition("half_open", "cooldown_elapsed", now);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
shouldTrip() {
|
|
299
|
+
const minimumSamples = this.config.minimumSamples ?? this.config.failureThreshold;
|
|
300
|
+
const total = this.samples.length;
|
|
301
|
+
if (total < minimumSamples) return false;
|
|
302
|
+
let consecutive = 0;
|
|
303
|
+
for (let index = total - 1; index >= 0 && this.samples[index].failure; index--) consecutive++;
|
|
304
|
+
if (consecutive >= this.config.failureThreshold) return true;
|
|
305
|
+
return this.samples.reduce((count, sample) => count + (sample.failure ? 1 : 0), 0) / total >= this.config.failureThreshold / minimumSamples;
|
|
306
|
+
}
|
|
307
|
+
prune(now) {
|
|
308
|
+
const cutoff = now - this.config.windowMs;
|
|
309
|
+
let drop = 0;
|
|
310
|
+
while (drop < this.samples.length && this.samples[drop].at < cutoff) drop++;
|
|
311
|
+
if (drop > 0) this.samples.splice(0, drop);
|
|
312
|
+
}
|
|
313
|
+
transition(to, reason, at) {
|
|
314
|
+
const from = this.state;
|
|
315
|
+
if (from === to) return;
|
|
316
|
+
this.state = to;
|
|
317
|
+
this.generation++;
|
|
318
|
+
this.emit({
|
|
319
|
+
type: "state_change",
|
|
320
|
+
key: this.key,
|
|
321
|
+
mode: this.mode,
|
|
322
|
+
at,
|
|
323
|
+
from,
|
|
324
|
+
to,
|
|
325
|
+
reason
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
reportedState() {
|
|
329
|
+
if (this.manualOverride) return this.manualOverride === "force_open" ? "open" : "closed";
|
|
330
|
+
return this.state;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Commit an elapsed cooldown as an operation of its own, delivering its
|
|
334
|
+
* event before the caller's operation reads state.
|
|
335
|
+
*/
|
|
336
|
+
settle(now) {
|
|
337
|
+
if (this.manualOverride) return;
|
|
338
|
+
this.operation(() => this.advance(now));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Run `step` to completion, then deliver the telemetry it produced. The
|
|
342
|
+
* return value is fixed before any listener runs.
|
|
343
|
+
*/
|
|
344
|
+
operation(step) {
|
|
345
|
+
this.operationDepth++;
|
|
346
|
+
try {
|
|
347
|
+
return step();
|
|
348
|
+
} finally {
|
|
349
|
+
this.operationDepth--;
|
|
350
|
+
if (this.operationDepth === 0) this.flush();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
emit(event) {
|
|
354
|
+
if (this.onEvent) this.outbox.push(event);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Deliver buffered events in order. A listener that calls back into the
|
|
358
|
+
* breaker queues its own events; this loop delivers them after it returns
|
|
359
|
+
* instead of recursing.
|
|
360
|
+
*/
|
|
361
|
+
flush() {
|
|
362
|
+
if (this.flushing) return;
|
|
363
|
+
this.flushing = true;
|
|
364
|
+
try {
|
|
365
|
+
for (let event = this.outbox.shift(); event; event = this.outbox.shift()) this.deliver(event);
|
|
366
|
+
} finally {
|
|
367
|
+
this.flushing = false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
deliver(event) {
|
|
371
|
+
const listener = this.onEvent;
|
|
372
|
+
if (!listener) return;
|
|
373
|
+
try {
|
|
374
|
+
const result = listener(event);
|
|
375
|
+
if (result && typeof result.then === "function") result.then(void 0, () => void 0);
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* Key by tenant, optional space, vendor, and adapter operation. Components are
|
|
381
|
+
* URI-encoded to distinguish embedded delimiters; ordinary adapter identifiers
|
|
382
|
+
* remain readable as the trailing `adapterType:operation`.
|
|
383
|
+
*
|
|
384
|
+
* Missing scope uses a separate `unscoped` namespace (still split by vendor)
|
|
385
|
+
* rather than rejecting the call. Calls without scope share that
|
|
386
|
+
* namespace; callers must supply scope for tenant isolation. Missing space or
|
|
387
|
+
* vendor is distinct from an empty string. `vendor` is required by the public
|
|
388
|
+
* adapter type, but its absence is tolerated for JavaScript callers.
|
|
389
|
+
* Keys appear in errors and telemetry: use non-secret identifiers.
|
|
390
|
+
*/
|
|
391
|
+
function scopedAdapterCircuitBreakerKey(adapter, scope) {
|
|
392
|
+
return [
|
|
393
|
+
scope === void 0 ? "unscoped" : `tenant=${encodeURIComponent(scope.tenantId)}`,
|
|
394
|
+
scope?.spaceId === void 0 ? "space" : `space=${encodeURIComponent(scope.spaceId)}`,
|
|
395
|
+
adapter.vendor === void 0 ? "vendor" : `vendor=${encodeURIComponent(adapter.vendor)}`,
|
|
396
|
+
encodeURIComponent(adapter.adapterType),
|
|
397
|
+
encodeURIComponent(adapter.operation)
|
|
398
|
+
].join(":");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Lazily creates one {@link InMemoryAdapterCircuitBreaker} per
|
|
402
|
+
* key, from that adapter's own `circuitBreaker` config. The default key is
|
|
403
|
+
* `adapterType:operation`, shared across tenants and vendors; override it with
|
|
404
|
+
* {@link AdapterCircuitBreakerRegistryOptions.key} to isolate invocation scopes.
|
|
405
|
+
* Adapters without a config get no breaker. The first config seen for a key
|
|
406
|
+
* wins; later conflicting configs reuse the existing breaker without throwing.
|
|
407
|
+
* Share a key only between adapters intended to use the same configuration.
|
|
408
|
+
*/
|
|
409
|
+
var AdapterCircuitBreakerRegistry = class {
|
|
410
|
+
options;
|
|
411
|
+
breakers = /* @__PURE__ */ new Map();
|
|
412
|
+
constructor(options = {}) {
|
|
413
|
+
this.options = options;
|
|
414
|
+
}
|
|
415
|
+
forAdapter(adapter, scope) {
|
|
416
|
+
if (!adapter.circuitBreaker) return void 0;
|
|
417
|
+
const key = this.options.key?.(adapter, scope) ?? `${adapter.adapterType}:${adapter.operation}`;
|
|
418
|
+
let breaker = this.breakers.get(key);
|
|
419
|
+
if (!breaker) {
|
|
420
|
+
breaker = new InMemoryAdapterCircuitBreaker({
|
|
421
|
+
key,
|
|
422
|
+
config: adapter.circuitBreaker,
|
|
423
|
+
...this.options.now ? { now: this.options.now } : {},
|
|
424
|
+
...this.options.onEvent ? { onEvent: this.options.onEvent } : {}
|
|
425
|
+
});
|
|
426
|
+
this.breakers.set(key, breaker);
|
|
427
|
+
}
|
|
428
|
+
return breaker;
|
|
429
|
+
}
|
|
430
|
+
/** Looks up the default `adapterType:operation` key without creating a breaker. */
|
|
431
|
+
get(adapterType, operation) {
|
|
432
|
+
return this.getByKey(`${adapterType}:${operation}`);
|
|
433
|
+
}
|
|
434
|
+
/** Looks up an exact key, including a custom key, without creating a breaker. */
|
|
435
|
+
getByKey(key) {
|
|
436
|
+
return this.breakers.get(key);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
/**
|
|
440
|
+
* Thrown by `executeWithAdapterRetry` when an `enforce`-mode breaker refuses
|
|
441
|
+
* an attempt. The refused attempt never reached `execute`, so no external
|
|
442
|
+
* effect happened for it.
|
|
443
|
+
*/
|
|
444
|
+
var AdapterCircuitOpenError = class extends Error {
|
|
445
|
+
name = "AdapterCircuitOpenError";
|
|
446
|
+
code = "ADAPTER_CIRCUIT_OPEN";
|
|
447
|
+
reason = "circuit_open";
|
|
448
|
+
adapterKey;
|
|
449
|
+
state;
|
|
450
|
+
/** The attempt number that was refused (1 = refused before any call). */
|
|
451
|
+
attempt;
|
|
452
|
+
retryAfterMs;
|
|
453
|
+
manualOverride;
|
|
454
|
+
/** Error of the previous attempt, when the breaker opened mid-retry. */
|
|
455
|
+
lastError;
|
|
456
|
+
constructor(input) {
|
|
457
|
+
const after = input.retryAfterMs !== void 0 ? `; retry after ${input.retryAfterMs} ms` : "";
|
|
458
|
+
super(`Adapter circuit is ${input.state === "half_open" ? "half-open with its probe in flight" : "open"} for ${input.adapterKey}; the call was not attempted${after}.`);
|
|
459
|
+
this.adapterKey = input.adapterKey;
|
|
460
|
+
this.state = input.state;
|
|
461
|
+
this.attempt = input.attempt;
|
|
462
|
+
if (input.retryAfterMs !== void 0) this.retryAfterMs = input.retryAfterMs;
|
|
463
|
+
if (input.manualOverride !== void 0) this.manualOverride = input.manualOverride;
|
|
464
|
+
if (input.lastError !== void 0) this.lastError = input.lastError;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Recognise an {@link AdapterCircuitOpenError}, including one constructed by a
|
|
469
|
+
* second copy of this package (for example an ESM and a CommonJS build loaded
|
|
470
|
+
* side by side), where `instanceof` fails.
|
|
471
|
+
*/
|
|
472
|
+
function isAdapterCircuitOpenError(error) {
|
|
473
|
+
if (error instanceof AdapterCircuitOpenError) return true;
|
|
474
|
+
return typeof error === "object" && error !== null && error.name === "AdapterCircuitOpenError" && error.code === "ADAPTER_CIRCUIT_OPEN";
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region adapters/index.ts
|
|
478
|
+
function adapterKey(adapterType, operation) {
|
|
479
|
+
return `${adapterType}:${operation}`;
|
|
480
|
+
}
|
|
481
|
+
var MissingAdapterRegistrationError = class extends Error {
|
|
482
|
+
constructor(adapterType, operation) {
|
|
483
|
+
super(`No adapter registered for adapterType "${adapterType}" and operation "${operation}"`);
|
|
484
|
+
this.name = "MissingAdapterRegistrationError";
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var AdapterRegistry = class {
|
|
488
|
+
adapters = /* @__PURE__ */ new Map();
|
|
489
|
+
register(adapter) {
|
|
490
|
+
const key = adapterKey(adapter.adapterType, adapter.operation);
|
|
491
|
+
if (this.adapters.has(key)) throw new Error(`Adapter already registered for adapterType "${adapter.adapterType}" and operation "${adapter.operation}"`);
|
|
492
|
+
this.adapters.set(key, adapter);
|
|
493
|
+
}
|
|
494
|
+
resolve(adapterType, operation) {
|
|
495
|
+
return this.adapters.get(adapterKey(adapterType, operation));
|
|
496
|
+
}
|
|
497
|
+
require(adapterType, operation) {
|
|
498
|
+
const adapter = this.resolve(adapterType, operation);
|
|
499
|
+
if (!adapter) throw new MissingAdapterRegistrationError(adapterType, operation);
|
|
500
|
+
return adapter;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const DEFAULT_RETRY_POLICY = {
|
|
504
|
+
idempotent: true,
|
|
505
|
+
maxAttempts: 3,
|
|
506
|
+
initialDelayMs: 100,
|
|
507
|
+
backoffMultiplier: 2,
|
|
508
|
+
maxDelayMs: 1e3
|
|
509
|
+
};
|
|
510
|
+
const NO_RETRY_POLICY = {
|
|
511
|
+
idempotent: false,
|
|
512
|
+
maxAttempts: 1,
|
|
513
|
+
initialDelayMs: 0,
|
|
514
|
+
backoffMultiplier: 1,
|
|
515
|
+
maxDelayMs: 0
|
|
516
|
+
};
|
|
517
|
+
function resolveAdapterRetryPolicy(policy, defaultIdempotent) {
|
|
518
|
+
if (!(policy?.idempotent ?? defaultIdempotent)) return NO_RETRY_POLICY;
|
|
519
|
+
return {
|
|
520
|
+
idempotent: true,
|
|
521
|
+
maxAttempts: Math.max(1, Math.min(policy?.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts, 5)),
|
|
522
|
+
initialDelayMs: Math.max(0, policy?.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs),
|
|
523
|
+
backoffMultiplier: Math.max(1, policy?.backoffMultiplier ?? DEFAULT_RETRY_POLICY.backoffMultiplier),
|
|
524
|
+
maxDelayMs: Math.max(0, policy?.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function getRetryDelayMs(attempt, policy) {
|
|
528
|
+
const exponentialDelay = policy.initialDelayMs * policy.backoffMultiplier ** Math.max(0, attempt - 1);
|
|
529
|
+
return Math.min(exponentialDelay, policy.maxDelayMs);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Classify an {@link AdapterExecutionResult} into an {@link AdapterOutcomeKind}.
|
|
533
|
+
* When `outcome` is explicitly set, it is returned directly, after rejecting a
|
|
534
|
+
* contradictory result where `success` is true but the outcome is non-success.
|
|
535
|
+
* Otherwise a successful result maps to `success` and a failed result maps to
|
|
536
|
+
* `transient_failure` (the historical default).
|
|
537
|
+
*/
|
|
538
|
+
function classifyAdapterOutcome(result) {
|
|
539
|
+
if (result.outcome) {
|
|
540
|
+
assertAdapterOutcomeConsistency(result);
|
|
541
|
+
return result.outcome;
|
|
542
|
+
}
|
|
543
|
+
return result.success ? "success" : "transient_failure";
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Determine whether an outcome kind is retryable. Only `transient_failure`
|
|
547
|
+
* is retryable, and only when the adapter policy is idempotent. Permanent
|
|
548
|
+
* failures, timeouts, and ambiguous outcomes are never retried.
|
|
549
|
+
*/
|
|
550
|
+
function shouldRetryAdapterOutcome(kind, idempotent) {
|
|
551
|
+
return kind === "transient_failure" && idempotent;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Reject a contradictory adapter result where `success` is true but the
|
|
555
|
+
* explicit `outcome` is a non-success kind. A successful result claiming an
|
|
556
|
+
* ambiguous, permanent, timeout, or transient outcome is a programming
|
|
557
|
+
* error — the adapter cannot have both succeeded and require reconciliation.
|
|
558
|
+
*/
|
|
559
|
+
function assertAdapterOutcomeConsistency(result) {
|
|
560
|
+
if (result.success && result.outcome && result.outcome !== "success") throw new Error(`Adapter returned success=true with contradictory outcome "${result.outcome}"; a successful result must not declare a non-success outcome.`);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Record an outcome on the breaker. A breaker that throws must not turn a
|
|
564
|
+
* call that already ran (and may have had its external effect) into a failure.
|
|
565
|
+
*/
|
|
566
|
+
function recordCircuitOutcome(breaker, outcome, admission) {
|
|
567
|
+
if (!breaker) return;
|
|
568
|
+
try {
|
|
569
|
+
breaker.record(outcome, admission);
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
function circuitOpenError(breaker, admission, attempt, lastError) {
|
|
573
|
+
return new AdapterCircuitOpenError({
|
|
574
|
+
adapterKey: breaker.key,
|
|
575
|
+
state: admission.state,
|
|
576
|
+
attempt,
|
|
577
|
+
...admission.retryAfterMs !== void 0 ? { retryAfterMs: admission.retryAfterMs } : {},
|
|
578
|
+
...admission.manualOverride !== void 0 ? { manualOverride: admission.manualOverride } : {},
|
|
579
|
+
...lastError !== void 0 ? { lastError } : {}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async function executeWithAdapterRetry(options) {
|
|
583
|
+
const policy = resolveAdapterRetryPolicy(options.policy, options.defaultIdempotent);
|
|
584
|
+
const sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
585
|
+
const breaker = options.circuitBreaker;
|
|
586
|
+
let lastError;
|
|
587
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
|
|
588
|
+
let admission;
|
|
589
|
+
let executed = false;
|
|
590
|
+
let recorded = false;
|
|
591
|
+
const record = (outcome) => {
|
|
592
|
+
if (!executed || recorded) return;
|
|
593
|
+
recorded = true;
|
|
594
|
+
recordCircuitOutcome(breaker, outcome, admission);
|
|
595
|
+
};
|
|
596
|
+
let refusal;
|
|
597
|
+
/**
|
|
598
|
+
* Classify an error thrown by `prepareAttempt`, `execute` or a later
|
|
599
|
+
* step of this attempt. Returns "retry" after sleeping, "rethrow", or the
|
|
600
|
+
* breaker admission that refuses the retry.
|
|
601
|
+
*/
|
|
602
|
+
async function handleThrown(error) {
|
|
603
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
604
|
+
const thrownOutcome = options.classifyThrownError ? options.classifyThrownError(error) : "transient_failure";
|
|
605
|
+
record(classifyCircuitBreakerOutcome(thrownOutcome));
|
|
606
|
+
let willRetry = shouldRetryAdapterOutcome(thrownOutcome, policy.idempotent) && attempt < policy.maxAttempts;
|
|
607
|
+
const blocked = willRetry ? refusedRetry(breaker) : void 0;
|
|
608
|
+
if (blocked) willRetry = false;
|
|
609
|
+
const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
|
|
610
|
+
await options.onAttempt?.({
|
|
611
|
+
attempt,
|
|
612
|
+
error: lastError,
|
|
613
|
+
willRetry,
|
|
614
|
+
nextDelayMs
|
|
615
|
+
});
|
|
616
|
+
if (blocked) return blocked;
|
|
617
|
+
if (!willRetry) return "rethrow";
|
|
618
|
+
await sleep(nextDelayMs ?? 0);
|
|
619
|
+
return "retry";
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
if (options.prepareAttempt) await options.prepareAttempt(attempt, policy.maxAttempts);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
const decision = await handleThrown(error);
|
|
625
|
+
if (decision === "rethrow") throw error;
|
|
626
|
+
if (decision === "retry") continue;
|
|
627
|
+
refusal = decision;
|
|
628
|
+
}
|
|
629
|
+
if (!refusal) {
|
|
630
|
+
if (breaker) {
|
|
631
|
+
admission = breaker.acquire();
|
|
632
|
+
if (!admission.allowed) throw circuitOpenError(breaker, admission, attempt, lastError);
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
executed = true;
|
|
636
|
+
const result = await options.execute(attempt, policy.maxAttempts);
|
|
637
|
+
if (options.isSuccessful(result)) {
|
|
638
|
+
record("success");
|
|
639
|
+
await options.onAttempt?.({
|
|
640
|
+
attempt,
|
|
641
|
+
result,
|
|
642
|
+
willRetry: false
|
|
643
|
+
});
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
lastError = options.getError(result) ?? "Adapter returned unsuccessful result";
|
|
647
|
+
const outcome = options.classifyOutcome ? options.classifyOutcome(result) : "transient_failure";
|
|
648
|
+
record(classifyCircuitBreakerOutcome(outcome));
|
|
649
|
+
let willRetry = shouldRetryAdapterOutcome(outcome, policy.idempotent) && attempt < policy.maxAttempts;
|
|
650
|
+
refusal = willRetry ? refusedRetry(breaker) : void 0;
|
|
651
|
+
if (refusal) willRetry = false;
|
|
652
|
+
const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
|
|
653
|
+
await options.onAttempt?.({
|
|
654
|
+
attempt,
|
|
655
|
+
result,
|
|
656
|
+
error: lastError,
|
|
657
|
+
willRetry,
|
|
658
|
+
nextDelayMs
|
|
659
|
+
});
|
|
660
|
+
if (!willRetry && !refusal) return result;
|
|
661
|
+
if (!refusal) await sleep(nextDelayMs ?? 0);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
const decision = await handleThrown(error);
|
|
664
|
+
if (decision === "rethrow") throw error;
|
|
665
|
+
refusal = decision === "retry" ? void 0 : decision;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (refusal && breaker) throw circuitOpenError(breaker, refusal, attempt + 1, lastError);
|
|
669
|
+
}
|
|
670
|
+
throw new Error(lastError ?? "Adapter retry exhausted");
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Preview whether an `enforce`-mode breaker would refuse the next attempt.
|
|
674
|
+
* Returns the refusing admission, or undefined when the retry may proceed.
|
|
675
|
+
*/
|
|
676
|
+
function refusedRetry(breaker) {
|
|
677
|
+
if (!breaker) return void 0;
|
|
678
|
+
const preview = breaker.check();
|
|
679
|
+
return preview.allowed ? void 0 : preview;
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
exports.AdapterCircuitBreakerRegistry = AdapterCircuitBreakerRegistry;
|
|
683
|
+
exports.AdapterCircuitOpenError = AdapterCircuitOpenError;
|
|
684
|
+
exports.AdapterRegistry = AdapterRegistry;
|
|
685
|
+
exports.InMemoryAdapterCircuitBreaker = InMemoryAdapterCircuitBreaker;
|
|
686
|
+
exports.MissingAdapterRegistrationError = MissingAdapterRegistrationError;
|
|
687
|
+
exports.assertAdapterCircuitBreakerConfig = assertAdapterCircuitBreakerConfig;
|
|
688
|
+
exports.assertAdapterOutcomeConsistency = assertAdapterOutcomeConsistency;
|
|
689
|
+
exports.classifyAdapterOutcome = classifyAdapterOutcome;
|
|
690
|
+
exports.classifyCircuitBreakerOutcome = classifyCircuitBreakerOutcome;
|
|
691
|
+
exports.executeWithAdapterRetry = executeWithAdapterRetry;
|
|
692
|
+
exports.getRetryDelayMs = getRetryDelayMs;
|
|
693
|
+
exports.isAdapterCircuitOpenError = isAdapterCircuitOpenError;
|
|
694
|
+
exports.resolveAdapterRetryPolicy = resolveAdapterRetryPolicy;
|
|
695
|
+
exports.scopedAdapterCircuitBreakerKey = scopedAdapterCircuitBreakerKey;
|
|
696
|
+
exports.shouldRetryAdapterOutcome = shouldRetryAdapterOutcome;
|
|
2
697
|
|
|
3
|
-
var chunkAFXKSIVI_cjs = require('../chunk-AFXKSIVI.cjs');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Object.defineProperty(exports, "AdapterCircuitBreakerRegistry", {
|
|
8
|
-
enumerable: true,
|
|
9
|
-
get: function () { return chunkAFXKSIVI_cjs.AdapterCircuitBreakerRegistry; }
|
|
10
|
-
});
|
|
11
|
-
Object.defineProperty(exports, "AdapterCircuitOpenError", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
get: function () { return chunkAFXKSIVI_cjs.AdapterCircuitOpenError; }
|
|
14
|
-
});
|
|
15
|
-
Object.defineProperty(exports, "AdapterRegistry", {
|
|
16
|
-
enumerable: true,
|
|
17
|
-
get: function () { return chunkAFXKSIVI_cjs.AdapterRegistry; }
|
|
18
|
-
});
|
|
19
|
-
Object.defineProperty(exports, "InMemoryAdapterCircuitBreaker", {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
get: function () { return chunkAFXKSIVI_cjs.InMemoryAdapterCircuitBreaker; }
|
|
22
|
-
});
|
|
23
|
-
Object.defineProperty(exports, "MissingAdapterRegistrationError", {
|
|
24
|
-
enumerable: true,
|
|
25
|
-
get: function () { return chunkAFXKSIVI_cjs.MissingAdapterRegistrationError; }
|
|
26
|
-
});
|
|
27
|
-
Object.defineProperty(exports, "assertAdapterCircuitBreakerConfig", {
|
|
28
|
-
enumerable: true,
|
|
29
|
-
get: function () { return chunkAFXKSIVI_cjs.assertAdapterCircuitBreakerConfig; }
|
|
30
|
-
});
|
|
31
|
-
Object.defineProperty(exports, "assertAdapterOutcomeConsistency", {
|
|
32
|
-
enumerable: true,
|
|
33
|
-
get: function () { return chunkAFXKSIVI_cjs.assertAdapterOutcomeConsistency; }
|
|
34
|
-
});
|
|
35
|
-
Object.defineProperty(exports, "classifyAdapterOutcome", {
|
|
36
|
-
enumerable: true,
|
|
37
|
-
get: function () { return chunkAFXKSIVI_cjs.classifyAdapterOutcome; }
|
|
38
|
-
});
|
|
39
|
-
Object.defineProperty(exports, "classifyCircuitBreakerOutcome", {
|
|
40
|
-
enumerable: true,
|
|
41
|
-
get: function () { return chunkAFXKSIVI_cjs.classifyCircuitBreakerOutcome; }
|
|
42
|
-
});
|
|
43
|
-
Object.defineProperty(exports, "executeWithAdapterRetry", {
|
|
44
|
-
enumerable: true,
|
|
45
|
-
get: function () { return chunkAFXKSIVI_cjs.executeWithAdapterRetry; }
|
|
46
|
-
});
|
|
47
|
-
Object.defineProperty(exports, "getRetryDelayMs", {
|
|
48
|
-
enumerable: true,
|
|
49
|
-
get: function () { return chunkAFXKSIVI_cjs.getRetryDelayMs; }
|
|
50
|
-
});
|
|
51
|
-
Object.defineProperty(exports, "isAdapterCircuitOpenError", {
|
|
52
|
-
enumerable: true,
|
|
53
|
-
get: function () { return chunkAFXKSIVI_cjs.isAdapterCircuitOpenError; }
|
|
54
|
-
});
|
|
55
|
-
Object.defineProperty(exports, "resolveAdapterRetryPolicy", {
|
|
56
|
-
enumerable: true,
|
|
57
|
-
get: function () { return chunkAFXKSIVI_cjs.resolveAdapterRetryPolicy; }
|
|
58
|
-
});
|
|
59
|
-
Object.defineProperty(exports, "scopedAdapterCircuitBreakerKey", {
|
|
60
|
-
enumerable: true,
|
|
61
|
-
get: function () { return chunkAFXKSIVI_cjs.scopedAdapterCircuitBreakerKey; }
|
|
62
|
-
});
|
|
63
|
-
Object.defineProperty(exports, "shouldRetryAdapterOutcome", {
|
|
64
|
-
enumerable: true,
|
|
65
|
-
get: function () { return chunkAFXKSIVI_cjs.shouldRetryAdapterOutcome; }
|
|
66
|
-
});
|
|
67
|
-
//# sourceMappingURL=index.cjs.map
|
|
68
698
|
//# sourceMappingURL=index.cjs.map
|