fix problems with race conditions and multiple DatabaseService instances
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user