| |
| |
| |
| |
|
|
| 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'; |
|
|
| |
| |
| |
| |
| export class LocalDemoCache implements IDemoStorage { |
| readonly type = 'local' as const; |
| private dbPromise: Promise<IDBDatabase> | null = null; |
|
|
| |
| |
| |
| static isAvailable(): boolean { |
| return typeof indexedDB !== 'undefined'; |
| } |
|
|
| |
| |
| |
| private async getDB(): Promise<IDBDatabase> { |
| if (!LocalDemoCache.isAvailable()) { |
| throw new Error('IndexedDB 不可用,可能是浏览器不支持或处于隐私模式'); |
| } |
|
|
| if (this.dbPromise) { |
| return this.dbPromise; |
| } |
|
|
| this.dbPromise = new Promise<IDBDatabase>((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); |
| } |
| |
| |
| db.createObjectStore(STORE_NAME, { keyPath: 'key' }); |
| }; |
| }); |
|
|
| return this.dbPromise; |
| } |
|
|
| |
| |
| |
| |
| |
| async save(data: AnalysisData, options: SaveOptions): Promise<SaveResult> { |
| 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}`; |
|
|
| 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) { |
| |
| 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') |
| }; |
| } |
| } |
|
|
| |
| |
| |
| |
| async load(key?: string): Promise<LoadResult> { |
| 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, '加载失败') |
| }; |
| } |
| } |
|
|
| |
| |
| |
| |
| async delete(key: string): Promise<boolean> { |
| 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<boolean> { |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| async list(): Promise<string[]> { |
| 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 []; |
| } |
| } |
| } |
|
|
|
|