/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * @file Bounded retry policy for {@linkcode APIClient} — which failures are worth another attempt, how * long to wait, and the hard ceiling on both. * * Lifted from `98c4dda1:filer/sdk/sec-client.ts`, where the `Retry-After` handling (the HTTP-date * form, the long fallback for a present-but-unparseable value, and the RFC 9110 `1*DIGIT` * tightening) was settled over two review rounds. The classifier is the same taxonomy that client * used, restated against Axios's error shape rather than a raw `Response`. */ /** * A hard ceiling on how long a single retry wait is ever allowed to be, REGARDLESS of what a server-supplied * `Retry-After` asks for. Honoring `Retry-After` is the right side of most fair-access policies, but an unbounded * honor-anything policy would let a pathological (or misconfigured) server hang a bulk crawl for hours; 60s is generous * for anything a real rate limiter would plausibly ask for. Also the fallback used when `Retry-After` is PRESENT but * unparseable — a malformed header is still the server asking us to back off, and guessing LONG is the safe failure * mode; guessing short (the exponential default) risks hammering a server that explicitly asked for space. */ export declare const MAX_RETRY_AFTER_MS = 60000; /** * Attempts a retrying client makes by default (INCLUDING the first) — a stated ceiling, not "until it works." */ export declare const DEFAULT_MAX_ATTEMPTS = 3; /** * Default base delay for the exponential backoff between attempts, in milliseconds. */ export declare const DEFAULT_BASE_RETRY_DELAY_MS = 500; /** * How a client should treat one failed attempt. */ export interface RetryDirective { /** * Whether THIS CLASS of failure is worth another attempt — `true` for 408/429/5xx and every network-class failure * (connect, DNS, timeout, mid-body-transfer drop), `false` for 403/404/other non-transient statuses, a * caller-initiated cancel, and a body that failed to decode. */ retryable: boolean; /** * The server-requested wait derived from a `Retry-After` response header, in milliseconds, or `null` when the header * was absent (the caller should fall back to its own exponential backoff). */ retryAfterMs: number | null; } /** * Parse a `Retry-After` header value — numeric `delay-seconds` or an HTTP-date, per RFC 9110 — into a clamped wait * duration in ms. * * Returns `null` only when the header is ABSENT. When the header IS present, this always returns a number: the parsed * (and {@linkcode MAX_RETRY_AFTER_MS}-clamped) value on success, or `MAX_RETRY_AFTER_MS` itself when the value is * present but matches neither valid form — see the constant's docstring for why unparseable fails open toward caution * rather than speed. * * The HTTP-date branch compares against REAL wall-clock time (`Date.now()`), not an injectable clock — an HTTP-date is * an absolute calendar timestamp, which only means something relative to the actual current time. */ export declare function parseRetryAfterMs(header: string | null | undefined): number | null; /** * Whether an HTTP status is worth another attempt: 408 and 429 by name, plus the whole 5xx range. Everything else — * every other 4xx, every 2xx/3xx that still produced an error — is terminal. * * The 4xx exclusion is the load-bearing part, and 403 is why. A 403 from a rate-limited public API means the request * failed to identify itself (for SEC EDGAR, a missing or non-descriptive `User-Agent`); it does NOT mean the resource * is gone or that this client is banned. Retrying it cannot succeed and burns rate budget on a request that was never * going to be served. An earlier revision spelled this out as a redundant `if (status === 403) return false` ahead of * the range check — no mutation could kill it, because the range check already excluded 403, so it was removed rather * than left as unfalsifiable decoration. The property is proved by mutating this range instead: broadening it to `>= * 400` makes the 403 and 404 tests fail. */ export declare function isRetryableStatus(status: number): boolean; /** * Classify one failed attempt: is this failure class worth retrying, and did the server name its own backoff? * * A NETWORK-class failure — a dropped socket, a DNS blip, this attempt's own timeout firing, or a body read that died * mid-transfer — is retryable. This is the case a bulk crawler hits most: fetching multi-MB documents, a dropped socket * is far more common than a 503, and the standalone SEC client shipped a version that treated it as terminal. * * A caller-initiated cancel (`ERR_CANCELED`, i.e. the caller's own `AbortSignal` fired) is NOT retryable — the caller * asked us to stop, and retrying would defy that. Axios reports its own `timeout` config as `ECONNABORTED`/`ETIMEDOUT`, * so the two are distinguishable. */ export declare function classifyAxiosFailure(error: unknown): RetryDirective; /** * Retry configuration for {@linkcode APIClient}. Pass `true` to accept every default. * * Retry is OPT-IN: an `APIClient` constructed without this option makes exactly one attempt, which is what the existing * `TileAPI` consumer has always done. Turning it on repo-wide would silently multiply every caller's failure latency. */ export interface RetryOptions { /** * Total attempts, INCLUDING the first, before giving up. A stated ceiling, not "until it works". Default * {@linkcode DEFAULT_MAX_ATTEMPTS}. */ maxAttempts?: number; /** * Base delay for the exponential backoff, in milliseconds. Attempt `n`'s wait is `baseDelayMs * 2^(n-1)`, UNLESS the * response carried a `Retry-After` header, which is honored instead. Default * {@linkcode DEFAULT_BASE_RETRY_DELAY_MS}. */ baseDelayMs?: number; } /** * A fully-resolved retry policy — {@linkcode RetryOptions} with every default filled in. */ export interface ResolvedRetryPolicy { maxAttempts: number; baseDelayMs: number; } /** * Fill in {@linkcode RetryOptions}' defaults. `undefined` (the absent option) resolves to a single attempt — no retry. */ export declare function resolveRetryPolicy(options: RetryOptions | boolean | undefined): ResolvedRetryPolicy; /** * The wait before attempt `attempt + 1`, given the directive from attempt `attempt` (1-based). A server-supplied * `Retry-After` always wins over the exponential default. */ export declare function retryDelayMs(attempt: number, directive: RetryDirective, policy: ResolvedRetryPolicy): number; //# sourceMappingURL=retry.d.ts.map