180 lines
No EOL
5.4 KiB
JavaScript
180 lines
No EOL
5.4 KiB
JavaScript
//js/cache.js
|
|
export class APICache {
|
|
constructor(options = {}) {
|
|
this.memoryCache = new Map();
|
|
this.maxSize = options.maxSize || 200;
|
|
this.ttl = options.ttl || 1000 * 60 * 30;
|
|
this.dbName = 'monochrome-cache';
|
|
this.dbVersion = 1;
|
|
this.db = null;
|
|
this.initDB();
|
|
}
|
|
|
|
async initDB() {
|
|
if (typeof indexedDB === 'undefined') return;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => {
|
|
this.db = request.result;
|
|
resolve(this.db);
|
|
};
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = event.target.result;
|
|
|
|
if (!db.objectStoreNames.contains('responses')) {
|
|
const store = db.createObjectStore('responses', { keyPath: 'key' });
|
|
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
generateKey(type, params) {
|
|
const paramString = typeof params === 'object'
|
|
? JSON.stringify(params)
|
|
: String(params);
|
|
return `${type}:${paramString}`;
|
|
}
|
|
|
|
async get(type, params) {
|
|
const key = this.generateKey(type, params);
|
|
|
|
if (this.memoryCache.has(key)) {
|
|
const cached = this.memoryCache.get(key);
|
|
if (Date.now() - cached.timestamp < this.ttl) {
|
|
return cached.data;
|
|
}
|
|
this.memoryCache.delete(key);
|
|
}
|
|
|
|
if (this.db) {
|
|
try {
|
|
const cached = await this.getFromIndexedDB(key);
|
|
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
this.memoryCache.set(key, cached);
|
|
return cached.data;
|
|
}
|
|
} catch (error) {
|
|
console.debug('IndexedDB read error:', error);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async set(type, params, data) {
|
|
const key = this.generateKey(type, params);
|
|
const entry = {
|
|
key,
|
|
data,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
this.memoryCache.set(key, entry);
|
|
|
|
if (this.memoryCache.size > this.maxSize) {
|
|
const firstKey = this.memoryCache.keys().next().value;
|
|
this.memoryCache.delete(firstKey);
|
|
}
|
|
|
|
if (this.db) {
|
|
try {
|
|
await this.setInIndexedDB(entry);
|
|
} catch (error) {
|
|
console.debug('IndexedDB write error:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
getFromIndexedDB(key) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.db) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
const transaction = this.db.transaction(['responses'], 'readonly');
|
|
const store = transaction.objectStore('responses');
|
|
const request = store.get(key);
|
|
|
|
request.onsuccess = () => resolve(request.result || null);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
setInIndexedDB(entry) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.db) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const transaction = this.db.transaction(['responses'], 'readwrite');
|
|
const store = transaction.objectStore('responses');
|
|
const request = store.put(entry);
|
|
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
async clear() {
|
|
this.memoryCache.clear();
|
|
|
|
if (this.db) {
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = this.db.transaction(['responses'], 'readwrite');
|
|
const store = transaction.objectStore('responses');
|
|
const request = store.clear();
|
|
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
}
|
|
|
|
async clearExpired() {
|
|
const now = Date.now();
|
|
const expired = [];
|
|
|
|
for (const [key, entry] of this.memoryCache.entries()) {
|
|
if (now - entry.timestamp >= this.ttl) {
|
|
expired.push(key);
|
|
}
|
|
}
|
|
|
|
expired.forEach(key => this.memoryCache.delete(key));
|
|
|
|
if (this.db) {
|
|
try {
|
|
const transaction = this.db.transaction(['responses'], 'readwrite');
|
|
const store = transaction.objectStore('responses');
|
|
const index = store.index('timestamp');
|
|
const range = IDBKeyRange.upperBound(now - this.ttl);
|
|
const request = index.openCursor(range);
|
|
|
|
request.onsuccess = (event) => {
|
|
const cursor = event.target.result;
|
|
if (cursor) {
|
|
cursor.delete();
|
|
cursor.continue();
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.debug('Failed to clear expired IndexedDB entries:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
getCacheStats() {
|
|
return {
|
|
memoryEntries: this.memoryCache.size,
|
|
maxSize: this.maxSize,
|
|
ttl: this.ttl
|
|
};
|
|
}
|
|
} |