UNPKG

@happy-ts/fetch-t

Version:

Type-safe Fetch API wrapper with abortable requests, timeout support, progress tracking, automatic retry, and Rust-like Result error handling.

632 lines (603 loc) 24.4 kB
import { AsyncIOResult } from 'happy-rusty'; import { IOResult } from 'happy-rusty'; /** * Error name for aborted fetch requests. * * This matches the standard `AbortError` name used by the Fetch API when a request * is cancelled via `AbortController.abort()`. * * @since 1.0.0 * @example * ```typescript * import { fetchT, ABORT_ERROR } from '@happy-ts/fetch-t'; * * const task = fetchT('https://api.example.com/data', { abortable: true }); * task.abort(); * * const result = await task.result; * result.inspectErr((err) => { * if (err.name === ABORT_ERROR) { * console.log('Request was aborted'); * } * }); * ``` */ export declare const ABORT_ERROR: "AbortError"; /** * Custom error class for HTTP error responses (non-2xx status codes). * * Thrown when `Response.ok` is `false`. Contains the HTTP status code * for programmatic error handling. * * @since 1.0.0 * @example * ```typescript * import { fetchT, FetchError } from '@happy-ts/fetch-t'; * * const result = await fetchT('https://api.example.com/not-found', { * responseType: 'json', * }); * * result.inspectErr((err) => { * if (err instanceof FetchError) { * console.log('HTTP Status:', err.status); // e.g., 404 * console.log('Status Text:', err.message); // e.g., "Not Found" * * // Handle specific status codes * switch (err.status) { * case 401: * console.log('Unauthorized - please login'); * break; * case 404: * console.log('Resource not found'); * break; * case 500: * console.log('Server error'); * break; * } * } * }); * ``` */ export declare class FetchError extends Error { /** * The error name, always `'FetchError'`. */ name: string; /** * The HTTP status code of the response (e.g., 404, 500). */ status: number; /** * Creates a new FetchError instance. * * @param message - The status text from the HTTP response (e.g., "Not Found"). * @param status - The HTTP status code (e.g., 404). */ constructor(message: string, status: number); } /** * Extended fetch options that add additional capabilities to the standard `RequestInit`. * * @since 1.0.0 * @example * ```typescript * import { fetchT, type FetchInit } from '@happy-ts/fetch-t'; * * const options: FetchInit = { * // Standard RequestInit options * method: 'POST', * headers: { 'Content-Type': 'application/json' }, * body: JSON.stringify({ key: 'value' }), * * // Extended options * abortable: true, // Return FetchTask for manual abort control * responseType: 'json', // Auto-parse response as JSON * timeout: 10000, // Abort after 10 seconds * onProgress: (result) => { // Track download progress * result.inspect(({ completedByteLength, totalByteLength }) => { * console.log(`${completedByteLength}/${totalByteLength}`); * }); * }, * onChunk: (chunk) => { // Receive raw data chunks * console.log('Received chunk:', chunk.byteLength, 'bytes'); * }, * }; * * const task = fetchT('https://api.example.com/upload', options); * ``` */ export declare interface FetchInit extends RequestInit { /** * When `true`, returns a `FetchTask` instead of `FetchResult`. * * The `FetchTask` provides `abort()` method and `aborted` status. * * @default false */ abortable?: boolean; /** * Specifies how the response body should be parsed. * * - `'text'` - Returns `string` * - `'json'` - Returns parsed JSON (type `T`) * - `'arraybuffer'` - Returns `ArrayBuffer` * - `'bytes'` - Returns `Uint8Array<ArrayBuffer>` (with fallback for older environments) * - `'blob'` - Returns `Blob` * - `'stream'` - Returns `ReadableStream<Uint8Array<ArrayBuffer>>` * - `undefined` - Returns raw `Response` object * * When using a dynamic string value (not a literal type), the return type * will be `FetchResponseData` (union of all possible types). */ responseType?: FetchResponseType; /** * Maximum time in milliseconds to wait for the request to complete. * * If exceeded, the request is automatically aborted with a `TimeoutError`. * Must be a positive number. */ timeout?: number; /** * Retry options. * * Can be a number (shorthand for retries count) or an options object. * * @example * ```typescript * // Retry up to 3 times on network errors * const result = await fetchT('https://api.example.com/data', { * retry: 3, * }); * * // Detailed configuration * const result = await fetchT('https://api.example.com/data', { * retry: { * retries: 3, * delay: 1000, * when: [500, 502], * onRetry: (error, attempt) => console.log(error), * }, * }); * ``` */ retry?: number | FetchRetryOptions; /** * Callback invoked during download to report progress. * * Receives an `IOResult<FetchProgress>`: * - `Ok(FetchProgress)` - Progress update with byte counts * - `Err(Error)` - If `Content-Length` header is missing (called once) * * **Note**: This feature uses `response.clone()` internally. The cloned stream shares * the same underlying data source (via `tee()`), so it does NOT double memory usage. * However, if the two streams consume data at different speeds, chunks may be buffered * temporarily until both streams have read them. * * @param progressResult - The progress result, either success with progress data or error. */ onProgress?: (progressResult: IOResult<FetchProgress>) => void; /** * Callback invoked when a chunk of data is received. * * Useful for streaming or processing data as it arrives. * Each chunk is a `Uint8Array<ArrayBuffer>` containing the raw bytes. * * **Note**: This feature uses `response.clone()` internally. The cloned stream shares * the same underlying data source (via `tee()`), so it does NOT double memory usage. * However, if the two streams consume data at different speeds, chunks may be buffered * temporarily until both streams have read them. * * @param chunk - The raw data chunk received from the response stream. */ onChunk?: (chunk: Uint8Array<ArrayBuffer>) => void; } /** * Represents the download progress of a fetch operation. * * Passed to the `onProgress` callback when tracking download progress. * Note: Progress tracking requires the server to send a `Content-Length` header. * * @since 1.0.0 * @example * ```typescript * import { fetchT, type FetchProgress } from '@happy-ts/fetch-t'; * * await fetchT('https://example.com/file.zip', { * responseType: 'blob', * onProgress: (result) => { * result.inspect((progress: FetchProgress) => { * const percent = (progress.completedByteLength / progress.totalByteLength) * 100; * console.log(`Downloaded: ${percent.toFixed(1)}%`); * }); * }, * }); * ``` */ export declare interface FetchProgress { /** * The total number of bytes to be received (from Content-Length header). */ totalByteLength: number; /** * The number of bytes received so far. */ completedByteLength: number; } /** * Union type of all possible fetchT response data types. * * Used when `responseType` is a dynamic string value rather than a literal type, * as the exact return type cannot be determined at compile time. * * @since 1.9.0 * @example * ```typescript * import { fetchT, type FetchResponseData } from '@happy-ts/fetch-t'; * * // When responseType is dynamic, return type is FetchResponseData * const responseType = getResponseType(); // returns string * const result = await fetchT('https://api.example.com/data', { responseType }); * // result is Result<FetchResponseData, Error> * ``` */ export declare type FetchResponseData = string | ArrayBuffer | Blob | Uint8Array<ArrayBuffer> | ReadableStream<Uint8Array<ArrayBuffer>> | Response | null; /** * Specifies the expected response type for automatic parsing. * * - `'text'` - Parse response as string via `Response.text()` * - `'json'` - Parse response as JSON via `Response.json()` * - `'arraybuffer'` - Parse response as ArrayBuffer via `Response.arrayBuffer()` * - `'bytes'` - Parse response as Uint8Array<ArrayBuffer> via `Response.bytes()` (with fallback for older environments) * - `'blob'` - Parse response as Blob via `Response.blob()` * - `'stream'` - Return the raw `ReadableStream` for streaming processing * * If not specified, the raw `Response` object is returned. * * @since 1.0.0 * @example * ```typescript * import { fetchT, type FetchResponseType } from '@happy-ts/fetch-t'; * * const responseType: FetchResponseType = 'json'; * * const result = await fetchT('https://api.example.com/data', { responseType }); * ``` */ export declare type FetchResponseType = 'text' | 'arraybuffer' | 'blob' | 'json' | 'bytes' | 'stream'; /** * Represents the result of a fetch operation as an async Result type. * * This is an alias for `AsyncIOResult<T>` from the `happy-rusty` library, * providing Rust-like error handling without throwing exceptions. * * @typeParam T - The type of the data expected in a successful response. * @since 1.0.0 * @example * ```typescript * import { fetchT, type FetchResult } from '@happy-ts/fetch-t'; * * // FetchResult is a Promise that resolves to Result<T, Error> * const result: FetchResult<string> = fetchT('https://api.example.com', { * responseType: 'text', * }); * * const res = await result; * res * .inspect((text) => console.log('Success:', text)) * .inspectErr((err) => console.error('Error:', err)); * ``` */ export declare type FetchResult<T> = AsyncIOResult<T>; /** * Options for configuring retry behavior. * * @since 1.8.0 */ export declare interface FetchRetryOptions { /** * Number of times to retry the request on failure. * * By default, only network errors trigger retries. HTTP errors (4xx, 5xx) * require explicit configuration via `when`. * * @default 0 (no retries) */ retries?: number; /** * Delay between retry attempts in milliseconds. * * Can be a static number or a function for custom strategies like exponential backoff. * The function receives the current attempt number (1-indexed). * * @default 0 (immediate retry) */ delay?: number | ((attempt: number) => number); /** * Conditions under which to retry the request. * * Can be an array of HTTP status codes or a custom function. * By default, only network errors (not FetchError) trigger retries. */ when?: number[] | ((error: Error, attempt: number) => boolean); /** * Callback invoked before each retry attempt. * * Useful for logging, metrics, or adjusting request parameters. */ onRetry?: (error: Error, attempt: number) => void; } /** * Fetches a resource from the network as a text string and returns an abortable `FetchTask`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'text'`. * @returns A `FetchTask` representing the abortable operation with a `string` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: 'text'; }): FetchTask<string>; /** * Fetches a resource from the network as an ArrayBuffer and returns an abortable `FetchTask`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'arraybuffer'`. * @returns A `FetchTask` representing the abortable operation with an `ArrayBuffer` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: 'arraybuffer'; }): FetchTask<ArrayBuffer>; /** * Fetches a resource from the network as a Blob and returns an abortable `FetchTask`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'blob'`. * @returns A `FetchTask` representing the abortable operation with a `Blob` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: 'blob'; }): FetchTask<Blob>; /** * Fetches a resource from the network and parses it as JSON, returning an abortable `FetchTask`. * * @typeParam T - The expected type of the parsed JSON data. * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'json'`. * @returns A `FetchTask` representing the abortable operation with a response parsed as type `T`. */ export declare function fetchT<T>(url: string | URL, init: FetchInit & { abortable: true; responseType: 'json'; }): FetchTask<T | null>; /** * Fetches a resource from the network as a ReadableStream and returns an abortable `FetchTask`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'stream'`. * @returns A `FetchTask` representing the abortable operation with a `ReadableStream<Uint8Array<ArrayBuffer>>` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: 'stream'; }): FetchTask<ReadableStream<Uint8Array<ArrayBuffer>> | null>; /** * Fetches a resource from the network as a Uint8Array<ArrayBuffer> and returns an abortable `FetchTask`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and `responseType: 'bytes'`. * @returns A `FetchTask` representing the abortable operation with a `Uint8Array<ArrayBuffer>` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: 'bytes'; }): FetchTask<Uint8Array<ArrayBuffer>>; /** * Fetches a resource from the network and returns an abortable `FetchTask` with a dynamic response type. * * Use this overload when `responseType` is a `FetchResponseType` union type. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true` and a `FetchResponseType`. * @returns A `FetchTask` representing the abortable operation with a `FetchResponseData` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; responseType: FetchResponseType; }): FetchTask<FetchResponseData>; /** * Fetches a resource from the network as a text string. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'text'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `string` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'text'; }): FetchResult<string>; /** * Fetches a resource from the network as an ArrayBuffer. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'arraybuffer'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with an `ArrayBuffer` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'arraybuffer'; }): FetchResult<ArrayBuffer>; /** * Fetches a resource from the network as a Blob. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'blob'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `Blob` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'blob'; }): FetchResult<Blob>; /** * Fetches a resource from the network and parses it as JSON. * * @typeParam T - The expected type of the parsed JSON data. * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'json'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a response parsed as type `T`. */ export declare function fetchT<T>(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'json'; }): FetchResult<T | null>; /** * Fetches a resource from the network as a ReadableStream. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'stream'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `ReadableStream<Uint8Array<ArrayBuffer>>` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'stream'; }): FetchResult<ReadableStream<Uint8Array<ArrayBuffer>> | null>; /** * Fetches a resource from the network as a Uint8Array<ArrayBuffer>. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `responseType: 'bytes'` and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `Uint8Array<ArrayBuffer>` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: 'bytes'; }): FetchResult<Uint8Array<ArrayBuffer>>; /** * Fetches a resource from the network with a dynamic response type (non-abortable). * * Use this overload when `responseType` is a `FetchResponseType` union type. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation with a `FetchResponseType`, and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `FetchResponseData` response. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable?: false; responseType: FetchResponseType; }): FetchResult<FetchResponseData>; /** * Fetches a resource from the network and returns an abortable `FetchTask` with a generic `Response`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Additional options for the fetch operation, must include `abortable: true`. * @returns A `FetchTask` representing the abortable operation with a `Response` object. */ export declare function fetchT(url: string | URL, init: FetchInit & { abortable: true; }): FetchTask<Response>; /** * Fetches a resource from the network and returns a `FetchResult` with a generic `Response` object. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Optional additional options for the fetch operation, and `abortable` must be `false` or omitted. * @returns A `FetchResult` representing the operation with a `Response` object. */ export declare function fetchT(url: string | URL, init?: FetchInit & { abortable?: false; }): FetchResult<Response>; /** * Fallback overload for when `FetchInit` is passed as a variable (not an object literal). * * When TypeScript cannot determine the literal value of `abortable` at compile time * (e.g., when passing a `FetchInit` variable), this overload is matched. * The return type is a union of `FetchTask` and `FetchResult`. * * @param url - The resource to fetch. Can be a URL object or a string representing a URL. * @param init - Optional fetch options. When passed as a `FetchInit` variable, the return type is a union. * @returns Either a `FetchTask` or `FetchResult` depending on the runtime value of `abortable`. */ export declare function fetchT(url: string | URL, init?: FetchInit): FetchTask<FetchResponseData> | FetchResult<FetchResponseData>; /** * Represents an abortable fetch operation with control methods. * * Returned when `abortable: true` is set in the fetch options. Provides * the ability to cancel the request and check its abort status. * * @typeParam T - The type of the data expected in the response. * @since 1.0.0 * @example * ```typescript * import { fetchT, type FetchTask } from '@happy-ts/fetch-t'; * * interface User { * id: number; * name: string; * } * * const task: FetchTask<User> = fetchT<User>('https://api.example.com/user/1', { * abortable: true, * responseType: 'json', * }); * * // Check if aborted * console.log('Is aborted:', task.aborted); // false * * // Abort with optional reason * task.abort('User navigated away'); * * // Access the result (will be an error after abort) * const result = await task.result; * result.inspectErr((err) => console.log('Aborted:', err.message)); * ``` */ export declare interface FetchTask<T> { /** * Aborts the fetch task, optionally with a reason. * * Once aborted, the `result` promise will resolve to an `Err` containing * an `AbortError`. The abort reason can be any value and will be passed * to the underlying `AbortController.abort()`. * * @param reason - An optional value indicating why the task was aborted. * This can be an Error, string, or any other value. */ abort(reason?: any): void; /** * Indicates whether the fetch task has been aborted. * * Returns `true` if `abort()` was called or if the request timed out. */ readonly aborted: boolean; /** * The result promise of the fetch task. * * Resolves to `Ok<T>` on success, or `Err<Error>` on failure (including abort). */ readonly result: FetchResult<T>; } /** * Error name for timed out fetch requests. * * This is set on the `Error.name` property when a request exceeds the specified * `timeout` duration and is automatically aborted. * * @since 1.0.0 * @example * ```typescript * import { fetchT, TIMEOUT_ERROR } from '@happy-ts/fetch-t'; * * const result = await fetchT('https://api.example.com/slow-endpoint', { * timeout: 5000, // 5 seconds * }); * * result.inspectErr((err) => { * if (err.name === TIMEOUT_ERROR) { * console.log('Request timed out after 5 seconds'); * } * }); * ``` */ export declare const TIMEOUT_ERROR: "TimeoutError"; export { }