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 = [
{
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,

15
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<string> => {
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);

73
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<void> | 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<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({
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<void> {
// 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<QueryExecResult[]> {
this.ensureInitialized();
await this.waitForInitialization();
return this.db!.exec(sql, params);
}
async getOneRow(sql: string, params: any[] = []): Promise<any[] | undefined> {
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<any[][]> {
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;
Loading…
Cancel
Save