@gkoos/caracal 0.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +311 -0
  4. package/dist/chunk-5CXDW7W6.js +202 -0
  5. package/dist/chunk-5CXDW7W6.js.map +1 -0
  6. package/dist/circuit-breaker-BSkcV0W_.d.ts +296 -0
  7. package/dist/fetch.d.ts +58 -0
  8. package/dist/fetch.js +117 -0
  9. package/dist/fetch.js.map +1 -0
  10. package/dist/index.d.ts +19 -0
  11. package/dist/index.js +1065 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/postgres.d.ts +28 -0
  14. package/dist/postgres.js +56 -0
  15. package/dist/postgres.js.map +1 -0
  16. package/dist/redis.d.ts +59 -0
  17. package/dist/redis.js +549 -0
  18. package/dist/redis.js.map +1 -0
  19. package/dist/retry-BFP_k3Hg.d.ts +26 -0
  20. package/dist/testing/index.d.ts +45 -0
  21. package/dist/testing/index.js +101 -0
  22. package/dist/testing/index.js.map +1 -0
  23. package/dist/types-Tf9T76C7.d.ts +187 -0
  24. package/package.json +127 -0
  25. package/src/adapters/fetch/adapter.ts +122 -0
  26. package/src/adapters/fetch/index.ts +15 -0
  27. package/src/adapters/fetch/retry-after.ts +111 -0
  28. package/src/adapters/postgres/adapter.ts +102 -0
  29. package/src/adapters/postgres/index.ts +7 -0
  30. package/src/coordination/redis/bulkhead.ts +61 -0
  31. package/src/coordination/redis/circuit-breaker.ts +270 -0
  32. package/src/coordination/redis/client.ts +78 -0
  33. package/src/coordination/redis/eval-script.ts +71 -0
  34. package/src/coordination/redis/keys.ts +32 -0
  35. package/src/coordination/redis/leases.ts +44 -0
  36. package/src/coordination/redis/scripts.ts +314 -0
  37. package/src/core/bulkhead.ts +336 -0
  38. package/src/core/circuit-breaker.ts +1066 -0
  39. package/src/core/index.ts +36 -0
  40. package/src/core/operation.ts +174 -0
  41. package/src/core/retry.ts +204 -0
  42. package/src/core/runtime.ts +123 -0
  43. package/src/core/scope-state-cache.ts +50 -0
  44. package/src/core/timeout.ts +73 -0
  45. package/src/core/types.ts +230 -0
  46. package/src/fetch.ts +17 -0
  47. package/src/index.ts +49 -0
  48. package/src/postgres.ts +9 -0
  49. package/src/redis.ts +8 -0
