forked from trent_larson/crowd-funder-for-time-pwa
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 = [
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user