import { RetryStrategy } from "./RetryStrategy"; import { RateLimiter } from "./RateLimiter"; import { RequestQueue } from "./RequestQueue"; import { OfflineStore } from "./OfflineStore"; import { RequestAdapter } from "../adapters"; import { FetchAdapter } from "../adapters/fetchAdapter"; import { AxiosAdapter } from "@adapters/axiosAdapter"; import { ApiClientConfig, ApiError, ApiRequest, ApiResponse, RequestOptions, } from "@utils/types"; import { logger } from "@utils/logger"; import { DEFAULT_CONFIG } from "@utils/constants"; import { createUniqueId } from "@utils/helpers"; export class ApiClient { private config: ApiClientConfig; private adapter: RequestAdapter; private retryStrategy: RetryStrategy; private rateLimiter: RateLimiter; private offlineStore: OfflineStore; private requestQueue: RequestQueue; /** * Initializes the ApiClient with configuration and sets up core components. * @param userConfig - Optional user-provided configuration object. */ constructor(userConfig: Partial = {}) { if (typeof userConfig.debug !== "undefined") { logger.setEnabled(userConfig.debug); } else { logger.setEnabled(false); } this.config = { ...DEFAULT_CONFIG, ...userConfig, retry: { ...DEFAULT_CONFIG.retry, ...userConfig.retry }, rateLimit: { ...DEFAULT_CONFIG.rateLimit, ...userConfig.rateLimit }, offline: { ...DEFAULT_CONFIG.offline, ...userConfig.offline }, queue: { ...DEFAULT_CONFIG.queue, ...userConfig.queue }, requestOptions: { ...DEFAULT_CONFIG.requestOptions, ...userConfig.requestOptions, }, }; this.adapter = this.selectAdapter(this.config.adapter); this.adapter.initialize(this.config); this.retryStrategy = new RetryStrategy(this.config); this.rateLimiter = new RateLimiter(this.config); this.offlineStore = new OfflineStore(this.config, this.enqueueRequest); this.requestQueue = new RequestQueue( this.config, this.rateLimiter, this.executeRequestWithHandling ); logger.log("ApiClient initialized successfully."); this.offlineStore.attemptSync(); } private selectAdapter( adapterName: ApiClientConfig["adapter"] ): RequestAdapter { if (adapterName === "fetch") { return new FetchAdapter(); } if (adapterName === "axios") { return new AxiosAdapter(); } throw new Error(`Adapter "${adapterName}" not supported yet.`); } private createApiRequest( method: ApiRequest["method"], url: string, data?: T, options: RequestOptions = {} ): Promise { return new Promise((resolve, reject) => { const request: ApiRequest = { id: createUniqueId(), url, method, data, params: method === "GET" ? (data as Record) : undefined, headers: options.headers || {}, options: { ...this.config.requestOptions, ...options }, status: "QUEUED", priority: options.priority || this.config.requestOptions.priority || 5, createdAt: Date.now(), retryCount: 0, resolve, reject, }; this.requestQueue.enqueue(request); }); } /** * Executes the request through the adapter and handles success/failure. * This is the function passed to the RequestQueue. * @param request The request object to execute. */ private executeRequestWithHandling = async ( request: ApiRequest ): Promise => { try { const response = await this.adapter.execute(request); request.status = "SUCCESS"; request.resolve(response); } catch (error: any) { const apiError: ApiError = error; request.lastAttemptAt = Date.now(); if (!this.config.offline.enabled || !apiError.isNetworkError) { if (this.retryStrategy.shouldRetry(request, apiError)) { request.retryCount++; request.status = "RETRYING"; const delay = this.retryStrategy.getNextDelay(request); logger.warn(`Request ${request.id} will retry in ${delay}ms...`); setTimeout(() => { this.requestQueue.enqueue(request); }, delay); return; } } else if (apiError.isNetworkError && !this.config.offline.enabled) { request.status = "FAILED"; request.reject(apiError); return; } else if (apiError.isNetworkError && this.config.offline.enabled) { logger.warn(`Request ${request.id} is offline. Storing...`); this.offlineStore.storeRequest(request); return; } request.status = "FAILED"; request.reject(apiError); } }; public enqueueRequest = (request: ApiRequest) => { this.requestQueue.enqueue(request); }; /** * HTTP GET request. * @param url - The request URL. * @param params - Query parameters (data). * @param options - Request options. */ public get( url: string, paramsOrOptions?: Record | RequestOptions, options?: RequestOptions ): Promise { let finalParams: Record | undefined; let finalOptions: RequestOptions | undefined; if (paramsOrOptions && !options) { if ( paramsOrOptions.hasOwnProperty("headers") || paramsOrOptions.hasOwnProperty("priority") ) { finalOptions = paramsOrOptions as RequestOptions; finalParams = undefined; } else { finalParams = paramsOrOptions; finalOptions = undefined; } } else { finalParams = paramsOrOptions as Record; finalOptions = options; } return this.createApiRequest("GET", url, finalParams, finalOptions); } /** * HTTP POST request. * @param url - The request URL. * @param data - The request body. * @param options - Request options. */ public post( url: string, data?: T, options?: RequestOptions ): Promise { return this.createApiRequest("POST", url, data, options); } public put( url: string, data?: T, options?: RequestOptions ): Promise { return this.createApiRequest("PUT", url, data, options); } public delete( url: string, data?: any, options?: RequestOptions ): Promise { return this.createApiRequest("DELETE", url, data, options); } public getRateLimiter(): RateLimiter { return this.rateLimiter; } public syncOfflineRequests(): Promise { return this.offlineStore.attemptSync(); } }