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