/** * IndexedDB local state persistence for the web client. * * Stores conversations, messages, and identity state in the browser. */ const DB_NAME = "quicproquo"; const DB_VERSION = 1; export interface StoredConversation { id: string; name: string; unread: number; lastActivity: number; } export interface StoredMessage { id: string; conversationId: string; sender: string; body: string; timestamp: number; isOutgoing: boolean; } export interface StoredIdentity { username: string; seed: Uint8Array; publicKey: Uint8Array; } function openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains("conversations")) { const convStore = db.createObjectStore("conversations", { keyPath: "id" }); convStore.createIndex("lastActivity", "lastActivity"); } if (!db.objectStoreNames.contains("messages")) { const msgStore = db.createObjectStore("messages", { keyPath: "id" }); msgStore.createIndex("conversationId", "conversationId"); msgStore.createIndex("timestamp", "timestamp"); } if (!db.objectStoreNames.contains("identity")) { db.createObjectStore("identity", { keyPath: "username" }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } export class LocalStore { private db: IDBDatabase | null = null; async open(): Promise { this.db = await openDB(); } close(): void { if (this.db) { this.db.close(); this.db = null; } } // --- Identity --- async saveIdentity(identity: StoredIdentity): Promise { const db = this.requireDB(); const tx = db.transaction("identity", "readwrite"); tx.objectStore("identity").put(identity); return txDone(tx); } async loadIdentity(username: string): Promise { const db = this.requireDB(); const tx = db.transaction("identity", "readonly"); const req = tx.objectStore("identity").get(username); return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => reject(req.error); }); } // --- Conversations --- async saveConversation(conv: StoredConversation): Promise { const db = this.requireDB(); const tx = db.transaction("conversations", "readwrite"); tx.objectStore("conversations").put(conv); return txDone(tx); } async listConversations(): Promise { const db = this.requireDB(); const tx = db.transaction("conversations", "readonly"); const req = tx.objectStore("conversations").index("lastActivity").getAll(); return new Promise((resolve, reject) => { req.onsuccess = () => resolve((req.result ?? []).reverse()); req.onerror = () => reject(req.error); }); } async getConversation(id: string): Promise { const db = this.requireDB(); const tx = db.transaction("conversations", "readonly"); const req = tx.objectStore("conversations").get(id); return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => reject(req.error); }); } // --- Messages --- async saveMessage(msg: StoredMessage): Promise { const db = this.requireDB(); const tx = db.transaction("messages", "readwrite"); tx.objectStore("messages").put(msg); return txDone(tx); } async loadMessages(conversationId: string, limit = 200): Promise { const db = this.requireDB(); const tx = db.transaction("messages", "readonly"); const index = tx.objectStore("messages").index("conversationId"); const req = index.getAll(conversationId); return new Promise((resolve, reject) => { req.onsuccess = () => { const all: StoredMessage[] = req.result ?? []; all.sort((a, b) => a.timestamp - b.timestamp); resolve(all.slice(-limit)); }; req.onerror = () => reject(req.error); }); } // --- Clear --- async clearAll(): Promise { const db = this.requireDB(); const tx = db.transaction( ["conversations", "messages", "identity"], "readwrite", ); tx.objectStore("conversations").clear(); tx.objectStore("messages").clear(); tx.objectStore("identity").clear(); return txDone(tx); } private requireDB(): IDBDatabase { if (!this.db) { throw new Error("LocalStore not opened. Call open() first."); } return this.db; } } function txDone(tx: IDBTransaction): Promise { return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); }