@ -1,3 +1,16 @@
/ * *
* @file AbsurdSqlDatabaseService . ts
* @description AbsurdSQL database service with comprehensive logging for failure tracking
*
* This service provides a SQLite database interface using absurd - sql for web browsers
* with IndexedDB as the backend storage . Includes extensive logging for debugging
* initialization failures , operation errors , and performance issues .
*
* @author Matthew Raymer
* @version 2.0 . 0
* @since 2025 - 07 - 01
* /
import initSqlJs from "@jlongster/sql.js" ;
import { SQLiteFS } from "absurd-sql" ;
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend" ;
@ -5,6 +18,7 @@ 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" ;
import { enableDatabaseLogging , disableDatabaseLogging } from "@/db/databaseUtil" ;
interface QueuedOperation {
type : "run" | "query" ;
@ -12,6 +26,9 @@ interface QueuedOperation {
params : unknown [ ] ;
resolve : ( value : unknown ) = > void ;
reject : ( reason : unknown ) = > void ;
// Enhanced tracking fields
queuedAt : number ;
operationId : string ;
}
interface AbsurdSqlDatabase {
@ -22,118 +39,446 @@ interface AbsurdSqlDatabase {
) = > Promise < { changes : number ; lastId? : number } > ;
}
/ * *
* AbsurdSQL Database Service with comprehensive logging and failure tracking
* /
class AbsurdSqlDatabaseService implements DatabaseService {
// Singleton pattern with proper async initialization
private static instance : AbsurdSqlDatabaseService | null = null ;
private static initializationPromise : Promise < AbsurdSqlDatabaseService > | null = null ;
private db : AbsurdSqlDatabase | null ;
private initialized : boolean ;
private initializationPromise : Promise < void > | null = null ;
private operationQueue : Array < QueuedOperation > = [ ] ;
private isProcessingQueue : boolean = false ;
// Enhanced tracking fields
private initStartTime : number = 0 ;
private totalOperations : number = 0 ;
private failedOperations : number = 0 ;
private queueHighWaterMark : number = 0 ;
private lastOperationTime : number = 0 ;
private operationIdCounter : number = 0 ;
// Write failure tracking for fallback mode
private writeFailureCount = 0 ;
private maxWriteFailures = 3 ;
private isWriteDisabled = false ;
private constructor ( ) {
this . db = null ;
this . initialized = false ;
// Reduced logging during construction to avoid circular dependency
console . log ( "[AbsurdSQL] Service instance created" , {
timestamp : new Date ( ) . toISOString ( ) ,
hasSharedArrayBuffer : typeof SharedArrayBuffer !== "undefined" ,
platform : process.env.VITE_PLATFORM ,
} ) ;
}
static getInstance ( ) : AbsurdSqlDatabaseService {
if ( ! AbsurdSqlDatabaseService . instance ) {
AbsurdSqlDatabaseService . instance = new AbsurdSqlDatabaseService ( ) ;
// If we already have an instance, return it immediately
if ( AbsurdSqlDatabaseService . instance ) {
return AbsurdSqlDatabaseService . instance ;
}
// If initialization is already in progress, this is a problem
// Return a new instance to prevent blocking (fallback behavior)
if ( AbsurdSqlDatabaseService . initializationPromise ) {
console . warn ( "[AbsurdSQL] Multiple getInstance calls during initialization - creating fallback instance" ) ;
return new AbsurdSqlDatabaseService ( ) ;
}
// Create and initialize the singleton
console . log ( "[AbsurdSQL] Creating singleton instance" ) ;
AbsurdSqlDatabaseService . instance = new AbsurdSqlDatabaseService ( ) ;
return AbsurdSqlDatabaseService . instance ;
}
/ * *
* Initialize the database with comprehensive logging
* /
async initialize ( ) : Promise < void > {
const startTime = performance . now ( ) ;
console . log ( "[AbsurdSQL] Initialization requested" , {
initialized : this.initialized ,
hasInitPromise : ! ! this . initializationPromise ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
// If already initialized, return immediately
if ( this . initialized ) {
console . log ( "[AbsurdSQL] Already initialized, returning immediately" ) ;
return ;
}
// If initialization is in progress, wait for it
if ( this . initializationPromise ) {
console . log ( "[AbsurdSQL] Initialization in progress, waiting..." ) ;
return this . initializationPromise ;
}
// Start initialization
this . initStartTime = startTime ;
this . initializationPromise = this . _initialize ( ) ;
try {
await this . initializationPromise ;
const duration = performance . now ( ) - startTime ;
// Enable database logging now that initialization is complete
enableDatabaseLogging ( ) ;
logger . info ( "[AbsurdSQL] Initialization completed successfully" , {
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} catch ( error ) {
logger . error ( ` AbsurdSqlDatabaseService initialize method failed: ` , error ) ;
const duration = performance . now ( ) - startTime ;
console . error ( "[AbsurdSQL] Initialization failed" , {
error : error instanceof Error ? error.message : String ( error ) ,
errorStack : error instanceof Error ? error.stack : undefined ,
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
userAgent : navigator.userAgent ,
hasSharedArrayBuffer : typeof SharedArrayBuffer !== "undefined" ,
platform : process.env.VITE_PLATFORM ,
} ) ;
this . initializationPromise = null ; // Reset on failure
throw error ;
}
}
/ * *
* Internal initialization with reduced logging to prevent circular dependency
* /
private async _initialize ( ) : Promise < void > {
if ( this . initialized ) {
console . log ( "[AbsurdSQL] Already initialized in _initialize" ) ;
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 ) ;
// Set up global error handler for IndexedDB write failures
this . setupGlobalErrorHandler ( ) ;
SQL . FS . mkdir ( "/sql" ) ;
SQL . FS . mount ( sqlFS , { } , "/sql" ) ;
console . log ( "[AbsurdSQL] Starting initialization process" , {
timestamp : new Date ( ) . toISOString ( ) ,
sharedArrayBufferSupported : typeof SharedArrayBuffer !== "undefined" ,
} ) ;
const path = "/sql/timesafari.absurd-sql" ;
if ( typeof SharedArrayBuffer === "undefined" ) {
const stream = SQL . FS . open ( path , "a+" ) ;
await stream . node . contents . readIfFallback ( ) ;
SQL . FS . close ( stream ) ;
}
try {
// Step 1: Initialize SQL.js
console . log ( "[AbsurdSQL] Step 1: Initializing SQL.js" ) ;
const sqlJsStartTime = performance . now ( ) ;
const SQL = await initSqlJs ( {
locateFile : ( file : string ) = > {
const url = new URL (
` /node_modules/@jlongster/sql.js/dist/ ${ file } ` ,
import . meta . url ,
) . href ;
return url ;
} ,
} ) ;
const sqlJsDuration = performance . now ( ) - sqlJsStartTime ;
console . log ( "[AbsurdSQL] SQL.js initialized successfully" , {
duration : ` ${ sqlJsDuration . toFixed ( 2 ) } ms ` ,
} ) ;
// Step 2: Setup file system
console . log ( "[AbsurdSQL] Step 2: Setting up SQLite file system" ) ;
const fsStartTime = performance . now ( ) ;
const sqlFS = new SQLiteFS ( SQL . FS , new IndexedDBBackend ( ) ) ;
SQL . register_for_idb ( sqlFS ) ;
SQL . FS . mkdir ( "/sql" ) ;
SQL . FS . mount ( sqlFS , { } , "/sql" ) ;
const fsDuration = performance . now ( ) - fsStartTime ;
console . log ( "[AbsurdSQL] File system setup completed" , {
duration : ` ${ fsDuration . toFixed ( 2 ) } ms ` ,
} ) ;
// Step 3: Handle SharedArrayBuffer fallback with enhanced error handling
const path = "/sql/timesafari.absurd-sql" ;
console . log ( "[AbsurdSQL] Step 3: Setting up database file" , { path } ) ;
if ( typeof SharedArrayBuffer === "undefined" ) {
console . warn ( "[AbsurdSQL] SharedArrayBuffer not available, using fallback mode" ) ;
console . warn ( "[AbsurdSQL] Proactively disabling database logging to prevent IndexedDB write failures" ) ;
// Proactively disable database logging in fallback mode
disableDatabaseLogging ( ) ;
this . isWriteDisabled = true ;
const fallbackStartTime = performance . now ( ) ;
try {
// Enhanced fallback initialization with retry logic
let retryCount = 0 ;
const maxRetries = 3 ;
while ( retryCount < maxRetries ) {
try {
const stream = SQL . FS . open ( path , "a+" ) ;
// Check if the file system is properly mounted and accessible
if ( stream ? . node ? . contents ) {
await stream . node . contents . readIfFallback ( ) ;
SQL . FS . close ( stream ) ;
break ;
} else {
throw new Error ( "File system not properly initialized" ) ;
}
} catch ( retryError ) {
retryCount ++ ;
console . warn ( ` [AbsurdSQL] Fallback mode attempt ${ retryCount } / ${ maxRetries } failed ` , {
error : retryError instanceof Error ? retryError.message : String ( retryError ) ,
retryCount ,
} ) ;
if ( retryCount >= maxRetries ) {
throw retryError ;
}
// Wait before retry
await new Promise ( resolve = > setTimeout ( resolve , 100 * retryCount ) ) ;
}
}
const fallbackDuration = performance . now ( ) - fallbackStartTime ;
console . log ( "[AbsurdSQL] Fallback mode setup completed" , {
duration : ` ${ fallbackDuration . toFixed ( 2 ) } ms ` ,
retries : retryCount ,
} ) ;
} catch ( fallbackError ) {
console . error ( "[AbsurdSQL] Fallback mode setup failed after retries" , {
error : fallbackError instanceof Error ? fallbackError.message : String ( fallbackError ) ,
errorStack : fallbackError instanceof Error ? fallbackError.stack : undefined ,
} ) ;
// Log additional diagnostic information
console . error ( "[AbsurdSQL] Fallback mode diagnostics" , {
hasIndexedDB : typeof indexedDB !== "undefined" ,
hasFileSystemAPI : 'showDirectoryPicker' in window ,
userAgent : navigator.userAgent ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw new Error ( ` Fallback mode initialization failed: ${ fallbackError instanceof Error ? fallbackError.message : String ( fallbackError ) } ` ) ;
}
} else {
console . log ( "[AbsurdSQL] 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." ,
) ;
}
// Step 4: Create database instance
console . log ( "[AbsurdSQL] Step 4: Creating database instance" ) ;
const dbStartTime = performance . now ( ) ;
this . db = new SQL . Database ( path , { filename : true } ) ;
if ( ! this . db ) {
const error = new Error ( "Database initialization failed - SQL.Database constructor returned null" ) ;
console . error ( "[AbsurdSQL] Database instance creation failed" , {
error : error.message ,
path ,
} ) ;
throw error ;
}
const dbDuration = performance . now ( ) - dbStartTime ;
console . log ( "[AbsurdSQL] Database instance created successfully" , {
duration : ` ${ dbDuration . toFixed ( 2 ) } ms ` ,
} ) ;
// Step 5: Set pragmas
console . log ( "[AbsurdSQL] Step 5: Setting database pragmas" ) ;
const pragmaStartTime = performance . now ( ) ;
try {
await this . db . exec ( ` PRAGMA journal_mode=MEMORY; ` ) ;
const pragmaDuration = performance . now ( ) - pragmaStartTime ;
console . log ( "[AbsurdSQL] Database pragmas set successfully" , {
duration : ` ${ pragmaDuration . toFixed ( 2 ) } ms ` ,
} ) ;
} catch ( pragmaError ) {
console . error ( "[AbsurdSQL] Failed to set database pragmas" , {
error : pragmaError instanceof Error ? pragmaError.message : String ( pragmaError ) ,
errorStack : pragmaError instanceof Error ? pragmaError.stack : undefined ,
} ) ;
throw pragmaError ;
}
// 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 ) ;
// Step 6: Setup migration functions
console . log ( "[AbsurdSQL] Step 6: Setting up migration functions" ) ;
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 < string > = (
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 ) ;
} ;
// Extract the migration names for the absurd-sql format
const extractMigrationNames : ( result : QueryExecResult [ ] ) = > Set < string > = (
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 ) ;
} ;
// Step 7: Run migrations
console . log ( "[AbsurdSQL] Step 7: Running database migrations" ) ;
const migrationStartTime = performance . now ( ) ;
try {
await runMigrations ( sqlExec , sqlQuery , extractMigrationNames ) ;
const migrationDuration = performance . now ( ) - migrationStartTime ;
console . log ( "[AbsurdSQL] Database migrations completed successfully" , {
duration : ` ${ migrationDuration . toFixed ( 2 ) } ms ` ,
} ) ;
} catch ( migrationError ) {
console . error ( "[AbsurdSQL] Database migrations failed" , {
error : migrationError instanceof Error ? migrationError.message : String ( migrationError ) ,
errorStack : migrationError instanceof Error ? migrationError.stack : undefined ,
} ) ;
throw migrationError ;
}
// Run migrations
await runMigrations ( sqlExec , sqlQuery , extractMigrationNames ) ;
// Step 8: Finalize initialization
this . initialized = true ;
const totalDuration = performance . now ( ) - this . initStartTime ;
console . log ( "[AbsurdSQL] Initialization completed successfully" , {
totalDuration : ` ${ totalDuration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
this . initialized = true ;
// Start processing the queue after initialization
console . log ( "[AbsurdSQL] Starting queue processing" ) ;
this . processQueue ( ) ;
// Start processing the queue after initialization
this . processQueue ( ) ;
} catch ( error ) {
const totalDuration = performance . now ( ) - this . initStartTime ;
console . error ( "[AbsurdSQL] Initialization failed in _initialize" , {
error : error instanceof Error ? error.message : String ( error ) ,
errorStack : error instanceof Error ? error.stack : undefined ,
totalDuration : ` ${ totalDuration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw error ;
}
}
/ * *
* Process the operation queue with minimal logging
* /
private async processQueue ( ) : Promise < void > {
if ( this . isProcessingQueue || ! this . initialized || ! this . db ) {
// Only log if there's actually a queue to process
if ( this . operationQueue . length > 0 && process . env . NODE_ENV === 'development' ) {
console . debug ( "[AbsurdSQL] Skipping queue processing" , {
isProcessingQueue : this.isProcessingQueue ,
initialized : this.initialized ,
hasDb : ! ! this . db ,
queueLength : this.operationQueue.length ,
} ) ;
}
return ;
}
this . isProcessingQueue = true ;
const startTime = performance . now ( ) ;
let processedCount = 0 ;
let errorCount = 0 ;
// Only log start for larger queues or if errors occur
const shouldLogDetails = this . operationQueue . length > 25 || errorCount > 0 ;
if ( shouldLogDetails ) {
console . info ( "[AbsurdSQL] Processing queue" , {
queueLength : this.operationQueue.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
while ( this . operationQueue . length > 0 ) {
while ( this . operationQueue . length > 0 && this . initialized && this . db ) {
const operation = this . operationQueue . shift ( ) ;
if ( ! operation ) continue ;
if ( ! operation ) break ;
try {
let result : unknown ;
// Only log individual operations for very large queues
if ( this . operationQueue . length > 50 ) {
console . debug ( "[AbsurdSQL] Processing operation" , {
operationId : operation.operationId ,
type : operation . type ,
queueRemaining : this.operationQueue.length ,
} ) ;
}
const result = await this . executeOperation ( operation ) ;
operation . resolve ( result ) ;
processedCount ++ ;
// Only log successful operations for very large queues
if ( this . operationQueue . length > 50 ) {
console . debug ( "[AbsurdSQL] Operation completed" , {
operationId : operation.operationId ,
type : operation . type ,
} ) ;
}
} catch ( error ) {
errorCount ++ ;
// Always log errors
console . error ( "[AbsurdSQL] Operation failed" , {
operationId : operation.operationId ,
type : operation . type ,
error : error instanceof Error ? error.message : String ( error ) ,
errorStack : error instanceof Error ? error.stack : undefined ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
operation . reject ( error ) ;
}
}
this . isProcessingQueue = false ;
const duration = performance . now ( ) - startTime ;
// Only log completion for larger queues, errors, or significant operations
if ( shouldLogDetails || errorCount > 0 || processedCount > 10 ) {
console . info ( "[AbsurdSQL] Queue processing completed" , {
processedCount ,
errorCount ,
totalDuration : ` ${ duration . toFixed ( 2 ) } ms ` ,
totalOperations : this.totalOperations ,
failedOperations : this.failedOperations ,
queueHighWaterMark : this.queueHighWaterMark ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
}
/ * *
* Execute a queued operation with fallback mode error handling
* /
private async executeOperation ( operation : QueuedOperation ) : Promise < unknown > {
if ( ! this . db ) {
throw new Error ( "Database not initialized" ) ;
}
// Check if writes are disabled due to persistent failures
if ( this . isWriteDisabled && operation . type === "run" ) {
console . warn ( "[AbsurdSQL] Skipping write operation - writes disabled due to persistent failures" ) ;
return { changes : 0 , lastId : undefined } ;
}
const operationStartTime = performance . now ( ) ;
let result : unknown ;
let retryCount = 0 ;
const maxRetries = typeof SharedArrayBuffer === "undefined" ? 3 : 1 ; // More retries in fallback mode
while ( retryCount <= maxRetries ) {
try {
switch ( operation . type ) {
case "run" :
result = await this . db . run ( operation . sql , operation . params ) ;
@ -141,87 +486,336 @@ class AbsurdSqlDatabaseService implements DatabaseService {
case "query" :
result = await this . db . exec ( operation . sql , operation . params ) ;
break ;
default :
throw new Error ( ` Unknown operation type: ${ operation . type } ` ) ;
}
operation . resolve ( result ) ;
// Reset write failure count on successful operation
if ( this . writeFailureCount > 0 ) {
this . writeFailureCount = 0 ;
console . log ( "[AbsurdSQL] Write operations recovered" ) ;
}
this . totalOperations ++ ;
this . lastOperationTime = performance . now ( ) ;
const duration = performance . now ( ) - operationStartTime ;
// Only log slow operations to reduce noise
if ( duration > 100 ) {
console . warn ( "[AbsurdSQL] Slow operation detected" , {
operationId : operation.operationId ,
type : operation . type ,
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
retryCount ,
} ) ;
}
return result ;
} catch ( error ) {
logger . error (
"Error while processing SQL queue:" ,
error ,
" ... for sql:" ,
operation . sql ,
" ... with params:" ,
operation . params ,
) ;
operation . reject ( error ) ;
retryCount ++ ;
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
// Check for IndexedDB write failures in fallback mode
const isFallbackWriteError = errorMessage . includes ( "Fallback mode unable to write" ) ||
errorMessage . includes ( "IndexedDB" ) ||
errorMessage . includes ( "write file changes" ) ;
if ( isFallbackWriteError ) {
this . writeFailureCount ++ ;
console . error ( "[AbsurdSQL] Fallback mode write failure detected" , {
operationId : operation.operationId ,
failureCount : this.writeFailureCount ,
maxFailures : this.maxWriteFailures ,
error : errorMessage ,
} ) ;
// Disable writes if too many failures
if ( this . writeFailureCount >= this . maxWriteFailures ) {
this . isWriteDisabled = true ;
console . error ( "[AbsurdSQL] CRITICAL: Database writes disabled due to persistent failures" ) ;
// Disable database logging to prevent feedback loop
disableDatabaseLogging ( ) ;
// Return a safe default for write operations
return { changes : 0 , lastId : undefined } ;
}
}
if ( retryCount > maxRetries ) {
console . error ( "[AbsurdSQL] Operation failed after retries" , {
operationId : operation.operationId ,
type : operation . type ,
error : errorMessage ,
retryCount : retryCount - 1 ,
isFallbackWriteError ,
} ) ;
throw error ;
}
// Wait before retry (exponential backoff)
const delay = Math . min ( 100 * Math . pow ( 2 , retryCount - 1 ) , 1000 ) ;
await new Promise ( resolve = > setTimeout ( resolve , delay ) ) ;
console . warn ( "[AbsurdSQL] Retrying operation" , {
operationId : operation.operationId ,
attempt : retryCount + 1 ,
maxRetries : maxRetries + 1 ,
delay : ` ${ delay } ms ` ,
} ) ;
}
}
this . isProcessingQueue = false ;
throw new Error ( "Operation failed - should not reach here" ) ;
}
private async queueOperation < R > (
type : QueuedOperation [ "type" ] ,
/ * *
* Queue an operation with reduced logging
* /
private queueOperation < T > (
type : "run" | "query" ,
sql : string ,
params : unknown [ ] = [ ] ,
) : Promise < R > {
return new Promise < R > ( ( resolve , reject ) = > {
) : Promise < T > {
const operationId = ` ${ type } _ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
return new Promise < T > ( ( resolve , reject ) = > {
const operation : QueuedOperation = {
operationId ,
type ,
sql ,
params ,
resolve : ( value : unknown ) = > resolve ( value as R ) ,
resolve : ( value : unknown ) = > resolve ( value as T ) ,
reject ,
queuedAt : Date.now ( ) ,
} ;
// Update high water mark tracking
if ( this . operationQueue . length > this . queueHighWaterMark ) {
this . queueHighWaterMark = this . operationQueue . length ;
// Only log new high water marks if they're significant
if ( this . queueHighWaterMark > 100 ) {
console . warn ( "[AbsurdSQL] Queue high water mark reached" , {
queueLength : this.operationQueue.length ,
operationId ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
}
// Log queue size warnings for very large queues
if ( this . operationQueue . length > 200 ) {
console . warn ( "[AbsurdSQL] Operation queue growing very large" , {
queueLength : this.operationQueue.length ,
operationId ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
this . operationQueue . push ( operation ) ;
// If we're already initialized, start processing the queue
if ( this . initialized && this . db ) {
this . processQueue ( ) ;
// Only log individual operations for extremely large queues
if ( this . operationQueue . length > 100 ) {
console . debug ( "[AbsurdSQL] Operation queued" , {
operationId ,
type ,
queueLength : this.operationQueue.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
// Process queue without logging every trigger
if ( ! this . isProcessingQueue ) {
this . processQueue ( ) . catch ( ( error ) = > {
console . error ( "[AbsurdSQL] Queue processing error" , {
error : error instanceof Error ? error.message : String ( error ) ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
} ) ;
}
} ) ;
}
/ * *
* Wait for database initialization to complete
* @private
* /
private async waitForInitialization ( ) : Promise < void > {
// Only log if debug mode is enabled to reduce spam
if ( process . env . NODE_ENV === 'development' ) {
console . debug ( "[AbsurdSQL] Waiting for initialization" , {
initialized : this.initialized ,
hasInitPromise : ! ! this . initializationPromise ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
// If we have an initialization promise, wait for it
if ( this . initializationPromise ) {
if ( process . env . NODE_ENV === 'development' ) {
console . debug ( "[AbsurdSQL] Waiting for initialization promise" ) ;
}
await this . initializationPromise ;
return ;
}
// If not initialized and no promise, start initialization
if ( ! this . initialized ) {
console . info ( "[AbsurdSQL] Starting initialization from waitForInitialization" ) ;
await this . initialize ( ) ;
return ;
}
// If initialized but no db, something went wrong
// Ensure database is properly set up
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. ` ,
) ;
const error = new Error ( "Database not properly initialized - initialized flag is true but db is null" ) ;
console . error ( "[AbsurdSQL] Database state inconsistency detected" , {
error : error.message ,
initialized : this.initialized ,
hasDb : ! ! this . db ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw error ;
}
if ( process . env . NODE_ENV === 'development' ) {
console . debug ( "[AbsurdSQL] Initialization wait completed" ) ;
}
}
// Used for inserts, updates, and deletes
/ * *
* Execute a run operation ( INSERT , UPDATE , DELETE ) with logging
* /
async run (
sql : string ,
params : unknown [ ] = [ ] ,
) : Promise < { changes : number ; lastId? : number } > {
await this . waitForInitialization ( ) ;
return this . queueOperation < { changes : number ; lastId? : number } > (
"run" ,
sql ,
params ,
) ;
const startTime = performance . now ( ) ;
try {
await this . waitForInitialization ( ) ;
const result = await this . queueOperation < { changes : number ; lastId? : number } > (
"run" ,
sql ,
params ,
) ;
const duration = performance . now ( ) - startTime ;
console . debug ( "[AbsurdSQL] Run operation completed" , {
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
changes : result.changes ,
lastId : result.lastId ,
sql : sql.substring ( 0 , 100 ) + ( sql . length > 100 ? "..." : "" ) ,
} ) ;
return result ;
} catch ( error ) {
const duration = performance . now ( ) - startTime ;
console . error ( "[AbsurdSQL] Run operation failed" , {
sql ,
params ,
error : error instanceof Error ? error.message : String ( error ) ,
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw error ;
}
}
// Note that the resulting array may be empty if there are no results from the query
/ * *
* Execute a query operation ( SELECT ) with logging
* /
async query ( sql : string , params : unknown [ ] = [ ] ) : Promise < QueryExecResult [ ] > {
await this . waitForInitialization ( ) ;
return this . queueOperation < QueryExecResult [ ] > ( "query" , sql , params ) ;
const startTime = performance . now ( ) ;
try {
await this . waitForInitialization ( ) ;
const result = await this . queueOperation < QueryExecResult [ ] > ( "query" , sql , params ) ;
const duration = performance . now ( ) - startTime ;
console . debug ( "[AbsurdSQL] Query operation completed" , {
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
resultCount : result.length ,
hasData : result.length > 0 && result [ 0 ] . values . length > 0 ,
sql : sql.substring ( 0 , 100 ) + ( sql . length > 100 ? "..." : "" ) ,
} ) ;
return result ;
} catch ( error ) {
const duration = performance . now ( ) - startTime ;
console . error ( "[AbsurdSQL] Query operation failed" , {
sql ,
params ,
error : error instanceof Error ? error.message : String ( error ) ,
duration : ` ${ duration . toFixed ( 2 ) } ms ` ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
throw error ;
}
}
/ * *
* Get diagnostic information about the database service state
* /
getDiagnostics ( ) : any {
const queueLength = this . operationQueue . length ;
const successRate = this . totalOperations > 0
? ( this . totalOperations - this . failedOperations ) / this . totalOperations * 100
: 100 ;
return {
initialized : this.initialized ,
isWriteDisabled : this.isWriteDisabled ,
writeFailureCount : this.writeFailureCount ,
maxWriteFailures : this.maxWriteFailures ,
queueLength ,
queueHighWaterMark : this.queueHighWaterMark ,
totalOperations : this.totalOperations ,
successfulOperations : this.totalOperations - this . failedOperations ,
failedOperations : this.failedOperations ,
successRate : parseFloat ( successRate . toFixed ( 2 ) ) ,
isProcessingQueue : this.isProcessingQueue ,
hasSharedArrayBuffer : typeof SharedArrayBuffer !== "undefined" ,
lastOperationTime : this.lastOperationTime ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
}
/ * *
* Set up global error handler for IndexedDB write failures
* /
private setupGlobalErrorHandler ( ) : void {
// Listen for unhandled promise rejections that indicate IndexedDB write failures
window . addEventListener ( 'unhandledrejection' , ( event ) = > {
const error = event . reason ;
const errorMessage = error instanceof Error ? error.message : String ( error ) ;
// Check if this is an IndexedDB write failure
if ( errorMessage . includes ( 'Fallback mode unable to write' ) ||
errorMessage . includes ( 'IndexedDB' ) ||
event . reason ? . stack ? . includes ( 'absurd-sql_dist_indexeddb-backend' ) ) {
this . writeFailureCount ++ ;
console . error ( "[AbsurdSQL] Global IndexedDB write failure detected" , {
error : errorMessage ,
failureCount : this.writeFailureCount ,
stack : error instanceof Error ? error.stack : undefined ,
} ) ;
// Disable writes and logging if too many failures
if ( this . writeFailureCount >= this . maxWriteFailures ) {
this . isWriteDisabled = true ;
disableDatabaseLogging ( ) ;
console . error ( "[AbsurdSQL] CRITICAL: Database writes and logging disabled due to persistent IndexedDB failures" ) ;
}
// Prevent the error from appearing in console
event . preventDefault ( ) ;
}
} ) ;
console . log ( "[AbsurdSQL] Global error handler installed for IndexedDB write failures" ) ;
}
}