@vereda/http 1.0.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/LICENSE +21 -0
- package/README.md +604 -0
- package/dist/adapters/zod.d.ts +14 -0
- package/dist/adapters/zod.d.ts.map +1 -0
- package/dist/adapters/zod.js +14 -0
- package/dist/adapters/zod.js.map +1 -0
- package/dist/core/backoff.d.ts +9 -0
- package/dist/core/backoff.d.ts.map +1 -0
- package/dist/core/backoff.js +23 -0
- package/dist/core/backoff.js.map +1 -0
- package/dist/core/client.d.ts +98 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +781 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +87 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +140 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/listeners.d.ts +14 -0
- package/dist/core/listeners.d.ts.map +1 -0
- package/dist/core/listeners.js +27 -0
- package/dist/core/listeners.js.map +1 -0
- package/dist/core/metrics.d.ts +33 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +24 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/nanoid.d.ts +2 -0
- package/dist/core/nanoid.d.ts.map +1 -0
- package/dist/core/nanoid.js +11 -0
- package/dist/core/nanoid.js.map +1 -0
- package/dist/core/redact.d.ts +12 -0
- package/dist/core/redact.d.ts.map +1 -0
- package/dist/core/redact.js +42 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/types.d.ts +261 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +41 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +19 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +135 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/middleware/index.d.ts +26 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +55 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/queue/bulkhead.d.ts +63 -0
- package/dist/queue/bulkhead.d.ts.map +1 -0
- package/dist/queue/bulkhead.js +192 -0
- package/dist/queue/bulkhead.js.map +1 -0
- package/dist/queue/circuit-breaker.d.ts +81 -0
- package/dist/queue/circuit-breaker.d.ts.map +1 -0
- package/dist/queue/circuit-breaker.js +283 -0
- package/dist/queue/circuit-breaker.js.map +1 -0
- package/dist/queue/executor.d.ts +67 -0
- package/dist/queue/executor.d.ts.map +1 -0
- package/dist/queue/executor.js +273 -0
- package/dist/queue/executor.js.map +1 -0
- package/dist/queue/policy.d.ts +26 -0
- package/dist/queue/policy.d.ts.map +1 -0
- package/dist/queue/policy.js +37 -0
- package/dist/queue/policy.js.map +1 -0
- package/dist/queue/retry.d.ts +58 -0
- package/dist/queue/retry.d.ts.map +1 -0
- package/dist/queue/retry.js +259 -0
- package/dist/queue/retry.js.map +1 -0
- package/dist/queue/semaphore.d.ts +32 -0
- package/dist/queue/semaphore.d.ts.map +1 -0
- package/dist/queue/semaphore.js +83 -0
- package/dist/queue/semaphore.js.map +1 -0
- package/dist/ticket/ticket.d.ts +77 -0
- package/dist/ticket/ticket.d.ts.map +1 -0
- package/dist/ticket/ticket.js +186 -0
- package/dist/ticket/ticket.js.map +1 -0
- package/package.json +85 -0
- package/src/adapters/zod.ts +16 -0
- package/src/core/backoff.ts +26 -0
- package/src/core/client.ts +1048 -0
- package/src/core/errors.ts +194 -0
- package/src/core/index.ts +56 -0
- package/src/core/listeners.ts +28 -0
- package/src/core/metrics.ts +42 -0
- package/src/core/nanoid.ts +11 -0
- package/src/core/redact.ts +46 -0
- package/src/core/types.ts +306 -0
- package/src/core/validate.ts +163 -0
- package/src/middleware/index.ts +63 -0
- package/src/queue/bulkhead.ts +243 -0
- package/src/queue/circuit-breaker.ts +373 -0
- package/src/queue/executor.ts +355 -0
- package/src/queue/policy.ts +49 -0
- package/src/queue/retry.ts +380 -0
- package/src/queue/semaphore.ts +91 -0
- package/src/ticket/ticket.ts +246 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { ConfigurationError, DeadlineExceededError, HttpError, NetworkError, NO_TIMEOUT_CONFIGURED, RetryableStatusError, TimeoutError, ValidationError, } from "../core/errors.js";
|
|
2
|
+
import { DEFAULT_RETRY_ON_STATUS, isBoundedMs } from "../core/types.js";
|
|
3
|
+
import { isReadableStream } from "../core/validate.js";
|
|
4
|
+
/**
|
|
5
|
+
* Executes a single HTTP request attempt.
|
|
6
|
+
* Returns a discriminated union describing what happened,
|
|
7
|
+
* so the caller (retry loop) can decide whether to retry or resolve.
|
|
8
|
+
*/
|
|
9
|
+
export async function executeRequest(req, middleware) {
|
|
10
|
+
const { url, options, timeoutConfig, retryConfig, signal } = req;
|
|
11
|
+
const attemptStart = Date.now();
|
|
12
|
+
if (signal.aborted || options.signal?.aborted) {
|
|
13
|
+
return { kind: "cancelled" };
|
|
14
|
+
}
|
|
15
|
+
// Set once this attempt hands the caller a live, unread Response body (the
|
|
16
|
+
// success-without-parse and HttpError returns below). An unconsumed body
|
|
17
|
+
// handed off this way gets bounded in `finally` instead of having its
|
|
18
|
+
// timer cleared, so `raw.json()`/`error.response.text()` read later can't
|
|
19
|
+
// hang forever (the parse path already reads the body inside the attempt,
|
|
20
|
+
// and the RetryableStatusError path already cancels it).
|
|
21
|
+
let handedOff = false;
|
|
22
|
+
// Resolve a replayable body factory fresh for this attempt so every attempt
|
|
23
|
+
// gets its own materialized body. Do not mutate the caller's options.
|
|
24
|
+
let resolvedBody;
|
|
25
|
+
if (typeof options.body === "function") {
|
|
26
|
+
try {
|
|
27
|
+
resolvedBody = options.body();
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return {
|
|
31
|
+
kind: "error",
|
|
32
|
+
error: new ConfigurationError(`body factory threw: ${err instanceof Error ? err.message : String(err)}`),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
resolvedBody = options.body;
|
|
38
|
+
}
|
|
39
|
+
const timeoutMs = timeoutConfig.attemptMs;
|
|
40
|
+
const retryOnStatus = retryConfig.retryOnStatus ?? DEFAULT_RETRY_ON_STATUS;
|
|
41
|
+
// Build the fetch call wrapped in middleware
|
|
42
|
+
const fetchCall = buildFetchCall(req.fetch);
|
|
43
|
+
const composed = composeMiddleware(middleware, fetchCall);
|
|
44
|
+
// Merge the per-attempt signals with AbortSignal.any. It wires its sources
|
|
45
|
+
// internally (no "abort" listeners attached to them), so neither the ticket
|
|
46
|
+
// signal nor the caller's signal accumulates one listener per attempt (#7).
|
|
47
|
+
// Wrapping even a lone ticket signal shields it from fetch's own abort
|
|
48
|
+
// listener, which undici only removes asynchronously after completion.
|
|
49
|
+
const sources = [signal];
|
|
50
|
+
if (options.signal)
|
|
51
|
+
sources.push(options.signal);
|
|
52
|
+
// `Infinity` is a legal, explicit "no cap" value (see isBoundedMs) — it must
|
|
53
|
+
// be treated the same as "not set" here. Node clamps any setTimeout delay
|
|
54
|
+
// over ~24.8 days to 1ms, so passing Infinity straight to setTimeout would
|
|
55
|
+
// fire the timer almost immediately instead of never.
|
|
56
|
+
const hasAttemptTimeout = isBoundedMs(timeoutMs);
|
|
57
|
+
// Always created (not just when attemptMs is bounded): a handed-off
|
|
58
|
+
// Response's body read may still need bounding by `deadlineAt` alone in
|
|
59
|
+
// `finally` below, even when this attempt itself has no attemptMs cap.
|
|
60
|
+
const timeoutController = new AbortController();
|
|
61
|
+
sources.push(timeoutController.signal);
|
|
62
|
+
const attemptSignal = AbortSignal.any(sources);
|
|
63
|
+
const timeoutId = hasAttemptTimeout ? setTimeout(() => timeoutController.abort(), timeoutMs) : undefined;
|
|
64
|
+
// A fresh Headers instance per attempt: middleware (e.g. defaultHeaders)
|
|
65
|
+
// mutates ctx.headers in place, and that must never leak into the next
|
|
66
|
+
// retry's "starting" headers.
|
|
67
|
+
const ctx = {
|
|
68
|
+
url,
|
|
69
|
+
method: options.method ?? "GET",
|
|
70
|
+
headers: new Headers(options.headers),
|
|
71
|
+
body: resolvedBody,
|
|
72
|
+
signal: attemptSignal,
|
|
73
|
+
attempt: req.attempt,
|
|
74
|
+
ticketId: req.ticketId,
|
|
75
|
+
partition: req.partition,
|
|
76
|
+
};
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await composed(ctx);
|
|
80
|
+
// The timeout may fire after fetch resolves but before this check runs;
|
|
81
|
+
// the timed-out attempt is not trustworthy, so it still surfaces as a
|
|
82
|
+
// timeout (cancellation is checked first in the catch path below).
|
|
83
|
+
if (timeoutController.signal.aborted) {
|
|
84
|
+
return { kind: "timeout" };
|
|
85
|
+
}
|
|
86
|
+
// Check if status code is retryable (was "queued_status", now typed error)
|
|
87
|
+
if (retryOnStatus.includes(response.status)) {
|
|
88
|
+
// Cancel the response body since caller won't read it.
|
|
89
|
+
// Fire-and-forget: cancel() may hang on stuck connections.
|
|
90
|
+
// The body will be GC'd when the response is collected.
|
|
91
|
+
response.body?.cancel().catch(() => { });
|
|
92
|
+
return {
|
|
93
|
+
kind: "error",
|
|
94
|
+
error: new RetryableStatusError(`HTTP ${response.status} ${response.statusText}`, response.status, response, parseRetryAfter(response.headers.get("retry-after"))),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Non-2xx responses are non-retryable errors. The body is handed back
|
|
98
|
+
// unread on `error.response` — bound its read window in `finally`.
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
handedOff = true;
|
|
101
|
+
return {
|
|
102
|
+
kind: "error",
|
|
103
|
+
error: new HttpError(`HTTP ${response.status} ${response.statusText}`, response.status, response),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Parse body — the timeout is still active so a slow body read is
|
|
107
|
+
// covered by the per-attempt deadline (#9).
|
|
108
|
+
if (options.parse) {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = await response.json();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Check timeout first — if our timer fired during response.json(),
|
|
115
|
+
// that is the cause regardless of whether the external signal also
|
|
116
|
+
// aborted (cancellation vs timeout precedence).
|
|
117
|
+
if (timeoutController.signal.aborted) {
|
|
118
|
+
return { kind: "timeout" };
|
|
119
|
+
}
|
|
120
|
+
if (signal.aborted || options.signal?.aborted) {
|
|
121
|
+
return { kind: "cancelled" };
|
|
122
|
+
}
|
|
123
|
+
// The body arrived but isn't JSON: the server answered, and asking
|
|
124
|
+
// again will get the same answer — a parse failure, never retried
|
|
125
|
+
// (B6). Anything else here is the body stream dying mid-read
|
|
126
|
+
// (undici's "terminated" TypeError), which is a real network error.
|
|
127
|
+
if (err instanceof SyntaxError) {
|
|
128
|
+
return {
|
|
129
|
+
kind: "error",
|
|
130
|
+
error: new ValidationError("Response body is not valid JSON", [err], err),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
kind: "error",
|
|
135
|
+
error: new NetworkError("Failed to parse response body as JSON", {
|
|
136
|
+
cause: err,
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const data = options.parse(raw);
|
|
142
|
+
return {
|
|
143
|
+
kind: "success",
|
|
144
|
+
result: { success: true, data, raw: response },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const issues = extractIssues(err);
|
|
149
|
+
return {
|
|
150
|
+
kind: "error",
|
|
151
|
+
error: new ValidationError("Response validation failed", issues, err),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// No parse fn — return raw response, unread. Bound its read window in
|
|
156
|
+
// `finally` instead of clearing the attempt timer.
|
|
157
|
+
handedOff = true;
|
|
158
|
+
return {
|
|
159
|
+
kind: "success",
|
|
160
|
+
result: { success: true, data: undefined, raw: response },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
// Precedence: cancellation wins over timeout. If the ticket or the
|
|
165
|
+
// external signal is aborted, report cancelled even if the timeout
|
|
166
|
+
// also fired — the ticket is already marked cancelled, and
|
|
167
|
+
// cancellation is the caller's terminal intent. A timeout that fires
|
|
168
|
+
// first still surfaces as "timeout": the external signal is not yet
|
|
169
|
+
// aborted when this check runs.
|
|
170
|
+
if (signal.aborted || options.signal?.aborted) {
|
|
171
|
+
return { kind: "cancelled" };
|
|
172
|
+
}
|
|
173
|
+
if (isAbortError(err)) {
|
|
174
|
+
// Could be our timeout abort
|
|
175
|
+
return { kind: "timeout" };
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
kind: "error",
|
|
179
|
+
error: new NetworkError(err instanceof Error ? err.message : "Network error", { cause: err }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
// Clear the in-attempt timer either way — it did its job (bounding the
|
|
184
|
+
// attempt) or the attempt failed on its own. Body has been read (or the
|
|
185
|
+
// attempt failed), so the abort controller can be released.
|
|
186
|
+
if (timeoutId !== undefined)
|
|
187
|
+
clearTimeout(timeoutId);
|
|
188
|
+
// A handed-off Response (unread body on a success-without-parse or
|
|
189
|
+
// HttpError result) gets the same bound its body would have had under
|
|
190
|
+
// `parse`: arm a fresh timer capped at whichever is sooner, this
|
|
191
|
+
// attempt's remaining attemptMs or the ticket's totalMs deadline. It
|
|
192
|
+
// fires only if the caller actually reads the body later — aborting
|
|
193
|
+
// `attemptSignal` (already wired into fetch) makes that read reject,
|
|
194
|
+
// same as any other abort observed after fetch() has resolved. No
|
|
195
|
+
// ticket state changes here: the ticket already resolved.
|
|
196
|
+
if (handedOff) {
|
|
197
|
+
const attemptTimeoutMs = isBoundedMs(timeoutMs) ? timeoutMs : undefined;
|
|
198
|
+
const attemptDeadline = attemptTimeoutMs !== undefined ? attemptStart + attemptTimeoutMs : Number.POSITIVE_INFINITY;
|
|
199
|
+
const totalDeadline = req.deadlineAt ?? Number.POSITIVE_INFINITY;
|
|
200
|
+
const boundAt = Math.min(attemptDeadline, totalDeadline);
|
|
201
|
+
if (Number.isFinite(boundAt)) {
|
|
202
|
+
const delay = Math.max(0, boundAt - Date.now());
|
|
203
|
+
// Name the abort reason after whichever bound fired — ties go to the
|
|
204
|
+
// attempt bound, since it's also the only bound when totalMs isn't
|
|
205
|
+
// configured — using Vereda's own error classes instead of a bare
|
|
206
|
+
// DOMException. That way a caller's `raw.json()`/`error.response.text()`
|
|
207
|
+
// rejects with something `instanceof TimeoutError`/`DeadlineExceededError`,
|
|
208
|
+
// `.kind`-narrowable, and redacted the same as every other error the
|
|
209
|
+
// client builds. The final branch is defensive only: `boundAt` being
|
|
210
|
+
// finite guarantees one of the two bounds above is actually configured.
|
|
211
|
+
const reason = attemptTimeoutMs !== undefined && attemptDeadline <= totalDeadline
|
|
212
|
+
? new TimeoutError(req.displayUrl, attemptTimeoutMs)
|
|
213
|
+
: isBoundedMs(req.timeoutConfig.totalMs)
|
|
214
|
+
? new DeadlineExceededError(req.displayUrl, req.timeoutConfig.totalMs)
|
|
215
|
+
: new TimeoutError(req.displayUrl, attemptTimeoutMs ?? NO_TIMEOUT_CONFIGURED);
|
|
216
|
+
const bodyReadTimer = setTimeout(() => {
|
|
217
|
+
timeoutController.abort(reason);
|
|
218
|
+
}, delay);
|
|
219
|
+
bodyReadTimer.unref();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function buildFetchCall(customFetch) {
|
|
225
|
+
const fetchFn = customFetch ?? globalThis.fetch.bind(globalThis);
|
|
226
|
+
return async (ctx) => {
|
|
227
|
+
const init = {
|
|
228
|
+
method: ctx.method,
|
|
229
|
+
headers: ctx.headers,
|
|
230
|
+
body: ctx.body,
|
|
231
|
+
signal: ctx.signal,
|
|
232
|
+
};
|
|
233
|
+
if (isReadableStream(ctx.body)) {
|
|
234
|
+
// Node's fetch requires duplex: "half" for stream bodies.
|
|
235
|
+
init.duplex = "half";
|
|
236
|
+
}
|
|
237
|
+
return fetchFn(ctx.url, init);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export function composeMiddleware(middlewares, core) {
|
|
241
|
+
return middlewares.reduceRight((next, middleware) => (ctx) => middleware(ctx, next), core);
|
|
242
|
+
}
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Helpers
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
/** Parse a `Retry-After` header value into a delay in ms, or undefined when
|
|
247
|
+
* absent/unparseable. Integer seconds → ms; HTTP-date → ms from now, clamped
|
|
248
|
+
* to ≥ 0; anything else (garbage, negative) → undefined. */
|
|
249
|
+
export function parseRetryAfter(header) {
|
|
250
|
+
if (!header)
|
|
251
|
+
return undefined;
|
|
252
|
+
const trimmed = header.trim();
|
|
253
|
+
if (/^\d+$/.test(trimmed)) {
|
|
254
|
+
return Number(trimmed) * 1000;
|
|
255
|
+
}
|
|
256
|
+
const parsed = Date.parse(trimmed);
|
|
257
|
+
if (Number.isNaN(parsed))
|
|
258
|
+
return undefined;
|
|
259
|
+
return Math.max(0, parsed - Date.now());
|
|
260
|
+
}
|
|
261
|
+
function isAbortError(err) {
|
|
262
|
+
return err instanceof Error && err.name === "AbortError";
|
|
263
|
+
}
|
|
264
|
+
function extractIssues(err) {
|
|
265
|
+
if (err && typeof err === "object" && "errors" in err) {
|
|
266
|
+
return err.errors;
|
|
267
|
+
}
|
|
268
|
+
if (err && typeof err === "object" && "issues" in err) {
|
|
269
|
+
return err.issues;
|
|
270
|
+
}
|
|
271
|
+
return [err];
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../src/queue/executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AppError } from "../core/errors.ts";
|
|
2
|
+
export interface RetryPolicyContext {
|
|
3
|
+
method: string;
|
|
4
|
+
headers?: HeadersInit;
|
|
5
|
+
/** value of merged retry.idempotent for this request */
|
|
6
|
+
idempotent?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type RetryPolicy = (error: AppError, attempt: number, ctx: RetryPolicyContext) => boolean;
|
|
9
|
+
export declare const RETRIABLE_KINDS: ReadonlySet<AppError["kind"]>;
|
|
10
|
+
/**
|
|
11
|
+
* The default retry policy (decision D3). An attempt is retried iff all hold:
|
|
12
|
+
* 1. attempts so far < maxRetries + 1 (enforced by the retry loop bounds)
|
|
13
|
+
* 2. error `kind` ∈ {network, timeout, retryable_status}
|
|
14
|
+
* 3. method is idempotent (GET HEAD OPTIONS PUT DELETE TRACE) OR
|
|
15
|
+
* `ctx.idempotent === true` OR the request has an `Idempotency-Key` header
|
|
16
|
+
* 4. user `retryWhen` (consulted separately via `shouldRetry`) returns true
|
|
17
|
+
* 5. the request is not cancelled / past its deadline (handled elsewhere)
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultRetryPolicy(error: AppError, _attempt: number, ctx: RetryPolicyContext): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Combined retry gate used by both call sites so rule ordering lives in one
|
|
22
|
+
* place: the default policy runs first, then the user's `retryWhen` is
|
|
23
|
+
* consulted. `retryWhen` can only veto a retry, never force one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldRetry(error: AppError, attempt: number, ctx: RetryPolicyContext, retryWhen?: (error: AppError, attempt: number) => boolean): boolean;
|
|
26
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/queue/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,wDAAwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC;AAEjG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAuD,CAAC;AAIlH;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAQtG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAC1B,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,kBAAkB,EACvB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,GACvD,OAAO,CAIT"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const RETRIABLE_KINDS = new Set(["network", "timeout", "retryable_status"]);
|
|
2
|
+
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"]);
|
|
3
|
+
/**
|
|
4
|
+
* The default retry policy (decision D3). An attempt is retried iff all hold:
|
|
5
|
+
* 1. attempts so far < maxRetries + 1 (enforced by the retry loop bounds)
|
|
6
|
+
* 2. error `kind` ∈ {network, timeout, retryable_status}
|
|
7
|
+
* 3. method is idempotent (GET HEAD OPTIONS PUT DELETE TRACE) OR
|
|
8
|
+
* `ctx.idempotent === true` OR the request has an `Idempotency-Key` header
|
|
9
|
+
* 4. user `retryWhen` (consulted separately via `shouldRetry`) returns true
|
|
10
|
+
* 5. the request is not cancelled / past its deadline (handled elsewhere)
|
|
11
|
+
*/
|
|
12
|
+
export function defaultRetryPolicy(error, _attempt, ctx) {
|
|
13
|
+
// Rule 2 — the error kind must be transient.
|
|
14
|
+
if (!RETRIABLE_KINDS.has(error.kind))
|
|
15
|
+
return false;
|
|
16
|
+
// Rule 3 — the request must be safe to repeat (idempotent by method or opt-in).
|
|
17
|
+
if (IDEMPOTENT_METHODS.has(ctx.method.toUpperCase()))
|
|
18
|
+
return true;
|
|
19
|
+
if (ctx.idempotent)
|
|
20
|
+
return true;
|
|
21
|
+
if (ctx.headers && new Headers(ctx.headers).has("idempotency-key"))
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Combined retry gate used by both call sites so rule ordering lives in one
|
|
27
|
+
* place: the default policy runs first, then the user's `retryWhen` is
|
|
28
|
+
* consulted. `retryWhen` can only veto a retry, never force one.
|
|
29
|
+
*/
|
|
30
|
+
export function shouldRetry(error, attempt, ctx, retryWhen) {
|
|
31
|
+
if (!defaultRetryPolicy(error, attempt, ctx))
|
|
32
|
+
return false;
|
|
33
|
+
if (retryWhen && !retryWhen(error, attempt))
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/queue/policy.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,eAAe,GAAkC,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAElH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAEzF;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAe,EAAE,QAAgB,EAAE,GAAuB;IAC5F,6CAA6C;IAC7C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,gFAAgF;IAChF,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IAClE,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAC1B,KAAe,EACf,OAAe,EACf,GAAuB,EACvB,SAAyD;IAEzD,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AppError } from "../core/errors.ts";
|
|
2
|
+
import { type RequestOptions, type RetryConfig, type TimeoutConfig } from "../core/types.ts";
|
|
3
|
+
import type { Ticket, TicketController } from "../ticket/ticket.ts";
|
|
4
|
+
import type { Bulkhead } from "./bulkhead.ts";
|
|
5
|
+
import type { CircuitBreaker } from "./circuit-breaker.ts";
|
|
6
|
+
import { type MiddlewareFn } from "./executor.ts";
|
|
7
|
+
import type { Semaphore } from "./semaphore.ts";
|
|
8
|
+
export interface RetryJobOptions {
|
|
9
|
+
/** Fully resolved request URL (baseUrl applied) — what each retry fetches. */
|
|
10
|
+
url: string;
|
|
11
|
+
/** `url` as it may appear in errors/logs (query-redacted unless disabled). */
|
|
12
|
+
displayUrl: string;
|
|
13
|
+
requestOptions: RequestOptions<unknown>;
|
|
14
|
+
timeoutConfig: TimeoutConfig;
|
|
15
|
+
retryConfig: RetryConfig;
|
|
16
|
+
/** Absolute `Date.now()` timestamp of the ticket's whole-ticket deadline
|
|
17
|
+
* (`startTime + timeoutConfig.totalMs`), or undefined when `totalMs`
|
|
18
|
+
* isn't bounded. Forwarded to `executeRequest` so a handed-off Response
|
|
19
|
+
* body (unread success or `HttpError`) is bounded by it — see
|
|
20
|
+
* `ExecuteRequest.deadlineAt`. */
|
|
21
|
+
deadlineAt?: number;
|
|
22
|
+
ticket: Ticket<unknown>;
|
|
23
|
+
controller: TicketController<unknown>;
|
|
24
|
+
middleware: MiddlewareFn[];
|
|
25
|
+
/** Per-attempt bulkhead for retry scheduling. */
|
|
26
|
+
bulkhead: Bulkhead;
|
|
27
|
+
/** Global concurrency semaphore acquired after the partition slot (D4). */
|
|
28
|
+
semaphore?: Semaphore;
|
|
29
|
+
/** Per-partition circuit breaker. Inert unless configured/enabled. */
|
|
30
|
+
circuitBreaker: CircuitBreaker;
|
|
31
|
+
/** Partition name, used to construct CircuitOpenError when the breaker is open. */
|
|
32
|
+
partition: string;
|
|
33
|
+
/** The error from the first attempt (fired client-side before queuing). */
|
|
34
|
+
firstError: AppError;
|
|
35
|
+
/** Ms the first attempt spent waiting for a bulkhead/semaphore permit,
|
|
36
|
+
* carried over so the final queuedMs reported to the client covers the
|
|
37
|
+
* whole ticket lifetime, not just the retries run inside this loop. */
|
|
38
|
+
initialQueuedMs: number;
|
|
39
|
+
onRetry?: (attempt: number, delayMs: number, error: AppError) => void;
|
|
40
|
+
/** Called on success so the client can emit the success event with the
|
|
41
|
+
* response status code, the total attempt count, and the cumulative ms
|
|
42
|
+
* spent waiting for a bulkhead/semaphore permit across all attempts. */
|
|
43
|
+
onSuccess?: (statusCode: number, attempts: number, queuedMs: number) => void;
|
|
44
|
+
/** Called on failure (exhausted retries or policy veto inside the loop)
|
|
45
|
+
* so the client can emit the failure event. `queuedMs` is cumulative
|
|
46
|
+
* across all attempts made so far. */
|
|
47
|
+
onFailure?: (error: AppError, attempts: number, queuedMs: number) => void;
|
|
48
|
+
/** Called on cancellation (user cancel, deadline, or external signal)
|
|
49
|
+
* so the client can emit the cancelled event. `queuedMs` is cumulative
|
|
50
|
+
* across all attempts made so far. */
|
|
51
|
+
onCancelled?: (attempts: number, queuedMs: number) => void;
|
|
52
|
+
/** Called before markDone to clean up external resources (e.g. signal listeners). */
|
|
53
|
+
onCleanup?: () => void;
|
|
54
|
+
/** Custom fetch function. Falls back to globalThis.fetch. */
|
|
55
|
+
fetch?: typeof globalThis.fetch;
|
|
56
|
+
}
|
|
57
|
+
export declare function runRetryLoop(job: RetryJobOptions): Promise<void>;
|
|
58
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/queue/retry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAWlD,OAAO,EAIN,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC/B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACxC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB;;;;uCAImC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,EAAE,QAAQ,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,sEAAsE;IACtE,cAAc,EAAE,cAAc,CAAC;IAC/B,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,UAAU,EAAE,QAAQ,CAAC;IACrB;;4EAEwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtE;;6EAEyE;IACzE,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7E;;2CAEuC;IACvC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E;;2CAEuC;IACvC,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA6QtE"}
|