/** * `@gusnips/http/retry`: whether a failed request is worth a second attempt, and when. * * This is frontkit's invariant 8, one layer down from `@gusnips/react`, so a zero-dependency SDK * can follow the same rule as the app: it imports nothing, and a generator can copy the file * into an SDK whole. Five SDKs in one fleet each wrote their own, and each missed part of it: * none retried a 408, none read an explicit `retryAfterSecs: null`, three read the body's wait * before the header, and the one that sent an idempotency key retried writes that had none. * * Every function reads an error by its fields, not its class: `status`, `code`, `details` (the * envelope's), and `retryAfterSecs` (the `Retry-After` header, parsed). An error with no HTTP * `status` is a request that got no answer at all: offline, DNS, a dropped connection. */ export interface RetryOptions { /** * Whether the request may run twice: a read, or a write that carries an idempotency key the * server honours. Required, because a default either way is wrong for half the calls, and the * wrong half of "yes" sends a second message or charges twice. * * When `false`, only an answer proving nothing ran is retried: 408, 425 or 429. A failure with * no answer, or a 5xx, may have run the write already. */ repeatable: boolean; /** * Error codes that mean "this limit does not clear by waiting": a spent monthly quota, a * balance to top up. They usually arrive as 402 or 429, and the status alone cannot tell them * from a burst limit. Not needed where the server sends `details.retryAfterSecs: null`. */ durableCodes?: readonly string[]; /** * The longest stated wait still worth retrying. Default 10 seconds. A burst limit that clears * in two seconds is worth sitting out; a limit that clears in an hour is an answer to show. */ maxWaitSecs?: number; } /** * Should this failure be retried? * * if (attempt < 3 && isRetryable(error, { repeatable: method === "GET" || !!key })) */ export declare function isRetryable(error: unknown, options: RetryOptions): boolean; /** * How long before retry number `attemptIndex` (0 for the first): the server's stated wait when * there is one, an exponential backoff otherwise, and never less than the backoff. A * `Retry-After: 0` says "now", which is still too soon for a client that was just refused. * * The backoff lands at random between half and one and a half times 1 s·2ⁿ (capped at 30 s * first, so the spread survives the cap). Clients cut off by one outage all fail at the same * moment, and an exact backoff brings them all back at the same moment too, into a server that * is still coming up. A stated wait is not moved: the server picked that time, and earlier is * refused again. */ export declare function retryDelayMs(attemptIndex: number, error: unknown): number; /** * Seconds until a refusal clears, wherever the API put them: the `Retry-After` header first, * because it is the standard place and a limiter writes it without being asked, then * `details.retryAfterSecs`, which some APIs send in the body instead. * * `null` means "no countdown to show". That is right both for a refusal that said nothing and * for one that said waiting never helps, and a countdown UI wants both to read the same. */ export declare function retryAfterSecs(error: unknown): number | null; /** * `Retry-After` in either form RFC 9110 allows: seconds, or an HTTP date. The date form is the * one that gets skipped, which turns a stated wait into none. A date already past is 0. * * Both forms round UP. A date names a whole second and the clock is somewhere inside the one * before, so rounding to nearest sends a retry up to half a second early, into the limiter that * refused it, about half the time. A wait until you are allowed rounds up; the server kit's * webhook sender has always read it that way. "1.5" is not the RFC's seconds form, but what it * asks for is plain, so it reads as 2. */ export declare function parseRetryAfter(value: string | null | undefined): number | undefined; //# sourceMappingURL=retry.d.ts.map