// IndexedDB-backed cache for node_modules snapshots. // Keyed by a hash of the package.json contents so stale caches auto-invalidate. import type { VolumeSnapshot } from '../engine-types'; const DB_NAME = 'nodepod-snapshots'; const STORE_NAME = 'snapshots'; const DB_VERSION = 1; const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days export interface IDBSnapshotCache { get(packageJsonHash: string): Promise; set(packageJsonHash: string, snapshot: VolumeSnapshot): Promise; close(): void; } function openDB(): Promise { if (typeof indexedDB === 'undefined') return Promise.resolve(null); return new Promise((resolve) => { try { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => resolve(null); } catch { resolve(null); } }); } function idbGet(db: IDBDatabase, key: string): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.get(key); req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => reject(req.error); }); } function idbPut(db: IDBDatabase, key: string, value: any): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); store.put(value, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function idbCleanExpired(db: IDBDatabase): void { try { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const req = store.openCursor(); const now = Date.now(); req.onsuccess = () => { const cursor = req.result; if (!cursor) return; const entry = cursor.value; if (entry?.createdAt && (now - entry.createdAt) > MAX_AGE_MS) { cursor.delete(); } cursor.continue(); }; } catch { /* best-effort cleanup */ } } export async function openSnapshotCache(): Promise { const db = await openDB(); if (!db) { console.warn('[nodepod:idb-cache] IndexedDB not available — snapshot cache disabled'); return null; } console.log('[nodepod:idb-cache] IndexedDB snapshot cache opened (db:', DB_NAME, ')'); // Background cleanup of expired entries idbCleanExpired(db); return { async get(packageJsonHash: string): Promise { try { console.log('[nodepod:idb-cache] GET snapshot for hash:', packageJsonHash.substring(0, 12) + '...'); const entry = await idbGet(db, packageJsonHash); if (!entry?.snapshot) { console.log('[nodepod:idb-cache] CACHE MISS — no snapshot found for this package.json hash'); return null; } // Check expiry if (entry.createdAt && (Date.now() - entry.createdAt) > MAX_AGE_MS) { console.log('[nodepod:idb-cache] CACHE EXPIRED — snapshot is older than 7 days'); return null; } const age = entry.createdAt ? ((Date.now() - entry.createdAt) / 1000).toFixed(0) + 's ago' : 'unknown age'; console.log('[nodepod:idb-cache] CACHE HIT — restoring snapshot (' + entry.snapshot.entries.length + ' entries, cached ' + age + ')'); return entry.snapshot as VolumeSnapshot; } catch (err) { console.warn('[nodepod:idb-cache] GET failed:', err); return null; } }, async set(packageJsonHash: string, snapshot: VolumeSnapshot): Promise { try { console.log('[nodepod:idb-cache] SET snapshot for hash:', packageJsonHash.substring(0, 12) + '...', '(' + snapshot.entries.length + ' entries)'); await idbPut(db, packageJsonHash, { snapshot, createdAt: Date.now(), }); console.log('[nodepod:idb-cache] SET complete — snapshot cached to IndexedDB'); } catch (err) { console.warn('[nodepod:idb-cache] SET failed:', err); } }, close(): void { try { db.close(); } catch { /* ignore */ } }, }; }