/** * 本地 Demo 缓存 - 浏览器内持久化存储 * 用于在浏览器中持久化本地加载的 demo 数据,支持刷新后恢复 */ import type { AnalysisData } from '../api/GLTR_API'; import type { IDemoStorage, SaveOptions, SaveResult, LoadResult } from './demoStorage'; import { ensureJsonExtension } from '../utils/localFileUtils'; import { extractErrorMessage } from '../utils/errorUtils'; import { hashContent, CryptoSubtleUnavailableError } from '../utils/hashUtils'; const DB_NAME = 'InfoRadarDB'; const DB_VERSION = 2; const STORE_NAME = 'demos'; /** * 本地 Demo 缓存实现 * 使用 IndexedDB 持久化本地 demo 数据,支持刷新后恢复 */ export class LocalDemoCache implements IDemoStorage { readonly type = 'local' as const; private dbPromise: Promise | null = null; /** * 检查 IndexedDB 是否可用 */ static isAvailable(): boolean { return typeof indexedDB !== 'undefined'; } /** * 初始化或获取数据库连接 */ private async getDB(): Promise { if (!LocalDemoCache.isAvailable()) { throw new Error('IndexedDB 不可用,可能是浏览器不支持或处于隐私模式'); } if (this.dbPromise) { return this.dbPromise; } this.dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => { reject(new Error('Failed to open IndexedDB')); }; request.onsuccess = () => { resolve(request.result); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; // 删除旧的对象存储(如果存在) if (db.objectStoreNames.contains(STORE_NAME)) { db.deleteObjectStore(STORE_NAME); } // 创建新的对象存储,使用 hash 作为主键 db.createObjectStore(STORE_NAME, { keyPath: 'key' }); }; }); return this.dbPromise; } /** * 保存 demo 到缓存 * @param data 要保存的数据 * @param options 保存选项(符合 IDemoStorage 接口) */ async save(data: AnalysisData, options: SaveOptions): Promise { try { // 内部计算内容哈希 const hash = await hashContent(data); const db = await this.getDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const filename = ensureJsonExtension(options.name); const key = `${filename}~${hash}`; // 使用 filename~hash 作为 key const record = { key, filename, data, timestamp: Date.now() }; const request = store.put(record); return new Promise((resolve) => { request.onsuccess = () => { resolve({ success: true, message: 'Saved to local cache', file: filename, hash // 返回计算好的哈希值 }); }; request.onerror = () => { const error = request.error; if (error && error.name === 'QuotaExceededError') { resolve({ success: false, message: 'Storage quota exceeded, please clear cache and try again' }); } else { resolve({ success: false, message: 'Failed to save to cache' }); } }; }); } catch (error) { // CryptoSubtleUnavailableError 需要特殊处理,直接重新抛出让调用方处理 if (error instanceof CryptoSubtleUnavailableError) { throw error; } if (error instanceof DOMException && error.name === 'QuotaExceededError') { return { success: false, message: 'Storage quota exceeded, please clear cache and try again' }; } return { success: false, message: extractErrorMessage(error, 'Save failed') }; } } /** * 从缓存加载 demo * @param key 完整的 key,格式为 "filename~hash" */ async load(key?: string): Promise { if (!key) { return { success: false, message: 'Key is missing' }; } try { const db = await this.getDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const request = store.get(key); return new Promise((resolve) => { request.onsuccess = () => { const record = request.result; if (!record || !record.data) { resolve({ success: false, message: 'File not found in local cache, please open again' }); return; } resolve({ success: true, data: record.data as AnalysisData }); }; request.onerror = () => { const error = request.error; console.error('从缓存读取失败:', error); resolve({ success: false, message: 'Failed to read from cache' }); }; }); } catch (error) { return { success: false, message: extractErrorMessage(error, '加载失败') }; } } /** * 删除指定的 demo * @param key 完整的 key,格式为 "filename~hash" */ async delete(key: string): Promise { try { const db = await this.getDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.delete(key); return new Promise((resolve) => { request.onsuccess = () => resolve(true); request.onerror = () => { console.error('删除失败:', request.error); resolve(false); }; }); } catch (error) { console.error('删除 demo 失败:', error); return false; } } /** * 清空所有本地缓存 */ async clear(): Promise { try { const db = await this.getDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.clear(); return new Promise((resolve) => { request.onsuccess = () => resolve(true); request.onerror = () => { console.error('清空失败:', request.error); resolve(false); }; }); } catch (error) { console.error('清空缓存失败:', error); return false; } } /** * 列出所有已缓存的 demo */ async list(): Promise { try { const db = await this.getDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const request = store.getAllKeys(); return new Promise((resolve) => { request.onsuccess = () => { resolve(request.result as string[]); }; request.onerror = () => { console.error('列出缓存失败:', request.error); resolve([]); }; }); } catch (error) { console.error('列出缓存失败:', error); return []; } } }