import { ApiResponse, RequestConfig } from './types'; class ApiError extends Error { status: number; code?: string; constructor(status: number, message: string) { super(message); this.name = 'ApiError'; this.status = status; this.code = status.toString(); } } class ApiClient { private baseUrl: string = ''; private defaultTimeout: number = 5000; private defaultRetries: number = 2; constructor(baseUrl?: string) { if (typeof window !== 'undefined') { this.baseUrl = window.location.origin; } if (baseUrl) { this.baseUrl = baseUrl; } } async get( endpoint: string, params?: Record, config?: RequestConfig ): Promise { const url = this.buildUrl(endpoint, params); return this.request(url, { method: 'GET', ...config, }); } async post( endpoint: string, data?: any, config?: RequestConfig ): Promise { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data), ...config, }); } async put( endpoint: string, data?: any, config?: RequestConfig ): Promise { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data), ...config, }); } async delete( endpoint: string, config?: RequestConfig ): Promise { return this.request(endpoint, { method: 'DELETE', ...config, }); } private async request( url: string, options: RequestInit & RequestConfig = {} ): Promise { const { retries = this.defaultRetries, timeout = this.defaultTimeout, headers = {}, ...fetchOptions } = options; return this.executeWithRetry( async () => { const response = await this.fetchWithTimeout(url, { ...fetchOptions, headers: { 'Content-Type': 'application/json', ...headers, }, }, timeout); if (!response) { throw new Error('No response received'); } if (!response.ok) { const errorData = await this.parseError(response); const error = this.createError(response.status, errorData.error || response.statusText); throw error; } const data: ApiResponse = await response.json(); if (!data.success) { const error = this.createError(response.status, data.error || 'Request failed'); throw error; } return data.data; }, retries, (error) => { const status = error?.status; if (typeof status === 'number') { return status >= 500 || status === 0; } return true; } ); } private async executeWithRetry( fn: () => Promise, retries: number, shouldRetry: (error: any) => boolean ): Promise { let lastError: any; for (let attempt = 0; attempt <= retries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === retries || !shouldRetry(error)) { throw error; } await this.delay(Math.pow(2, attempt) * 1000); } } throw lastError; } private async fetchWithTimeout( url: string, options: RequestInit, timeout: number ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timeoutId); if (!response) { throw new Error('No response received'); } return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { const timeoutError = new Error('Request timeout') as any; timeoutError.status = 0; throw timeoutError; } throw error; } } private async parseError(response: Response): Promise<{ error: string }> { try { const data = await response.json(); return data; } catch { return { error: response.statusText }; } } private createError(status: number, message: string): ApiError { return new ApiError(status, message); } private buildUrl(endpoint: string, params?: Record): string { let url = endpoint; if (this.baseUrl && !url.startsWith('http')) { url = `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}`; } if (!params || Object.keys(params).length === 0) { return url; } const queryString = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryString.append(key, String(value)); } }); return `${url}?${queryString.toString()}`; } private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } } export const apiClient = new ApiClient(); export { ApiClient, ApiError };