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.
 
 
 
 
 

223 lines
7.3 KiB

/**
* Base IndexedDB store operations
*/
import { openDB, type IDBPDatabase } from 'idb';
const DB_NAME = 'aitherboard';
const DB_VERSION = 9; // Version 7: Added RSS cache store. Version 8: Added markdown cache store. Version 9: Added event archive 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 };
};
markdown: {
key: string; // content hash
value: unknown;
indexes: { cached_at: number };
};
eventArchive: {
key: string; // event id
value: unknown;
indexes: { kind: number; pubkey: string; created_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 });
}
// Markdown cache store
if (!db.objectStoreNames.contains('markdown')) {
const markdownStore = db.createObjectStore('markdown', { keyPath: 'hash' });
markdownStore.createIndex('cached_at', 'cached_at', { unique: false });
}
// Event archive store (compressed old events)
if (!db.objectStoreNames.contains('eventArchive')) {
const archiveStore = db.createObjectStore('eventArchive', { keyPath: 'id' });
archiveStore.createIndex('kind', 'kind', { unique: false });
archiveStore.createIndex('pubkey', 'pubkey', { unique: false });
archiveStore.createIndex('created_at', 'created_at', { unique: false });
}
},
blocked() {
// IndexedDB 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') ||
!dbInstance.objectStoreNames.contains('markdown') ||
!dbInstance.objectStoreNames.contains('eventArchive')) {
// Database is corrupted - close and delete it, then recreate
// Database schema outdated, 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 = () => {
// 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 });
const markdownStore = db.createObjectStore('markdown', { keyPath: 'hash' });
markdownStore.createIndex('cached_at', 'cached_at', { unique: false });
const archiveStore = db.createObjectStore('eventArchive', { keyPath: 'id' });
archiveStore.createIndex('kind', 'kind', { unique: false });
archiveStore.createIndex('pubkey', 'pubkey', { unique: false });
archiveStore.createIndex('created_at', 'created_at', { unique: false });
},
blocked() {
// IndexedDB blocked (another tab may have it open)
},
blocking() {
if (dbInstance) {
dbInstance.close();
dbInstance = null;
}
}
});
}
return dbInstance;
} catch (error) {
// Failed to open IndexedDB
// 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;
}
}