snapshotStore.ts3.0 KBView on GitHub
import type { FreezeSnapshot, TimelineEntry } from '../types';

export interface StoredSnapshot {
  id: string;
  ts: number;
  reason: 'longtask' | 'worker-heartbeat-miss' | 'raf-gap';
  blockedMs: number;
  recentTimeline: TimelineEntry[];
  storeKeys: string[];
  memory?: FreezeSnapshot['memory'];
  userAgent: string;
  url: string;
  viewport: { w: number; h: number };
}

const DB_NAME = 'cedar-deep-debugger';
const STORE_NAME = 'freeze-snapshots';
const DB_VERSION = 1;
const MAX_SNAPSHOTS = 20;

function openDb(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    if (typeof indexedDB === 'undefined') {
      reject(new Error('IndexedDB unavailable'));
      return;
    }
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
        store.createIndex('ts', 'ts');
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

export async function saveSnapshot(snap: StoredSnapshot): Promise<void> {
  try {
    const db = await openDb();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).put(snap);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
    // Trim to MAX_SNAPSHOTS (oldest first)
    const all = await listSnapshots();
    if (all.length > MAX_SNAPSHOTS) {
      const toRemove = all.slice(0, all.length - MAX_SNAPSHOTS);
      await Promise.all(toRemove.map((s) => deleteSnapshot(s.id)));
    }
  } catch {
    // ignore — snapshots are best-effort
  }
}

export async function listSnapshots(): Promise<StoredSnapshot[]> {
  try {
    const db = await openDb();
    return await new Promise<StoredSnapshot[]>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readonly');
      const req = tx.objectStore(STORE_NAME).getAll();
      req.onsuccess = () => {
        const items = (req.result as StoredSnapshot[]).sort((a, b) => a.ts - b.ts);
        resolve(items);
      };
      req.onerror = () => reject(req.error);
    });
  } catch {
    return [];
  }
}

export async function deleteSnapshot(id: string): Promise<void> {
  try {
    const db = await openDb();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).delete(id);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch {
    // ignore
  }
}

export async function clearSnapshots(): Promise<void> {
  try {
    const db = await openDb();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).clear();
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch {
    // ignore
  }
}