|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|