@gkoos/caracal 0.1.0 → 0.2.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/CHANGELOG.md +40 -5
- package/README.md +13 -3
- package/dist/chunk-FD7IZJKY.js +84 -0
- package/dist/chunk-FD7IZJKY.js.map +1 -0
- package/dist/{chunk-5CXDW7W6.js → chunk-JV2OLYOF.js} +5 -82
- package/dist/chunk-JV2OLYOF.js.map +1 -0
- package/dist/{circuit-breaker-BSkcV0W_.d.ts → circuit-breaker-xZ8uenT8.d.ts} +7 -2
- package/dist/fetch.d.ts +6 -3
- package/dist/fetch.js +4 -2
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +96 -41
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/redis.d.ts +10 -4
- package/dist/redis.js +15 -1
- package/dist/redis.js.map +1 -1
- package/dist/{retry-BFP_k3Hg.d.ts → retry-DD85oXL9.d.ts} +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-Tf9T76C7.d.ts → types-C-Ml-MKp.d.ts} +7 -0
- package/package.json +9 -5
- package/src/adapters/fetch/retry-after.ts +11 -3
- package/src/coordination/redis/client.ts +15 -1
- package/src/coordination/redis/scripts.ts +20 -3
- package/src/core/bulkhead.ts +20 -3
- package/src/core/circuit-breaker.ts +81 -36
- package/src/core/operation.ts +2 -5
- package/src/core/retry.ts +46 -2
- package/src/core/runtime.ts +26 -5
- package/src/core/timeout.ts +11 -1
- package/src/core/types.ts +8 -0
- package/test/harness/adapter-contract.ts +170 -0
- package/test/harness/index.ts +15 -0
- package/dist/chunk-5CXDW7W6.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 93d34ec: Fixed
|
|
8
|
+
|
|
9
|
+
- a `Retry-After` header larger than `2147483647` ms is now clamped by `retryAfterMs` (and by `createRetryAfterDelay`) instead of producing a wait the platform cannot schedule. Previously a delay composed from the parser could exceed the largest `setTimeout` delay and be rejected by `retry`'s own bounds check, replacing the original error
|
|
10
|
+
- the distributed bulkhead now emits `bulkhead.rejected` with reason `admission-expired` when a permit's lease deadline passes before the call starts, so the rejection is visible to event sinks instead of only to the caller
|
|
11
|
+
- `retry.declined` (`reason: "replay-unsafe" | "not-retryable"`) reports a call that was eligible for retry but not retried - previously indistinguishable from a call with no retry policy. This adds a member to the `OperationEvent` union, so exhaustive switches over `event.type` need a case for it
|
|
12
|
+
- `npm run audit:bundle` now also asserts the `fetch` and `postgres` subpath export sets
|
|
13
|
+
|
|
14
|
+
Changed
|
|
15
|
+
|
|
16
|
+
- documentation corrections: the breaker `classify` signature is `(error, isSuccess) => BreakerOutcome`; the `CoordinatorUnavailableError` claim applies to the Redis coordinator (and says where the symbol is exported from); `development.md` states the real Node floor; the local-breaker identity asymmetry (one instance merges windows) is documented; the ACL table attributes commands to the shipped `bulkheadLeaseV1`; and the events reference documents the error-only rejection reasons, the stripped `outcome` payloads and the fail-closed `state: "open"` signal
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- fc8d667: Fixed
|
|
21
|
+
|
|
22
|
+
- an `async` event sink can no longer crash the process: a sink whose `emit` returns a rejected promise is now observed and dropped, matching the documented promise that a failing sink cannot affect execution (synchronous throws were already contained)
|
|
23
|
+
- `timeout({ ms })` and retry delays now reject durations above `2147483647` ms, which `setTimeout` clamps to 1 ms and would otherwise turn into an immediate timeout or an immediate retry
|
|
24
|
+
- `createCoordinationClusterClient` accepts an optional third argument for connection options, so a secured cluster (ACL credentials, TLS) can be configured through the documented factory while the coordination safeguards stay pinned
|
|
25
|
+
- the events reference no longer claims the distributed bulkhead omits `bulkhead.admitted` and `bulkhead.released` (it emits both, and `bulkhead.released` can carry an `already-expired-or-released` reason), the custom-policy guidance now distinguishes `phase` from array order, and `abort: "unsupported"` is documented as suppressing policy-generated cancellation rather than all cancellation
|
|
26
|
+
|
|
27
|
+
## 0.1.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- cf60c44: Fixed
|
|
32
|
+
|
|
33
|
+
- `failureThreshold` values that the distributed coordinator cannot resolve to thousandths are rejected at construction instead of being silently reinterpreted. Below `0.0005` the numerator rounded to 0, which made the comparison unconditionally true - the breaker opened on a success-only window and re-opened after every recovery; at `0.9995` and above it rounded to 1000, requiring every observation to fail, so the breaker effectively never opened
|
|
34
|
+
- `circuitBreaker.local` enforces the same `0.0005 <= failureThreshold < 0.9995` range, so one policy config behaves the same under either coordination
|
|
35
|
+
- an ignored result recorded nothing but also settled nothing, so a distributed half-open probe kept its slot until the probe lease elapsed: after an operation whose classifier returned `ignored`, the next call was rejected with `CircuitOpenError` for up to `probeLeaseTtlMs` (60s with the defaults), while the local breaker released its slot immediately. An ignored probe result now releases its slot without recording an outcome or advancing recovery, so one policy config behaves the same under either coordination
|
|
36
|
+
- packaging and event fixes: `engines.node` now requires `>=20.3.0` (the runtime, timeout and bulkhead policies, and the fetch adapter use `AbortSignal.any`, which landed in 20.3.0, so 20.0-20.2 installed cleanly and then failed at runtime); the published package now also ships `test/harness`, so the `@gkoos/caracal/testing` entry point has published source like every other entry point; and a local bulkhead's `bulkhead.released` event now reports its occupancy before the queued successor is admitted, instead of appearing to include it
|
|
37
|
+
|
|
38
|
+
## 0.1.0 - 2026-09-09
|
|
39
|
+
|
|
40
|
+
Initial public release.
|
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
<img src="docs/caracal.svg" alt="Caracal" width="140">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
[](https://scorecard.dev/viewer/?uri=github.com/gkoos/caracal)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
|
|
5
15
|
# Caracal
|
|
6
16
|
|
|
7
17
|
**Scoped distributed resilience for asynchronous operations.**
|
|
@@ -183,7 +193,7 @@ const sharedCapacity = bulkhead.distributed({
|
|
|
183
193
|
})
|
|
184
194
|
```
|
|
185
195
|
|
|
186
|
-
Permits are held for the duration of the underlying adapter call only, not for the caller's wait or the retry loop. **A distributed bulkhead rejects immediately when full, there is no distributed queue**. On coordinator loss, admission always fails closed with `CoordinatorUnavailableError`.
|
|
196
|
+
Permits are held for the duration of the underlying adapter call only, not for the caller's wait or the retry loop. **A distributed bulkhead rejects immediately when full, there is no distributed queue**. On coordinator loss, admission always fails closed - with the Redis coordinator that is a `CoordinatorUnavailableError`, exported from `@gkoos/caracal/redis`.
|
|
187
197
|
|
|
188
198
|
#### Circuit breaker
|
|
189
199
|
|
|
@@ -228,7 +238,7 @@ Use stable, non-secret values, scope keys are observable in the Redis keyspace.
|
|
|
228
238
|
|
|
229
239
|
### Events and observability
|
|
230
240
|
|
|
231
|
-
Every operation accepts an `events` sink - or an array of sinks - that receives structured events from every policy decision. Sinks are output-only and isolated:
|
|
241
|
+
Every operation accepts an `events` sink - or an array of sinks - that receives structured events from every policy decision. Sinks are output-only and isolated: a synchronous throw and a rejected promise are both dropped, so a failing sink cannot affect execution and an `async` sink is allowed (though never awaited).
|
|
232
242
|
|
|
233
243
|
```ts
|
|
234
244
|
const op = operation({
|
|
@@ -256,7 +266,7 @@ There are many libraries for applying resilience policies to operations, but mos
|
|
|
256
266
|
|
|
257
267
|
## Development
|
|
258
268
|
|
|
259
|
-
Requires Node.js 20+.
|
|
269
|
+
Requires Node.js 20.3+.
|
|
260
270
|
|
|
261
271
|
```sh
|
|
262
272
|
npm install
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/core/runtime.ts
|
|
2
|
+
var eventSinks = /* @__PURE__ */ Symbol("caracal.eventSinks");
|
|
3
|
+
var admissionSignals = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
function admissionSignal(context) {
|
|
5
|
+
const admission = admissionSignals.get(context);
|
|
6
|
+
return admission && context.signal ? AbortSignal.any([admission, context.signal]) : admission ?? context.signal;
|
|
7
|
+
}
|
|
8
|
+
function withAdmissionSignal(context, signal) {
|
|
9
|
+
const derived = attachRuntime(
|
|
10
|
+
{ ...context },
|
|
11
|
+
runtimeContext(context)[eventSinks] ?? []
|
|
12
|
+
);
|
|
13
|
+
const previous = admissionSignal(context);
|
|
14
|
+
admissionSignals.set(
|
|
15
|
+
derived,
|
|
16
|
+
previous ? AbortSignal.any([previous, signal]) : signal
|
|
17
|
+
);
|
|
18
|
+
return derived;
|
|
19
|
+
}
|
|
20
|
+
function inheritAdmission(source, target) {
|
|
21
|
+
const signal = admissionSignals.get(source);
|
|
22
|
+
if (signal) admissionSignals.set(target, signal);
|
|
23
|
+
return target;
|
|
24
|
+
}
|
|
25
|
+
function runtimeContext(context) {
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
function attachRuntime(values, sinks) {
|
|
29
|
+
const context = values;
|
|
30
|
+
Object.defineProperty(context, eventSinks, { value: sinks });
|
|
31
|
+
return Object.freeze(context);
|
|
32
|
+
}
|
|
33
|
+
function createExecutionContext(values, sinks) {
|
|
34
|
+
return attachRuntime({ attempt: 1, ...values }, sinks);
|
|
35
|
+
}
|
|
36
|
+
function nextAttempt(context) {
|
|
37
|
+
return inheritAdmission(
|
|
38
|
+
context,
|
|
39
|
+
attachRuntime(
|
|
40
|
+
{ ...context, attempt: context.attempt + 1 },
|
|
41
|
+
runtimeContext(context)[eventSinks] ?? []
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function withSignal(context, signal) {
|
|
46
|
+
return inheritAdmission(
|
|
47
|
+
context,
|
|
48
|
+
attachRuntime(
|
|
49
|
+
{ ...context, signal },
|
|
50
|
+
runtimeContext(context)[eventSinks] ?? []
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
var MAX_TIMER_MS = 2147483647;
|
|
55
|
+
function emitToSink(sink, event) {
|
|
56
|
+
try {
|
|
57
|
+
const pending = sink.emit(event);
|
|
58
|
+
const thenable = pending;
|
|
59
|
+
if (typeof thenable?.catch === "function") {
|
|
60
|
+
void pending.catch(() => {
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function emitRuntimeEvent(context, event) {
|
|
67
|
+
const sinks = runtimeContext(context)[eventSinks] ?? [];
|
|
68
|
+
const fullEvent = { ...event, at: Date.now(), context };
|
|
69
|
+
for (const sink of sinks) {
|
|
70
|
+
emitToSink(sink, fullEvent);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function createClassifier(classify) {
|
|
74
|
+
return (outcome) => {
|
|
75
|
+
if (classify === void 0) {
|
|
76
|
+
return outcome.status === "success" ? "success" : "failure";
|
|
77
|
+
}
|
|
78
|
+
return classify(outcome);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { MAX_TIMER_MS, admissionSignal, createClassifier, createExecutionContext, emitRuntimeEvent, emitToSink, nextAttempt, withAdmissionSignal, withSignal };
|
|
83
|
+
//# sourceMappingURL=chunk-FD7IZJKY.js.map
|
|
84
|
+
//# sourceMappingURL=chunk-FD7IZJKY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/runtime.ts"],"names":[],"mappings":"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","file":"chunk-FD7IZJKY.js","sourcesContent":["import type {\n Classification,\n EventSink,\n ExecutionContext,\n OperationEvent,\n Outcome,\n OutcomeClassifier,\n} from \"./types.js\"\n\nconst eventSinks = Symbol(\"caracal.eventSinks\")\nconst admissionSignals = new WeakMap<ExecutionContext, AbortSignal>()\nexport function admissionSignal(\n context: ExecutionContext,\n): AbortSignal | undefined {\n const admission = admissionSignals.get(context)\n return admission && context.signal\n ? AbortSignal.any([admission, context.signal])\n : (admission ?? context.signal)\n}\nexport function withAdmissionSignal(\n context: ExecutionContext,\n signal: AbortSignal,\n): ExecutionContext {\n const derived = attachRuntime(\n { ...context },\n runtimeContext(context)[eventSinks] ?? [],\n )\n const previous = admissionSignal(context)\n admissionSignals.set(\n derived,\n previous ? AbortSignal.any([previous, signal]) : signal,\n )\n return derived\n}\nfunction inheritAdmission(\n source: ExecutionContext,\n target: ExecutionContext,\n): ExecutionContext {\n const signal = admissionSignals.get(source)\n if (signal) admissionSignals.set(target, signal)\n return target\n}\n\ntype EventWithoutRuntimeFields = OperationEvent extends infer Event\n ? Event extends OperationEvent\n ? Omit<Event, \"at\" | \"context\">\n : never\n : never\n\ntype RuntimeExecutionContext = ExecutionContext & {\n readonly [eventSinks]: readonly EventSink[]\n}\n\nfunction runtimeContext(context: ExecutionContext): RuntimeExecutionContext {\n return context as RuntimeExecutionContext\n}\n\nfunction attachRuntime(\n values: ExecutionContext,\n sinks: readonly EventSink[],\n): ExecutionContext {\n const context = values as RuntimeExecutionContext\n Object.defineProperty(context, eventSinks, { value: sinks })\n return Object.freeze(context)\n}\n\nexport function createExecutionContext(\n values: Omit<ExecutionContext, \"attempt\">,\n sinks: readonly EventSink[],\n): ExecutionContext {\n return attachRuntime({ attempt: 1, ...values }, sinks)\n}\n\nexport function nextAttempt(context: ExecutionContext): ExecutionContext {\n return inheritAdmission(\n context,\n attachRuntime(\n { ...context, attempt: context.attempt + 1 },\n runtimeContext(context)[eventSinks] ?? [],\n ),\n )\n}\n\nexport function withSignal(\n context: ExecutionContext,\n signal: AbortSignal | undefined,\n): ExecutionContext {\n return inheritAdmission(\n context,\n attachRuntime(\n { ...context, signal },\n runtimeContext(context)[eventSinks] ?? [],\n ),\n )\n}\n\n/**\n * Largest delay `setTimeout` honours. Anything above it is silently clamped to\n * 1 ms by the platform, so accepting larger values would turn a long wait into\n * an immediate one.\n */\nexport const MAX_TIMER_MS = 2_147_483_647\n\n/**\n * Delivers one event to one sink, isolating execution from it. Sinks are\n * fire-and-forget: a synchronous throw and a rejected promise are both dropped,\n * so a failing sink can neither modify resilience execution nor surface as an\n * unhandled rejection.\n */\nexport function emitToSink(sink: EventSink, event: OperationEvent): void {\n try {\n const pending = sink.emit(event) as unknown\n const thenable = pending as { catch?: unknown } | null | undefined\n if (typeof thenable?.catch === \"function\") {\n void (pending as Promise<unknown>).catch(() => {})\n }\n } catch {\n // See above: a failing sink must never modify execution.\n }\n}\n\nexport function emitRuntimeEvent(\n context: ExecutionContext,\n event: EventWithoutRuntimeFields,\n): void {\n const sinks = runtimeContext(context)[eventSinks] ?? []\n const fullEvent = { ...event, at: Date.now(), context } as OperationEvent\n\n for (const sink of sinks) {\n emitToSink(sink, fullEvent)\n }\n}\n\nexport function createClassifier<Result>(\n classify: ((outcome: Outcome<Result>) => Classification) | undefined,\n): OutcomeClassifier {\n return (outcome) => {\n if (classify === undefined) {\n return outcome.status === \"success\" ? \"success\" : \"failure\"\n }\n\n return classify(outcome as Outcome<Result>)\n }\n}\n"]}
|
|
@@ -1,80 +1,6 @@
|
|
|
1
|
+
import { createExecutionContext, createClassifier, admissionSignal, emitToSink } from './chunk-FD7IZJKY.js';
|
|
1
2
|
import { randomUUID } from 'crypto';
|
|
2
3
|
|
|
3
|
-
// src/core/operation.ts
|
|
4
|
-
|
|
5
|
-
// src/core/runtime.ts
|
|
6
|
-
var eventSinks = /* @__PURE__ */ Symbol("caracal.eventSinks");
|
|
7
|
-
var admissionSignals = /* @__PURE__ */ new WeakMap();
|
|
8
|
-
function admissionSignal(context) {
|
|
9
|
-
const admission = admissionSignals.get(context);
|
|
10
|
-
return admission && context.signal ? AbortSignal.any([admission, context.signal]) : admission ?? context.signal;
|
|
11
|
-
}
|
|
12
|
-
function withAdmissionSignal(context, signal) {
|
|
13
|
-
const derived = attachRuntime(
|
|
14
|
-
{ ...context },
|
|
15
|
-
runtimeContext(context)[eventSinks] ?? []
|
|
16
|
-
);
|
|
17
|
-
const previous = admissionSignal(context);
|
|
18
|
-
admissionSignals.set(
|
|
19
|
-
derived,
|
|
20
|
-
previous ? AbortSignal.any([previous, signal]) : signal
|
|
21
|
-
);
|
|
22
|
-
return derived;
|
|
23
|
-
}
|
|
24
|
-
function inheritAdmission(source, target) {
|
|
25
|
-
const signal = admissionSignals.get(source);
|
|
26
|
-
if (signal) admissionSignals.set(target, signal);
|
|
27
|
-
return target;
|
|
28
|
-
}
|
|
29
|
-
function runtimeContext(context) {
|
|
30
|
-
return context;
|
|
31
|
-
}
|
|
32
|
-
function attachRuntime(values, sinks) {
|
|
33
|
-
const context = values;
|
|
34
|
-
Object.defineProperty(context, eventSinks, { value: sinks });
|
|
35
|
-
return Object.freeze(context);
|
|
36
|
-
}
|
|
37
|
-
function createExecutionContext(values, sinks) {
|
|
38
|
-
return attachRuntime({ attempt: 1, ...values }, sinks);
|
|
39
|
-
}
|
|
40
|
-
function nextAttempt(context) {
|
|
41
|
-
return inheritAdmission(
|
|
42
|
-
context,
|
|
43
|
-
attachRuntime(
|
|
44
|
-
{ ...context, attempt: context.attempt + 1 },
|
|
45
|
-
runtimeContext(context)[eventSinks] ?? []
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
function withSignal(context, signal) {
|
|
50
|
-
return inheritAdmission(
|
|
51
|
-
context,
|
|
52
|
-
attachRuntime(
|
|
53
|
-
{ ...context, signal },
|
|
54
|
-
runtimeContext(context)[eventSinks] ?? []
|
|
55
|
-
)
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
function emitRuntimeEvent(context, event) {
|
|
59
|
-
const sinks = runtimeContext(context)[eventSinks] ?? [];
|
|
60
|
-
const fullEvent = { ...event, at: Date.now(), context };
|
|
61
|
-
for (const sink of sinks) {
|
|
62
|
-
try {
|
|
63
|
-
sink.emit(fullEvent);
|
|
64
|
-
} catch {
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function createClassifier(classify) {
|
|
69
|
-
return (outcome) => {
|
|
70
|
-
if (classify === void 0) {
|
|
71
|
-
return outcome.status === "success" ? "success" : "failure";
|
|
72
|
-
}
|
|
73
|
-
return classify(outcome);
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/core/operation.ts
|
|
78
4
|
var summarizeSuccess = () => ({
|
|
79
5
|
status: "success",
|
|
80
6
|
value: void 0
|
|
@@ -97,10 +23,7 @@ function normalizeSinks(events) {
|
|
|
97
23
|
}
|
|
98
24
|
function emit(sinks, event) {
|
|
99
25
|
for (const sink of sinks) {
|
|
100
|
-
|
|
101
|
-
sink.emit(event);
|
|
102
|
-
} catch {
|
|
103
|
-
}
|
|
26
|
+
emitToSink(sink, event);
|
|
104
27
|
}
|
|
105
28
|
}
|
|
106
29
|
function validateName(name, kind) {
|
|
@@ -197,6 +120,6 @@ function operation(options) {
|
|
|
197
120
|
});
|
|
198
121
|
}
|
|
199
122
|
|
|
200
|
-
export {
|
|
201
|
-
//# sourceMappingURL=chunk-
|
|
202
|
-
//# sourceMappingURL=chunk-
|
|
123
|
+
export { operation };
|
|
124
|
+
//# sourceMappingURL=chunk-JV2OLYOF.js.map
|
|
125
|
+
//# sourceMappingURL=chunk-JV2OLYOF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/operation.ts"],"names":[],"mappings":"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","file":"chunk-JV2OLYOF.js","sourcesContent":["import { randomUUID } from \"node:crypto\"\n\nimport {\n admissionSignal,\n createClassifier,\n createExecutionContext,\n emitToSink,\n} from \"./runtime.js\"\nimport type {\n Adapter,\n EventSink,\n EventSinks,\n ExecutionMetadata,\n Next,\n Operation,\n OperationCapabilities,\n OperationEvent,\n OperationExecuteOptions,\n OperationOptions,\n Outcome,\n Policy,\n} from \"./types.js\"\n\nconst summarizeSuccess = (): Outcome<undefined> => ({\n status: \"success\",\n value: undefined,\n})\nconst summarizeFailure = (error: unknown): Outcome<undefined> => ({\n status: \"failure\",\n error,\n})\n\nfunction immutableCapabilities(\n capabilities: OperationCapabilities,\n): OperationCapabilities {\n return Object.freeze({ ...capabilities })\n}\n\nfunction immutableMetadata(\n metadata: Readonly<Record<string, unknown>> | undefined,\n): ExecutionMetadata {\n return Object.freeze({ ...(metadata ?? {}) })\n}\n\nfunction normalizeSinks(events: EventSinks | undefined): readonly EventSink[] {\n if (events === undefined) {\n return []\n }\n\n return \"emit\" in events ? [events] : events\n}\n\nfunction emit(sinks: readonly EventSink[], event: OperationEvent): void {\n for (const sink of sinks) {\n emitToSink(sink, event)\n }\n}\n\nfunction validateName(name: string, kind: \"operation\" | \"policy\"): void {\n if (name.trim().length === 0) {\n throw new Error(`${kind} name must not be empty`)\n }\n}\n\nfunction createPipeline<Result>(\n policies: readonly Policy[],\n adapter: Next<Result>,\n): Next<Result> {\n return policies.reduceRight<Next<Result>>(\n (next, policy) => async (context) => policy.execute(context, next),\n adapter,\n )\n}\n\nfunction invokeAdapter<Args, Result>(\n adapter: Adapter<Args, Result>,\n args: Args,\n sinks: readonly EventSink[],\n): Next<Result> {\n return async (context) => {\n admissionSignal(context)?.throwIfAborted()\n emit(sinks, { type: \"attempt.started\", at: Date.now(), context })\n\n try {\n const value = await adapter.execute(args, context)\n const outcome: Outcome<Result> = { status: \"success\", value }\n const classification = context.classify(outcome)\n emit(sinks, {\n type: \"attempt.settled\",\n at: Date.now(),\n context,\n outcome: summarizeSuccess(),\n classification,\n })\n return value\n } catch (error) {\n const outcome: Outcome<Result> = { status: \"failure\", error }\n const classification = context.classify(outcome)\n emit(sinks, {\n type: \"attempt.settled\",\n at: Date.now(),\n context,\n outcome: summarizeFailure(error),\n classification,\n })\n throw error\n }\n }\n}\n\n/** Creates a named, protocol-agnostic operation. */\nexport function operation<Args, Result>(\n options: OperationOptions<Args, Result>,\n): Operation<Args, Result> {\n validateName(options.name, \"operation\")\n for (const policy of options.policies ?? []) {\n validateName(policy.name, \"policy\")\n }\n\n const policies = Object.freeze([...(options.policies ?? [])])\n const sinks = Object.freeze(normalizeSinks(options.events))\n\n return Object.freeze({\n name: options.name,\n async execute(\n args: Args,\n executeOptions: OperationExecuteOptions = {},\n ): Promise<Result> {\n const capabilities = options.adapter.capabilities(args)\n const context = createExecutionContext(\n {\n operationName: options.name,\n executionId: executeOptions.executionId ?? randomUUID(),\n signal: executeOptions.signal,\n metadata: immutableMetadata(executeOptions.metadata),\n capabilities: immutableCapabilities(capabilities),\n classify: createClassifier(options.adapter.classify),\n },\n sinks,\n )\n const adapter = createPipeline(\n policies.filter((policy) => policy.phase === \"attempt\"),\n invokeAdapter(options.adapter, args, sinks),\n )\n const pipeline = createPipeline(\n policies.filter((policy) => policy.phase !== \"attempt\"),\n adapter,\n )\n\n emit(sinks, { type: \"execution.started\", at: Date.now(), context })\n try {\n const value = await pipeline(context)\n emit(sinks, {\n type: \"execution.settled\",\n at: Date.now(),\n context,\n outcome: summarizeSuccess(),\n })\n return value\n } catch (error) {\n emit(sinks, {\n type: \"execution.settled\",\n at: Date.now(),\n context,\n outcome: summarizeFailure(error),\n })\n throw error\n }\n },\n })\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as ExecutionContext, P as Policy } from './types-
|
|
1
|
+
import { d as ExecutionContext, P as Policy } from './types-C-Ml-MKp.js';
|
|
2
2
|
|
|
3
3
|
declare class BulkheadRejectedError extends Error {
|
|
4
4
|
readonly coordination: "local" | "distributed";
|
|
@@ -185,7 +185,12 @@ interface BreakerCoordinator {
|
|
|
185
185
|
}): Promise<AdmitProbeResult>;
|
|
186
186
|
settleProbe(identity: BreakerIdentity, params: {
|
|
187
187
|
readonly probeToken: string;
|
|
188
|
-
|
|
188
|
+
/**
|
|
189
|
+
* `"success"` and `"failure"` record an outcome; `"ignored"` releases the
|
|
190
|
+
* probe's slot without recording anything, which is what the policy sends
|
|
191
|
+
* when its classifier ignores the result.
|
|
192
|
+
*/
|
|
193
|
+
readonly outcome: "success" | "failure" | "ignored";
|
|
189
194
|
readonly generation: number;
|
|
190
195
|
readonly halfOpenSuccesses: number;
|
|
191
196
|
readonly openMs: number;
|
package/dist/fetch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as OperationCapabilities, C as Classification, A as Adapter } from './types-
|
|
2
|
-
import { R as RetryContext } from './retry-
|
|
1
|
+
import { O as OperationCapabilities, C as Classification, A as Adapter } from './types-C-Ml-MKp.js';
|
|
2
|
+
import { R as RetryContext } from './retry-DD85oXL9.js';
|
|
3
3
|
|
|
4
4
|
type FetchOperationArgs = Readonly<{
|
|
5
5
|
url: RequestInfo | URL;
|
|
@@ -25,7 +25,10 @@ declare function fetchAdapter(options?: FetchAdapterOptions): Adapter<FetchOpera
|
|
|
25
25
|
* response-bearing error.
|
|
26
26
|
*
|
|
27
27
|
* Returns `undefined` when no usable header is present. Malformed values
|
|
28
|
-
* are ignored rather than thrown.
|
|
28
|
+
* are ignored rather than thrown. Values above `MAX_TIMER_MS` are clamped,
|
|
29
|
+
* because `setTimeout` cannot schedule them and would silently fire almost
|
|
30
|
+
* immediately; `createRetryAfterDelay` applies the same ceiling to the wait it
|
|
31
|
+
* returns, so a delay composed from this parser stays schedulable.
|
|
29
32
|
*/
|
|
30
33
|
declare function retryAfterMs(context: RetryContext, now?: number): number | undefined;
|
|
31
34
|
/** Options for `createRetryAfterDelay`. */
|
package/dist/fetch.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MAX_TIMER_MS } from './chunk-FD7IZJKY.js';
|
|
2
|
+
|
|
1
3
|
// src/adapters/fetch/adapter.ts
|
|
2
4
|
function requestMethod(args) {
|
|
3
5
|
if (args.options?.method !== void 0) {
|
|
@@ -83,10 +85,10 @@ function retryAfterMs(context, now = Date.now()) {
|
|
|
83
85
|
if (!value) return void 0;
|
|
84
86
|
if (deltaSecondsPattern.test(value)) {
|
|
85
87
|
const seconds = Number(value);
|
|
86
|
-
return Number.isSafeInteger(seconds) ? seconds * 1e3 : void 0;
|
|
88
|
+
return Number.isSafeInteger(seconds) ? Math.min(seconds * 1e3, MAX_TIMER_MS) : void 0;
|
|
87
89
|
}
|
|
88
90
|
const timestamp = Date.parse(value);
|
|
89
|
-
return Number.isNaN(timestamp) ? void 0 : Math.max(0, timestamp - now);
|
|
91
|
+
return Number.isNaN(timestamp) ? void 0 : Math.min(Math.max(0, timestamp - now), MAX_TIMER_MS);
|
|
90
92
|
}
|
|
91
93
|
function createRetryAfterDelay(options = {}) {
|
|
92
94
|
const baseMs = options.baseMs ?? DEFAULTS.baseMs;
|
package/dist/fetch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/fetch/adapter.ts","../src/adapters/fetch/retry-after.ts"],"names":[],"mappings":"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","file":"fetch.js","sourcesContent":["import type {\n Adapter,\n Classification,\n ExecutionContext,\n OperationCapabilities,\n Outcome,\n} from \"../../core/types.js\"\n\nexport type FetchOperationArgs = Readonly<{\n url: RequestInfo | URL\n options?: RequestInit\n}>\n\nexport type FetchReplay =\n | OperationCapabilities[\"replay\"]\n | ((args: FetchOperationArgs) => OperationCapabilities[\"replay\"])\n\nexport interface FetchAdapterOptions {\n readonly fetch?: typeof globalThis.fetch\n readonly replay?: FetchReplay\n readonly classifyResponse?: (response: Response) => Classification\n readonly classifyError?: (error: unknown) => Classification\n}\n\nfunction requestMethod(args: FetchOperationArgs): string {\n if (args.options?.method !== undefined) {\n return args.options.method.toUpperCase()\n }\n\n if (args.url instanceof Request) {\n return args.url.method.toUpperCase()\n }\n\n return \"GET\"\n}\n\nfunction defaultReplay(\n args: FetchOperationArgs,\n): OperationCapabilities[\"replay\"] {\n const method = requestMethod(args)\n if (method === \"GET\" || method === \"HEAD\") {\n return \"safe\"\n }\n\n if (method === \"POST\" || method === \"PATCH\") {\n return \"unsafe\"\n }\n\n return \"unknown\"\n}\n\nfunction defaultResponseClassification(response: Response): Classification {\n return response.status >= 500 ||\n response.status === 408 ||\n response.status === 429\n ? \"retryable\"\n : \"success\"\n}\n\nfunction urlSignal(url: RequestInfo | URL): AbortSignal | undefined {\n return url instanceof Request ? url.signal : undefined\n}\n\nfunction combinedSignal(\n first: AbortSignal | undefined,\n second: AbortSignal | undefined,\n third: AbortSignal | undefined,\n): AbortSignal | undefined {\n const signals = [first, second, third].filter(\n (signal): signal is AbortSignal => signal !== undefined,\n )\n if (signals.length === 0) {\n return undefined\n }\n\n return signals.length === 1 ? signals[0] : AbortSignal.any(signals)\n}\n\n/**\n * Creates a fetch adapter with conservative per-request replay traits.\n * It performs no implicit retry or timeout; those remain operation policies.\n */\nexport function fetchAdapter(\n options: FetchAdapterOptions = {},\n): Adapter<FetchOperationArgs, Response> {\n const fetchImplementation = options.fetch ?? globalThis.fetch\n if (fetchImplementation === undefined) {\n throw new Error(\"fetch is not available; provide FetchAdapterOptions.fetch\")\n }\n\n return Object.freeze({\n capabilities(args: FetchOperationArgs): OperationCapabilities {\n const configured = options.replay\n const replay =\n typeof configured === \"function\"\n ? configured(args)\n : (configured ?? defaultReplay(args))\n return { abort: \"supported\", replay }\n },\n async execute(\n args: FetchOperationArgs,\n context: ExecutionContext,\n ): Promise<Response> {\n const signal = combinedSignal(\n urlSignal(args.url),\n args.options?.signal ?? undefined,\n context.signal,\n )\n return fetchImplementation(args.url, { ...args.options, signal })\n },\n classify(outcome: Outcome<Response>): Classification {\n if (outcome.status === \"failure\") {\n return options.classifyError?.(outcome.error) ?? \"retryable\"\n }\n\n return (\n options.classifyResponse?.(outcome.value) ??\n defaultResponseClassification(outcome.value)\n )\n },\n })\n}\n","import type { RetryContext } from \"../../core/retry.js\"\n\n/**\n * Opt-in retry pacing for `@gkoos/caracal/fetch` that honours the HTTP\n * `Retry-After` header. It is protocol-specific, so it lives with the\n * adapter rather than in the protocol-agnostic core.\n */\n\nconst DEFAULTS = {\n baseMs: 100,\n factor: 2,\n maxDelayMs: 30_000,\n jitterRatio: 0.1,\n} as const\n\nconst deltaSecondsPattern = /^\\d+$/\n\n/**\n * Structurally extracts a `Headers`-like object. `instanceof Response` is\n * unreliable across realms and custom fetch implementations.\n */\nfunction headersOf(value: unknown): Headers | undefined {\n if (typeof value !== \"object\" || value === null) return undefined\n const headers = (value as { headers?: unknown }).headers\n if (typeof headers !== \"object\" || headers === null) return undefined\n const get = (headers as { get?: unknown }).get\n return typeof get === \"function\" ? (headers as Headers) : undefined\n}\n\n/**\n * Parses `Retry-After` (delta-seconds or HTTP-date) from a settled fetch\n * outcome. Reads the response from `context.result`, or from\n * `context.error` when a custom fetch implementation throws a\n * response-bearing error.\n *\n * Returns `undefined` when no usable header is present. Malformed values\n * are ignored rather than thrown.\n */\nexport function retryAfterMs(\n context: RetryContext,\n now: number = Date.now(),\n): number | undefined {\n const headers = headersOf(context.result) ?? headersOf(context.error)\n if (headers === undefined) return undefined\n\n const value = headers.get(\"retry-after\")?.trim()\n if (!value) return undefined\n\n if (deltaSecondsPattern.test(value)) {\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds * 1000 : undefined\n }\n\n const timestamp = Date.parse(value)\n return Number.isNaN(timestamp) ? undefined : Math.max(0, timestamp - now)\n}\n\n/** Options for `createRetryAfterDelay`. */\nexport interface RetryAfterDelayOptions {\n /** Base of the exponential backoff in ms. Default: 100. */\n readonly baseMs?: number\n /** Exponential growth factor (>= 1). Default: 2. */\n readonly factor?: number\n /** Ceiling applied to the deterministic wait in ms. Default: 30 000. */\n readonly maxDelayMs?: number\n /** Additive jitter as a fraction of the wait, within [0, 1]. Default: 0.1. */\n readonly jitterRatio?: number\n}\n\n/** A `RetryDelay` that reads `Retry-After` from the settled outcome. */\nexport type RetryAfterDelay = (attempt: number, context: RetryContext) => number\n\n/**\n * Builds a `RetryDelay` that waits for the longer of exponential backoff\n * and the `Retry-After` the server sent, then adds additive jitter.\n *\n * Jitter only ever lengthens the wait, so a server-provided minimum is\n * never retried early.\n */\nexport function createRetryAfterDelay(\n options: RetryAfterDelayOptions = {},\n): RetryAfterDelay {\n const baseMs = options.baseMs ?? DEFAULTS.baseMs\n const factor = options.factor ?? DEFAULTS.factor\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs\n const jitterRatio = options.jitterRatio ?? DEFAULTS.jitterRatio\n\n if (!Number.isFinite(baseMs) || baseMs < 0)\n throw new RangeError(\"retryAfterDelay baseMs must be finite and >= 0\")\n if (!Number.isFinite(factor) || factor < 1)\n throw new RangeError(\"retryAfterDelay factor must be finite and >= 1\")\n if (!Number.isFinite(maxDelayMs) || maxDelayMs < 0)\n throw new RangeError(\"retryAfterDelay maxDelayMs must be finite and >= 0\")\n if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1)\n throw new RangeError(\"retryAfterDelay jitterRatio must be within [0, 1]\")\n\n return (attempt, context) => {\n const backoff = Math.min(baseMs * factor ** (attempt - 1), maxDelayMs)\n const base = Math.min(\n Math.max(backoff, retryAfterMs(context) ?? 0),\n maxDelayMs,\n )\n return base + Math.random() * base * jitterRatio\n }\n}\n\n/**\n * Ready-to-use default: `retry({ maxAttempts: 3, delay: retryAfterDelay })`.\n * Use `createRetryAfterDelay()` to change the pacing.\n */\nexport const retryAfterDelay: RetryAfterDelay = createRetryAfterDelay()\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapters/fetch/adapter.ts","../src/adapters/fetch/retry-after.ts"],"names":[],"mappings":"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","file":"fetch.js","sourcesContent":["import type {\n Adapter,\n Classification,\n ExecutionContext,\n OperationCapabilities,\n Outcome,\n} from \"../../core/types.js\"\n\nexport type FetchOperationArgs = Readonly<{\n url: RequestInfo | URL\n options?: RequestInit\n}>\n\nexport type FetchReplay =\n | OperationCapabilities[\"replay\"]\n | ((args: FetchOperationArgs) => OperationCapabilities[\"replay\"])\n\nexport interface FetchAdapterOptions {\n readonly fetch?: typeof globalThis.fetch\n readonly replay?: FetchReplay\n readonly classifyResponse?: (response: Response) => Classification\n readonly classifyError?: (error: unknown) => Classification\n}\n\nfunction requestMethod(args: FetchOperationArgs): string {\n if (args.options?.method !== undefined) {\n return args.options.method.toUpperCase()\n }\n\n if (args.url instanceof Request) {\n return args.url.method.toUpperCase()\n }\n\n return \"GET\"\n}\n\nfunction defaultReplay(\n args: FetchOperationArgs,\n): OperationCapabilities[\"replay\"] {\n const method = requestMethod(args)\n if (method === \"GET\" || method === \"HEAD\") {\n return \"safe\"\n }\n\n if (method === \"POST\" || method === \"PATCH\") {\n return \"unsafe\"\n }\n\n return \"unknown\"\n}\n\nfunction defaultResponseClassification(response: Response): Classification {\n return response.status >= 500 ||\n response.status === 408 ||\n response.status === 429\n ? \"retryable\"\n : \"success\"\n}\n\nfunction urlSignal(url: RequestInfo | URL): AbortSignal | undefined {\n return url instanceof Request ? url.signal : undefined\n}\n\nfunction combinedSignal(\n first: AbortSignal | undefined,\n second: AbortSignal | undefined,\n third: AbortSignal | undefined,\n): AbortSignal | undefined {\n const signals = [first, second, third].filter(\n (signal): signal is AbortSignal => signal !== undefined,\n )\n if (signals.length === 0) {\n return undefined\n }\n\n return signals.length === 1 ? signals[0] : AbortSignal.any(signals)\n}\n\n/**\n * Creates a fetch adapter with conservative per-request replay traits.\n * It performs no implicit retry or timeout; those remain operation policies.\n */\nexport function fetchAdapter(\n options: FetchAdapterOptions = {},\n): Adapter<FetchOperationArgs, Response> {\n const fetchImplementation = options.fetch ?? globalThis.fetch\n if (fetchImplementation === undefined) {\n throw new Error(\"fetch is not available; provide FetchAdapterOptions.fetch\")\n }\n\n return Object.freeze({\n capabilities(args: FetchOperationArgs): OperationCapabilities {\n const configured = options.replay\n const replay =\n typeof configured === \"function\"\n ? configured(args)\n : (configured ?? defaultReplay(args))\n return { abort: \"supported\", replay }\n },\n async execute(\n args: FetchOperationArgs,\n context: ExecutionContext,\n ): Promise<Response> {\n const signal = combinedSignal(\n urlSignal(args.url),\n args.options?.signal ?? undefined,\n context.signal,\n )\n return fetchImplementation(args.url, { ...args.options, signal })\n },\n classify(outcome: Outcome<Response>): Classification {\n if (outcome.status === \"failure\") {\n return options.classifyError?.(outcome.error) ?? \"retryable\"\n }\n\n return (\n options.classifyResponse?.(outcome.value) ??\n defaultResponseClassification(outcome.value)\n )\n },\n })\n}\n","import { MAX_TIMER_MS } from \"../../core/runtime.js\"\nimport type { RetryContext } from \"../../core/retry.js\"\n\n/**\n * Opt-in retry pacing for `@gkoos/caracal/fetch` that honours the HTTP\n * `Retry-After` header. It is protocol-specific, so it lives with the\n * adapter rather than in the protocol-agnostic core.\n */\n\nconst DEFAULTS = {\n baseMs: 100,\n factor: 2,\n maxDelayMs: 30_000,\n jitterRatio: 0.1,\n} as const\n\nconst deltaSecondsPattern = /^\\d+$/\n\n/**\n * Structurally extracts a `Headers`-like object. `instanceof Response` is\n * unreliable across realms and custom fetch implementations.\n */\nfunction headersOf(value: unknown): Headers | undefined {\n if (typeof value !== \"object\" || value === null) return undefined\n const headers = (value as { headers?: unknown }).headers\n if (typeof headers !== \"object\" || headers === null) return undefined\n const get = (headers as { get?: unknown }).get\n return typeof get === \"function\" ? (headers as Headers) : undefined\n}\n\n/**\n * Parses `Retry-After` (delta-seconds or HTTP-date) from a settled fetch\n * outcome. Reads the response from `context.result`, or from\n * `context.error` when a custom fetch implementation throws a\n * response-bearing error.\n *\n * Returns `undefined` when no usable header is present. Malformed values\n * are ignored rather than thrown. Values above `MAX_TIMER_MS` are clamped,\n * because `setTimeout` cannot schedule them and would silently fire almost\n * immediately; `createRetryAfterDelay` applies the same ceiling to the wait it\n * returns, so a delay composed from this parser stays schedulable.\n */\nexport function retryAfterMs(\n context: RetryContext,\n now: number = Date.now(),\n): number | undefined {\n const headers = headersOf(context.result) ?? headersOf(context.error)\n if (headers === undefined) return undefined\n\n const value = headers.get(\"retry-after\")?.trim()\n if (!value) return undefined\n\n if (deltaSecondsPattern.test(value)) {\n const seconds = Number(value)\n return Number.isSafeInteger(seconds)\n ? Math.min(seconds * 1000, MAX_TIMER_MS)\n : undefined\n }\n\n const timestamp = Date.parse(value)\n return Number.isNaN(timestamp)\n ? undefined\n : Math.min(Math.max(0, timestamp - now), MAX_TIMER_MS)\n}\n\n/** Options for `createRetryAfterDelay`. */\nexport interface RetryAfterDelayOptions {\n /** Base of the exponential backoff in ms. Default: 100. */\n readonly baseMs?: number\n /** Exponential growth factor (>= 1). Default: 2. */\n readonly factor?: number\n /** Ceiling applied to the deterministic wait in ms. Default: 30 000. */\n readonly maxDelayMs?: number\n /** Additive jitter as a fraction of the wait, within [0, 1]. Default: 0.1. */\n readonly jitterRatio?: number\n}\n\n/** A `RetryDelay` that reads `Retry-After` from the settled outcome. */\nexport type RetryAfterDelay = (attempt: number, context: RetryContext) => number\n\n/**\n * Builds a `RetryDelay` that waits for the longer of exponential backoff\n * and the `Retry-After` the server sent, then adds additive jitter.\n *\n * Jitter only ever lengthens the wait, so a server-provided minimum is\n * never retried early.\n */\nexport function createRetryAfterDelay(\n options: RetryAfterDelayOptions = {},\n): RetryAfterDelay {\n const baseMs = options.baseMs ?? DEFAULTS.baseMs\n const factor = options.factor ?? DEFAULTS.factor\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs\n const jitterRatio = options.jitterRatio ?? DEFAULTS.jitterRatio\n\n if (!Number.isFinite(baseMs) || baseMs < 0)\n throw new RangeError(\"retryAfterDelay baseMs must be finite and >= 0\")\n if (!Number.isFinite(factor) || factor < 1)\n throw new RangeError(\"retryAfterDelay factor must be finite and >= 1\")\n if (!Number.isFinite(maxDelayMs) || maxDelayMs < 0)\n throw new RangeError(\"retryAfterDelay maxDelayMs must be finite and >= 0\")\n if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1)\n throw new RangeError(\"retryAfterDelay jitterRatio must be within [0, 1]\")\n\n return (attempt, context) => {\n const backoff = Math.min(baseMs * factor ** (attempt - 1), maxDelayMs)\n const base = Math.min(\n Math.max(backoff, retryAfterMs(context) ?? 0),\n maxDelayMs,\n )\n return base + Math.random() * base * jitterRatio\n }\n}\n\n/**\n * Ready-to-use default: `retry({ maxAttempts: 3, delay: retryAfterDelay })`.\n * Use `createRetryAfterDelay()` to change the pacing.\n */\nexport const retryAfterDelay: RetryAfterDelay = createRetryAfterDelay()\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { A as AdmitProbeResult, B as BreakerClassifier, a as BreakerCoordinator, b as BreakerIdentity, c as BreakerOutcome, d as BreakerSnapshot, e as BreakerState, f as BulkheadCoordinator, g as BulkheadRejectedError, C as CircuitOpenError, D as DistributedBreakerOptions, h as DistributedBulkheadOptions, L as LocalBreakerOptions, i as LocalBulkheadOptions, O as ObserveResult, S as SettleProbeResult, j as bulkhead, k as circuitBreaker } from './circuit-breaker-
|
|
2
|
-
import { a as OperationOptions, b as Operation, P as Policy } from './types-
|
|
3
|
-
export { A as Adapter, C as Classification, E as EventSink, c as EventSinks, d as ExecutionContext, e as ExecutionMetadata, N as Next, O as OperationCapabilities, f as OperationEvent, g as OperationExecuteOptions, h as Outcome, i as OutcomeClassifier } from './types-
|
|
4
|
-
export { R as RetryContext, a as RetryDelay, b as RetryOptions, r as retry } from './retry-
|
|
1
|
+
export { A as AdmitProbeResult, B as BreakerClassifier, a as BreakerCoordinator, b as BreakerIdentity, c as BreakerOutcome, d as BreakerSnapshot, e as BreakerState, f as BulkheadCoordinator, g as BulkheadRejectedError, C as CircuitOpenError, D as DistributedBreakerOptions, h as DistributedBulkheadOptions, L as LocalBreakerOptions, i as LocalBulkheadOptions, O as ObserveResult, S as SettleProbeResult, j as bulkhead, k as circuitBreaker } from './circuit-breaker-xZ8uenT8.js';
|
|
2
|
+
import { a as OperationOptions, b as Operation, P as Policy } from './types-C-Ml-MKp.js';
|
|
3
|
+
export { A as Adapter, C as Classification, E as EventSink, c as EventSinks, d as ExecutionContext, e as ExecutionMetadata, N as Next, O as OperationCapabilities, f as OperationEvent, g as OperationExecuteOptions, h as Outcome, i as OutcomeClassifier } from './types-C-Ml-MKp.js';
|
|
4
|
+
export { R as RetryContext, a as RetryDelay, b as RetryOptions, r as retry } from './retry-DD85oXL9.js';
|
|
5
5
|
|
|
6
6
|
/** Creates a named, protocol-agnostic operation. */
|
|
7
7
|
declare function operation<Args, Result>(options: OperationOptions<Args, Result>): Operation<Args, Result>;
|