import { DBManager, DBSchema } from '@/src/service/DBManager/DBManager'; /* ======================= 类型定义 ======================= */ // 策略:下载后 tempFilePath 立即可用,saveFile 延迟批量执行,实现无感持久化 // TODO: 增加删除功能 实现文件删除 interface CacheIndexRow { key: string; url: string; local_path: string; size: number; create_time: number; last_access: number; } interface PendingSaveItem { key: string; url: string; tempFilePath: string; createTime: number; } /* ======================= 任务队列 ======================= */ class TaskQueue { private running = 0; private queue: Array<() => Promise> = []; constructor( private concurrency: number, private spaceDelay: number = 0 ) {} add(task: () => Promise) { this.queue.push(task); this.run(); } private async run() { if (this.running >= this.concurrency) return; const task = this.queue.shift(); if (!task) return; this.running++; setTimeout(() => { task() .catch(console.error) .finally(() => { this.running--; this.run(); }); }, this.spaceDelay); } } /* ======================= CacheManager ======================= */ class CacheManager { /* -------- 基础配置 -------- */ private readonly rootPath = '_doc/cache.db'; private readonly PERSIST_DELAY = 3000; private readonly PERSIST_BATCH_SIZE = 5; private readonly PERSIST_INTERVAL = 200; /* -------- 内存结构 -------- */ private memoryIndex = new Map(); private downloading = new Set(); private checkingDB = new Set(); private dirtyKeys = new Set(); /* -------- 延迟持久化队列 -------- */ private pendingSave: PendingSaveItem[] = []; private persistTimer: any = null; private flushTimer: any = null; /* -------- IO 队列 -------- */ private downloadQueue = new TaskQueue(2); private checkQueue = new TaskQueue(2, 10); /* -------- 异步等待 -------- */ private waitingCallbacks = new Map void>>(); private preloadReady = false; // 标记 preloadIndex 是否完成 /* -------- 数据库 -------- */ private db: DBManager; /* ======================= 初始化 ======================= */ constructor() { const schema: DBSchema = { dbName: 'cache.db', dbPath: this.rootPath, tableName: 'cache_index', columns: ` key TEXT PRIMARY KEY, url TEXT, local_path TEXT, size INTEGER, create_time INTEGER, last_access INTEGER `, indexes: ['last_access'], primaryKey: 'key' }; this.db = new DBManager(schema); this.preloadIndex(); this.listenAppLifecycle(); } public async preloadIndex() { try { await this.db.open(); const rows = await this.db.find({ columns: ['key', 'local_path'], orderBy: 'last_access DESC', limit: 2000, }); rows.forEach(r => { if (r.local_path) { this.memoryIndex.set(r.key, r.local_path); } }); console.log(`[CacheManager] 索引预热完成,共 ${rows.length} 条`); this.preloadReady = true; // 标记预热完成 } catch (e) { console.error(e); this.preloadReady = true; // 即使失败也要标记,避免一直等待 } } private listenAppLifecycle() { // @ts-ignore if (typeof uni !== 'undefined' && uni.onAppHide) { // @ts-ignore uni.onAppHide(() => { this.flushPendingSave(); this.flushAccessTimes(); }); } } /* ======================= 对外接口 ======================= */ /** * 同步缓存入口(显代存) * 立即返回,若无缓存则返回原 URL,后台自动下载 */ public GetCache(url: string): string { if (!url) return ''; // 非 HTTP/HTTPS 链接直接返回原值,不尝试下载 if (!this.isHttpUrl(url)) return url; const key = this.hash(url); // 内存命中直接返回 if (this.memoryIndex.has(key)) { this.touch(key); return this.memoryIndex.get(key)!; } // 如果 preload 还没完成,等待完成后再检查 if (!this.preloadReady) { // 同步等待 preload 完成(最多等待 100ms) const startTime = Date.now(); while (!this.preloadReady && Date.now() - startTime < 100) { // 空循环等待 } // 等待完成后再次检查内存 if (this.memoryIndex.has(key)) { this.touch(key); return this.memoryIndex.get(key)!; } } // 延迟到下一 tick 执行,避免 UI 卡顿 setTimeout(() => {this.checkQueue.add(() => this.checkDBOrDownload(url, key));}, 0); // 立即返回原 URL return url; } /** * 异步缓存入口 * 等待缓存就绪后返回本地路径,全程 await */ public async GetCacheAsync(url: string): Promise { if (!url) return ''; // 非 HTTP/HTTPS 链接直接返回原值,不尝试下载 if (!this.isHttpUrl(url)) return url; const key = this.hash(url); // 如果 preload 还没完成,先等待完成 if (!this.preloadReady) { await this.waitForPreload(); } // 内存命中直接返回 if (this.memoryIndex.has(key)) { this.touch(key); return this.memoryIndex.get(key)!; } // 检查 DB const dbPath = await this.checkDB(key); if (dbPath) { this.memoryIndex.set(key, dbPath); this.touch(key); return dbPath; } // 需要下载,等待下载完成 return this.downloadAndWait(url, key); } /** * 等待 preloadIndex 完成 */ private async waitForPreload(): Promise { if (this.preloadReady) return; return new Promise(resolve => { const check = () => { if (this.preloadReady) { resolve(); } else { setTimeout(check, 10); } }; check(); }); } /* ======================= 异步下载等待 ======================= */ private async checkDB(key: string): Promise { try { const rows = await this.db.find({ where: { key }, columns: ['local_path'], limit: 1 }); if (rows.length > 0 && rows[0].local_path) { return rows[0].local_path; } } catch (e) { console.error(e); } return null; } private downloadAndWait(url: string, key: string): Promise { return new Promise((resolve) => { // 如果已经在下载,加入等待队列 if (this.downloading.has(key)) { this.addWaitingCallback(key, resolve); return; } // 如果已有缓存路径,直接返回 if (this.memoryIndex.has(key)) { resolve(this.memoryIndex.get(key)!); return; } // 加入等待队列并开始下载 this.addWaitingCallback(key, resolve); this.enqueueDownload(url, key); }); } private addWaitingCallback(key: string, callback: (path: string) => void) { if (!this.waitingCallbacks.has(key)) { this.waitingCallbacks.set(key, []); } this.waitingCallbacks.get(key)!.push(callback); } private resolveWaitingCallbacks(key: string, path: string) { const callbacks = this.waitingCallbacks.get(key); if (callbacks) { callbacks.forEach(cb => cb(path)); this.waitingCallbacks.delete(key); } } /* ======================= 下载与写盘 ======================= */ private async checkDBOrDownload(url: string, key: string) { // 防重:正在查 / 正在下 / 已在内存 → 跳过 if (this.checkingDB.has(key) || this.downloading.has(key) || this.memoryIndex.has(key)) { return; } this.checkingDB.add(key); try { const rows = await this.db.find({ where: { key }, columns: ['local_path'], limit: 1 }); if (rows.length > 0 && rows[0].local_path) { this.memoryIndex.set(key, rows[0].local_path); this.touch(key); } else { this.enqueueDownload(url, key); } } catch (e) { console.error(e); } finally { this.checkingDB.delete(key); } } private enqueueDownload(url: string, key: string) { if (this.downloading.has(key)) return; this.downloading.add(key); this.downloadQueue.add(async () => { try { const res = await uni.downloadFile({ url }); if (res.statusCode !== 200) { this.resolveWaitingCallbacks(key, url); // 下载失败返回原 URL return; } // 下载完立即用 tempFilePath,UI 无感 const tempUrl = plus.io.convertLocalFileSystemURL(res.tempFilePath); this.memoryIndex.set(key, tempUrl); // 通知等待中的异步调用 this.resolveWaitingCallbacks(key, tempUrl); // 丢进待持久化队列 this.pendingSave.push({ key, url, tempFilePath: res.tempFilePath, createTime: Date.now() }); this.schedulePersist(); } catch (e) { console.warn('[CacheManager] 下载失败:', url, e); this.resolveWaitingCallbacks(key, url); // 下载失败返回原 URL } finally { this.downloading.delete(key); } }); } /* ======================= 延迟批量持久化 ======================= */ private schedulePersist() { if (this.persistTimer) return; this.persistTimer = setTimeout(() => { this.persistTimer = null; this.runPersistBatch(); }, this.PERSIST_DELAY); } private async runPersistBatch() { if (this.pendingSave.length === 0) return; const batch = this.pendingSave.splice(0, this.PERSIST_BATCH_SIZE); for (let i = 0; i < batch.length; i++) { const item = batch[i]; try { const saveRes = await uni.saveFile({ tempFilePath: item.tempFilePath }); const finalUrl = plus.io.convertLocalFileSystemURL(saveRes.savedFilePath); this.memoryIndex.set(item.key, finalUrl); // 写 DB(不阻塞) this.db.Upsert({ key: item.key, url: item.url, local_path: finalUrl, size: 0, create_time: item.createTime, last_access: Date.now() }).catch(e => console.error('[CacheManager] DB写入失败:', e)); } catch (e) { console.warn('[CacheManager] 持久化失败:', item.url, e); } // 每个文件之间间隔,避免 IO 堆积 if (i < batch.length - 1) { await this.sleep(this.PERSIST_INTERVAL); } } // 如果还有剩余,继续调度 if (this.pendingSave.length > 0) { this.schedulePersist(); } } private async flushPendingSave() { if (this.persistTimer) { clearTimeout(this.persistTimer); this.persistTimer = null; } const all = this.pendingSave.splice(0); for (const item of all) { try { const saveRes = await uni.saveFile({ tempFilePath: item.tempFilePath }); const finalUrl = plus.io.convertLocalFileSystemURL(saveRes.savedFilePath); this.memoryIndex.set(item.key, finalUrl); await this.db.Upsert({ key: item.key, url: item.url, local_path: finalUrl, size: 0, create_time: item.createTime, last_access: Date.now() }); } catch (e) { console.warn('[CacheManager] flushPendingSave 失败:', item.url, e); } } } /* ======================= 工具方法 ======================= */ /** * 判断是否为 HTTP/HTTPS 链接 */ private isHttpUrl(url: string): boolean { return url.startsWith('http://') || url.startsWith('https://'); } private hash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return (hash >>> 0).toString(36); } private touch(key: string) { this.dirtyKeys.add(key); this.scheduleFlush(); } private scheduleFlush() { if (this.flushTimer) return; this.flushTimer = setTimeout(() => { this.flushAccessTimes(); }, 5000); } private async flushAccessTimes() { if (this.dirtyKeys.size === 0) return; const keys = Array.from(this.dirtyKeys); this.dirtyKeys.clear(); this.flushTimer = null; const now = Date.now(); const keyString = keys.map(k => `'${k}'`).join(','); try { await this.db.executeSql( `UPDATE cache_index SET last_access=${now} WHERE key IN (${keyString})` ); } catch (e) { console.error('Flush error', e); } } private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } } /* ======================= 导出单例 ======================= */ export default new CacheManager();