You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
5.8 KiB
191 lines
5.8 KiB
/** |
|
* 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_<eventId>') |
|
value: unknown; |
|
}; |
|
rss: { |
|
key: string; // feed URL |
|
value: unknown; |
|
indexes: { cached_at: number }; |
|
}; |
|
} |
|
|
|
let dbInstance: IDBPDatabase<DatabaseSchema> | null = null; |
|
|
|
/** |
|
* Get or create database instance |
|
*/ |
|
export async function getDB(): Promise<IDBPDatabase<DatabaseSchema>> { |
|
if (dbInstance) return dbInstance; |
|
|
|
try { |
|
dbInstance = await openDB<DatabaseSchema>(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<void>((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<DatabaseSchema>(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<void> { |
|
if (dbInstance) { |
|
dbInstance.close(); |
|
dbInstance = null; |
|
} |
|
} |
|
|
|
/** |
|
* Reset database instance (useful for recovery from errors) |
|
*/ |
|
export function resetDB(): void { |
|
if (dbInstance) { |
|
dbInstance.close(); |
|
dbInstance = null; |
|
} |
|
}
|
|
|