import { ApiClientConfig, ApiError, ApiRequest, ApiResponse, } from "@utils/types"; import { RequestAdapter } from "./index"; import { logger } from "@utils/logger"; import { ERROR_CLASSIFICATIONS } from "@utils/constants"; export class FetchAdapter implements RequestAdapter { private config?: ApiClientConfig; public initialize(config: ApiClientConfig): void { this.config = config; logger.log("FetchAdapter initialized."); } public async execute(request: ApiRequest): Promise { if (!this.config) { throw new Error("Adapter not initialized."); } const startTime = Date.now(); const { url, method, data, params, headers, options } = request; const controller = new AbortController(); const signal = controller.signal; let timeoutTimer: NodeJS.Timeout | number | undefined; const timeout = options.timeout ?? this.config.requestOptions.timeout; if (timeout !== undefined && timeout > 0) { timeoutTimer = setTimeout(() => { controller.abort(); logger.warn(`Request ${request.id} timed out after ${timeout}ms.`); }, timeout); } if (options.cancelToken?.promise) { options.cancelToken.promise.then(() => { controller.abort(); options.cancelToken?.onCancel?.(); }); } const finalUrl = new URL(url, this.config.baseUrl); if (params) { Object.keys(params).forEach((key) => finalUrl.searchParams.append(key, params[key]) ); } try { const response = await fetch(finalUrl.toString(), { method: method, headers: { ...this.config.defaultHeaders, ...headers, }, body: ["GET", "HEAD"].includes(method) ? undefined : JSON.stringify(data), signal: signal, }); if (timeoutTimer) clearTimeout(timeoutTimer); const responseData = await response.json().catch(() => ({})); const apiResponse: ApiResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), config: request, requestTime: Date.now() - startTime, }; if (!response.ok) { throw { response: apiResponse }; } return apiResponse; } catch (rawError: any) { if (timeoutTimer) clearTimeout(timeoutTimer); let classifiedError: Partial = { name: "ApiRequestError", message: rawError.message || "Unknown API request error", classification: ERROR_CLASSIFICATIONS.UNKNOWN, request: request, }; if (rawError.name === "AbortError") { if (signal.aborted) { classifiedError.isTimeoutError = true; classifiedError.classification = ERROR_CLASSIFICATIONS.TIMEOUT_REACHED; classifiedError.message = `Request ${request.id} timed out or was canceled.`; } else { classifiedError.message = `Request ${request.id} canceled.`; } } else if (rawError.response) { classifiedError.response = rawError.response; const status = rawError.response.status; if (status >= 500) { classifiedError.isServerError = true; classifiedError.classification = ERROR_CLASSIFICATIONS.SERVER_ERROR; } else if (status === 429) { classifiedError.isRateLimitError = true; classifiedError.classification = ERROR_CLASSIFICATIONS.TOO_MANY_REQUESTS; } else if (status >= 400 && status < 500) { classifiedError.isClientError = true; classifiedError.classification = ERROR_CLASSIFICATIONS.CLIENT_ERROR; } } else if (rawError instanceof TypeError) { classifiedError.isNetworkError = true; classifiedError.classification = ERROR_CLASSIFICATIONS.NETWORK_FAIL; classifiedError.message = `Network failure for request ${request.id}.`; } throw classifiedError as ApiError; } } }