| |
| |
| |
| |
|
|
| class APIClient { |
| constructor() { |
| this.cache = new Map(); |
| this.requestQueue = new Map(); |
| this.retryDelays = new Map(); |
| this.maxRetries = 3; |
| this.defaultCacheTTL = 30000; |
| this.requestTimeout = 8000; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async fetch(url, options = {}, cacheTTL = this.defaultCacheTTL) { |
| const cacheKey = `${url}:${JSON.stringify(options)}`; |
| |
| |
| if (cacheTTL > 0 && this.cache.has(cacheKey)) { |
| const cached = this.cache.get(cacheKey); |
| if (Date.now() - cached.timestamp < cacheTTL) { |
| return cached.response.clone(); |
| } |
| this.cache.delete(cacheKey); |
| } |
|
|
| |
| if (this.requestQueue.has(cacheKey)) { |
| return this.requestQueue.get(cacheKey); |
| } |
|
|
| |
| const requestPromise = this._makeRequest(url, options, cacheKey, cacheTTL); |
| this.requestQueue.set(cacheKey, requestPromise); |
|
|
| try { |
| const response = await requestPromise; |
| return response; |
| } finally { |
| |
| setTimeout(() => { |
| this.requestQueue.delete(cacheKey); |
| }, 100); |
| } |
| } |
|
|
| |
| |
| |
| |
| async _makeRequest(url, options, cacheKey, cacheTTL) { |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout); |
|
|
| let lastError; |
| let retryCount = 0; |
|
|
| while (retryCount <= this.maxRetries) { |
| try { |
| const response = await fetch(url, { |
| ...options, |
| signal: controller.signal, |
| headers: { |
| 'Accept': 'application/json', |
| ...options.headers |
| } |
| }); |
|
|
| clearTimeout(timeoutId); |
|
|
| |
| if (response.status === 403 || response.status === 429) { |
| |
| const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); |
| await this._delay(delay); |
| |
| if (retryCount < this.maxRetries) { |
| retryCount++; |
| continue; |
| } |
| |
| |
| return this._createFallbackResponse(url); |
| } |
|
|
| |
| if (response.ok && cacheTTL > 0) { |
| this.cache.set(cacheKey, { |
| response: response.clone(), |
| timestamp: Date.now() |
| }); |
| } |
|
|
| return response; |
| } catch (error) { |
| clearTimeout(timeoutId); |
| lastError = error; |
|
|
| |
| if (error.name === 'AbortError') { |
| break; |
| } |
|
|
| |
| if (retryCount < this.maxRetries) { |
| const delay = this._getRetryDelay(retryCount); |
| await this._delay(delay); |
| retryCount++; |
| |
| |
| const newController = new AbortController(); |
| const newTimeoutId = setTimeout(() => newController.abort(), this.requestTimeout); |
| Object.assign(controller, newController); |
| timeoutId = newTimeoutId; |
| } else { |
| break; |
| } |
| } |
| } |
|
|
| |
| console.warn(`[APIClient] Request failed after ${retryCount} retries:`, url); |
| return this._createFallbackResponse(url); |
| } |
|
|
| |
| |
| |
| |
| _getRetryDelay(retryCount) { |
| const baseDelay = 500; |
| return Math.min(baseDelay * Math.pow(2, retryCount), 5000); |
| } |
|
|
| |
| |
| |
| |
| _delay(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
|
|
| |
| |
| |
| |
| _createFallbackResponse(url) { |
| return new Response( |
| JSON.stringify({ |
| error: 'Service temporarily unavailable', |
| fallback: true, |
| url |
| }), |
| { |
| status: 200, |
| statusText: 'OK', |
| headers: { 'Content-Type': 'application/json' } |
| } |
| ); |
| } |
|
|
| |
| |
| |
| clearCache() { |
| this.cache.clear(); |
| } |
|
|
| |
| |
| |
| clearCacheFor(urlPattern) { |
| for (const key of this.cache.keys()) { |
| if (key.includes(urlPattern)) { |
| this.cache.delete(key); |
| } |
| } |
| } |
| } |
|
|
| |
| export const apiClient = new APIClient(); |
| export default apiClient; |
|
|