diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 225c9d8d..e06a63cb 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -38,7 +38,7 @@ interface SQLiteConnectionOptions { // Define valid channels for security const VALID_CHANNELS = { - send: ['toMain', 'sqlite-status'] as const, + send: ['toMain'] as const, receive: ['fromMain', 'sqlite-ready', 'database-status'] as const, invoke: [ 'sqlite-is-available', @@ -51,6 +51,7 @@ const VALID_CHANNELS = { 'sqlite-open', 'sqlite-close', 'sqlite-is-db-open', + 'sqlite-status', 'get-path', 'get-base-path' ] as const diff --git a/electron/src/rt/sqlite-init.ts b/electron/src/rt/sqlite-init.ts index bf382082..6ce1b1d4 100644 --- a/electron/src/rt/sqlite-init.ts +++ b/electron/src/rt/sqlite-init.ts @@ -868,5 +868,35 @@ export function setupSQLiteHandlers(): void { } }); + // Handler for SQLite status updates + registerHandler('sqlite-status', async (_event, status: { status: string; database: string; timestamp: number }) => { + logger.debug('SQLite status update:', status); + try { + startDatabaseOperation(); + if (!pluginState.instance) { + throw new SQLiteError('Plugin not initialized', 'sqlite-status'); + } + + // Verify database is still open + const isOpen = await pluginState.instance.isDBOpen({ database: status.database }); + if (!isOpen) { + throw new SQLiteError('Database not open', 'sqlite-status'); + } + + logger.info('SQLite status update processed:', { + status: status.status, + database: status.database, + timestamp: new Date(status.timestamp).toISOString() + }); + + return { success: true, isOpen }; + } catch (error) { + logger.error('SQLite status update failed:', error); + throw error; + } finally { + endDatabaseOperation(); + } + }); + logger.info('SQLite IPC handlers setup complete'); } \ No newline at end of file diff --git a/src/main.electron.ts b/src/main.electron.ts index 9614c8a2..1392c7b2 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -54,15 +54,15 @@ const sqliteReady = new Promise((resolve, reject) => { // Set timeout for this attempt initializationTimeout = setTimeout(() => { - if (retryCount < SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) { + if (retryCount < 3) { // Use same retry count as ElectronPlatformService retryCount++; logger.warn(`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`); - setTimeout(attemptInitialization, SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS); + setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService } else { logger.error("[Main Electron] SQLite initialization failed after all retries"); reject(new Error("SQLite initialization timeout after all retries")); } - }, SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS); + }, 10000); // Use same timeout as ElectronPlatformService // Wait for electron bridge to be available const checkElectronBridge = () => { @@ -143,27 +143,24 @@ const sqliteReady = new Promise((resolve, reject) => { logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query"); const testQuery = await ipcRenderer.invoke('sqlite-query', { database: 'timesafari', - statement: 'SELECT * FROM secret;' + statement: 'SELECT 1 as test;' // Safe test query }) as SQLiteQueryResult; logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", { hasResults: Boolean(testQuery?.values), - resultCount: testQuery?.values?.length, - results: testQuery?.values + resultCount: testQuery?.values?.length }); - // Close the database - logger.debug("[Main Electron] [IPC:sqlite-close] Closing database"); - await ipcRenderer.invoke('sqlite-close', { - database: 'timesafari' - }); - logger.info("[Main Electron] [IPC:sqlite-close] Database closed successfully"); - - // Close the connection - logger.debug("[Main Electron] [IPC:sqlite-close-connection] Closing database connection"); - await ipcRenderer.invoke('sqlite-close-connection', { - database: 'timesafari' + // Signal that SQLite is ready - database stays open + logger.debug("[Main Electron] [IPC:sqlite-status] Sending SQLite ready status"); + await ipcRenderer.invoke('sqlite-status', { + status: 'ready', + database: 'timesafari', + timestamp: Date.now() }); - logger.info("[Main Electron] [IPC:sqlite-close-connection] Database connection closed successfully"); + logger.info("[Main Electron] SQLite ready status sent, database connection maintained"); + + // Remove the close operations - database stays open for component use + // Database will be closed during app shutdown } catch (error) { logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", { error, diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index fd7713b9..0e58d5ee 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -44,6 +44,26 @@ export interface SQLiteQueryResult { changes?: { changes: number; lastId?: number }; } +/** + * Shared SQLite initialization state + * Used to coordinate initialization between main and service + * + * @author Matthew Raymer + */ +export interface SQLiteInitState { + isReady: boolean; + isInitializing: boolean; + error?: Error; + lastReadyCheck?: number; +} + +// Singleton instance for shared state +const sqliteInitState: SQLiteInitState = { + isReady: false, + isInitializing: false, + lastReadyCheck: 0 +}; + /** * Platform service implementation for Electron (desktop) platform. * Provides native desktop functionality through Electron and Capacitor plugins for: @@ -51,6 +71,8 @@ export interface SQLiteQueryResult { * - Camera integration (TODO) * - SQLite database operations * - System-level features (TODO) + * + * @author Matthew Raymer */ export class ElectronPlatformService implements PlatformService { private sqlite: any; @@ -58,53 +80,152 @@ export class ElectronPlatformService implements PlatformService { private isInitialized = false; private dbFatalError = false; private sqliteReadyPromise: Promise | null = null; + private initializationTimeout: NodeJS.Timeout | null = null; + + // SQLite initialization configuration + private static readonly SQLITE_CONFIG = { + INITIALIZATION: { + TIMEOUT_MS: 10000, // 10 seconds for initial setup + RETRY_ATTEMPTS: 3, + RETRY_DELAY_MS: 1000, + READY_CHECK_INTERVAL_MS: 100 // How often to check if SQLite is already ready + } + }; constructor() { - this.sqliteReadyPromise = new Promise(async (resolve, reject) => { - try { - // Verify Electron API exposure first - await verifyElectronAPI(); - logger.info('[ElectronPlatformService] Electron API verification successful'); + this.sqliteReadyPromise = new Promise((resolve, reject) => { + let retryCount = 0; + + const cleanup = () => { + if (this.initializationTimeout) { + clearTimeout(this.initializationTimeout); + this.initializationTimeout = null; + } + }; - if (!window.electron?.ipcRenderer) { - logger.warn('[ElectronPlatformService] IPC renderer not available'); - reject(new Error('IPC renderer not available')); - return; + const checkExistingReadiness = async (): Promise => { + try { + if (!window.electron?.ipcRenderer) { + return false; + } + + // Check if SQLite is already 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 + }); + + if (isOpen) { + logger.info('[ElectronPlatformService] SQLite is already ready and database is open'); + sqliteInitState.isReady = true; + sqliteInitState.isInitializing = false; + sqliteInitState.lastReadyCheck = Date.now(); + return true; + } + + return false; + } catch (error) { + logger.warn('[ElectronPlatformService] Error checking existing readiness:', error); + return false; } + }; + + const attemptInitialization = async () => { + cleanup(); // Clear any existing timeout - const timeout = setTimeout(() => { - reject(new Error('SQLite initialization timeout')); - }, 30000); + // Check if SQLite is already ready + if (await checkExistingReadiness()) { + this.isInitialized = true; + resolve(); + return; + } - window.electron.ipcRenderer.once('sqlite-ready', async () => { - clearTimeout(timeout); - logger.info('[ElectronPlatformService] Received SQLite ready signal'); + // 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); + return; + } + + try { + sqliteInitState.isInitializing = true; - try { - // Test SQLite operations after receiving ready signal - await testSQLiteOperations(); - logger.info('[ElectronPlatformService] SQLite operations test successful'); - - this.isInitialized = true; - resolve(); - } catch (error) { - logger.error('[ElectronPlatformService] SQLite operations test failed:', error); - reject(error); + // Verify Electron API exposure first + await verifyElectronAPI(); + logger.info('[ElectronPlatformService] Electron API verification successful'); + + if (!window.electron?.ipcRenderer) { + logger.warn('[ElectronPlatformService] IPC renderer not available'); + reject(new Error('IPC renderer not available')); + return; } - }); - window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => { - clearTimeout(timeout); - const status = args[0] as { status: string; error?: string }; - if (status.status === 'error') { - this.dbFatalError = true; - reject(new Error(status.error || 'Database initialization failed')); - } - }); - } catch (error) { - logger.error('[ElectronPlatformService] Initialization failed:', error); - reject(error); - } + // 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 + window.electron.ipcRenderer.once('sqlite-ready', async () => { + cleanup(); + logger.info('[ElectronPlatformService] Received SQLite ready signal'); + + try { + // Test SQLite operations after receiving ready signal + await testSQLiteOperations(); + logger.info('[ElectronPlatformService] SQLite operations test successful'); + + this.isInitialized = true; + sqliteInitState.isReady = true; + sqliteInitState.isInitializing = false; + sqliteInitState.lastReadyCheck = Date.now(); + resolve(); + } catch (error) { + sqliteInitState.error = error as Error; + sqliteInitState.isInitializing = false; + logger.error('[ElectronPlatformService] SQLite operations test failed:', error); + reject(error); + } + }); + + // Set up error handler + 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); + } + }); + + } catch (error) { + cleanup(); + sqliteInitState.error = error as Error; + sqliteInitState.isInitializing = false; + logger.error('[ElectronPlatformService] Initialization failed:', error); + reject(error); + } + }; + + // Start first initialization attempt + attemptInitialization(); }); }