dmMessageStore.ts
1 import { openDB, type IDBPDatabase } from 'idb'; 2 import type { NostrEvent } from '@nostrify/nostrify'; 3 4 // ============================================================================ 5 // IndexedDB Schema 6 // ============================================================================ 7 8 // Use domain-based naming to avoid conflicts between apps on same domain 9 const getDBName = () => { 10 // Use hostname for unique DB per app (e.g., 'nostr-dm-store-localhost', 'nostr-dm-store-myapp.com') 11 const hostname = typeof window !== 'undefined' ? window.location.hostname : 'default'; 12 return `nostr-dm-store-${hostname}`; 13 }; 14 const DB_NAME = getDBName(); 15 const DB_VERSION = 1; 16 const STORE_NAME = 'messages'; 17 18 interface StoredParticipant { 19 messages: NostrEvent[]; 20 lastActivity: number; 21 hasNIP4: boolean; 22 hasNIP17: boolean; 23 } 24 25 export interface MessageStore { 26 participants: Record<string, StoredParticipant>; 27 lastSync: { 28 nip4: number | null; 29 nip17: number | null; 30 }; 31 } 32 33 // ============================================================================ 34 // Database Operations 35 // ============================================================================ 36 37 /** 38 * Open the IndexedDB database 39 */ 40 async function openDatabase(): Promise<IDBPDatabase> { 41 return openDB(DB_NAME, DB_VERSION, { 42 upgrade(db) { 43 // Create the messages store if it doesn't exist 44 if (!db.objectStoreNames.contains(STORE_NAME)) { 45 db.createObjectStore(STORE_NAME); 46 } 47 }, 48 }); 49 } 50 51 /** 52 * Write messages to IndexedDB for a specific user 53 * Messages are stored in their original encrypted form (kind 4 or kind 13) 54 */ 55 export async function writeMessagesToDB( 56 userPubkey: string, 57 messageStore: MessageStore 58 ): Promise<void> { 59 try { 60 const db = await openDatabase(); 61 62 // Store messages in their original encrypted form (no NIP-44 wrapper needed) 63 // Each message content is already encrypted by the sender 64 await db.put(STORE_NAME, messageStore, userPubkey); 65 } catch (error) { 66 console.error('[MessageStore] ❌ Error writing to IndexedDB:', error); 67 throw error; 68 } 69 } 70 71 /** 72 * Read messages from IndexedDB for a specific user 73 * Messages are stored in their original encrypted form (kind 4 or kind 13) 74 */ 75 export async function readMessagesFromDB( 76 userPubkey: string 77 ): Promise<MessageStore | undefined> { 78 try { 79 const db = await openDatabase(); 80 const data = await db.get(STORE_NAME, userPubkey); 81 82 if (!data) { 83 return undefined; 84 } 85 86 return data as MessageStore; 87 } catch (error) { 88 console.error('[MessageStore] Error reading from IndexedDB:', error); 89 throw error; 90 } 91 } 92 93 /** 94 * Delete messages from IndexedDB for a specific user 95 */ 96 export async function deleteMessagesFromDB(userPubkey: string): Promise<void> { 97 try { 98 const db = await openDatabase(); 99 await db.delete(STORE_NAME, userPubkey); 100 } catch (error) { 101 console.error('[MessageStore] Error deleting from IndexedDB:', error); 102 throw error; 103 } 104 } 105 106 /** 107 * Clear all messages from IndexedDB 108 */ 109 export async function clearAllMessages(): Promise<void> { 110 try { 111 const db = await openDatabase(); 112 await db.clear(STORE_NAME); 113 } catch (error) { 114 console.error('[MessageStore] Error clearing IndexedDB:', error); 115 throw error; 116 } 117 }