UNPKG

@modern-js/runtime-utils

Version:

A Progressive React Framework for modern web development.

279 lines (278 loc) • 8.61 kB
import { LRUCache } from "lru-cache"; import { getAsyncLocalStorage } from "./async_storage"; const CacheSize = { KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 }; const CacheTime = { SECOND: 1e3, MINUTE: 60 * 1e3, HOUR: 60 * 60 * 1e3, DAY: 24 * 60 * 60 * 1e3, WEEK: 7 * 24 * 60 * 60 * 1e3, MONTH: 30 * 24 * 60 * 60 * 1e3 }; const isServer = typeof window === "undefined"; const requestCacheMap = /* @__PURE__ */ new WeakMap(); let lruCache; let cacheConfig = { maxSize: CacheSize.GB }; const tagKeyMap = /* @__PURE__ */ new Map(); function addTagKeyRelation(tag, key) { let keys = tagKeyMap.get(tag); if (!keys) { keys = /* @__PURE__ */ new Set(); tagKeyMap.set(tag, keys); } keys.add(key); } function configureCache(config) { cacheConfig = { ...cacheConfig, ...config }; } function getLRUCache() { if (!lruCache) { var _cacheConfig_maxSize; lruCache = new LRUCache({ maxSize: (_cacheConfig_maxSize = cacheConfig.maxSize) !== null && _cacheConfig_maxSize !== void 0 ? _cacheConfig_maxSize : CacheSize.GB, sizeCalculation: (value) => { if (!value.size) { return 1; } let size = 0; for (const [k, item] of value.entries()) { size += k.length * 2; size += estimateObjectSize(item.data); size += 8; } return size; }, updateAgeOnGet: true, updateAgeOnHas: true }); } return lruCache; } function estimateObjectSize(data) { const type = typeof data; if (type === "number") return 8; if (type === "boolean") return 4; if (type === "string") return Math.max(data.length * 2, 1); if (data === null || data === void 0) return 1; if (ArrayBuffer.isView(data)) { return Math.max(data.byteLength, 1); } if (Array.isArray(data)) { return Math.max(data.reduce((acc, item) => acc + estimateObjectSize(item), 0), 1); } if (data instanceof Map || data instanceof Set) { return 1024; } if (data instanceof Date) { return 8; } if (type === "object") { return Math.max(Object.entries(data).reduce((acc, [key, value]) => acc + key.length * 2 + estimateObjectSize(value), 0), 1); } return 1; } function generateKey(args) { return JSON.stringify(args, (_, value) => { if (value && typeof value === "object" && !Array.isArray(value)) { return Object.keys(value).sort().reduce((result, key) => { result[key] = value[key]; return result; }, {}); } return value; }); } function cache(fn, options) { const { tag = "default", maxAge = CacheTime.MINUTE * 5, revalidate = 0, customKey, onCache, getKey } = options || {}; const store = getLRUCache(); const tags = Array.isArray(tag) ? tag : [ tag ]; const getCacheKey = (args, generatedKey) => { return customKey ? customKey({ params: args, fn, generatedKey }) : fn; }; return async (...args) => { if (isServer && typeof options === "undefined") { var _storage_useContext; const storage = getAsyncLocalStorage(); const request = storage === null || storage === void 0 ? void 0 : (_storage_useContext = storage.useContext()) === null || _storage_useContext === void 0 ? void 0 : _storage_useContext.request; if (request) { let shouldDisableCaching = false; if (cacheConfig.unstable_shouldDisable) { shouldDisableCaching = await cacheConfig.unstable_shouldDisable({ request }); } if (shouldDisableCaching) { return fn(...args); } let requestCache = requestCacheMap.get(request); if (!requestCache) { requestCache = /* @__PURE__ */ new Map(); requestCacheMap.set(request, requestCache); } let fnCache = requestCache.get(fn); if (!fnCache) { fnCache = /* @__PURE__ */ new Map(); requestCache.set(fn, fnCache); } const key = generateKey(args); if (fnCache.has(key)) { return fnCache.get(key); } const promise = fn(...args); fnCache.set(key, promise); try { const data = await promise; return data; } catch (error) { fnCache.delete(key); throw error; } } } else if (typeof options !== "undefined") { const genKey = getKey ? getKey(...args) : generateKey(args); const now = Date.now(); const cacheKey = getCacheKey(args, genKey); const finalKey = typeof cacheKey === "function" ? genKey : cacheKey; tags.forEach((t) => addTagKeyRelation(t, cacheKey)); let cacheStore = store.get(cacheKey); if (!cacheStore) { cacheStore = /* @__PURE__ */ new Map(); } const storeKey = customKey && typeof cacheKey === "symbol" ? "symbol-key" : genKey; let shouldDisableCaching = false; if (isServer && cacheConfig.unstable_shouldDisable) { var _storage_useContext1; const storage = getAsyncLocalStorage(); const request = storage === null || storage === void 0 ? void 0 : (_storage_useContext1 = storage.useContext()) === null || _storage_useContext1 === void 0 ? void 0 : _storage_useContext1.request; if (request) { shouldDisableCaching = await cacheConfig.unstable_shouldDisable({ request }); } } const cached = cacheStore.get(storeKey); if (cached && !shouldDisableCaching) { const age = now - cached.timestamp; if (age < maxAge) { if (onCache) { onCache({ status: "hit", key: finalKey, params: args, result: cached.data }); } return cached.data; } if (revalidate > 0 && age < maxAge + revalidate) { if (onCache) { onCache({ status: "stale", key: finalKey, params: args, result: cached.data }); } if (!cached.isRevalidating) { cached.isRevalidating = true; Promise.resolve().then(async () => { try { const newData = await fn(...args); cacheStore.set(storeKey, { data: newData, timestamp: Date.now(), isRevalidating: false }); store.set(cacheKey, cacheStore); } catch (error) { cached.isRevalidating = false; if (isServer) { var _storage_useContext_monitors, _storage_useContext2; const storage = getAsyncLocalStorage(); storage === null || storage === void 0 ? void 0 : (_storage_useContext2 = storage.useContext()) === null || _storage_useContext2 === void 0 ? void 0 : (_storage_useContext_monitors = _storage_useContext2.monitors) === null || _storage_useContext_monitors === void 0 ? void 0 : _storage_useContext_monitors.error(error.message); } else { console.error("Background revalidation failed:", error); } } }); } return cached.data; } } const data = await fn(...args); if (!shouldDisableCaching) { cacheStore.set(storeKey, { data, timestamp: now, isRevalidating: false }); store.set(cacheKey, cacheStore); } if (onCache) { onCache({ status: "miss", key: finalKey, params: args, result: data }); } return data; } else { console.warn("The cache function will not work because it runs on the browser and there are no options are provided."); return fn(...args); } }; } function withRequestCache(handler) { if (!isServer) { return handler; } return async (req, ...args) => { const storage = getAsyncLocalStorage(); return storage.run({ request: req }, () => handler(req, ...args)); }; } function revalidateTag(tag) { const keys = tagKeyMap.get(tag); if (keys) { keys.forEach((key) => { lruCache === null || lruCache === void 0 ? void 0 : lruCache.delete(key); }); } } function clearStore() { lruCache === null || lruCache === void 0 ? void 0 : lruCache.clear(); lruCache = void 0; tagKeyMap.clear(); } export { CacheSize, CacheTime, cache, clearStore, configureCache, generateKey, revalidateTag, withRequestCache };