InfoRadar / client /src /ts /storage /localDemoCache.ts
dqy08's picture
增强多语言支持,网页默认加载示例
bb35e88
/**
* 本地 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<IDBDatabase> | null = null;
/**
* 检查 IndexedDB 是否可用
*/
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);
}
// 创建新的对象存储,使用 hash 作为主键
db.createObjectStore(STORE_NAME, { keyPath: 'key' });
};
});
return this.dbPromise;
}
/**
* 保存 demo 到缓存
* @param data 要保存的数据
* @param options 保存选项(符合 IDemoStorage 接口)
*/
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}`; // 使用 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<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, '加载失败')
};
}
}
/**
* 删除指定的 demo
* @param key 完整的 key,格式为 "filename~hash"
*/
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;
}
}
/**
* 列出所有已缓存的 demo
*/
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 [];
}
}
}