From 03f4efbfebd863562a586fbfce844b79a783a9cb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 03:41:19 +0000 Subject: [PATCH] refactor(dm-db): use singleton IndexedDB connection to avoid N+1 overhead Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- benchmarks/dmDb-benchmark.mjs | 55 +++++++++++++++++++++++++++++++ js/storage/dmDb.js | 62 ++++++++++++++++++++++++++++------- tests/dm-db.test.mjs | 2 ++ 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 benchmarks/dmDb-benchmark.mjs diff --git a/benchmarks/dmDb-benchmark.mjs b/benchmarks/dmDb-benchmark.mjs new file mode 100644 index 00000000..41a1888d --- /dev/null +++ b/benchmarks/dmDb-benchmark.mjs @@ -0,0 +1,55 @@ + +import { performance } from "perf_hooks"; + +// Mock IndexedDB if running in Node +if (!globalThis.indexedDB) { + const { indexedDB, IDBKeyRange } = await import("fake-indexeddb"); + globalThis.indexedDB = indexedDB; + globalThis.IDBKeyRange = IDBKeyRange; +} + +// Mock console to suppress logs +const originalConsoleWarn = console.warn; +console.warn = () => {}; + +// Import after mocking console +const { writeMessages, clearDmDb } = await import("../js/storage/dmDb.js"); + +async function runBenchmark() { + await clearDmDb(); + + const iterations = 1000; + const messages = []; + + for (let i = 0; i < iterations; i++) { + messages.push({ + id: `msg-${i}`, + conversation_id: "conv-1", + sender_pubkey: "sender", + receiver_pubkey: "receiver", + created_at: Date.now(), + kind: 4, + content: `Message ${i}`, + }); + } + + console.log(`Starting benchmark with ${iterations} writes...`); + + const start = performance.now(); + + // Simulate N+1 writes + for (const msg of messages) { + await writeMessages(msg); + } + + const end = performance.now(); + const duration = end - start; + + console.log(`Total time: ${duration.toFixed(2)}ms`); + console.log(`Average time per write: ${(duration / iterations).toFixed(2)}ms`); + + // Restore console + console.warn = originalConsoleWarn; +} + +runBenchmark().catch(console.error); diff --git a/js/storage/dmDb.js b/js/storage/dmDb.js index 2b41ff2e..f004bed6 100644 --- a/js/storage/dmDb.js +++ b/js/storage/dmDb.js @@ -84,6 +84,9 @@ const MIGRATIONS = [ }, ]; +let dbInstance = null; +let dbPromise = null; + function isIndexedDbAvailable() { try { return typeof indexedDB !== "undefined"; @@ -101,7 +104,15 @@ function applyMigrations(db, oldVersion, newVersion, transaction) { } function openDmDb() { - return new Promise((resolve, reject) => { + if (dbInstance) { + return Promise.resolve(dbInstance); + } + + if (dbPromise) { + return dbPromise; + } + + dbPromise = new Promise((resolve, reject) => { if (!isIndexedDbAvailable()) { resolve(null); return; @@ -118,10 +129,39 @@ function openDmDb() { applyMigrations(db, oldVersion, newVersion, upgradeTransaction); }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => + request.onsuccess = () => { + const db = request.result; + dbInstance = db; + + db.onversionchange = () => { + db.close(); + dbInstance = null; + dbPromise = null; + }; + + db.onclose = () => { + dbInstance = null; + dbPromise = null; + }; + + resolve(db); + }; + + request.onerror = () => { + dbPromise = null; reject(request.error || new Error("Failed to open DM database.")); + }; }); + + return dbPromise; +} + +export async function closeDmDb() { + if (dbInstance) { + dbInstance.close(); + dbInstance = null; + } + dbPromise = null; } function sanitizeTimestamp(value) { @@ -264,7 +304,7 @@ export async function writeConversationMetadata(conversation) { const tx = db.transaction(STORES.CONVERSATIONS, "readwrite"); tx.objectStore(STORES.CONVERSATIONS).put(normalized); await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return normalized; } catch (error) { userLogger.warn("[dmDb] Failed to write conversation metadata", error); @@ -296,7 +336,7 @@ export async function writeMessages(messages) { } await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return normalized; } catch (error) { userLogger.warn("[dmDb] Failed to persist messages", error); @@ -319,7 +359,7 @@ export async function getConversation(conversationId) { const store = tx.objectStore(STORES.CONVERSATIONS); const result = await runRequest(store.get(conversationId.trim())); await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return result || null; } catch (error) { userLogger.warn("[dmDb] Failed to read conversation metadata", error); @@ -369,7 +409,7 @@ export async function listMessagesByConversation(conversationId, options = {}) { userLogger.warn("[dmDb] Failed to list messages", request.error); resolve(results); }; - }).finally(() => db.close()); + }); // removed finally db.close() } catch (error) { userLogger.warn("[dmDb] Failed to list messages", error); return []; @@ -429,7 +469,7 @@ export async function updateConversationFromMessage(message, options = {}) { store.put(next); await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return next; } catch (error) { userLogger.warn("[dmDb] Failed to update conversation metadata", error); @@ -478,7 +518,7 @@ export async function updateConversationOpenState( store.put(next); await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return next; } catch (error) { userLogger.warn("[dmDb] Failed to update conversation open state", error); @@ -512,7 +552,7 @@ export async function listConversations() { userLogger.warn("[dmDb] Failed to list conversations", request.error); resolve(results); }; - }).finally(() => db.close()); + }); // removed finally db.close() } catch (error) { userLogger.warn("[dmDb] Failed to list conversations", error); return []; @@ -542,7 +582,7 @@ export async function clearDmDb() { } await finalizeTransaction(tx); - db.close(); + // db.close(); // Keep connection open return true; } catch (error) { devLogger.warn("[dmDb] Failed to clear DM database", error); diff --git a/tests/dm-db.test.mjs b/tests/dm-db.test.mjs index bd726819..854e1741 100644 --- a/tests/dm-db.test.mjs +++ b/tests/dm-db.test.mjs @@ -6,6 +6,7 @@ import { listMessagesByConversation, updateConversationFromMessage, writeMessages, + closeDmDb, } from "../js/storage/dmDb.js"; const { indexedDB, IDBKeyRange } = await import("fake-indexeddb"); @@ -25,6 +26,7 @@ async function deleteDatabase(name) { } afterEach(async () => { + await closeDmDb(); await deleteDatabase("bitvidDm"); });