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