diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index b519c6a1..357c69c4 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -90,14 +90,19 @@ export class ElectronPlatformService implements PlatformService { private dbFatalError = false; private sqliteReadyPromise: Promise | null = null; private initializationTimeout: NodeJS.Timeout | null = null; + private isConnectionOpen = false; + private operationQueue: Promise = Promise.resolve(); + private queueLock = false; + private connectionState: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected'; + private connectionPromise: Promise | null = null; // SQLite initialization configuration private static readonly SQLITE_CONFIG = { INITIALIZATION: { - TIMEOUT_MS: 1000, // with retries, stay under 5 seconds + TIMEOUT_MS: 5000, // Increase timeout to 5 seconds RETRY_ATTEMPTS: 3, RETRY_DELAY_MS: 1000, - READY_CHECK_INTERVAL_MS: 100, // How often to check if SQLite is already ready + READY_CHECK_INTERVAL_MS: 100, }, }; @@ -119,25 +124,18 @@ export class ElectronPlatformService implements PlatformService { } // Check if SQLite is already available - const isAvailable = await window.electron.ipcRenderer.invoke( - "sqlite-is-available", - ); + const isAvailable = await window.electron.ipcRenderer.invoke("sqlite-is-available"); if (!isAvailable) { return false; } // Check if database is already open - const isOpen = await window.electron.ipcRenderer.invoke( - "sqlite-is-db-open", - { - database: this.dbName, - }, - ); + const isOpen = await window.electron.ipcRenderer.invoke("sqlite-is-db-open", { + database: this.dbName, + }); if (isOpen) { - logger.info( - "[ElectronPlatformService] SQLite is already ready and database is open", - ); + logger.info("[ElectronPlatformService] SQLite is already ready and database is open"); sqliteInitState.isReady = true; sqliteInitState.isInitializing = false; sqliteInitState.lastReadyCheck = Date.now(); @@ -146,16 +144,13 @@ export class ElectronPlatformService implements PlatformService { return false; } catch (error) { - logger.warn( - "[ElectronPlatformService] Error checking existing readiness:", - error, - ); + logger.warn("[ElectronPlatformService] Error checking existing readiness:", error); return false; } }; const attemptInitialization = async () => { - cleanup(); // Clear any existing timeout + cleanup(); // Check if SQLite is already ready if (await checkExistingReadiness()) { @@ -166,14 +161,8 @@ export class ElectronPlatformService implements PlatformService { // If someone else is initializing, wait for them if (sqliteInitState.isInitializing) { - logger.info( - "[ElectronPlatformService] Another initialization in progress, waiting...", - ); - setTimeout( - attemptInitialization, - ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION - .READY_CHECK_INTERVAL_MS, - ); + logger.info("[ElectronPlatformService] Another initialization in progress, waiting..."); + setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.READY_CHECK_INTERVAL_MS); return; } @@ -182,9 +171,7 @@ export class ElectronPlatformService implements PlatformService { // Verify Electron API exposure first await verifyElectronAPI(); - logger.info( - "[ElectronPlatformService] Electron API verification successful", - ); + logger.info("[ElectronPlatformService] Electron API verification successful"); if (!window.electron?.ipcRenderer) { logger.warn("[ElectronPlatformService] IPC renderer not available"); @@ -192,48 +179,15 @@ export class ElectronPlatformService implements PlatformService { return; } - // Set timeout for this attempt - this.initializationTimeout = setTimeout(() => { - if ( - retryCount < - ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION - .RETRY_ATTEMPTS - ) { - retryCount++; - logger.warn( - `[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`, - ); - setTimeout( - attemptInitialization, - ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION - .RETRY_DELAY_MS, - ); - } else { - cleanup(); - sqliteInitState.isInitializing = false; - sqliteInitState.error = new Error( - "SQLite initialization timeout after all retries", - ); - logger.error( - "[ElectronPlatformService] SQLite initialization failed after all retries", - ); - reject(sqliteInitState.error); - } - }, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS); - - // Set up ready signal handler + // Set up ready signal handler BEFORE setting timeout window.electron.ipcRenderer.once("sqlite-ready", async () => { cleanup(); - logger.info( - "[ElectronPlatformService] Received SQLite ready signal", - ); + logger.info("[ElectronPlatformService] Received SQLite ready signal"); try { // Test SQLite operations after receiving ready signal await testSQLiteOperations(); - logger.info( - "[ElectronPlatformService] SQLite operations test successful", - ); + logger.info("[ElectronPlatformService] SQLite operations test successful"); this.isInitialized = true; sqliteInitState.isReady = true; @@ -243,38 +197,43 @@ export class ElectronPlatformService implements PlatformService { } catch (error) { sqliteInitState.error = error as Error; sqliteInitState.isInitializing = false; - logger.error( - "[ElectronPlatformService] SQLite operations test failed:", - error, - ); + logger.error("[ElectronPlatformService] SQLite operations test failed:", error); reject(error); } }); // Set up error handler - window.electron.ipcRenderer.once( - "database-status", - (...args: unknown[]) => { + window.electron.ipcRenderer.once("database-status", (...args: unknown[]) => { + cleanup(); + const status = args[0] as { status: string; error?: string }; + if (status.status === "error") { + this.dbFatalError = true; + sqliteInitState.error = new Error(status.error || "Database initialization failed"); + sqliteInitState.isInitializing = false; + reject(sqliteInitState.error); + } + }); + + // Set timeout for this attempt AFTER setting up handlers + this.initializationTimeout = setTimeout(() => { + if (retryCount < ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) { + retryCount++; + logger.warn(`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`); + setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS); + } else { cleanup(); - const status = args[0] as { status: string; error?: string }; - if (status.status === "error") { - this.dbFatalError = true; - sqliteInitState.error = new Error( - status.error || "Database initialization failed", - ); - sqliteInitState.isInitializing = false; - reject(sqliteInitState.error); - } - }, - ); + sqliteInitState.isInitializing = false; + sqliteInitState.error = new Error("SQLite initialization timeout after all retries"); + logger.error("[ElectronPlatformService] SQLite initialization failed after all retries"); + reject(sqliteInitState.error); + } + }, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS); + } catch (error) { cleanup(); sqliteInitState.error = error as Error; sqliteInitState.isInitializing = false; - logger.error( - "[ElectronPlatformService] Initialization failed:", - error, - ); + logger.error("[ElectronPlatformService] Initialization failed:", error); reject(error); } }; @@ -295,17 +254,27 @@ export class ElectronPlatformService implements PlatformService { // Use IPC bridge with specific methods this.sqlite = { createConnection: async (options) => { - await window.electron.ipcRenderer.invoke('sqlite-create-connection', options); + await window.electron.ipcRenderer.invoke('sqlite-create-connection', { + ...options, + database: this.dbName + }); }, query: async (options) => { - return await window.electron.ipcRenderer.invoke('sqlite-query', options); + return await window.electron.ipcRenderer.invoke('sqlite-query', { + ...options, + database: this.dbName + }); }, run: async (options) => { - return await window.electron.ipcRenderer.invoke('sqlite-run', options); + return await window.electron.ipcRenderer.invoke('sqlite-run', { + ...options, + database: this.dbName + }); }, execute: async (options) => { await window.electron.ipcRenderer.invoke('sqlite-execute', { - database: options.database, + ...options, + database: this.dbName, statements: [{ statement: options.statements }] }); } @@ -559,112 +528,160 @@ export class ElectronPlatformService implements PlatformService { throw new Error("Not implemented"); } - /** - * Executes a database query with proper connection lifecycle management. - * Opens connection, executes query, and ensures proper cleanup. - * - * @param sql - SQL query to execute - * @param params - Optional parameters for the query - * @returns Promise resolving to query results - * @throws Error if database operations fail - */ - async dbQuery( - sql: string, - params: unknown[] = [], - ): Promise> { - logger.debug( - "[ElectronPlatformService] [dbQuery] TEMPORARY TEST: Returning empty result for query:", - { - sql, - params, - timestamp: new Date().toISOString(), - }, - ); + private async enqueueOperation(operation: () => Promise): Promise { + // Wait for any existing operations to complete + await this.operationQueue; - // TEMPORARY TEST: Return empty result - return { - columns: [], - values: [], - }; + // Create a new promise for this operation + const operationPromise = (async () => { + try { + // Acquire lock + while (this.queueLock) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + this.queueLock = true; - // Original implementation commented out for testing - /* - if (this.dbFatalError) { - throw new Error("Database is in a fatal error state. Please restart the app."); + // Execute operation + return await operation(); + } finally { + // Release lock + this.queueLock = false; + } + })(); + + // Update the queue + this.operationQueue = operationPromise; + + return operationPromise; + } + + private async getConnection(): Promise { + // If we already have a connection promise, return it + if (this.connectionPromise) { + return this.connectionPromise; } - if (!window.electron?.ipcRenderer) { - throw new Error("IPC renderer not available"); + // If we're already connected, return immediately + if (this.connectionState === 'connected') { + return Promise.resolve(); } - try { - // Check SQLite availability first - const isAvailable = await window.electron.ipcRenderer.invoke('sqlite-is-available'); - if (!isAvailable) { - throw new Error('[ElectronPlatformService] [dbQuery] SQLite is not available'); + // Create new connection promise + this.connectionPromise = (async () => { + try { + this.connectionState = 'connecting'; + + // Wait for any existing operations + await this.operationQueue; + + // Create connection + await window.electron!.ipcRenderer.invoke('sqlite-create-connection', { + database: this.dbName, + encrypted: false, + mode: "no-encryption", + }); + logger.debug("[ElectronPlatformService] Database connection created"); + + // Open database + await window.electron!.ipcRenderer.invoke('sqlite-open', { + database: this.dbName + }); + logger.debug("[ElectronPlatformService] Database opened"); + + // Verify database is open + const isOpen = await window.electron!.ipcRenderer.invoke('sqlite-is-db-open', { + database: this.dbName + }); + if (!isOpen) { + throw new Error('[ElectronPlatformService] Database failed to open'); + } + + this.connectionState = 'connected'; + this.isConnectionOpen = true; + } catch (error) { + this.connectionState = 'error'; + this.connectionPromise = null; + throw error; } - logger.debug("[ElectronPlatformService] [dbQuery] SQLite is available"); + })(); - // Create database connection - await window.electron.ipcRenderer.invoke('sqlite-create-connection', { - database: this.dbName, - version: 1 - }); - logger.debug("[ElectronPlatformService] [dbQuery] Database connection created"); + return this.connectionPromise; + } + + private async releaseConnection(): Promise { + if (this.connectionState !== 'connected') { + return; + } - // Open database - await window.electron.ipcRenderer.invoke('sqlite-open', { + try { + // Close database + await window.electron!.ipcRenderer.invoke('sqlite-close', { database: this.dbName }); - logger.debug("[ElectronPlatformService] [dbQuery] Database opened"); + logger.debug("[ElectronPlatformService] Database closed"); - // Verify database is open - const isOpen = await window.electron.ipcRenderer.invoke('sqlite-is-db-open', { + // Close connection + await window.electron!.ipcRenderer.invoke('sqlite-close-connection', { database: this.dbName }); - if (!isOpen) { - throw new Error('[ElectronPlatformService] [dbQuery] Database failed to open'); - } + logger.debug("[ElectronPlatformService] Database connection closed"); - // Execute query - const result = await window.electron.ipcRenderer.invoke('sqlite-query', { - database: this.dbName, - statement: sql, - values: params - }) as SQLiteQueryResult; - logger.debug("[ElectronPlatformService] [dbQuery] Query executed successfully"); - - // Process results - const columns = result.values?.[0] ? Object.keys(result.values[0]) : []; - const processedResult = { - columns, - values: (result.values || []).map((row: Record) => row as T) - }; - - return processedResult; + this.connectionState = 'disconnected'; + this.isConnectionOpen = false; } catch (error) { - logger.error("[ElectronPlatformService] [dbQuery] Query failed:", error); - throw error; + logger.error("[ElectronPlatformService] Failed to close database:", error); + this.connectionState = 'error'; } finally { - // Ensure proper cleanup + this.connectionPromise = null; + } + } + + /** + * Executes a database query with proper connection lifecycle management. + * Opens connection, executes query, and ensures proper cleanup. + * + * @param sql - SQL query to execute + * @param params - Optional parameters for the query + * @returns Promise resolving to query results + * @throws Error if database operations fail + */ + async dbQuery( + sql: string, + params: unknown[] = [], + ): Promise> { + if (this.dbFatalError) { + throw new Error("Database is in a fatal error state. Please restart the app."); + } + + return this.enqueueOperation(async () => { try { - // Close database - await window.electron.ipcRenderer.invoke('sqlite-close', { - database: this.dbName - }); - logger.debug("[ElectronPlatformService] [dbQuery] Database closed"); + // Get connection (will wait for existing connection if any) + await this.getConnection(); - // Close connection - await window.electron.ipcRenderer.invoke('sqlite-close-connection', { - database: this.dbName - }); - logger.debug("[ElectronPlatformService] [dbQuery] Database connection closed"); - } catch (closeError) { - logger.error("[ElectronPlatformService] [dbQuery] Failed to cleanup database:", closeError); - // Don't throw here - we want to preserve the original error if any + // Execute query + const result = await window.electron!.ipcRenderer.invoke('sqlite-query', { + database: this.dbName, + statement: sql, + values: params + }) as SQLiteQueryResult; + logger.debug("[ElectronPlatformService] [dbQuery] Query executed successfully"); + + // Process results + const columns = result.values?.[0] ? Object.keys(result.values[0]) : []; + const processedResult = { + columns, + values: (result.values || []).map((row: Record) => row as T) + }; + + return processedResult; + } catch (error) { + logger.error("[ElectronPlatformService] [dbQuery] Query failed:", error); + throw error; + } finally { + // Release connection after query + await this.releaseConnection(); } - } - */ + }); } /**