// **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top // This prevents "crypto is not defined" errors when running in worker context if (typeof window === 'undefined' && typeof crypto === 'undefined') { (globalThis as any).crypto = { getRandomValues: (array: any) => { // Simple fallback for worker context for (let i = 0; i < array.length; i++) { array[i] = Math.floor(Math.random() * 256); } return array; } }; } 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 { DatabaseService, QueryExecResult } from "../interfaces/database"; import { logger } from "@/utils/logger"; interface QueuedOperation { type: "run" | "query"; sql: string; params: unknown[]; resolve: (value: unknown) => void; reject: (reason: unknown) => void; } interface AbsurdSqlDatabase { exec: (sql: string, params?: unknown[]) => Promise; run: ( sql: string, params?: unknown[], ) => Promise<{ changes: number; lastId?: number }>; } class AbsurdSqlDatabaseService implements DatabaseService { private static instance: AbsurdSqlDatabaseService | null = null; private db: AbsurdSqlDatabase | null; private initialized: boolean; private initializationPromise: Promise | null = null; private operationQueue: Array = []; private isProcessingQueue: boolean = false; private constructor() { this.db = null; this.initialized = false; } static getInstance(): AbsurdSqlDatabaseService { if (!AbsurdSqlDatabaseService.instance) { AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService(); } return AbsurdSqlDatabaseService.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(`AbsurdSqlDatabaseService initialize method failed:`, error); // DISABLED logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); this.initializationPromise = null; // Reset on failure throw error; } } private async _initialize(): Promise { if (this.initialized) { return; } // **PLATFORM CHECK**: AbsurdSqlDatabaseService should only run on web-based platforms // This prevents SharedArrayBuffer checks and web-specific initialization on Electron/Capacitor // Allow both 'web' (production) and 'development' (dev server) platforms const webBasedPlatforms = ["web", "development"]; if (!webBasedPlatforms.includes(process.env.VITE_PLATFORM || "")) { throw new Error( `AbsurdSqlDatabaseService is only supported on web-based platforms (web, development). Current platform: ${process.env.VITE_PLATFORM}`, ); } 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/timesafari.absurd-sql"; // **SHARED ARRAY BUFFER FALLBACK**: Only needed for web platform // This check handles Safari and other browsers without SharedArrayBuffer support if (typeof SharedArrayBuffer === "undefined") { logger.debug( "[AbsurdSqlDatabaseService] SharedArrayBuffer not available, using fallback mode", ); const stream = SQL.FS.open(path, "a+"); await stream.node.contents.readIfFallback(); SQL.FS.close(stream); } else { logger.debug( "[AbsurdSqlDatabaseService] SharedArrayBuffer available, using optimized mode", ); } this.db = new SQL.Database(path, { filename: true }); if (!this.db) { throw new Error( "The database initialization failed. We recommend you restart or reinstall.", ); } // An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)" await this.db.exec(`PRAGMA journal_mode=MEMORY;`); const sqlExec = this.db.run.bind(this.db); const sqlQuery = this.db.exec.bind(this.db); // Extract the migration names for the absurd-sql format const extractMigrationNames: (result: QueryExecResult[]) => Set = ( result, ) => { // Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me). const names = result?.[0]?.values.map((row) => row[0] as string) || []; return new Set(names); }; // Run migrations await runMigrations(sqlExec, sqlQuery, extractMigrationNames); this.initialized = true; // Start processing the queue after initialization this.processQueue(); } private async processQueue(): Promise { if (this.isProcessingQueue || !this.initialized || !this.db) { return; } this.isProcessingQueue = true; while (this.operationQueue.length > 0) { const operation = this.operationQueue.shift(); if (!operation) continue; try { let result: unknown; switch (operation.type) { case "run": result = await this.db.run(operation.sql, operation.params); break; case "query": result = await this.db.exec(operation.sql, operation.params); break; } operation.resolve(result); } catch (error) { // logger.error( // DISABLED // "Error while processing SQL queue:", // error, // " ... for sql:", // operation.sql, // " ... with params:", // operation.params, // ); logger.error( "Error while processing SQL queue:", error, " ... for sql:", operation.sql, " ... with params:", operation.params, ); operation.reject(error); } } this.isProcessingQueue = false; } private async queueOperation( type: QueuedOperation["type"], sql: string, params: unknown[] = [], ): Promise { return new Promise((resolve, reject) => { const operation: QueuedOperation = { type, sql, params, resolve: (value: unknown) => resolve(value as R), reject, }; this.operationQueue.push(operation); // If we're already initialized, start processing the queue if (this.initialized && this.db) { this.processQueue(); } }); } 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( // DISABLED // `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, // ); 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.queueOperation<{ changes: number; lastId?: number }>( "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.queueOperation("query", sql, params); } } // Export the service class for lazy instantiation // The singleton will only be created when actually needed (web platform only) export default AbsurdSqlDatabaseService;