/** * API Client Utility * * Standardized API client for ThinkRank admin interface. * Provides consistent API interaction patterns extracted from Site Identity * with proper error handling, request/response formatting, and caching. * * @package ThinkRank * @since 1.0.0 */ import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; /** * Create API client for a specific tab/module * * @param {string} basePath Base API path (e.g., 'site-identity', 'homepage-seo') * @param {Object} options Client configuration options * @param {boolean} options.enableCaching Enable response caching (default: false) * @param {number} options.cacheTimeout Cache timeout in milliseconds (default: 300000 - 5 minutes) * @param {boolean} options.enableRetry Enable automatic retry on failure (default: true) * @param {number} options.maxRetries Maximum retry attempts (default: 2) * * @return {Object} API client methods */ export const createApiClient = (basePath, options = {}) => { const { enableCaching = false, cacheTimeout = 300000, // 5 minutes enableRetry = true, maxRetries = 2 } = options; const apiBasePath = `/thinkrank/v1/${basePath}`; const cache = new Map(); /** * Get cache key for request */ const getCacheKey = (endpoint, method, data) => { return `${method}:${endpoint}:${JSON.stringify(data || {})}`; }; /** * Check if cached response is valid */ const isCacheValid = (cacheEntry) => { return cacheEntry && (Date.now() - cacheEntry.timestamp) < cacheTimeout; }; /** * Enhanced error handling following Site Identity patterns */ const handleApiError = (error, endpoint) => { console.error(`API Error [${basePath}${endpoint}]:`, error); let errorMessage = __('Request failed. Please try again.', 'thinkrank'); if (error.message) { if (error.message.includes('API key')) { errorMessage = __('AI optimization requires an API key. Please configure your API key in ThinkRank settings.', 'thinkrank'); } else if (error.message.includes('rate limit')) { errorMessage = __('Rate limit reached. Please try again in a few minutes.', 'thinkrank'); } else if (error.message.includes('network')) { errorMessage = __('Network error. Please check your connection and try again.', 'thinkrank'); } else if (error.message.includes('permission')) { errorMessage = __('Permission denied. Please check your user permissions.', 'thinkrank'); } } return { success: false, error: errorMessage, originalError: error }; }; /** * Make API request with retry logic */ const makeRequest = async (endpoint, options, retryCount = 0) => { try { const response = await apiFetch({ path: `${apiBasePath}${endpoint}`, ...options }); return response; } catch (error) { if (enableRetry && retryCount < maxRetries) { // Wait before retry (exponential backoff) const delay = Math.pow(2, retryCount) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); return makeRequest(endpoint, options, retryCount + 1); } throw error; } }; /** * GET request */ const get = async (endpoint, params = {}) => { const cacheKey = getCacheKey(endpoint, 'GET', params); // Check cache if enabled if (enableCaching && cache.has(cacheKey)) { const cacheEntry = cache.get(cacheKey); if (isCacheValid(cacheEntry)) { return cacheEntry.data; } cache.delete(cacheKey); } try { // Add query parameters if provided let fullEndpoint = endpoint; if (Object.keys(params).length > 0) { const queryString = new URLSearchParams(params).toString(); fullEndpoint += `?${queryString}`; } const response = await makeRequest(fullEndpoint, { method: 'GET' }); // Cache successful responses if (enableCaching && response.success) { cache.set(cacheKey, { data: response, timestamp: Date.now() }); } return response; } catch (error) { return handleApiError(error, endpoint); } }; /** * POST request */ const post = async (endpoint, data = {}) => { try { const response = await makeRequest(endpoint, { method: 'POST', data }); // Clear related cache entries on successful POST if (enableCaching && response.success) { // Clear cache entries that might be affected by this update for (const key of cache.keys()) { if (key.includes(endpoint) || key.includes('GET:')) { cache.delete(key); } } } return response; } catch (error) { return handleApiError(error, endpoint); } }; /** * PUT request */ const put = async (endpoint, data = {}) => { try { const response = await makeRequest(endpoint, { method: 'PUT', data }); // Clear related cache entries on successful PUT if (enableCaching && response.success) { for (const key of cache.keys()) { if (key.includes(endpoint)) { cache.delete(key); } } } return response; } catch (error) { return handleApiError(error, endpoint); } }; /** * DELETE request */ const del = async (endpoint) => { try { const response = await makeRequest(endpoint, { method: 'DELETE' }); // Clear related cache entries on successful DELETE if (enableCaching && response.success) { for (const key of cache.keys()) { if (key.includes(endpoint)) { cache.delete(key); } } } return response; } catch (error) { return handleApiError(error, endpoint); } }; /** * Clear cache */ const clearCache = (pattern = null) => { if (!enableCaching) return; if (pattern) { for (const key of cache.keys()) { if (key.includes(pattern)) { cache.delete(key); } } } else { cache.clear(); } }; /** * Get cache statistics */ const getCacheStats = () => { if (!enableCaching) return null; return { size: cache.size, keys: Array.from(cache.keys()) }; }; return { get, post, put, delete: del, clearCache, getCacheStats }; }; /** * Default API client for general use */ export const apiClient = createApiClient('', { enableCaching: true, enableRetry: true }); export default createApiClient;