// FeedService.js - Handles API interactions for the feed block import cache from '../utils/cache'; class FeedService { constructor(apiUrl, nonce) { this.apiUrl = apiUrl; this.nonce = nonce; this.currentRequest = null; this.timeoutId = null; this.pendingRequests = new Map(); // Track in-flight requests by cache key } /** * Fetch feed data from the API with request deduplication and caching * @param {Object} params - Request parameters * @param {boolean} resetCache - Whether to bypass cache * @returns {Promise} - Feed data */ async fetchFeed(params, resetCache = false) { // Create cache key from params const cacheKey = this.createCacheKey(params); resetCache = true; //TODO: remove default reset // Check cache first (unless reset requested) if (!resetCache) { const cached = cache.get(cacheKey); if (cached) { return cached; } } // Check if this exact request is already in flight if (this.pendingRequests.has(cacheKey)) { // Return the existing promise for the in-flight request return this.pendingRequests.get(cacheKey); } // Cancel any existing request for different params this.abortCurrentRequest(); // Create a new request try { // Create abort controller for this request const controller = new AbortController(); this.currentRequest = controller; // Set timeout for request this.timeoutId = setTimeout(() => { if (this.currentRequest === controller) { controller.abort(); } }, 30000); // 30 second timeout // Build query parameters const queryParams = this.buildQueryParams(params); // Add cache busting if needed if (resetCache) { queryParams.append('_', Date.now()); } const requestUrl = `${this.apiUrl}feed?${queryParams.toString()}`; // Create the promise for this request const requestPromise = (async () => { try { const response = await fetch(requestUrl, { headers: { 'X-WP-Nonce': this.nonce }, signal: controller.signal }); // Clear timeout if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } // Check for successful response if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Parse response const data = await response.json(); // Reset error handler retry count on success if (this.errorHandler) { this.errorHandler.resetRetryCount(); } // Cache the result for 5 minutes (unless it's a reset request) if (!resetCache) { cache.set(cacheKey, data, 300000); } return data; } finally { // Remove from pending requests this.pendingRequests.delete(cacheKey); } })(); // Store the promise in the pending requests map this.pendingRequests.set(cacheKey, requestPromise); return requestPromise; } catch (error) { // Clean up pending request this.pendingRequests.delete(cacheKey); if (this.errorHandler) { return this.errorHandler.handleApiError( error, { params }, () => this.fetchFeed(params, resetCache) ); } // Don't throw for aborted requests if (error.name === 'AbortError') { return null; } // Re-throw other errors throw error; } finally { this.currentRequest = null; if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } } } /** * Create a cache key from params object * @param {Object} params - Request parameters * @returns {string} - Cache key */ createCacheKey(params) { // Clone to avoid modifying original const normalized = JSON.parse(JSON.stringify(params)); // Sort arrays to ensure consistent keys if (normalized.filters) { Object.keys(normalized.filters).forEach(key => { if (Array.isArray(normalized.filters[key])) { normalized.filters[key].sort(); } }); } return `feed_${JSON.stringify(normalized)}`; } /** * Build query parameters from params object * @param {Object} params - Request parameters * @returns {URLSearchParams} - URL search params */ buildQueryParams(params) { const queryParams = new URLSearchParams(); // Add page parameter queryParams.append('page', params.page || 1); // Add context and other metadata if (params.context) { queryParams.append('context', JSON.stringify(params.context)); } if (params.highlight) { queryParams.append('highlight', JSON.stringify(params.highlight)); } // Add source and source type if (params.source) { queryParams.append('source', params.source); } if (params.sourceType) { queryParams.append('type', params.sourceType); } // Add filters const filterParams = params.filters || {}; // Ensure content type is explicitly added as a filter if (filterParams.content) { queryParams.append('filters[content]', filterParams.content); } // Add remaining filters Object.entries(filterParams).forEach(([key, value]) => { // Skip content since we added it separately if (key === 'content') return; if (Array.isArray(value) && value.length > 0) { value.forEach(v => { queryParams.append(`filters[${key}][]`, v); }); } else if (value !== null && value !== undefined && value !== '') { if(typeof value === 'object' && Object.keys(value).length === 0){ return; } queryParams.append(`filters[${key}]`, value); } }); return queryParams; } /** * Fetch taxonomy terms with caching * @param {string} taxonomy - Taxonomy name * @param {string} search - Search query * @param {number} page - Page number * @returns {Promise} - Terms data */ async fetchTerms(taxonomy, search = '', page = 1) { // Create cache key const cacheKey = `terms_${taxonomy}_${search}_${page}`; // Check cache first const cached = cache.get(cacheKey); if (cached) { return cached; } try { const params = new URLSearchParams({ search, page }); const response = await fetch(`${this.apiUrl}terms/${taxonomy}?${params.toString()}`, { headers: { 'X-WP-Nonce': this.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Cache for 5 minutes cache.set(cacheKey, data, 300000); return data; } catch (error) { console.error('Error fetching terms:', error); throw error; } } /** * Fetch taxonomy terms specific to a content type * @param {string} taxonomy - Taxonomy name * @param {string} contentType - Content type * @param {string} search - Search query * @param {number} page - Page number * @returns {Promise} - Terms data */ async fetchTermsForContentType(taxonomy, contentType, search = '', page = 1) { // Create cache key const cacheKey = `terms_${contentType}_${taxonomy}_${search}_${page}`; // Check cache first const cached = cache.get(cacheKey); if (cached) { return cached; } try { const params = new URLSearchParams({ search, page, per_page: 20, min_count: 1 }); const response = await fetch(`${this.apiUrl}terms/for/${contentType}/${taxonomy}?${params.toString()}`, { headers: { 'X-WP-Nonce': this.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Cache for 5 minutes cache.set(cacheKey, data, 300000); return data; } catch (error) { console.error('Error fetching terms for content type:', error); throw error; } } /** * Fetch popular terms * @param {string} taxonomy - Taxonomy name * @returns {Promise} - Popular terms data */ async fetchPopularTerms(taxonomy) { // Create cache key const cacheKey = `popular_terms_${taxonomy}`; // Check cache first const cached = cache.get(cacheKey); if (cached) { return cached; } try { const response = await fetch(`${this.apiUrl}terms/popular/${taxonomy}?limit=10`, { headers: { 'X-WP-Nonce': this.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Cache for 1 hour cache.set(cacheKey, data, 3600000); return data; } catch (error) { console.error('Error fetching popular terms:', error); throw error; } } /** * Fetch related terms * @param {string} taxonomy - Taxonomy name * @param {number} termId - Term ID * @returns {Promise} - Related terms data */ async fetchRelatedTerms(taxonomy, termId) { // Create cache key const cacheKey = `related_terms_${taxonomy}_${termId}`; // Check cache first const cached = cache.get(cacheKey); if (cached) { return cached; } try { const response = await fetch(`${this.apiUrl}terms/related/${taxonomy}/${termId}`, { headers: { 'X-WP-Nonce': this.nonce } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Cache for 1 hour cache.set(cacheKey, data, 3600000); return data; } catch (error) { console.error('Error fetching related terms:', error); throw error; } } /** * Abort the current request if it exists */ abortCurrentRequest() { if (this.currentRequest) { this.currentRequest.abort(); this.currentRequest = null; } if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } } /** * Clear feed cache * @param {string} contentType - Optional content type to clear cache for */ clearCache(contentType = null) { if (contentType) { // Find and delete cache entries for specific content type Object.keys(cache.storage).forEach(key => { if (key.startsWith('feed_') && key.includes(`"content":"${contentType}"`)) { cache.remove(key.replace(cache.options.namespace, '')); } }); } else { // Clear all feed-related cache entries Object.keys(cache.storage).forEach(key => { if (key.startsWith(cache.options.namespace + 'feed_')) { cache.remove(key.replace(cache.options.namespace, '')); } }); } } } export default FeedService;