/ src / lib / dmMessageStore.ts
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  }