// 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<Object>} - 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<Object>} - 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<Object>} - 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<Object>} - 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<Object>} - 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;
|