UNPKG

@alwatr/fetch

Version:

`@alwatr/fetch` is an enhanced, lightweight, and dependency-free wrapper for the native `fetch` API. It provides modern features like caching strategies, request retries, timeouts, and intelligent duplicate request handling, all in a compact package.

199 lines (180 loc) 6.29 kB
/** * @module @alwatr/fetch * * An enhanced, lightweight, and dependency-free wrapper for the native `fetch` * API. It provides modern features like caching strategies, request retries, * timeouts, and duplicate request handling. */ import type {JsonObject} from '@alwatr/type-helper'; import {_processOptions, handleCacheStrategy_, logger_, cacheSupported} from './core.js'; import {FetchError} from './error.js'; import type {FetchJsonOptions, FetchOptions, FetchResponse} from './type.js'; export {cacheSupported}; export * from './error.js'; export type * from './type.js'; /** * An enhanced wrapper for the native `fetch` function. * * This function extends the standard `fetch` with additional features such as: * - **Timeout**: Aborts the request if it takes too long. * - **Retry Pattern**: Automatically retries the request on failure (e.g., server errors or network issues). * - **Duplicate Request Handling**: Prevents sending multiple identical requests in parallel. * - **Cache Strategies**: Provides various caching mechanisms using the browser's Cache API. * - **Simplified API**: Offers convenient options for adding query parameters, JSON bodies, and auth tokens. * * @see {@link FetchOptions} for a detailed list of available options. * * @param {string} url - The URL to fetch. * @param {FetchOptions} options - Optional configuration for the fetch request. * @returns {Promise<FetchResponse>} A promise that resolves to a tuple. On * success, it returns `[response, null]`. On failure, it returns `[null, * FetchError]`. * * @example * ```typescript * import {fetch} from '@alwatr/fetch'; * * async function fetchProducts() { * const [response, error] = await fetch('/api/products', { * queryParams: { limit: 10 }, * timeout: 5_000, * }); * * if (error) { * console.error('Request failed:', error.reason); * return; * } * * // At this point, response is guaranteed to be valid and ok. * const data = await response.json(); * console.log('Products:', data); * } * * fetchProducts(); * ``` */ export async function fetch(url: string, options: FetchOptions = {}): Promise<FetchResponse> { logger_.logMethodArgs?.('fetch', {url, options}); const options_ = _processOptions(url, options); try { // Start the fetch lifecycle, beginning with the cache strategy. const response = await handleCacheStrategy_(options_); if (!response.ok) { throw new FetchError('http_error', `HTTP error! status: ${response.status} ${response.statusText}`, response); } return [response, null]; } catch (err) { let error: FetchError; if (err instanceof FetchError) { error = err; if (error.response !== undefined && error.data === undefined) { const bodyText = await error.response.text().catch(() => ''); if (bodyText.trim().length > 0) { try { // Try to parse as JSON error.data = JSON.parse(bodyText); } catch { error.data = bodyText; } } } } else if (err instanceof Error) { if (err.name === 'AbortError') { error = new FetchError('aborted', err.message); } else { error = new FetchError('network_error', err.message); } } else { error = new FetchError('unknown_error', String(err ?? 'unknown_error')); } logger_.error('fetch', error.reason, {error}); return [null, error]; } } fetch.version = __package_version__; /** * An enhanced wrapper for the native `fetch` function that automatically parses JSON responses. * * This function extends the standard `fetch` with the same features (timeout, retry, caching, etc.) * and automatically parses the response body as JSON. It returns a tuple with the parsed data or an error. * * @template T - The expected type of the JSON response data. * * @param {string} url - The URL to fetch. * @param {FetchOptions} options - Optional configuration for the fetch request. * @returns {Promise<[T, null] | [null, FetchError]>} A promise that resolves to a tuple. * On success, it returns `[data, null]` where data is the parsed JSON. * On failure, it returns `[null, FetchError]`. * * @example * ```typescript * import {fetchJson} from '@alwatr/fetch'; * * interface Product { * ok: true; * id: number; * name: string; * price: number; * } * * async function getProduct(id: number) { * const [data, error] = await fetchJson<Product>(`/api/products/${id}`, { * timeout: 5_000, * cacheStrategy: 'cache_first', * requireResponseJsonWithOkTrue: true, * }); * * if (error) { * console.error('Failed to fetch product:', error.reason); * return; * } * * // data is now typed as Product and guaranteed to be valid * console.log('Product name:', data.name); * } * ``` */ export async function fetchJson<T extends JsonObject = JsonObject>( url: string, options: FetchJsonOptions = {}, ): Promise<[T, null] | [null, FetchError]> { logger_.logMethodArgs?.('fetchJson', {url, options}); const [response, error] = await fetch(url, options); if (error) { return [null, error]; } const bodyText = await response.text().catch(() => ''); if (bodyText.trim().length === 0) { const parseError = new FetchError( 'json_parse_error', 'Response body is empty, cannot parse JSON', response, bodyText, ); logger_.error('fetchJson', parseError.reason, {error: parseError}); return [null, parseError]; } try { const data = JSON.parse(bodyText) as T; if (options.requireJsonResponseWithOkTrue && data.ok !== true) { const parseError = new FetchError( 'json_response_error', 'Response JSON "ok" property is not true', response, data, ); logger_.error('fetchJson', parseError.reason, {error: parseError}); return [null, parseError]; } return [data, null]; } catch (err) { const parseError = new FetchError( 'json_parse_error', err instanceof Error ? err.message : 'Failed to parse JSON response', response, bodyText, ); logger_.error('fetchJson', parseError.reason, {error: parseError}); return [null, parseError]; } }