UNPKG

ghost-cache

Version:

A lightweight auto-caching wrapper for fetch() and Axios with multi-storage support (localStorage, sessionStorage, IndexedDB, Redis)

264 lines (263 loc) 9.03 kB
import { LocalStorageAdapter } from "./storage/LocalStorageAdapter.js"; import { SessionStorageAdapter } from "./storage/SessionStorageAdapter.js"; import { InMemoryStorageAdapter } from "./storage/InMemoryStorageAdapter.js"; // For universal usage in Node or browser const globalObj = globalThis; // Polyfill fetch if not defined if (typeof globalObj.fetch === "undefined") { try { const fetchPoly = require("cross-fetch").fetch; globalObj.fetch = fetchPoly; } catch (err) { throw new Error("fetch is not defined. Install cross-fetch or another polyfill."); } } // Polyfill Response if not defined if (typeof globalObj.Response === "undefined") { try { const { Response: FetchResponse } = require("cross-fetch"); globalObj.Response = FetchResponse; } catch (err) { throw new Error("Response is not defined. Install cross-fetch or another polyfill."); } } const inMemoryCache = new Map(); const defaultConfig = { ttl: 60000, persistent: false, maxEntries: 100, storage: new InMemoryStorageAdapter(), }; let storageAdapter = null; let originalFetch = null; let axiosInstances = []; /** * Picks correct storage adapter based on config. */ function determineStorageAdapter(opt) { if (typeof opt === "object") return opt; switch (opt) { case "localStorage": return new LocalStorageAdapter(); case "sessionStorage": return new SessionStorageAdapter(); default: return new InMemoryStorageAdapter(); } } /** * Enable GhostCache: intercept fetch and (optionally) Axios requests. */ export function enableGhostCache(options = {}) { // Merge user options into defaults Object.assign(defaultConfig, options); // Setup storage adapter if persistent storageAdapter = defaultConfig.persistent ? determineStorageAdapter(defaultConfig.storage) : null; // Intercept global fetch if we haven't already if (!originalFetch) { originalFetch = globalObj.fetch.bind(globalObj); globalObj.fetch = async (url, config) => { return handleRequest({ url, config }); }; } // Set up Axios interceptors axiosInstances.forEach((instance) => { instance.interceptors.request.use(async (config) => { const safeUrl = config.url ?? ""; const cacheKey = JSON.stringify({ url: safeUrl, params: config.params, method: config.method, }); // Check in-memory cache if (inMemoryCache.has(cacheKey)) { const entry = inMemoryCache.get(cacheKey); if (Date.now() - entry.timestamp < defaultConfig.ttl) { console.log(`[GhostCache] Using in-memory cache for request: ${safeUrl}`); return Promise.reject({ __ghostCache__: true, data: JSON.parse(entry.data), config, }); } else { inMemoryCache.delete(cacheKey); } } // Check persistent storage if (storageAdapter) { const stored = await storageAdapter.getItem(cacheKey); if (stored) { const parsed = JSON.parse(stored); if (Date.now() - parsed.timestamp < defaultConfig.ttl) { console.log(`[GhostCache] Using persistent cache for request: ${safeUrl}`); return Promise.reject({ __ghostCache__: true, data: JSON.parse(parsed.data), config, }); } else { await storageAdapter.removeItem(cacheKey); } } } return config; }, (err) => Promise.reject(err)); instance.interceptors.response.use((res) => { const safeUrl = res.config.url ?? ""; const cacheKey = JSON.stringify({ url: safeUrl, params: res.config.params, method: res.config.method, }); cacheResponse(cacheKey, JSON.stringify(res.data)); return res; }, (err) => { const e = err; if (e && e.__ghostCache__ && e.data) { return Promise.resolve({ data: e.data, status: 200, statusText: "OK", headers: {}, config: e.config, }); } return Promise.reject(err); }); }); } /** * Handle fetch() requests with caching. */ async function handleRequest(request) { const cacheKey = JSON.stringify(request); // In-memory cache check if (inMemoryCache.has(cacheKey)) { const entry = inMemoryCache.get(cacheKey); if (Date.now() - entry.timestamp < defaultConfig.ttl) { console.log(`[GhostCache] (fetch) Using in-memory cache for: ${JSON.stringify(request)}`); return new globalObj.Response(entry.data, { status: 200, headers: { "Content-Type": "application/json" }, }); } else { inMemoryCache.delete(cacheKey); } } // Persistent cache check if (storageAdapter) { const stored = await storageAdapter.getItem(cacheKey); if (stored) { const entry = JSON.parse(stored); if (Date.now() - entry.timestamp < defaultConfig.ttl) { console.log(`[GhostCache] (fetch) Using persistent cache for: ${JSON.stringify(request)}`); return new globalObj.Response(entry.data, { status: 200, headers: { "Content-Type": "application/json" }, }); } else { await storageAdapter.removeItem(cacheKey); } } } console.log(`[GhostCache] (fetch) Fetching from network: ${JSON.stringify(request)}`); // Network fetch if (!originalFetch) { throw new Error("GhostCache: original fetch is missing."); } const response = await originalFetch(request.url, request.config); const cloned = response.clone(); const textData = await cloned.text(); cacheResponse(cacheKey, textData); return response; } /** * Save to in-memory cache and persistent storage. */ function cacheResponse(cacheKey, data) { const entry = { timestamp: Date.now(), data }; inMemoryCache.set(cacheKey, entry); // Enforce maximum cache entries if (inMemoryCache.size > defaultConfig.maxEntries) { const firstKey = inMemoryCache.keys().next().value; if (firstKey) inMemoryCache.delete(firstKey); } if (storageAdapter) { storageAdapter.setItem(cacheKey, JSON.stringify(entry)); } console.log(`[GhostCache] Cached response for key: ${cacheKey}`); } /** * Manually set a cache entry. */ export async function setCache(key, value) { const cacheKey = JSON.stringify(key); const entry = { timestamp: Date.now(), data: JSON.stringify(value), }; inMemoryCache.set(cacheKey, entry); if (storageAdapter) { await storageAdapter.setItem(cacheKey, JSON.stringify(entry)); } console.log(`[GhostCache] Manually cached key: ${cacheKey}`); } /** * Retrieve a cache entry. */ export async function getCache(key) { const cacheKey = JSON.stringify(key); if (inMemoryCache.has(cacheKey)) { const entry = inMemoryCache.get(cacheKey); console.log(`[GhostCache] Retrieved from in-memory cache for key: ${cacheKey}`); return JSON.parse(entry.data); } if (storageAdapter) { const stored = await storageAdapter.getItem(cacheKey); if (stored) { const entry = JSON.parse(stored); console.log(`[GhostCache] Retrieved from persistent cache for key: ${cacheKey}`); return JSON.parse(entry.data); } } return null; } /** * Clear all cache entries. */ export function clearGhostCache() { inMemoryCache.clear(); if (storageAdapter) { storageAdapter.clear(); } console.log("[GhostCache] Cache cleared"); } /** * Disable GhostCache and restore the original fetch. */ export function disableGhostCache() { if (originalFetch) { globalObj.fetch = originalFetch; originalFetch = null; } axiosInstances = []; clearGhostCache(); console.log("[GhostCache] Disabled and restored original fetch"); } /** * Register an Axios instance to intercept its requests. */ export function registerAxios(instance) { axiosInstances.push(instance); console.log("[GhostCache] Registered an Axios instance"); }