@gkoos/caracal 0.1.1 → 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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
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
+
3
27
  ## 0.1.1
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -193,7 +193,7 @@ const sharedCapacity = bulkhead.distributed({
193
193
  })
194
194
  ```
195
195
 
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 `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`.
197
197
 
198
198
  #### Circuit breaker
199
199
 
@@ -238,7 +238,7 @@ Use stable, non-secret values, scope keys are observable in the Redis keyspace.
238
238
 
239
239
  ### Events and observability
240
240
 
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: an exception in a sink cannot affect execution.
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).
242
242
 
243
243
  ```ts
244
244
  const op = operation({
@@ -266,7 +266,7 @@ There are many libraries for applying resilience policies to operations, but mos
266
266
 
267
267
  ## Development
268
268
 
269
- Requires Node.js 20+.
269
+ Requires Node.js 20.3+.
270
270
 
271
271
  ```sh
272
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
- try {
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 { admissionSignal, emitRuntimeEvent, nextAttempt, operation, withAdmissionSignal, withSignal };
201
- //# sourceMappingURL=chunk-5CXDW7W6.js.map
202
- //# sourceMappingURL=chunk-5CXDW7W6.js.map
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-Tf9T76C7.js';
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";
package/dist/fetch.d.ts CHANGED
@@ -1,5 +1,5 @@
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';
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-DHEz0YLV.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';
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>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { admissionSignal, emitRuntimeEvent, nextAttempt, withAdmissionSignal, withSignal } from './chunk-5CXDW7W6.js';
2
- export { operation } from './chunk-5CXDW7W6.js';
1
+ export { operation } from './chunk-JV2OLYOF.js';
2
+ import { MAX_TIMER_MS, admissionSignal, emitRuntimeEvent, nextAttempt, withAdmissionSignal, withSignal } from './chunk-FD7IZJKY.js';
3
3
  import { randomUUID } from 'crypto';
4
4
 
5
5
  var BulkheadRejectedError = class extends Error {
@@ -34,7 +34,7 @@ function local(options) {
34
34
  const { name, limit } = options;
35
35
  const queue = options.queue && { ...options.queue };
36
36
  validate(name, limit);
37
- if (queue && (!Number.isSafeInteger(queue.limit) || queue.limit < 1 || !Number.isSafeInteger(queue.timeoutMs) || queue.timeoutMs < 1 || queue.timeoutMs > 2147483647))
37
+ if (queue && (!Number.isSafeInteger(queue.limit) || queue.limit < 1 || !Number.isSafeInteger(queue.timeoutMs) || queue.timeoutMs < 1 || queue.timeoutMs > MAX_TIMER_MS))
38
38
  throw new RangeError("Invalid bounded queue");
39
39
  let occupancy = 0;
40
40
  const waiting = [];
@@ -212,13 +212,23 @@ function distributed(options) {
212
212
  }
213
213
  };
214
214
  try {
215
- if (performance.now() >= deadline)
215
+ if (performance.now() >= deadline) {
216
+ event(
217
+ context,
218
+ "distributed",
219
+ name,
220
+ scope,
221
+ "rejected",
222
+ admitted.occupancy,
223
+ "admission-expired"
224
+ );
216
225
  throw new BulkheadRejectedError(
217
226
  "distributed",
218
227
  name,
219
228
  scope,
220
229
  "admission-expired"
221
230
  );
231
+ }
222
232
  admissionSignal(context)?.throwIfAborted();
223
233
  event(
224
234
  context,
@@ -926,6 +936,11 @@ function delayFor(options, context, attempt, outcome) {
926
936
  if (!Number.isFinite(delay) || delay < 0) {
927
937
  throw new RangeError("retry delay must be a finite non-negative number");
928
938
  }
939
+ if (delay > MAX_TIMER_MS) {
940
+ throw new RangeError(
941
+ `retry delay must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`
942
+ );
943
+ }
929
944
  return delay;
930
945
  }
931
946
  function wait(delayMs, signal) {
@@ -962,6 +977,11 @@ function retry(options) {
962
977
  if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) {
963
978
  throw new RangeError("maxAttempts must be a positive integer");
964
979
  }
980
+ if (typeof options.delay === "number" && (!Number.isFinite(options.delay) || options.delay < 0 || options.delay > MAX_TIMER_MS)) {
981
+ throw new RangeError(
982
+ `retry delay must be a finite number within 0..${MAX_TIMER_MS} ms`
983
+ );
984
+ }
965
985
  return Object.freeze({
966
986
  name: "retry",
967
987
  async execute(initialContext, next) {
@@ -973,6 +993,12 @@ function retry(options) {
973
993
  const outcome = { status: "success", value };
974
994
  const outcomeClassification = classification(context, outcome);
975
995
  if (outcomeClassification !== "retryable" || context.capabilities.replay !== "safe") {
996
+ emitRuntimeEvent(context, {
997
+ type: "retry.declined",
998
+ outcome: summarized(outcome),
999
+ classification: outcomeClassification,
1000
+ reason: context.capabilities.replay !== "safe" ? "replay-unsafe" : "not-retryable"
1001
+ });
976
1002
  return value;
977
1003
  }
978
1004
  if (context.attempt >= options.maxAttempts) {
@@ -996,7 +1022,16 @@ function retry(options) {
996
1022
  } catch (error) {
997
1023
  const outcome = { status: "failure", error };
998
1024
  const outcomeClassification = classification(context, outcome);
999
- if (admissionSignal(context)?.aborted || context.capabilities.replay !== "safe" || outcomeClassification !== "retryable") {
1025
+ if (admissionSignal(context)?.aborted) {
1026
+ throw error;
1027
+ }
1028
+ if (context.capabilities.replay !== "safe" || outcomeClassification !== "retryable") {
1029
+ emitRuntimeEvent(context, {
1030
+ type: "retry.declined",
1031
+ outcome: summarized(outcome),
1032
+ classification: outcomeClassification,
1033
+ reason: context.capabilities.replay !== "safe" ? "replay-unsafe" : "not-retryable"
1034
+ });
1000
1035
  throw error;
1001
1036
  }
1002
1037
  if (context.attempt >= options.maxAttempts) {
@@ -1042,6 +1077,11 @@ function timeout(options) {
1042
1077
  if (!Number.isFinite(options.ms) || options.ms <= 0) {
1043
1078
  throw new RangeError("timeout ms must be a finite positive number");
1044
1079
  }
1080
+ if (options.ms > MAX_TIMER_MS) {
1081
+ throw new RangeError(
1082
+ `timeout ms must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`
1083
+ );
1084
+ }
1045
1085
  return Object.freeze({
1046
1086
  name: "timeout",
1047
1087
  async execute(context, next) {