/** * Base IndexedDB store operations */ import { openDB, type IDBPDatabase } from 'idb'; const DB_NAME = 'aitherboard'; const DB_VERSION = 7; // Version 6: Removed opengraph store. Version 7: Added RSS cache store export interface DatabaseSchema { events: { key: string; // event id value: unknown; indexes: { kind: number; pubkey: string; created_at: number }; }; profiles: { key: string; // pubkey value: unknown; }; keys: { key: string; // key id value: unknown; }; search: { key: string; value: unknown; }; preferences: { key: string; // preference key value: unknown; }; drafts: { key: string; // draft id (e.g., 'write', 'comment_') value: unknown; }; rss: { key: string; // feed URL value: unknown; indexes: { cached_at: number }; }; } let dbInstance: IDBPDatabase | null = null; /** * Get or create database instance */ export async function getDB(): Promise> { if (dbInstance) return dbInstance; try { dbInstance = await openDB(DB_NAME, DB_VERSION, { upgrade(db, oldVersion) { // Migration: Remove opengraph store (was added in version 4, removed in version 6) if (db.objectStoreNames.contains('opengraph')) { db.deleteObjectStore('opengraph'); } // Events store if (!db.objectStoreNames.contains('events')) { const eventStore = db.createObjectStore('events', { keyPath: 'id' }); eventStore.createIndex('kind', 'kind', { unique: false }); eventStore.createIndex('pubkey', 'pubkey', { unique: false }); eventStore.createIndex('created_at', 'created_at', { unique: false }); } // Profiles store if (!db.objectStoreNames.contains('profiles')) { db.createObjectStore('profiles', { keyPath: 'pubkey' }); } // Keys store if (!db.objectStoreNames.contains('keys')) { db.createObjectStore('keys', { keyPath: 'id' }); } // Search index store if (!db.objectStoreNames.contains('search')) { db.createObjectStore('search', { keyPath: 'id' }); } // Preferences store if (!db.objectStoreNames.contains('preferences')) { db.createObjectStore('preferences', { keyPath: 'key' }); } // Drafts store if (!db.objectStoreNames.contains('drafts')) { db.createObjectStore('drafts', { keyPath: 'id' }); } // RSS cache store if (!db.objectStoreNames.contains('rss')) { const rssStore = db.createObjectStore('rss', { keyPath: 'feedUrl' }); rssStore.createIndex('cached_at', 'cached_at', { unique: false }); } }, blocked() { console.warn('IndexedDB is blocked - another tab may have it open'); }, blocking() { // Close connection if another tab wants to upgrade if (dbInstance) { dbInstance.close(); dbInstance = null; } } }); // Verify all stores exist after opening - if not, database is corrupted if (!dbInstance.objectStoreNames.contains('events') || !dbInstance.objectStoreNames.contains('profiles') || !dbInstance.objectStoreNames.contains('keys') || !dbInstance.objectStoreNames.contains('search') || !dbInstance.objectStoreNames.contains('preferences') || !dbInstance.objectStoreNames.contains('drafts') || !dbInstance.objectStoreNames.contains('rss')) { // Database is corrupted - close and delete it, then recreate console.warn('Database missing required stores, recreating...'); dbInstance.close(); dbInstance = null; // Delete the corrupted database const deleteReq = indexedDB.deleteDatabase(DB_NAME); await new Promise((resolve, reject) => { deleteReq.onsuccess = () => resolve(); deleteReq.onerror = () => reject(deleteReq.error); deleteReq.onblocked = () => { console.warn('Database deletion blocked - another tab may have it open'); resolve(); // Continue anyway }; }); // Wait a bit for deletion to complete await new Promise(resolve => setTimeout(resolve, 100)); // Recreate database dbInstance = await openDB(DB_NAME, DB_VERSION, { upgrade(db) { const eventStore = db.createObjectStore('events', { keyPath: 'id' }); eventStore.createIndex('kind', 'kind', { unique: false }); eventStore.createIndex('pubkey', 'pubkey', { unique: false }); eventStore.createIndex('created_at', 'created_at', { unique: false }); db.createObjectStore('profiles', { keyPath: 'pubkey' }); db.createObjectStore('keys', { keyPath: 'id' }); db.createObjectStore('search', { keyPath: 'id' }); db.createObjectStore('preferences', { keyPath: 'key' }); db.createObjectStore('drafts', { keyPath: 'id' }); const rssStore = db.createObjectStore('rss', { keyPath: 'feedUrl' }); rssStore.createIndex('cached_at', 'cached_at', { unique: false }); }, blocked() { console.warn('IndexedDB is blocked - another tab may have it open'); }, blocking() { if (dbInstance) { dbInstance.close(); dbInstance = null; } } }); } return dbInstance; } catch (error) { console.error('Failed to open IndexedDB:', error); // Reset instance so we can retry dbInstance = null; throw error; } } /** * Close database connection */ export async function closeDB(): Promise { if (dbInstance) { dbInstance.close(); dbInstance = null; } } /** * Reset database instance (useful for recovery from errors) */ export function resetDB(): void { if (dbInstance) { dbInstance.close(); dbInstance = null; } }