kv-music/js/cache.js
Eduard Prigoana 0f1a9841d1 cc
2025-10-11 19:22:53 +03:00

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
};
}
}