Browse Source

fix(db): resolve SQLite channel and initialization issues

- Add sqlite-status to valid IPC channels
- Fix SQLite ready signal handling
- Improve database status tracking
- Add proper error handling for status updates
- Keep database connection open during initialization

Technical Details:
- Added sqlite-status to VALID_CHANNELS.invoke list
- Implemented sqlite-status handler with proper verification
- Added database open state verification
- Improved error handling and logging
- Fixed premature database closing

Testing Notes:
- Verify SQLite ready signal is received correctly
- Confirm database stays open after initialization
- Check status updates are processed properly
- Verify error handling for invalid states

Security:
- Validates all IPC channels
- Verifies database state before operations
- Maintains proper connection lifecycle
- Implements proper error boundaries

Author: Matthew Raymer
sql-absurd-sql-further
Matthew Raymer 4 days ago
parent
commit
409de21fc4
  1. 3
      electron/src/preload.ts
  2. 30
      electron/src/rt/sqlite-init.ts
  3. 33
      src/main.electron.ts
  4. 197
      src/services/platforms/ElectronPlatformService.ts

3
electron/src/preload.ts

@ -38,7 +38,7 @@ interface SQLiteConnectionOptions {
// Define valid channels for security // Define valid channels for security
const VALID_CHANNELS = { const VALID_CHANNELS = {
send: ['toMain', 'sqlite-status'] as const, send: ['toMain'] as const,
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const, receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
invoke: [ invoke: [
'sqlite-is-available', 'sqlite-is-available',
@ -51,6 +51,7 @@ const VALID_CHANNELS = {
'sqlite-open', 'sqlite-open',
'sqlite-close', 'sqlite-close',
'sqlite-is-db-open', 'sqlite-is-db-open',
'sqlite-status',
'get-path', 'get-path',
'get-base-path' 'get-base-path'
] as const ] as const

30
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'); logger.info('SQLite IPC handlers setup complete');
} }

33
src/main.electron.ts

@ -54,15 +54,15 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
// Set timeout for this attempt // Set timeout for this attempt
initializationTimeout = setTimeout(() => { initializationTimeout = setTimeout(() => {
if (retryCount < SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) { if (retryCount < 3) { // Use same retry count as ElectronPlatformService
retryCount++; retryCount++;
logger.warn(`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`); 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 { } else {
logger.error("[Main Electron] SQLite initialization failed after all retries"); logger.error("[Main Electron] SQLite initialization failed after all retries");
reject(new Error("SQLite initialization timeout 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 // Wait for electron bridge to be available
const checkElectronBridge = () => { const checkElectronBridge = () => {
@ -143,27 +143,24 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query"); logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query");
const testQuery = await ipcRenderer.invoke('sqlite-query', { const testQuery = await ipcRenderer.invoke('sqlite-query', {
database: 'timesafari', database: 'timesafari',
statement: 'SELECT * FROM secret;' statement: 'SELECT 1 as test;' // Safe test query
}) as SQLiteQueryResult; }) as SQLiteQueryResult;
logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", { logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", {
hasResults: Boolean(testQuery?.values), hasResults: Boolean(testQuery?.values),
resultCount: testQuery?.values?.length, resultCount: testQuery?.values?.length
results: testQuery?.values
}); });
// Close the database // Signal that SQLite is ready - database stays open
logger.debug("[Main Electron] [IPC:sqlite-close] Closing database"); logger.debug("[Main Electron] [IPC:sqlite-status] Sending SQLite ready status");
await ipcRenderer.invoke('sqlite-close', { await ipcRenderer.invoke('sqlite-status', {
database: 'timesafari' status: 'ready',
}); database: 'timesafari',
logger.info("[Main Electron] [IPC:sqlite-close] Database closed successfully"); timestamp: Date.now()
// Close the connection
logger.debug("[Main Electron] [IPC:sqlite-close-connection] Closing database connection");
await ipcRenderer.invoke('sqlite-close-connection', {
database: 'timesafari'
}); });
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) { } catch (error) {
logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", { logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", {
error, error,

197
src/services/platforms/ElectronPlatformService.ts

@ -44,6 +44,26 @@ export interface SQLiteQueryResult {
changes?: { changes: number; lastId?: number }; 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. * Platform service implementation for Electron (desktop) platform.
* Provides native desktop functionality through Electron and Capacitor plugins for: * Provides native desktop functionality through Electron and Capacitor plugins for:
@ -51,6 +71,8 @@ export interface SQLiteQueryResult {
* - Camera integration (TODO) * - Camera integration (TODO)
* - SQLite database operations * - SQLite database operations
* - System-level features (TODO) * - System-level features (TODO)
*
* @author Matthew Raymer
*/ */
export class ElectronPlatformService implements PlatformService { export class ElectronPlatformService implements PlatformService {
private sqlite: any; private sqlite: any;
@ -58,53 +80,152 @@ export class ElectronPlatformService implements PlatformService {
private isInitialized = false; private isInitialized = false;
private dbFatalError = false; private dbFatalError = false;
private sqliteReadyPromise: Promise<void> | null = null; private sqliteReadyPromise: Promise<void> | 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() { constructor() {
this.sqliteReadyPromise = new Promise<void>(async (resolve, reject) => { this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
try { let retryCount = 0;
// Verify Electron API exposure first
await verifyElectronAPI(); const cleanup = () => {
logger.info('[ElectronPlatformService] Electron API verification successful'); if (this.initializationTimeout) {
clearTimeout(this.initializationTimeout);
this.initializationTimeout = null;
}
};
if (!window.electron?.ipcRenderer) { const checkExistingReadiness = async (): Promise<boolean> => {
logger.warn('[ElectronPlatformService] IPC renderer not available'); try {
reject(new Error('IPC renderer not available')); if (!window.electron?.ipcRenderer) {
return; 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(() => { // Check if SQLite is already ready
reject(new Error('SQLite initialization timeout')); if (await checkExistingReadiness()) {
}, 30000); this.isInitialized = true;
resolve();
return;
}
window.electron.ipcRenderer.once('sqlite-ready', async () => { // If someone else is initializing, wait for them
clearTimeout(timeout); if (sqliteInitState.isInitializing) {
logger.info('[ElectronPlatformService] Received SQLite ready signal'); logger.info('[ElectronPlatformService] Another initialization in progress, waiting...');
setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.READY_CHECK_INTERVAL_MS);
return;
}
try {
sqliteInitState.isInitializing = true;
try { // Verify Electron API exposure first
// Test SQLite operations after receiving ready signal await verifyElectronAPI();
await testSQLiteOperations(); logger.info('[ElectronPlatformService] Electron API verification successful');
logger.info('[ElectronPlatformService] SQLite operations test successful');
if (!window.electron?.ipcRenderer) {
this.isInitialized = true; logger.warn('[ElectronPlatformService] IPC renderer not available');
resolve(); reject(new Error('IPC renderer not available'));
} catch (error) { return;
logger.error('[ElectronPlatformService] SQLite operations test failed:', error);
reject(error);
} }
});
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => { // Set timeout for this attempt
clearTimeout(timeout); this.initializationTimeout = setTimeout(() => {
const status = args[0] as { status: string; error?: string }; if (retryCount < ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) {
if (status.status === 'error') { retryCount++;
this.dbFatalError = true; logger.warn(`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`);
reject(new Error(status.error || 'Database initialization failed')); setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS);
} } else {
}); cleanup();
} catch (error) { sqliteInitState.isInitializing = false;
logger.error('[ElectronPlatformService] Initialization failed:', error); sqliteInitState.error = new Error('SQLite initialization timeout after all retries');
reject(error); 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();
}); });
} }

Loading…
Cancel
Save