@@ -0,0 +1,296 @@
1
+ import { d as ExecutionContext, P as Policy } from './types-Tf9T76C7.js';
2
+
3
+ declare class BulkheadRejectedError extends Error {
4
+ readonly coordination: "local" | "distributed";
5
+ readonly policyName: string;
6
+ readonly scope: string;
7
+ readonly reason: string;
8
+ constructor(coordination: "local" | "distributed", policyName: string, scope: string, reason: string);
9
+ }
10
+ interface LocalBulkheadOptions {
11
+ readonly name: string;
12
+ readonly limit: number;
13
+ readonly queue?: {
14
+ readonly limit: number;
15
+ readonly timeoutMs: number;
16
+ };
17
+ }
18
+ /** Policy-specific capability supplied by caracal/redis. */
19
+ interface BulkheadCoordinator {
20
+ command(identity: {
21
+ name: string;
22
+ operation: string;
23
+ scope: string;
24
+ }, action: "acquire" | "renew" | "release", token: string, leaseMs: number, limit: number): Promise<{
25
+ allowed: boolean;
26
+ occupancy: number;
27
+ }>;
28
+ }
29
+ interface DistributedBulkheadOptions {
30
+ readonly name: string;
31
+ readonly limit: number;
32
+ readonly coordinator: BulkheadCoordinator;
33
+ readonly scope: (context: ExecutionContext) => string;
34
+ readonly leaseMs?: number;
35
+ }
36
+ declare function local$1(options: LocalBulkheadOptions): Policy & {
37
+ readonly coordination: "local";
38
+ snapshot(): {
39
+ coordination: "local";
40
+ occupancy: number;
41
+ waiting: number;
42
+ };
43
+ };
44
+ declare function distributed$1(options: DistributedBulkheadOptions): Policy & {
45
+ readonly coordination: "distributed";
46
+ };
47
+ declare const bulkhead: Readonly<{
48
+ local: typeof local$1;
49
+ distributed: typeof distributed$1;
50
+ }>;
51
+
52
+ type BreakerState = "closed" | "open" | "half-open";
53
+ /** Classification of an attempt outcome for circuit-breaker purposes. */
54
+ type BreakerOutcome = "success" | "failure" | "ignored";
55
+ /**
56
+ * Called after every attempt that the breaker observes. Returning "ignored"
57
+ * means the outcome neither counts toward nor clears failures.
58
+ */
59
+ type BreakerClassifier = (error: unknown, isSuccess: boolean) => BreakerOutcome;
60
+ interface LocalBreakerOptions {
61
+ /** Identifies this policy in events and introspection. */
62
+ readonly name: string;
63
+ /**
64
+ * Minimum number of observations in the window before the breaker may open.
65
+ * Default: 5.
66
+ */
67
+ readonly minimumThroughput?: number;
68
+ /**
69
+ * Fraction of failures (0–1 exclusive) that triggers opening.
70
+ * Default: 0.5.
71
+ */
72
+ readonly failureThreshold?: number;
73
+ /**
74
+ * How long (ms) the breaker stays open before entering half-open.
75
+ * Default: 10 000.
76
+ */
77
+ readonly openMs?: number;
78
+ /**
79
+ * Number of consecutive successes needed to close from half-open.
80
+ * Default: 1.
81
+ */
82
+ readonly halfOpenSuccesses?: number;
83
+ /**
84
+ * Maximum concurrent probes allowed in half-open state.
85
+ * Default: 1.
86
+ */
87
+ readonly halfOpenProbes?: number;
88
+ /**
89
+ * Sliding-window size (number of observations retained).
90
+ * Default: 100.
91
+ */
92
+ readonly windowSize?: number;
93
+ /** Custom outcome classifier. Defaults to the adapter classification; retryable counts as failure. */
94
+ readonly classify?: BreakerClassifier;
95
+ }
96
+ interface BreakerSnapshot {
97
+ readonly coordination: "local";
98
+ readonly state: BreakerState;
99
+ readonly failures: number;
100
+ readonly successes: number;
101
+ readonly observations: number;
102
+ /** Only meaningful in half-open; number of probes currently in-flight. */
103
+ readonly probesInFlight: number;
104
+ /** Only meaningful in half-open; consecutive successes so far this epoch. */
105
+ readonly halfOpenSuccesses: number;
106
+ }
107
+ declare class CircuitOpenError extends Error {
108
+ readonly policyName: string;
109
+ readonly coordination: "local" | "distributed";
110
+ readonly scope: string;
111
+ constructor(policyName: string, coordination: "local" | "distributed", scope: string);
112
+ }
113
+ type LocalBreakerPolicy = Policy & {
114
+ readonly coordination: "local";
115
+ snapshot(): BreakerSnapshot;
116
+ };
117
+ declare function local(options: LocalBreakerOptions): LocalBreakerPolicy;
118
+ interface BreakerIdentity {
119
+ readonly name: string;
120
+ readonly operation: string;
121
+ readonly scope: string;
122
+ }
123
+ type ObserveResult = {
124
+ readonly type: "stale";
125
+ readonly currentGeneration: number;
126
+ } | {
127
+ readonly type: "observed";
128
+ readonly generation: number;
129
+ readonly windowTotal: number;
130
+ readonly windowFailures: number;
131
+ } | {
132
+ readonly type: "opened";
133
+ readonly newGeneration: number;
134
+ readonly windowTotal: number;
135
+ readonly windowFailures: number;
136
+ };
137
+ type AdmitProbeResult = {
138
+ readonly type: "rejected";
139
+ readonly reason: "closed" | "open" | "probe-limit";
140
+ readonly generation: number;
141
+ } | {
142
+ readonly type: "admitted";
143
+ readonly generation: number;
144
+ readonly probeCount: number;
145
+ readonly stateChanged: boolean;
146
+ };
147
+ type SettleProbeResult = {
148
+ readonly type: "stale";
149
+ readonly generation: number;
150
+ } | {
151
+ readonly type: "settled";
152
+ readonly state: BreakerState;
153
+ readonly generation: number;
154
+ } | {
155
+ readonly type: "transitioned";
156
+ readonly newState: BreakerState;
157
+ readonly newGeneration: number;
158
+ readonly previousState: "half-open";
159
+ };
160
+ /**
161
+ * Policy-specific coordinator capability for `circuitBreaker.distributed()`.
162
+ * The Redis implementation lives in `@gkoos/caracal/redis`; the memory implementation
163
+ * lives in `test/support/memory-coordinator` and must not be a production export.
164
+ */
165
+ interface BreakerCoordinator {
166
+ readState(identity: BreakerIdentity): Promise<{
167
+ state: BreakerState;
168
+ generation: number;
169
+ } | null>;
170
+ observe(identity: BreakerIdentity, params: {
171
+ readonly generation: number;
172
+ readonly outcome: "success" | "failure";
173
+ readonly uuid: string;
174
+ readonly windowTtlMs: number;
175
+ readonly minimumThroughput: number;
176
+ readonly failureThresholdNumerator: number;
177
+ readonly windowSize: number;
178
+ readonly openMs: number;
179
+ }): Promise<ObserveResult>;
180
+ admitProbe(identity: BreakerIdentity, params: {
181
+ readonly probeToken: string;
182
+ readonly openMs: number;
183
+ readonly halfOpenProbes: number;
184
+ readonly probeLeaseTtlMs: number;
185
+ }): Promise<AdmitProbeResult>;
186
+ settleProbe(identity: BreakerIdentity, params: {
187
+ readonly probeToken: string;
188
+ readonly outcome: "success" | "failure";
189
+ readonly generation: number;
190
+ readonly halfOpenSuccesses: number;
191
+ readonly openMs: number;
192
+ /**
193
+ * Observation retention period, used as a floor for the CLOSED cleanup
194
+ * TTL: the state hash must not expire before the window it governs, or
195
+ * retained members would outlive their epoch. Optional so that custom
196
+ * coordinators keep compiling; the policy always supplies it and the
197
+ * Redis coordinator falls back to `openMs × 2` when it is missing.
198
+ */
199
+ readonly windowTtlMs?: number;
200
+ }): Promise<SettleProbeResult>;
201
+ }
202
+ /**
203
+ * Distributed (Redis-backed) circuit breaker options.
204
+ *
205
+ * The defaults are mutually consistent. Overriding one timer or count usually
206
+ * means revisiting the related ones; see the Redis coordination guide
207
+ * (`docs/redis.md`, "Keeping the breaker knobs consistent") for the constraints
208
+ * and the symptoms of getting them wrong.
209
+ */
210
+ interface DistributedBreakerOptions {
211
+ /** Identifies this policy in events and introspection. */
212
+ readonly name: string;
213
+ /** Redis-backed coordinator. See `redisCircuitBreakerCoordinator` in `@gkoos/caracal/redis`. */
214
+ readonly coordinator: BreakerCoordinator;
215
+ /**
216
+ * Maps an execution context to the coordination scope key.
217
+ * `scope: ctx => 'process'` is NOT local mode, it's still Redis-backed
218
+ * with full coordinator-failure semantics.
219
+ */
220
+ readonly scope: (context: ExecutionContext) => string;
221
+ /**
222
+ * Minimum observations in the window before the breaker may open. Default: 20.
223
+ *
224
+ * Requires `windowSize >= minimumThroughput`; a smaller window can never
225
+ * reach this count, so the breaker would never open.
226
+ */
227
+ readonly minimumThroughput?: number;
228
+ /** Failure fraction (0–1 exclusive) that triggers opening. Default: 0.5. */
229
+ readonly failureThreshold?: number;
230
+ /**
231
+ * Sliding-window size (observation count). Default: 100.
232
+ *
233
+ * Must be at least `minimumThroughput`: the count cap trims the window, so a
234
+ * smaller window keeps the observed total below the opening threshold and the
235
+ * breaker never opens.
236
+ */
237
+ readonly windowSize?: number;
238
+ /**
239
+ * Observation retention period in ms. Observations older than this are
240
+ * pruned regardless of `windowSize`. Default: max(openMs × 3, 60_000).
241
+ *
242
+ * Must be long enough to accumulate `minimumThroughput` observations at your
243
+ * traffic rate. If pruning fires first the window never fills and the
244
+ * breaker never opens; the default is a proxy for that, not a measurement.
245
+ */
246
+ readonly windowTtlMs?: number;
247
+ /** How long (ms) the breaker stays open before half-open. Default: 30_000. */
248
+ readonly openMs?: number;
249
+ /** Maximum concurrent half-open probes per scope. Default: 3. */
250
+ readonly halfOpenProbes?: number;
251
+ /**
252
+ * Probe successes needed to close from half-open. Default: 2.
253
+ *
254
+ * Any probe failure resets progress back to OPEN, so a value that is large
255
+ * relative to the probe rate keeps traffic throttled long after the
256
+ * downstream recovered.
257
+ */
258
+ readonly halfOpenSuccesses?: number;
259
+ /**
260
+ * Probe token TTL in ms. A dead worker's probe expires without blocking
261
+ * recovery. Default: openMs × 2.
262
+ *
263
+ * Must exceed the slowest probe settle time (at least `timeoutMs`): if a live
264
+ * token expires mid-probe the slot is re-issued, more than `halfOpenProbes`
265
+ * probes run concurrently, and the late settle is dropped as stale. It is
266
+ * also the worst case a HALF_OPEN window stalls while crashed workers hold
267
+ * every slot, so do not make it arbitrarily large.
268
+ */
269
+ readonly probeLeaseTtlMs?: number;
270
+ /**
271
+ * What to do when the coordinator is unreachable and the last known state
272
+ * for the scope was CLOSED (or no prior successful read has occurred).
273
+ * `"fail-open"` allows the attempt through (default).
274
+ * `"fail-closed"` rejects it with CircuitOpenError.
275
+ *
276
+ * If the last successfully-read state was OPEN or HALF_OPEN the attempt is
277
+ * always rejected, regardless of this setting. Admitting work into a
278
+ * known-open breaker removes the protection it exists to provide.
279
+ *
280
+ * Coordinator unavailability during probe admission (after a successful
281
+ * readState that returned OPEN/HALF_OPEN) also always fails closed.
282
+ */
283
+ readonly onCoordinatorError?: "fail-open" | "fail-closed";
284
+ /** Custom outcome classifier. Defaults to the adapter classification; retryable counts as failure. */
285
+ readonly classify?: BreakerClassifier;
286
+ }
287
+ type DistributedBreakerPolicy = Policy & {
288
+ readonly coordination: "distributed";
289
+ };
290
+ declare function distributed(options: DistributedBreakerOptions): DistributedBreakerPolicy;
291
+ declare const circuitBreaker: Readonly<{
292
+ local: typeof local;
293
+ distributed: typeof distributed;
294
+ }>;
295
+
296
+ export { type AdmitProbeResult as A, type BreakerClassifier as B, CircuitOpenError as C, type DistributedBreakerOptions as D, type LocalBreakerOptions as L, type ObserveResult as O, type SettleProbeResult as S, type BreakerCoordinator as a, type BreakerIdentity as b, type BreakerOutcome as c, type BreakerSnapshot as d, type BreakerState as e, type BulkheadCoordinator as f, BulkheadRejectedError as g, type DistributedBulkheadOptions as h, type LocalBulkheadOptions as i, bulkhead as j, circuitBreaker as k };
@@ -0,0 +1,58 @@
1
+ import { O as OperationCapabilities, C as Classification, A as Adapter } from './types-Tf9T76C7.js';
2
+ import { R as RetryContext } from './retry-BFP_k3Hg.js';
3
+
4
+ type FetchOperationArgs = Readonly<{
5
+ url: RequestInfo | URL;
6
+ options?: RequestInit;
7
+ }>;
8
+ type FetchReplay = OperationCapabilities["replay"] | ((args: FetchOperationArgs) => OperationCapabilities["replay"]);
9
+ interface FetchAdapterOptions {
10
+ readonly fetch?: typeof globalThis.fetch;
11
+ readonly replay?: FetchReplay;
12
+ readonly classifyResponse?: (response: Response) => Classification;
13
+ readonly classifyError?: (error: unknown) => Classification;
14
+ }
15
+ /**
16
+ * Creates a fetch adapter with conservative per-request replay traits.
17
+ * It performs no implicit retry or timeout; those remain operation policies.
18
+ */
19
+ declare function fetchAdapter(options?: FetchAdapterOptions): Adapter<FetchOperationArgs, Response>;
20
+
21
+ /**
22
+ * Parses `Retry-After` (delta-seconds or HTTP-date) from a settled fetch
23
+ * outcome. Reads the response from `context.result`, or from
24
+ * `context.error` when a custom fetch implementation throws a
25
+ * response-bearing error.
26
+ *
27
+ * Returns `undefined` when no usable header is present. Malformed values
28
+ * are ignored rather than thrown.
29
+ */
30
+ declare function retryAfterMs(context: RetryContext, now?: number): number | undefined;
31
+ /** Options for `createRetryAfterDelay`. */
32
+ interface RetryAfterDelayOptions {
33
+ /** Base of the exponential backoff in ms. Default: 100. */
34
+ readonly baseMs?: number;
35
+ /** Exponential growth factor (>= 1). Default: 2. */
36
+ readonly factor?: number;
37
+ /** Ceiling applied to the deterministic wait in ms. Default: 30 000. */
38
+ readonly maxDelayMs?: number;
39
+ /** Additive jitter as a fraction of the wait, within [0, 1]. Default: 0.1. */
40
+ readonly jitterRatio?: number;
41
+ }
42
+ /** A `RetryDelay` that reads `Retry-After` from the settled outcome. */
43
+ type RetryAfterDelay = (attempt: number, context: RetryContext) => number;
44
+ /**
45
+ * Builds a `RetryDelay` that waits for the longer of exponential backoff
46
+ * and the `Retry-After` the server sent, then adds additive jitter.
47
+ *
48
+ * Jitter only ever lengthens the wait, so a server-provided minimum is
49
+ * never retried early.
50
+ */
51
+ declare function createRetryAfterDelay(options?: RetryAfterDelayOptions): RetryAfterDelay;
52
+ /**
53
+ * Ready-to-use default: `retry({ maxAttempts: 3, delay: retryAfterDelay })`.
54
+ * Use `createRetryAfterDelay()` to change the pacing.
55
+ */
56
+ declare const retryAfterDelay: RetryAfterDelay;
57
+
58
+ export { type FetchAdapterOptions, type FetchOperationArgs, type FetchReplay, type RetryAfterDelay, type RetryAfterDelayOptions, createRetryAfterDelay, fetchAdapter, retryAfterDelay, retryAfterMs };
package/dist/fetch.js ADDED
@@ -0,0 +1,117 @@
1
+ // src/adapters/fetch/adapter.ts
2
+ function requestMethod(args) {
3
+ if (args.options?.method !== void 0) {
4
+ return args.options.method.toUpperCase();
5
+ }
6
+ if (args.url instanceof Request) {
7
+ return args.url.method.toUpperCase();
8
+ }
9
+ return "GET";
10
+ }
11
+ function defaultReplay(args) {
12
+ const method = requestMethod(args);
13
+ if (method === "GET" || method === "HEAD") {
14
+ return "safe";
15
+ }
16
+ if (method === "POST" || method === "PATCH") {
17
+ return "unsafe";
18
+ }
19
+ return "unknown";
20
+ }
21
+ function defaultResponseClassification(response) {
22
+ return response.status >= 500 || response.status === 408 || response.status === 429 ? "retryable" : "success";
23
+ }
24
+ function urlSignal(url) {
25
+ return url instanceof Request ? url.signal : void 0;
26
+ }
27
+ function combinedSignal(first, second, third) {
28
+ const signals = [first, second, third].filter(
29
+ (signal) => signal !== void 0
30
+ );
31
+ if (signals.length === 0) {
32
+ return void 0;
33
+ }
34
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
35
+ }
36
+ function fetchAdapter(options = {}) {
37
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
38
+ if (fetchImplementation === void 0) {
39
+ throw new Error("fetch is not available; provide FetchAdapterOptions.fetch");
40
+ }
41
+ return Object.freeze({
42
+ capabilities(args) {
43
+ const configured = options.replay;
44
+ const replay = typeof configured === "function" ? configured(args) : configured ?? defaultReplay(args);
45
+ return { abort: "supported", replay };
46
+ },
47
+ async execute(args, context) {
48
+ const signal = combinedSignal(
49
+ urlSignal(args.url),
50
+ args.options?.signal ?? void 0,
51
+ context.signal
52
+ );
53
+ return fetchImplementation(args.url, { ...args.options, signal });
54
+ },
55
+ classify(outcome) {
56
+ if (outcome.status === "failure") {
57
+ return options.classifyError?.(outcome.error) ?? "retryable";
58
+ }
59
+ return options.classifyResponse?.(outcome.value) ?? defaultResponseClassification(outcome.value);
60
+ }
61
+ });
62
+ }
63
+
64
+ // src/adapters/fetch/retry-after.ts
65
+ var DEFAULTS = {
66
+ baseMs: 100,
67
+ factor: 2,
68
+ maxDelayMs: 3e4,
69
+ jitterRatio: 0.1
70
+ };
71
+ var deltaSecondsPattern = /^\d+$/;
72
+ function headersOf(value) {
73
+ if (typeof value !== "object" || value === null) return void 0;
74
+ const headers = value.headers;
75
+ if (typeof headers !== "object" || headers === null) return void 0;
76
+ const get = headers.get;
77
+ return typeof get === "function" ? headers : void 0;
78
+ }
79
+ function retryAfterMs(context, now = Date.now()) {
80
+ const headers = headersOf(context.result) ?? headersOf(context.error);
81
+ if (headers === void 0) return void 0;
82
+ const value = headers.get("retry-after")?.trim();
83
+ if (!value) return void 0;
84
+ if (deltaSecondsPattern.test(value)) {
85
+ const seconds = Number(value);
86
+ return Number.isSafeInteger(seconds) ? seconds * 1e3 : void 0;
87
+ }
88
+ const timestamp = Date.parse(value);
89
+ return Number.isNaN(timestamp) ? void 0 : Math.max(0, timestamp - now);
90
+ }
91
+ function createRetryAfterDelay(options = {}) {
92
+ const baseMs = options.baseMs ?? DEFAULTS.baseMs;
93
+ const factor = options.factor ?? DEFAULTS.factor;
94
+ const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;
95
+ const jitterRatio = options.jitterRatio ?? DEFAULTS.jitterRatio;
96
+ if (!Number.isFinite(baseMs) || baseMs < 0)
97
+ throw new RangeError("retryAfterDelay baseMs must be finite and >= 0");
98
+ if (!Number.isFinite(factor) || factor < 1)
99
+ throw new RangeError("retryAfterDelay factor must be finite and >= 1");
100
+ if (!Number.isFinite(maxDelayMs) || maxDelayMs < 0)
101
+ throw new RangeError("retryAfterDelay maxDelayMs must be finite and >= 0");
102
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1)
103
+ throw new RangeError("retryAfterDelay jitterRatio must be within [0, 1]");
104
+ return (attempt, context) => {
105
+ const backoff = Math.min(baseMs * factor ** (attempt - 1), maxDelayMs);
106
+ const base = Math.min(
107
+ Math.max(backoff, retryAfterMs(context) ?? 0),
108
+ maxDelayMs
109
+ );
110
+ return base + Math.random() * base * jitterRatio;
111
+ };
112
+ }
113
+ var retryAfterDelay = createRetryAfterDelay();
114
+
115
+ export { createRetryAfterDelay, fetchAdapter, retryAfterDelay, retryAfterMs };
116
+ //# sourceMappingURL=fetch.js.map
117
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,19 @@
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-BSkcV0W_.js';
2
+ import { a as OperationOptions, b as Operation, P as Policy } from './types-Tf9T76C7.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-Tf9T76C7.js';
4
+ export { R as RetryContext, a as RetryDelay, b as RetryOptions, r as retry } from './retry-BFP_k3Hg.js';
5
+
6
+ /** Creates a named, protocol-agnostic operation. */
7
+ declare function operation<Args, Result>(options: OperationOptions<Args, Result>): Operation<Args, Result>;
8
+
9
+ declare class TimeoutError extends Error {
10
+ readonly timeoutMs: number;
11
+ constructor(timeoutMs: number);
12
+ }
13
+ interface TimeoutOptions {
14
+ readonly ms: number;
15
+ }
16
+ /** Bounds caller wait time and requests cancellation only when the adapter supports it. */
17
+ declare function timeout(options: TimeoutOptions): Policy;
18
+
19
+ export { Operation, OperationOptions, Policy, TimeoutError, type TimeoutOptions, operation, timeout };