Browse Source

fix problems with race conditions and multiple DatabaseService instances

sql-absurd-sql
Trent Larson 2 weeks ago
parent
commit
a38934e38d
  1. 2
      src/db-sql/migration.ts
  2. 15
      src/libs/util.ts
  3. 73
      src/services/database.ts

2
src/db-sql/migration.ts

@ -5,7 +5,7 @@ import type { QueryExecResult } from '../services/migrationService';
const MIGRATIONS = [ const MIGRATIONS = [
{ {
name: '001_initial', name: '001_initial',
// see ../db/tables files for explanations // see ../db/tables files for explanations of the fields
sql: ` sql: `
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,

15
src/libs/util.ts

@ -12,6 +12,7 @@ import {
updateAccountSettings, updateAccountSettings,
updateDefaultSettings, updateDefaultSettings,
} from "../db/index"; } from "../db/index";
import databaseService from "../services/database";
import { Account } from "../db/tables/accounts"; import { Account } from "../db/tables/accounts";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings"; import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
@ -550,6 +551,20 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
publicKeyHex: newId.keys[0].publicKeyHex, 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 }); await updateDefaultSettings({ activeDid: newId.did });
} catch (error) { } catch (error) {
console.error("Failed to update default settings:", error); console.error("Failed to update default settings:", error);

73
src/services/database.ts

@ -16,16 +16,49 @@ interface SQLDatabase {
} }
class DatabaseService { class DatabaseService {
private static instance: DatabaseService | null = null;
private db: SQLDatabase | null; private db: SQLDatabase | null;
private initialized: boolean; private initialized: boolean;
private initializationPromise: Promise<void> | null = null;
constructor() { private constructor() {
this.db = null; this.db = null;
this.initialized = false; this.initialized = false;
} }
static getInstance(): DatabaseService {
if (!DatabaseService.instance) {
DatabaseService.instance = new DatabaseService();
}
return DatabaseService.instance;
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
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<void> {
if (this.initialized) {
return;
}
const SQL = await initSqlJs({ const SQL = await initSqlJs({
locateFile: (file: string) => { locateFile: (file: string) => {
@ -48,12 +81,10 @@ class DatabaseService {
this.db = new SQL.Database(path, { filename: true }); this.db = new SQL.Database(path, { filename: true });
if (!this.db) { 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(` await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
PRAGMA journal_mode=MEMORY;
`);
const sqlExec = this.db.exec.bind(this.db); const sqlExec = this.db.exec.bind(this.db);
// Run migrations // Run migrations
@ -62,38 +93,52 @@ class DatabaseService {
this.initialized = true; this.initialized = true;
} }
private ensureInitialized(): void { private async waitForInitialization(): Promise<void> {
if (!this.initialized || !this.db) { // If we have an initialization promise, wait for it
throw new Error('Database not initialized'); 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 // Used for inserts, updates, and deletes
async run(sql: string, params: any[] = []): Promise<{ changes: number; lastId?: number }> { async run(sql: string, params: any[] = []): Promise<{ changes: number; lastId?: number }> {
this.ensureInitialized(); await this.waitForInitialization();
return this.db!.run(sql, params); return this.db!.run(sql, params);
} }
// Note that the resulting array may be empty if there are no results from the query // Note that the resulting array may be empty if there are no results from the query
async query(sql: string, params: any[] = []): Promise<QueryExecResult[]> { async query(sql: string, params: any[] = []): Promise<QueryExecResult[]> {
this.ensureInitialized(); await this.waitForInitialization();
return this.db!.exec(sql, params); return this.db!.exec(sql, params);
} }
async getOneRow(sql: string, params: any[] = []): Promise<any[] | undefined> { async getOneRow(sql: string, params: any[] = []): Promise<any[] | undefined> {
this.ensureInitialized(); await this.waitForInitialization();
const result = await this.db!.exec(sql, params); const result = await this.db!.exec(sql, params);
return result[0]?.values[0]; return result[0]?.values[0];
} }
async all(sql: string, params: any[] = []): Promise<any[][]> { async all(sql: string, params: any[] = []): Promise<any[][]> {
this.ensureInitialized(); await this.waitForInitialization();
const result = await this.db!.exec(sql, params); const result = await this.db!.exec(sql, params);
return result[0]?.values || []; return result[0]?.values || [];
} }
} }
// Create a singleton instance // Create a singleton instance
const databaseService = new DatabaseService(); const databaseService = DatabaseService.getInstance();
export default databaseService; export default databaseService;
Loading…
Cancel
Save