Created
November 22, 2024 20:33
-
-
Save WomB0ComB0/f3f903c3476bbc78ea887dc2e701d635 to your computer and use it in GitHub Desktop.
Configuration options for enhanced fetching that extends the base Axios request config
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios, { type AxiosRequestConfig, AxiosError } from 'axios'; | |
/** | |
* Wraps a promise to return a tuple containing either the resolved value or an error. | |
* Provides a cleaner way to handle promise rejections without try/catch blocks. | |
* | |
* @template T - The type of value that the promise resolves to | |
* @param {Promise<T>} promise - The promise to handle | |
* @returns {Promise<[undefined, T] | [Error]>} A promise that resolves to a tuple containing either: | |
* - [undefined, T] if the promise resolves successfully, where T is the resolved value | |
* - [Error] if the promise rejects, containing the error | |
* | |
* @example | |
* ```ts | |
* // Success case | |
* const [error, data] = await catchError(fetchUserData(userId)); | |
* if (error) { | |
* handleError(error); | |
* return; | |
* } | |
* // Use data safely here | |
* | |
* // Error case | |
* const [error] = await catchError(Promise.reject(new Error("Failed"))); | |
* console.log(error.message); // "Failed" | |
* ``` | |
* | |
* @remarks | |
* - Inspired by Go's error handling pattern | |
* - Eliminates need for try/catch blocks | |
* - Makes error handling more explicit and predictable | |
* - Type-safe with TypeScript | |
* - Useful for async/await operations | |
* - Can be chained with other promise operations | |
*/ | |
export const catchError = async <T>(promise: Promise<T>): Promise<[undefined, T] | [Error]> => { | |
return promise | |
.then((data) => { | |
return [undefined, data] as [undefined, T]; | |
}) | |
.catch((error: Error) => { | |
return [error]; | |
}); | |
}; | |
/** | |
* Parses the code path and returns a formatted string with the location and function name. | |
* | |
* @param {any} context - The context to include in the formatted string. | |
* @param {Function} fnName - The function to include in the formatted string. | |
* @returns {string} - The formatted string with the location and function name. | |
*/ | |
export const parseCodePath = (context: any, fnName: Function): string => | |
`location: ${process.cwd()}${__filename} @${fnName.name}: ${context}`; | |
/** | |
* Configuration options for enhanced fetching that extends the base Axios request config | |
* | |
* @interface FetcherOptions | |
* @extends {AxiosRequestConfig} | |
* @property {number} [retries=0] - Number of retry attempts in case of failure. Defaults to 0 (no retries) | |
* @property {number} [retryDelay=1000] - Base delay between retry attempts in milliseconds. Actual delay increases exponentially with each retry | |
* @property {(error: AxiosError) => void} [onError] - Optional callback for custom error handling. Called before throwing on final retry | |
* @property {number} [timeout=10000] - Request timeout in milliseconds. Defaults to 10 seconds | |
* | |
* @example | |
* ```ts | |
* const options: FetcherOptions = { | |
* retries: 3, | |
* retryDelay: 2000, | |
* onError: (error) => console.error(error), | |
* timeout: 5000 | |
* }; | |
* ``` | |
*/ | |
export interface FetcherOptions extends AxiosRequestConfig { | |
retries?: number; | |
retryDelay?: number; | |
onError?: (error: AxiosError) => void; | |
timeout?: number; | |
} | |
/** | |
* Custom error class for handling fetcher-specific errors with additional context | |
* | |
* @class FetcherError | |
* @extends {Error} | |
* @property {string} url - The URL that was being fetched when the error occurred | |
* @property {number} [status] - HTTP status code of the failed response, if available | |
* @property {unknown} [responseData] - Response data from the failed request, if available | |
* @property {number} [attempt] - The retry attempt number when the error occurred | |
* | |
* @example | |
* ```ts | |
* throw new FetcherError( | |
* 'Request failed', | |
* 'https://api.example.com/data', | |
* 404, | |
* { message: 'Not found' }, | |
* 2 | |
* ); | |
* ``` | |
*/ | |
export class FetcherError extends Error { | |
constructor( | |
message: string, | |
public readonly url: string, | |
public readonly status?: number, | |
public readonly responseData?: unknown, | |
public readonly attempt?: number, | |
) { | |
super(message); | |
this.name = 'FetcherError'; | |
Object.setPrototypeOf(this, FetcherError.prototype); | |
} | |
toString(): string { | |
return `FetcherError: ${this.message} (URL: ${this.url}${this.status ? `, Status: ${this.status}` : ''}${this.attempt ? `, Attempt: ${this.attempt}` : ''})`; | |
} | |
} | |
/** | |
* Enhanced data fetching utility that provides advanced error handling, retry mechanism, and type safety. | |
* Built on top of Axios with additional features for robust API interactions. | |
* | |
* @template T - The expected type of the successful response data | |
* @template E - The expected type of the error response data | |
* | |
* @param {string} input - The URL or endpoint to fetch from | |
* @param {FetcherOptions} [options={}] - Configuration options for the request | |
* @returns {Promise<T>} A promise that resolves to the response data | |
* | |
* @throws {FetcherError} | |
* - When max retries are exceeded | |
* - When an Axios error occurs | |
* - When an unexpected error occurs | |
* | |
* @example | |
* ```ts | |
* interface UserData { | |
* id: number; | |
* name: string; | |
* } | |
* | |
* interface ErrorResponse { | |
* message: string; | |
* } | |
* | |
* try { | |
* const userData = await fetcher<UserData, ErrorResponse>('/api/user', { | |
* retries: 3, | |
* retryDelay: 1000, | |
* timeout: 5000 | |
* }); | |
* console.log(userData.name); | |
* } catch (error) { | |
* if (error instanceof FetcherError) { | |
* console.error(`Failed to fetch: ${error.message}`); | |
* } | |
* } | |
* ``` | |
* | |
* @remarks | |
* - Implements exponential backoff for retries | |
* - Provides detailed error context through FetcherError | |
* - Supports custom error handling through onError callback | |
* - Preserves type safety throughout the request lifecycle | |
* - Integrates with Axios interceptors for request/response processing | |
*/ | |
export async function fetcher<T, E = unknown>( | |
input: string, | |
options: FetcherOptions = {}, | |
): Promise<T> { | |
const { retries = 0, retryDelay = 1000, onError, timeout = 10000, ...axiosConfig } = options; | |
const instance = axios.create({ | |
timeout, | |
...axiosConfig, | |
}); | |
instance.interceptors.request.use( | |
(config) => { | |
return config; | |
}, | |
(error) => Promise.reject(error), | |
); | |
instance.interceptors.response.use( | |
(response) => response, | |
(error) => Promise.reject(error), | |
); | |
try { | |
let attempt = 0; | |
while (attempt <= retries) { | |
const path = parseCodePath(input, fetcher); | |
const [error, response] = await catchError(instance.get<T>(path)); | |
if (!error) { | |
return response.data; | |
} | |
if (attempt === retries) { | |
if (onError && error instanceof AxiosError) onError(error); | |
throw new FetcherError( | |
error.message, | |
path, | |
error instanceof AxiosError ? error.response?.status : undefined, | |
error instanceof AxiosError ? error.response?.data : undefined, | |
attempt, | |
); | |
} | |
await new Promise((resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempt))); | |
attempt++; | |
} | |
throw new FetcherError(`Max retries exceeded`, input, undefined, undefined, retries); | |
} catch (error) { | |
if (error instanceof FetcherError) { | |
throw error; | |
} | |
if (axios.isAxiosError(error)) { | |
const axiosError = error as AxiosError<E>; | |
throw new FetcherError( | |
axiosError.message, | |
input, | |
axiosError.response?.status, | |
axiosError.response?.data, | |
); | |
} | |
throw new FetcherError(error instanceof Error ? error.message : 'Unknown error', input); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment