import { type ByteWindow } from '../../utils/ranges.ts';
export declare const MAX_BODY_BYTES: number;
export type ErrorOf = (response: Response, body: string) => Error;
/**
 * `fetchFn` (the global fetch, read per call, when absent) bounded by how
 * long a request may wait on the server: to connect and answer, then for
 * each chunk of the body the caller asks for. The clock restarts when the
 * headers arrive and on every chunk, and stops while the caller is not
 * reading, once the body ends and when the caller cancels it, so a download
 * that keeps flowing is never cut off. A request that waits longer fails
 * with a `TimeoutError` DOMException. The body is relayed, locking the
 * original only while a read is pending, so a body drained, failed or
 * cancelled is left unlocked; the relay keeps the original's `url` and
 * `redirected`. Python's twin is `stall_timeout`, which aiohttp spells as
 * sock_connect and sock_read.
 */
export declare function stallTimeout(seconds?: number, fetchFn?: typeof fetch): typeof fetch;
/**
 * Pull a GET response in bounded chunks, cancelling its body on early exit.
 * `retry` applies to opening the response only: once a chunk has reached the
 * caller a failure propagates, since a second attempt would hand it the same
 * bytes twice.
 */
export declare function apiStream(url: string, options: Pick<ApiRequestOptions, 'errorOf' | 'headers' | 'fetchFn' | 'retry' | 'sleep' | 'timeout'> & {
    signal?: AbortSignal;
    /** Told the answering response's headers, lower-cased, once and before
     * the first chunk, so an empty body reports them too. */
    onResponse?: (headers: Record<string, string>) => void;
}): AsyncGenerator<Uint8Array, void>;
/** Pull a response body in bounded chunks, cancelling it on early exit. */
export declare function bodyChunks(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array, void>;
export interface RetryPolicy {
    /** Response statuses worth retrying. */
    readonly statuses: ReadonlySet<number>;
    /** Retries allowed after the first attempt. */
    readonly maxRetries: number;
    /**
     * Ceiling on every inter-attempt wait, whether the server asked for it
     * or the exponential fallback chose it.
     */
    readonly maxBackoff: number;
    /**
     * Where the wait between attempts comes from: 'header' reads Retry-After
     * and falls back to exponential backoff (Graph's convention); 'body'
     * reads a JSON `retry_after` field and falls back to 1s (Discord's).
     */
    readonly delaySource: 'header' | 'body';
    /**
     * Also retry connection-level failures, which never carry a response;
     * the wait for those is the exponential backoff.
     */
    readonly retryTransport?: boolean;
    /**
     * A veto over a listed status, given the status and the body text, for
     * an API whose one status means two things. Airtable answers both "slow
     * down" and "monthly quota spent" with a 429, and only the first is worth
     * waiting for; a vetoed response maps through `errorOf` at once. Absent
     * retries every listed status.
     */
    readonly retryable?: (status: number, text: string) => boolean;
    /**
     * Per-status floor under the wait, for an API that documents a penalty
     * window: after a 429, Airtable refuses every request for 30 seconds, so
     * a shorter backoff only spends the retries inside the window.
     * `maxBackoff` still caps the result.
     */
    readonly minDelays?: Readonly<Record<number, number>>;
}
export declare const NO_RETRY: RetryPolicy;
/**
 * How to read the reply: 'json' parses the body (an empty one reads as
 * null); 'none' ignores it; 'bytes' returns it raw, trimmed to the window
 * when the server ignored the Range; 'text' returns it as a string;
 * 'location' returns the Location header; 'response' returns the decoded body
 * with status and lower-cased headers; 'bytes_response' returns the 'bytes'
 * body with the same status and headers, for a caller that needs a content
 * token the response carries.
 */
export type ReadMode = 'json' | 'none' | 'bytes' | 'bytes_response' | 'text' | 'location' | 'response';
/**
 * `base` and `path` joined by one slash, with each defined `query` entry
 * URI-encoded onto the end; an undefined value is left out.
 */
export declare function buildUrl(base: string, path: string, query: Record<string, string | number | undefined>): string;
/**
 * A response's headers as a plain record, names lower-cased; fetch already
 * joins a repeated header, as python's lowered_headers does.
 */
export declare function loweredHeaders(headers: Headers): Record<string, string>;
/** Decoded body plus the wire metadata cursor pagination reads. Mirrors
 * python's `ApiResponse`; a caller asking for `read: 'response'` gets this
 * rather than the bare body, because a `Link` header is the only thing that
 * says whether another page exists. */
export interface ApiResponse {
    data: unknown;
    status: number;
    /** Lower-cased, the way python's dump spells them, so one header is read
     * by one name in both languages. */
    headers: Record<string, string>;
}
export interface ApiRequestOptions {
    errorOf: ErrorOf;
    /** Request headers, already merged by the caller. */
    headers?: Record<string, string> | undefined;
    params?: Record<string, string | number | boolean> | undefined;
    /** JSON request body; absent sends no body, so a caller that means "send
     * an empty object" passes `{}` explicitly. */
    json?: unknown;
    /** Raw request body (bytes, form data), for endpoints that do not speak
     * JSON; exclusive with `json`. */
    body?: BodyInit | undefined;
    retry?: RetryPolicy | undefined;
    read?: ReadMode | undefined;
    /** The byte range to request; the Range header and the trim-if-unranged
     * guard both come from it. */
    window?: ByteWindow | undefined;
    /** Maximum decoded bytes retained by successful bytes/bytes_response reads.
     * Defaults to 64 MiB; use a stream for larger downloads. Ignored Range
     * prefixes do not count, and overflow fails rather than truncates. */
    maxBodyBytes?: number | undefined;
    /** Seconds a request may wait to connect, or between two reads of its
     * response, before it fails (`stallTimeout`); absent is `DEFAULT_TIMEOUT`. */
    timeout?: number | undefined;
    /** The fetch to use, so transports keep their injection seam. */
    fetchFn?: typeof fetch | undefined;
    /** The wait between attempts, in seconds, injectable so a test or a
     * simulated clock spends no real time; absent is a real timer. */
    sleep?: ((seconds: number) => Promise<void>) | undefined;
    /** The signal the request answers to in place of the call's. Cleanup that
     * must still run once the call stopped, as aborting an upload does,
     * passes a bounded one of its own. */
    signal?: AbortSignal | undefined;
}
/**
 * Whether a server-supplied delay is one we can actually wait out. NaN and
 * infinity are unusable (`setTimeout` silently clamps both to 1ms, turning
 * the wait into a hot retry), and a negative delay is malformed per RFC
 * 9110, so all three fall back the way an unparseable header does.
 */
export declare function usableDelay(value: number): boolean;
export declare function headerDelay(response: Response, attempt: number, retry: RetryPolicy): number;
export declare function bodyDelay(text: string, retry: RetryPolicy): number;
/** Raise a wait to the policy's floor for this status, then cap it. */
export declare function flooredDelay(delay: number, status: number, retry: RetryPolicy): number;
/** Collect a bounded byte response, discarding an ignored Range prefix. */
export declare function readBytes(response: Pick<Response, 'body' | 'status'>, window: ByteWindow | undefined, maxBodyBytes: number): Promise<Uint8Array<ArrayBuffer>>;
/**
 * One round-trip against an HTTP API, with retry and error mapping.
 * Returns the reply per `options.read`, defaulting to the parsed JSON
 * body (null when the body is empty, e.g. a 204).
 */
export declare function apiRequest(method: string, url: string, options: ApiRequestOptions): Promise<unknown>;
//# sourceMappingURL=client.d.ts.map