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