@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
153 lines • 8.22 kB
TypeScript
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