@spoolcms/nextjs
Version:
The beautiful headless CMS for Next.js developers
209 lines (208 loc) • 6.7 kB
JavaScript
;
/**
* Request deduplication and caching utilities for Spool CMS
* Provides different caching strategies for server and client contexts
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.globalCache = exports.UnifiedCache = void 0;
exports.generateCacheKey = generateCacheKey;
exports.createCachedFetch = createCachedFetch;
exports.clearAllCaches = clearAllCaches;
// React cache is only available in React 18+ canary builds
// For now, we'll implement our own server-side caching
// import { cache } from 'react';
const environment_1 = require("./environment");
/**
* Default cache options
*/
const DEFAULT_CACHE_OPTIONS = {
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 100, // 100 entries
};
/**
* Client-side cache implementation using Map with TTL
*/
class ClientCache {
constructor(options) {
this.cache = new Map();
this.options = { ...DEFAULT_CACHE_OPTIONS, ...options };
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if entry has expired
if (Date.now() - entry.timestamp > this.options.ttl) {
this.cache.delete(key);
return null;
}
return entry;
}
set(key, data) {
// Clean up expired entries if cache is getting full
if (this.cache.size >= this.options.maxSize) {
this.cleanup();
}
this.cache.set(key, {
data,
timestamp: Date.now(),
});
}
setPromise(key, promise) {
this.cache.set(key, {
data: null, // Will be replaced when promise resolves
timestamp: Date.now(),
promise,
});
}
clear() {
this.cache.clear();
}
cleanup() {
const now = Date.now();
const expiredKeys = [];
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.options.ttl) {
expiredKeys.push(key);
}
}
expiredKeys.forEach(key => this.cache.delete(key));
// If still too full, remove oldest entries
if (this.cache.size >= this.options.maxSize) {
const entries = Array.from(this.cache.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = entries.slice(0, Math.floor(this.options.maxSize * 0.2));
toRemove.forEach(([key]) => this.cache.delete(key));
}
}
}
/**
* Global client-side cache instance
*/
const clientCache = new ClientCache();
/**
* Server-side cached fetch function
* For now, we'll use a simple Map-based cache until React cache() is stable
*/
const serverCache = new Map();
const serverCachedFetch = async (url, options) => {
const cacheKey = `${url}:${JSON.stringify(options)}`;
const cached = serverCache.get(cacheKey);
// Simple 5-minute cache for server-side requests
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
// Return a new Response with the cached data to avoid "Body is unusable" error
return new Response(JSON.stringify(cached.data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
const response = await fetch(url, options);
// Only cache successful responses
if (response.ok) {
try {
// Parse and cache the JSON data, not the Response object
const data = await response.clone().json();
serverCache.set(cacheKey, { data, timestamp: Date.now() });
}
catch (error) {
// If JSON parsing fails, don't cache
console.warn('Failed to parse response for caching:', error);
}
}
return response;
};
/**
* Generate a cache key for a request
*/
function generateCacheKey(baseUrl, siteId, collection, slug, options) {
const envKey = (0, environment_1.getEnvironmentCacheKey)();
const optionsKey = options ? JSON.stringify(options) : '';
const slugKey = slug || 'collection';
return `${envKey}:${baseUrl}:${siteId}:${collection}:${slugKey}:${optionsKey}`;
}
/**
* Unified caching interface that works in both server and client contexts
*/
class UnifiedCache {
constructor() {
this.environment = (0, environment_1.detectEnvironment)();
}
async get(key) {
if (this.environment.isClient) {
const entry = clientCache.get(key);
return entry?.data || null;
}
// Server-side: no persistent cache, rely on server cache for deduplication
return null;
}
async set(key, data) {
if (this.environment.isClient) {
clientCache.set(key, data);
}
// Server-side: no action needed, React cache() handles deduplication
}
async getOrFetch(key, fetcher) {
if (this.environment.isClient) {
// Client-side: check cache first
const cached = clientCache.get(key);
if (cached) {
// If we have a promise, wait for it
if (cached.promise) {
return cached.promise;
}
// If we have data, return it
if (cached.data) {
return cached.data;
}
}
// No cache hit, create new request
const promise = fetcher();
clientCache.setPromise(key, promise);
try {
const data = await promise;
clientCache.set(key, data);
return data;
}
catch (error) {
// Remove failed promise from cache
clientCache.clear();
throw error;
}
}
else {
// Server-side: rely on server cache for deduplication
return fetcher();
}
}
clear() {
if (this.environment.isClient) {
clientCache.clear();
}
}
}
exports.UnifiedCache = UnifiedCache;
/**
* Create a cached fetch function that works in both server and client contexts
*/
function createCachedFetch() {
const environment = (0, environment_1.detectEnvironment)();
if (environment.isServer) {
// Use React's cache() for server-side deduplication
return serverCachedFetch;
}
else {
// Use regular fetch for client-side (caching handled at higher level)
return fetch;
}
}
/**
* Global cache instance
*/
exports.globalCache = new UnifiedCache();
/**
* Clear all caches (useful for testing)
*/
function clearAllCaches() {
exports.globalCache.clear();
}