const HOLE_CACHE_DB_NAME = 'hole_cache_db'; const CACHE_DB_VER = 1; const MAINTENANCE_STEP = 150; const MAINTENANCE_COUNT = 1000; const ENC_KEY = 42; class Cache { constructor() { this.db = null; this.added_items_since_maintenance = 0; this.encrypt = this.encrypt.bind(this); this.decrypt = this.decrypt.bind(this); const open_req = indexedDB.open(HOLE_CACHE_DB_NAME, CACHE_DB_VER); open_req.onerror = console.error.bind(console); open_req.onupgradeneeded = (event) => { console.log('comment cache db upgrade'); const db = event.target.result; const store = db.createObjectStore('comment', { keyPath: 'pid', }); store.createIndex('last_access', 'last_access', { unique: false }); }; open_req.onsuccess = (event) => { console.log('comment cache db loaded'); this.db = event.target.result; setTimeout(this.maintenance.bind(this), 1); }; } // use window.hole_cache.encrypt() only after cache is loaded! encrypt(pid, data) { let s = JSON.stringify(data); let o = ''; for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) { let c = s.charCodeAt(i); let new_key = (key ^ (c / 2)) % 128; o += String.fromCharCode(key ^ s.charCodeAt(i)); key = new_key; } return o; } // use window.hole_cache.decrypt() only after cache is loaded! decrypt(pid, s) { let o = ''; if (typeof s !== typeof 'str') return null; for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) { let c = key ^ s.charCodeAt(i); o += String.fromCharCode(c); key = (key ^ (c / 2)) % 128; } try { return JSON.parse(o); } catch (e) { console.error('decrypt failed'); console.trace(e); return null; } } get(pid, target_version) { pid = parseInt(pid); return new Promise((resolve, reject) => { if (!this.db) return resolve(null); const tx = this.db.transaction(['comment'], 'readwrite'); const store = tx.objectStore('comment'); const get_req = store.get(pid); get_req.onsuccess = () => { let res = get_req.result; if (!res || !res.data_str) { //console.log('comment cache miss '+pid); resolve(null); } else if (target_version === res.version) { // hit //console.log('comment cache hit', pid); res.last_access = +new Date(); store.put(res); let data = this.decrypt(pid, res.data_str); resolve(data); // obj or null } else { // expired console.log( 'comment cache expired', pid, ': ver', res.version, 'target', target_version, ); store.delete(pid); resolve(null); } }; get_req.onerror = (e) => { console.warn('comment cache indexeddb open failed'); console.error(e); resolve(null); }; }); } put(pid, target_version, data) { pid = parseInt(pid); return new Promise((resolve, reject) => { if (!this.db) return resolve(); const tx = this.db.transaction(['comment'], 'readwrite'); const store = tx.objectStore('comment'); store.put({ pid: pid, version: target_version, data_str: this.encrypt(pid, data), last_access: +new Date(), }); //console.log('comment cache put', pid); if (++this.added_items_since_maintenance === MAINTENANCE_STEP) setTimeout(this.maintenance.bind(this), 1); }); } delete(pid) { pid = parseInt(pid); return new Promise((resolve, reject) => { if (!this.db) return resolve(); const tx = this.db.transaction(['comment'], 'readwrite'); const store = tx.objectStore('comment'); let req = store.delete(pid); console.log('comment cache delete', pid); req.onerror = () => { console.warn('comment cache delete failed ', pid); return resolve(); }; req.onsuccess = () => resolve(); }); } maintenance() { if (!this.db) return; const tx = this.db.transaction(['comment'], 'readwrite'); const store = tx.objectStore('comment'); let count_req = store.count(); count_req.onsuccess = () => { let count = count_req.result; if (count > MAINTENANCE_COUNT) { console.log('comment cache db maintenance', count); store.index('last_access').openKeyCursor().onsuccess = (e) => { let cur = e.target.result; if (cur) { //console.log('maintenance: delete',cur); store.delete(cur.primaryKey); if (--count > MAINTENANCE_COUNT) cur.continue(); } }; } else { console.log('comment cache db no need to maintenance', count); } this.added_items_since_maintenance = 0; }; count_req.onerror = console.error.bind(console); } clear() { if (!this.db) return; indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME); console.log('delete comment cache db'); } } export function cache() { if (!window.hole_cache) window.hole_cache = new Cache(); return window.hole_cache; }