You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
4.5 KiB
163 lines
4.5 KiB
// 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<QueryExecResult[]>;
|
|
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<void> | 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<void> {
|
|
// 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<void> {
|
|
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<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) {
|
|
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<QueryExecResult[]> {
|
|
await this.waitForInitialization();
|
|
return this.db!.exec(sql, params);
|
|
}
|
|
|
|
async getOneRow(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<unknown[] | undefined> {
|
|
await this.waitForInitialization();
|
|
const result = await this.db!.exec(sql, params);
|
|
return result[0]?.values[0];
|
|
}
|
|
|
|
async all(sql: string, params: unknown[] = []): Promise<unknown[][]> {
|
|
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;
|
|
|