// Add type declarations for external modules declare module "@jlongster/sql.js"; declare module "absurd-sql"; declare module "absurd-sql/dist/indexeddb-backend"; import initSqlJs from "@jlongster/sql.js"; import { SQLiteFS } from "absurd-sql"; import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; import { runMigrations } from "../db-sql/migration"; import type { QueryExecResult } from "../interfaces/database"; import { logger } from "@/utils/logger"; interface SQLDatabase { exec: (sql: string, params?: unknown[]) => Promise; run: ( sql: string, params?: unknown[], ) => Promise<{ changes: number; lastId?: number }>; } class DatabaseService { private static instance: DatabaseService | null = null; private db: SQLDatabase | null; private initialized: boolean; private initializationPromise: Promise | null = null; 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 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) { logger.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) => { return new URL( `/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url, ).href; }, }); const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); SQL.register_for_idb(sqlFS); SQL.FS.mkdir("/sql"); SQL.FS.mount(sqlFS, {}, "/sql"); const path = "/sql/db.sqlite"; if (typeof SharedArrayBuffer === "undefined") { const stream = SQL.FS.open(path, "a+"); await stream.node.contents.readIfFallback(); SQL.FS.close(stream); } this.db = new SQL.Database(path, { filename: true }); if (!this.db) { throw new Error( "The database initialization failed. We recommend you restart or reinstall.", ); } await this.db.exec(`PRAGMA journal_mode=MEMORY;`); const sqlExec = this.db.exec.bind(this.db); // Run migrations await runMigrations(sqlExec); this.initialized = true; } 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) { logger.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: unknown[] = [], ): Promise<{ changes: number; lastId?: number }> { 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: unknown[] = []): Promise { await this.waitForInitialization(); return this.db!.exec(sql, params); } async getOneRow( sql: string, params: unknown[] = [], ): Promise { await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values[0]; } async all(sql: string, params: unknown[] = []): Promise { await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values || []; } } // Create a singleton instance const databaseService = DatabaseService.getInstance(); export default databaseService;