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