From a38934e38ddcca88a79bdeee52cb50bfaca43957 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 19:46:15 -0600 Subject: [PATCH] fix problems with race conditions and multiple DatabaseService instances --- src/db-sql/migration.ts | 2 +- src/libs/util.ts | 15 +++++++++ src/services/database.ts | 73 ++++++++++++++++++++++++++++++++-------- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index b81a93d8..bf7c50fb 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -5,7 +5,7 @@ import type { QueryExecResult } from '../services/migrationService'; const MIGRATIONS = [ { name: '001_initial', - // see ../db/tables files for explanations + // see ../db/tables files for explanations of the fields sql: ` CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/src/libs/util.ts b/src/libs/util.ts index 3c7c794d..573a5b37 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -12,6 +12,7 @@ import { updateAccountSettings, updateDefaultSettings, } from "../db/index"; +import databaseService from "../services/database"; import { Account } from "../db/tables/accounts"; import { Contact } from "../db/tables/contacts"; import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings"; @@ -550,6 +551,20 @@ export const generateSaveAndActivateIdentity = async (): Promise => { publicKeyHex: newId.keys[0].publicKeyHex, }); + // add to the new sql db + await databaseService.run( + `INSERT INTO accounts (dateCreated, derivationPath, did, identity, mnemonic, publicKeyHex) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + new Date().toISOString(), + derivationPath, + newId.did, + identity, + mnemonic, + newId.keys[0].publicKeyHex + ] + ); + await updateDefaultSettings({ activeDid: newId.did }); } catch (error) { console.error("Failed to update default settings:", error); diff --git a/src/services/database.ts b/src/services/database.ts index 74da316d..0b645adc 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -16,16 +16,49 @@ interface SQLDatabase { } class DatabaseService { + private static instance: DatabaseService | null = null; private db: SQLDatabase | null; private initialized: boolean; + private initializationPromise: Promise | null = null; - constructor() { + private constructor() { this.db = null; this.initialized = false; } + static getInstance(): DatabaseService { + if (!DatabaseService.instance) { + DatabaseService.instance = new DatabaseService(); + } + return DatabaseService.instance; + } + async initialize(): Promise { - if (this.initialized) return; + // If already initialized, return immediately + if (this.initialized) { + return; + } + + // If initialization is in progress, wait for it + if (this.initializationPromise) { + return this.initializationPromise; + } + + // Start initialization + this.initializationPromise = this._initialize(); + try { + await this.initializationPromise; + } catch (error) { + console.error(`DatabaseService initialize method failed:`, error); + this.initializationPromise = null; // Reset on failure + throw error; + } + } + + private async _initialize(): Promise { + if (this.initialized) { + return; + } const SQL = await initSqlJs({ locateFile: (file: string) => { @@ -48,12 +81,10 @@ class DatabaseService { this.db = new SQL.Database(path, { filename: true }); if (!this.db) { - throw new Error('Failed to initialize database'); + throw new Error('The database initialization failed. We recommend you restart or reinstall.'); } - await this.db.exec(` - PRAGMA journal_mode=MEMORY; - `); + await this.db.exec(`PRAGMA journal_mode=MEMORY;`); const sqlExec = this.db.exec.bind(this.db); // Run migrations @@ -62,38 +93,52 @@ class DatabaseService { this.initialized = true; } - private ensureInitialized(): void { - if (!this.initialized || !this.db) { - throw new Error('Database not initialized'); + private async waitForInitialization(): Promise { + // If we have an initialization promise, wait for it + if (this.initializationPromise) { + await this.initializationPromise; + return; + } + + // If not initialized and no promise, start initialization + if (!this.initialized) { + await this.initialize(); + return; + } + + // If initialized but no db, something went wrong + if (!this.db) { + console.error(`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`); + throw new Error(`The database could not be initialized. We recommend you restart or reinstall.`); } } // Used for inserts, updates, and deletes async run(sql: string, params: any[] = []): Promise<{ changes: number; lastId?: number }> { - this.ensureInitialized(); + await this.waitForInitialization(); return this.db!.run(sql, params); } // Note that the resulting array may be empty if there are no results from the query async query(sql: string, params: any[] = []): Promise { - this.ensureInitialized(); + await this.waitForInitialization(); return this.db!.exec(sql, params); } async getOneRow(sql: string, params: any[] = []): Promise { - this.ensureInitialized(); + await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values[0]; } async all(sql: string, params: any[] = []): Promise { - this.ensureInitialized(); + await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values || []; } } // Create a singleton instance -const databaseService = new DatabaseService(); +const databaseService = DatabaseService.getInstance(); export default databaseService; \ No newline at end of file