@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 +24 -0
- package/README.md +3 -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-DHEz0YLV.d.ts → circuit-breaker-xZ8uenT8.d.ts} +1 -1
- 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 +45 -5
- 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 +4 -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 +1 -1
- package/src/adapters/fetch/retry-after.ts +11 -3
- package/src/coordination/redis/client.ts +15 -1
- package/src/core/bulkhead.ts +16 -2
- 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/dist/chunk-5CXDW7W6.js.map +0 -1
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:
|
|
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
|
-
|
|
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"]}
|
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>;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 >
|
|
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
|
|
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) {
|