@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
TypeScript
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 { }