// frontend/utils/preloadApi/core.js // 核心缓存引擎 — 零外部依赖(不依赖 Vue / uni API / api.js / store) // 内存 Map + LRU + inFlight 去重 + TTL // 依赖关系(内模块):import { readEntry, writeEntry, clearForUser as storageClearForUser, getTotalCacheSize, evictOldest } from './storage' import { readEntry, writeEntry, clearForUser as storageClearForUser, getTotalCacheSize, evictOldest } from './storage' // ── 常量 ── const NAMESPACE = 'preload' const DEFAULT_MAX_MEMORY_ENTRIES = 100 const DEFAULT_MAX_ENTRY_SIZE_KB = 1024 const DEFAULT_MAX_FILE_CACHE_MB = 50 // ── 内部状态 ── const memoryMap = new Map() // Map const inFlightMap = new Map() // Map // 统计 let hitCount = 0 let missCount = 0 // 运行时配置(由外部 setConfig 写入) let _config = { defaults: { ttl: 5 * 60 * 1000, persistence: 'memory', concurrency: 4, timeout: 10000, silent: true, limits: { maxEntrySizeKB: DEFAULT_MAX_ENTRY_SIZE_KB, maxMemoryEntries: DEFAULT_MAX_MEMORY_ENTRIES, maxFileCacheMB: DEFAULT_MAX_FILE_CACHE_MB } } } // fetcher 注册表:{ [logicalKey]: fetcherFunction } let _fetchers = {} // userId 获取函数(由外部注入) let _getUserId = () => { try { const userStr = uni.getStorageSync('user') if (userStr) { const user = JSON.parse(userStr) return user?.uid || null } } catch (e) { /* ignore */ } return null } // ── 并发控制 semaphore ── function createSemaphore(max) { let running = 0 const queue = [] return { acquire: () => new Promise(resolve => { if (running < max) { running++; resolve() } else { queue.push(resolve) } }), release: () => { running-- const next = queue.shift() if (next) { running++; next() } } } } let _semaphore = createSemaphore(_config.defaults.concurrency) // ── hash 工具 ── function djb2(str) { let hash = 5381 for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0 } return (hash >>> 0).toString(16) } function hashParams(params) { if (!params || Object.keys(params).length === 0) return '' const sorted = {} Object.keys(params).sort().forEach(k => { sorted[k] = params[k] }) return djb2(JSON.stringify(sorted)) } function buildCacheKey(userId, logicalKey, params) { const uid = userId || 'guest' const paramHash = hashParams(params) return `${uid}::${NAMESPACE}::${logicalKey}::${paramHash}` } // ── LRU touch ── function touchLRU(map, key, value) { if (map.has(key)) map.delete(key) map.set(key, value) if (map.size > _config.defaults.limits.maxMemoryEntries) { const oldestKey = map.keys().next().value map.delete(oldestKey) if (typeof console !== 'undefined') { console.log('[preload] LRU evict:', oldestKey) } } } // ── 401/7/16 swallow ── function _swallowAuth(err) { if (err && (err.code === 7 || err.code === 16)) return null if (err && /登录已过期/.test(err.message || '')) return null return err } // ── 数据大小检查 ── function getDataSizeKB(data) { try { return new Blob([JSON.stringify(data)]).size / 1024 } catch (e) { return JSON.stringify(data || '').length / 1024 } } // ── 配置 API ── /** * 设置运行时配置 + fetcher 注册表 * 由 config.js 在初始化时调用 */ export function setConfig(config, fetchers) { if (config) { _config = { defaults: { ..._config.defaults, ...(config.defaults || {}), limits: { ..._config.defaults.limits, ...((config.defaults && config.defaults.limits) || {}) } }, startup: config.startup || _config.startup || [], idle: config.idle || _config.idle || [], pages: config.pages || _config.pages || {} } _semaphore = createSemaphore(_config.defaults.concurrency) } if (fetchers) { _fetchers = { ..._fetchers, ...fetchers } } } /** * 设置 userId 获取函数(用于测试注入) */ export function setUserIdGetter(fn) { if (typeof fn === 'function') _getUserId = fn } // ── 核心 API ── /** * 获取缓存值(组件使用) * 命中内存 → 同步 resolve;文件缓存命中 / fetch → async resolve */ export function get(logicalKey, params) { const userId = _getUserId() const cacheKey = buildCacheKey(userId, logicalKey, params) // 1. 查内存 const memEntry = memoryMap.get(cacheKey) if (memEntry && (Date.now() - memEntry.ts) < memEntry.ttl) { touchLRU(memoryMap, cacheKey, memEntry) hitCount++ return Promise.resolve(memEntry.data) } // 2. inFlight 去重 const inFlight = inFlightMap.get(cacheKey) if (inFlight) { return inFlight.promise.then(result => result.data) } // 3. 发起 fetch(含文件缓存回退) return _doFetch(logicalKey, params, cacheKey, userId, false) } /** * 触发预拉(fire-and-forget) * 查内存 → inFlight → 发起 fetch */ export function run(logicalKey, params) { const userId = _getUserId() const cacheKey = buildCacheKey(userId, logicalKey, params) // 1. 查内存 const memEntry = memoryMap.get(cacheKey) if (memEntry && (Date.now() - memEntry.ts) < memEntry.ttl) { return // 未过期,跳过 } // 2. inFlight 去重 if (inFlightMap.has(cacheKey)) { return // 已在请求中 } // 3. 发起 fetch(fire-and-forget,不返回 Promise) _doFetch(logicalKey, params, cacheKey, userId, true) } /** * 命令式刷新 */ export function refresh(logicalKey, params, force = false) { const userId = _getUserId() const cacheKey = buildCacheKey(userId, logicalKey, params) // force=true 时先删除内存缓存 + 跳过文件缓存 if (force) { memoryMap.delete(cacheKey) } return _doFetch(logicalKey, params, cacheKey, userId, false, force) } /** * 失效单个 key(仅内存) */ export function invalidate(logicalKey, params) { const cacheKey = buildCacheKey(_getUserId(), logicalKey, params) memoryMap.delete(cacheKey) } /** * 按前缀失效(仅内存) */ export function invalidatePrefix(prefix) { const fullPrefix = `${_getUserId() || 'guest'}::${NAMESPACE}::${prefix}` for (const key of memoryMap.keys()) { if (key.startsWith(fullPrefix)) { memoryMap.delete(key) } } } /** * 清空全部内存缓存(不动文件缓存) */ export function invalidateAll() { memoryMap.clear() } /** * 删除指定用户的文件缓存目录(不动内存) */ export function clearForUser(userId) { storageClearForUser(userId) } /** * 登出专用:清空内存 + 删除文件缓存目录 */ export function clearUser(userId) { memoryMap.clear() storageClearForUser(userId) } /** * 按目标页路径触发预拉(navigate.js 内部调用) */ export function prefetchFor(targetPath, params) { const pages = _config.pages || {} const entries = pages[targetPath] if (!entries || !Array.isArray(entries)) return for (const entry of entries) { run(entry.key, params) } } /** * 取消指定 key 的 in-flight 请求(供 composable unmount / params 变化时使用) */ export function abortRequest(logicalKey, params) { const userId = _getUserId() const cacheKey = buildCacheKey(userId, logicalKey, params) const entry = inFlightMap.get(cacheKey) if (entry) { entry.abort() inFlightMap.delete(cacheKey) } } /** * 获取调试统计 */ export function getStats() { return { hits: hitCount, misses: missCount, memorySize: memoryMap.size, inFlightSize: inFlightMap.size, fileCacheBytes: _lastFileCacheSize } } // 上次文件缓存大小(由 checkAndEvict 异步更新) let _lastFileCacheSize = 0 // 内部:更新文件缓存大小跟踪 function _updateFileCacheSize() { const userId = _getUserId() if (userId) { getTotalCacheSize(userId).then(size => { _lastFileCacheSize = size }).catch(() => {}) } } /** * dump 内存缓存(调试用) */ export function dumpMemory() { const result = [] for (const [key, entry] of memoryMap.entries()) { result.push({ key, age: Date.now() - entry.ts, ttl: entry.ttl, persistence: entry.persistence }) } return result } // ── 内部:执行 fetch ── async function _doFetch(logicalKey, params, cacheKey, userId, isRun, skipFileCache = false) { const fetcher = _fetchers[logicalKey] if (!fetcher) { if (!isRun) throw new Error(`[preload] unknown key: ${logicalKey}`) console.warn(`[preload] unknown key: ${logicalKey}`) return } const cfg = _resolveEntryConfig(logicalKey) const ttl = cfg.ttl || _config.defaults.ttl const persistence = cfg.persistence || _config.defaults.persistence const silent = cfg.silent !== undefined ? cfg.silent : _config.defaults.silent const timeout = cfg.timeout || _config.defaults.timeout // 3. 先查本地文件缓存(非 run 路径,且未强制跳过) if (!isRun && !skipFileCache && persistence === 'file') { try { const fileEntry = await readEntry(userId, cacheKey) if (fileEntry && (Date.now() - fileEntry.ts) < fileEntry.ttl) { // 文件命中 → 写回内存 touchLRU(memoryMap, cacheKey, { data: fileEntry.data, ts: fileEntry.ts, ttl: fileEntry.ttl, persistence: 'file' }) hitCount++ return fileEntry.data } } catch (e) { // 文件读取失败 → 走 fetch } } // 4. 创建 inFlight 条目 let resolveInFlight, rejectInFlight const sharedPromise = new Promise((res, rej) => { resolveInFlight = res rejectInFlight = rej }) let abortFn = () => {} const inFlightEntry = { promise: sharedPromise.then(data => ({ data })), abort: () => abortFn() } inFlightMap.set(cacheKey, inFlightEntry) const cleanup = () => { inFlightMap.delete(cacheKey) } // 5. 并发控制 + 超时 await _semaphore.acquire() try { const startTime = Date.now() const fetchPromise = fetcher(params) // 设置 abort abortFn = () => { if (fetchPromise && typeof fetchPromise.abort === 'function') { fetchPromise.abort() } cleanup() } // 超时控制 let timeoutId const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error('timeout')), timeout) }) const result = await Promise.race([fetchPromise, timeoutPromise]) clearTimeout(timeoutId) const elapsed = Date.now() - startTime console.log('[preload] fetch done:', logicalKey, elapsed + 'ms') // 6. 写内存缓存 const entry = { data: result, ts: Date.now(), ttl, persistence } touchLRU(memoryMap, cacheKey, entry) // 7. 异步写文件缓存(fire-and-forget,不阻塞 fetch 返回) if (persistence === 'file') { const sizeKB = getDataSizeKB(result) if (sizeKB <= _config.defaults.limits.maxEntrySizeKB) { writeEntry(userId, cacheKey, result, entry.ts, ttl) // fire-and-forget 容量检查:延迟到下一 tick 确保 writeEntry 已启动 setTimeout(() => { checkAndEvict(userId) }, 0) } } missCount++ resolveInFlight(result) return result } catch (err) { // 8. 错误处理 const swallowed = _swallowAuth(err) if (swallowed === null) { // 401/7/16 → swallow console.warn('[preload] auth-expired, swallowed:', logicalKey) resolveInFlight(null) return null } if (silent || isRun) { // run / silent → 静默 console.warn('[preload] fetch fail (swallowed):', logicalKey, err.message) resolveInFlight(null) return null } // get 路径 → 抛错给调用方 rejectInFlight(err) throw err } finally { cleanup() _semaphore.release() } } // ── 内部:fire-and-forget 容量检查 + 淘汰 ── async function checkAndEvict(userId) { try { const totalSize = await getTotalCacheSize(userId) _lastFileCacheSize = totalSize const maxBytes = _config.defaults.limits.maxFileCacheMB * 1024 * 1024 if (totalSize > maxBytes) { await evictOldest(userId, maxBytes) // 淘汰后更新大小 const newSize = await getTotalCacheSize(userId) _lastFileCacheSize = newSize } } catch (e) { console.warn('[preload] eviction check failed:', e.message) } } // ── 内部:解析 per-key 配置 ── function _resolveEntryConfig(logicalKey) { // 从 _config 中查找该 key 的配置(startup/idle/pages 任一数组) const all = [ ...(_config.startup || []), ...(_config.idle || []), ] if (_config.pages) { for (const entries of Object.values(_config.pages)) { if (Array.isArray(entries)) all.push(...entries) } } const found = all.find(e => e.key === logicalKey) return found || {} } // ── 开发调试 ── if (typeof window !== 'undefined' && (typeof import.meta === 'undefined' || import.meta.env?.DEV)) { window.__PRELOAD_DEBUG__ = { dumpMemory, stats: getStats, all: () => ({ memory: dumpMemory(), inFlight: Array.from(inFlightMap.keys()) }) } }