UNPKG

arcx

Version:

A lightweight, dependency-free fetch utility for APIs and React.

151 lines (150 loc) 5 kB
import { globalConfig } from "./config"; import { HTTPError, NetworkError } from "./errors"; /** A small delay utility for retries. */ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Unified parse for various response types. * Forces the result to type `T` after parsing. */ async function parseResponse(response, parseAs) { let parsed; switch (parseAs) { case "text": parsed = await response.text(); break; case "blob": parsed = await response.blob(); break; case "arrayBuffer": parsed = await response.arrayBuffer(); break; case "json": default: parsed = await response.json(); } return parsed; } /** Convert `HeadersInit` to a `Record<string, string>` if possible. */ function toRecord(headersInit) { if (!headersInit) return undefined; if (headersInit instanceof Headers) { const result = {}; headersInit.forEach((value, key) => { result[key] = value; }); return result; } else if (Array.isArray(headersInit)) { const result = {}; for (const [k, v] of headersInit) { result[k] = v; } return result; } else { // It's already a record return headersInit; } } /** * Merges multiple header objects into one, giving precedence * to headers from later objects in case of conflicts. */ function mergeHeaders(...headerObjects) { return headerObjects.reduce((acc, obj) => { if (!obj) return acc; for (const [k, v] of Object.entries(obj)) { acc[k] = v; } return acc; }, {}); } /** * A typed fetch request with retries, timeouts, interceptors, etc. * * @template T - The shape of the final parsed response. * @param url - Endpoint path * @param options - Additional ArcX fetch options */ export async function fetchRequest(url, options = {}) { // Combine config from global + user options const { interceptors, headers: globalHeaders, ...globalRequestInit } = globalConfig; // Build final URL (including optional query params) let finalUrl = globalConfig.baseUrl ? globalConfig.baseUrl + url : url; if (options.queryParams) { const params = new URLSearchParams(Object.entries(options.queryParams).reduce((acc, [key, val]) => { acc[key] = String(val); return acc; }, {})).toString(); finalUrl += `?${params}`; } // Merge headers const mergedHeaders = mergeHeaders(toRecord(globalHeaders), toRecord(options.headers)); // Construct final RequestInit const config = { ...globalRequestInit, ...options, headers: mergedHeaders, }; // onRequest interceptor if (interceptors?.onRequest) { Object.assign(config, interceptors.onRequest(config)); } // If body is a plain object, JSONify it if (config.body && typeof config.body === "object" && !(config.body instanceof FormData)) { config.body = JSON.stringify(config.body); config.headers = { ...config.headers, "Content-Type": "application/json", }; } // Timeout with AbortController const controller = new AbortController(); config.signal = controller.signal; if (options.timeout) { setTimeout(() => controller.abort(), options.timeout); } // Handling retries let attempt = 0; const maxRetries = options.retries ?? 0; const parseType = options.parseAs ?? "json"; while (attempt <= maxRetries) { try { const response = await fetch(finalUrl, config); if (!response.ok) { const responseBody = await response.text().catch(() => "Unknown error"); throw new HTTPError(`HTTP Error: ${response.status} (${response.statusText}) [URL: ${finalUrl}]`, response.status, responseBody); } // Parse according to parseType let result = await parseResponse(response, parseType); // onResponse interceptor must return T or Promise<T> if (interceptors?.onResponse) { result = await interceptors.onResponse(result); } return result; } catch (error) { // onError interceptor if (interceptors?.onError) { interceptors.onError(error); } // If final attempt fails, throw if (attempt === maxRetries) { if (!(error instanceof Error)) { throw new NetworkError("Unknown network error occurred."); } throw error; } attempt++; // Exponential backoff await delay(2 ** attempt * 100); } } throw new NetworkError("Request failed after retries."); }