paper-burner/js/chatbot/utils/katex-cache-persistence.js

348 lines
9.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Paper Burner - KaTeX 缓存持久化系统
* Phase 4.2+: 自动保存和恢复公式缓存到 localStorage
*
* 功能:
* - 页面关闭时自动保存缓存
* - 页面加载时自动恢复缓存
* - 自动清理过期缓存7 天)
* - 管理存储配额(最多 5MB
*
* 预期收益:
* - 二次访问时公式渲染速度提升 99%
* - 完全消除重复公式的渲染时间
*/
(function() {
'use strict';
const STORAGE_CONFIG = {
KEY: 'paperburner_katex_cache',
VERSION: 1,
MAX_AGE_DAYS: 7, // 缓存有效期(天)
MAX_SIZE_MB: 5, // 最大存储空间MB
AUTO_SAVE_INTERVAL: 30000, // 自动保存间隔30秒
ENABLE_AUTO_SAVE: true, // 是否启用自动保存
ENABLE_DEBUG: false // 调试模式
};
/**
* KaTeX 缓存持久化管理器
*/
class KaTeXCachePersistence {
constructor(cache, options = {}) {
this.cache = cache;
this.storageKey = options.storageKey || STORAGE_CONFIG.KEY;
this.maxAgeDays = options.maxAgeDays || STORAGE_CONFIG.MAX_AGE_DAYS;
this.maxSizeMB = options.maxSizeMB || STORAGE_CONFIG.MAX_SIZE_MB;
this.autoSaveInterval = options.autoSaveInterval || STORAGE_CONFIG.AUTO_SAVE_INTERVAL;
this.enableAutoSave = options.enableAutoSave !== false;
this.enableDebug = options.enableDebug || STORAGE_CONFIG.ENABLE_DEBUG;
this.autoSaveTimer = null;
this.lastSaveTime = 0;
this.saveCount = 0;
this.init();
}
/**
* 初始化持久化系统
*/
init() {
// 加载缓存
this.load();
// 设置自动保存
if (this.enableAutoSave) {
this.startAutoSave();
}
// 页面卸载时保存
window.addEventListener('beforeunload', () => {
this.save();
});
// 页面隐藏时保存(移动端)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.save();
}
});
this.log('Persistence system initialized', 'info');
}
/**
* 从 localStorage 加载缓存
*/
load() {
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
this.log('No cached data found', 'info');
return false;
}
const data = JSON.parse(stored);
// 验证版本
if (data.version !== STORAGE_CONFIG.VERSION) {
this.log(`Version mismatch: ${data.version} vs ${STORAGE_CONFIG.VERSION}`, 'warn');
this.clear();
return false;
}
// 检查过期时间
const age = Date.now() - data.timestamp;
const maxAge = this.maxAgeDays * 24 * 60 * 60 * 1000;
if (age > maxAge) {
this.log(`Cache expired: ${Math.round(age / 86400000)} days old`, 'warn');
this.clear();
return false;
}
// 导入缓存
const success = this.cache.import(data.cacheData);
if (success) {
const size = this.estimateSize(stored);
this.log(`✅ Loaded ${data.cacheData.entries.length} formulas from cache (${size} KB, ${Math.round(age / 60000)} min old)`, 'success');
return true;
} else {
this.log('Failed to import cache data', 'error');
return false;
}
} catch (error) {
this.log(`Load error: ${error.message}`, 'error');
this.clear();
return false;
}
}
/**
* 保存缓存到 localStorage
*/
save() {
try {
const cacheData = this.cache.export();
const data = {
version: STORAGE_CONFIG.VERSION,
timestamp: Date.now(),
cacheData: cacheData
};
const json = JSON.stringify(data);
const size = this.estimateSize(json);
// 检查大小限制
if (size > this.maxSizeMB * 1024) {
this.log(`Cache too large: ${size} KB > ${this.maxSizeMB * 1024} KB`, 'warn');
// 尝试清理旧条目
this.pruneCache(cacheData);
return this.save(); // 递归保存清理后的缓存
}
localStorage.setItem(this.storageKey, json);
this.lastSaveTime = Date.now();
this.saveCount++;
this.log(`💾 Saved ${cacheData.entries.length} formulas (${size} KB) [#${this.saveCount}]`, 'success');
return true;
} catch (error) {
if (error.name === 'QuotaExceededError') {
this.log('Storage quota exceeded, clearing old cache', 'warn');
this.clear();
return false;
}
this.log(`Save error: ${error.message}`, 'error');
return false;
}
}
/**
* 清理缓存(保留最近使用的条目)
*/
pruneCache(cacheData) {
const maxEntries = Math.floor(cacheData.entries.length * 0.7); // 保留 70%
const prunedEntries = cacheData.entries.slice(-maxEntries);
this.log(`Pruning cache: ${cacheData.entries.length}${prunedEntries.length}`, 'warn');
cacheData.entries = prunedEntries;
this.cache.import(cacheData);
}
/**
* 清空缓存
*/
clear() {
try {
localStorage.removeItem(this.storageKey);
this.cache.clear();
this.log('Cache cleared', 'info');
return true;
} catch (error) {
this.log(`Clear error: ${error.message}`, 'error');
return false;
}
}
/**
* 启动自动保存
*/
startAutoSave() {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
}
this.autoSaveTimer = setInterval(() => {
const stats = this.cache.getStats();
// 只有缓存有更新时才保存
if (stats.hits > 0 || stats.misses > 0) {
this.save();
}
}, this.autoSaveInterval);
this.log(`Auto-save enabled: every ${this.autoSaveInterval / 1000}s`, 'info');
}
/**
* 停止自动保存
*/
stopAutoSave() {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
this.autoSaveTimer = null;
this.log('Auto-save disabled', 'info');
}
}
/**
* 估算数据大小KB
*/
estimateSize(data) {
const bytes = new Blob([data]).size;
return Math.round(bytes / 1024);
}
/**
* 获取持久化统计信息
*/
getStats() {
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
return {
exists: false,
size: 0,
age: 0,
entries: 0
};
}
const data = JSON.parse(stored);
const size = this.estimateSize(stored);
const age = Date.now() - data.timestamp;
return {
exists: true,
size: size,
sizeFormatted: `${size} KB`,
age: age,
ageFormatted: this.formatAge(age),
entries: data.cacheData?.entries?.length || 0,
version: data.version,
lastSaveTime: this.lastSaveTime,
saveCount: this.saveCount
};
} catch (error) {
return {
exists: false,
error: error.message
};
}
}
/**
* 格式化时间差
*/
formatAge(ms) {
const minutes = Math.floor(ms / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days} 天前`;
if (hours > 0) return `${hours} 小时前`;
if (minutes > 0) return `${minutes} 分钟前`;
return '刚刚';
}
/**
* 日志输出
*/
log(message, type = 'info') {
if (!this.enableDebug && type !== 'error') return;
const prefix = '[KaTeXCache Persistence]';
const timestamp = new Date().toLocaleTimeString();
switch (type) {
case 'success':
console.log(`%c${prefix} ${message}`, 'color: #48bb78; font-weight: bold');
break;
case 'warn':
console.warn(`${prefix} ${message}`);
break;
case 'error':
console.error(`${prefix} ${message}`);
break;
default:
console.log(`${prefix} ${message}`);
}
}
}
// 自动初始化持久化系统
if (window.katexCache && !window.katexCachePersistence) {
window.KaTeXCachePersistence = KaTeXCachePersistence;
window.katexCachePersistence = new KaTeXCachePersistence(window.katexCache, {
enableAutoSave: STORAGE_CONFIG.ENABLE_AUTO_SAVE,
enableDebug: STORAGE_CONFIG.ENABLE_DEBUG
});
console.log('[KaTeXCache] Cache persistence enabled (auto-save every 30s)');
// 提供全局查看方法
window.getKatexPersistenceStats = function() {
const stats = window.katexCachePersistence.getStats();
console.log('KaTeX 缓存持久化统计:');
console.table(stats);
return stats;
};
// 提供手动保存方法
window.saveKatexCache = function() {
return window.katexCachePersistence.save();
};
// 提供手动清除方法
window.clearKatexCache = function() {
return window.katexCachePersistence.clear();
};
} else if (!window.katexCache) {
console.warn('[KaTeXCache Persistence] Cannot initialize: katexCache not found');
}
})();
console.log('[KaTeXCache Persistence] System loaded');
console.log(' Stats: getKatexPersistenceStats()');
console.log(' Manual save: saveKatexCache()');
console.log(' Clear cache: clearKatexCache()');