Files
clean-architecture-frontend…/src/adapters/http/retry-policy.js
T

80 lines
2.2 KiB
JavaScript

const retryKinds = new Set([
"NETWORK_UNREACHABLE",
"REQUEST_TIMEOUT",
"RATE_LIMITED",
"SERVER_FAILURE",
]);
/**
* @param {number} retryIndex
* @param {() => number} [random]
* @param {number} [baseDelayMs]
* @param {number} [maxDelayMs]
*/
export function calculateBackoff(
retryIndex,
random = Math.random,
baseDelayMs = 250,
maxDelayMs = 2_000,
) {
return Math.min(maxDelayMs, baseDelayMs * 2 ** retryIndex) * random();
}
/** @param {string | null | undefined} value @param {number} [now] */
export function parseRetryAfter(value, now = Date.now()) {
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) {
return seconds < 0 ? null : seconds * 1_000;
}
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return null;
return Math.max(0, timestamp - now);
}
/**
* @param {{ idempotency: "safe" | "keyed" | "none" }} operation
* @param {{ kind: string, retryAfterMs?: number, httpStatus?: number }} failure
* @param {number} retryCount
* @param {number} [maxRetries]
*/
export function shouldRetry(operation, failure, retryCount, maxRetries = 2) {
if (retryCount >= maxRetries) return false;
if (!retryKinds.has(failure.kind)) return false;
if (
failure.kind === "SERVER_FAILURE" &&
failure.httpStatus !== undefined &&
![502, 503, 504].includes(failure.httpStatus)
) {
return false;
}
if (
failure.kind === "RATE_LIMITED" &&
typeof failure.retryAfterMs === "number" &&
failure.retryAfterMs > 30_000
) {
return false;
}
return operation.idempotency === "safe" || operation.idempotency === "keyed";
}
/**
* @param {{ kind: string, retryAfterMs?: number, retryAfter?: string }} failure
* @param {number} retryIndex
* @param {() => number} [random]
* @param {number} [now]
*/
export function retryDelay(failure, retryIndex, random = Math.random, now = Date.now()) {
const localBackoff = calculateBackoff(retryIndex, random);
if (failure.kind !== "RATE_LIMITED") return localBackoff;
const retryAfterMs =
typeof failure.retryAfterMs === "number"
? failure.retryAfterMs
: parseRetryAfter(failure.retryAfter, now);
return retryAfterMs === null ? localBackoff : Math.max(localBackoff, retryAfterMs);
}