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. 31
      src/main.electron.ts
  4. 193
      src/services/platforms/ElectronPlatformService.ts

3
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

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');
}

31
src/main.electron.ts

@ -54,15 +54,15 @@ const sqliteReady = new Promise<void>((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<void>((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'
// 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] Database closed successfully");
logger.info("[Main Electron] SQLite ready status sent, database connection maintained");
// 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");
// 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,

193
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<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() {
this.sqliteReadyPromise = new Promise<void>(async (resolve, reject) => {
try {
// Verify Electron API exposure first
await verifyElectronAPI();
logger.info('[ElectronPlatformService] Electron API verification successful');
this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
let retryCount = 0;
if (!window.electron?.ipcRenderer) {
logger.warn('[ElectronPlatformService] IPC renderer not available');
reject(new Error('IPC renderer not available'));
const cleanup = () => {
if (this.initializationTimeout) {
clearTimeout(this.initializationTimeout);
this.initializationTimeout = null;
}
};
const checkExistingReadiness = async (): Promise<boolean> => {
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
// Check if SQLite is already ready
if (await checkExistingReadiness()) {
this.isInitialized = true;
resolve();
return;
}
const timeout = setTimeout(() => {
reject(new Error('SQLite initialization timeout'));
}, 30000);
// 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;
}
window.electron.ipcRenderer.once('sqlite-ready', async () => {
clearTimeout(timeout);
logger.info('[ElectronPlatformService] Received SQLite ready signal');
try {
sqliteInitState.isInitializing = true;
try {
// Test SQLite operations after receiving ready signal
await testSQLiteOperations();
logger.info('[ElectronPlatformService] SQLite operations test successful');
// Verify Electron API exposure first
await verifyElectronAPI();
logger.info('[ElectronPlatformService] Electron API verification successful');
this.isInitialized = true;
resolve();
} catch (error) {
logger.error('[ElectronPlatformService] SQLite operations test failed:', error);
reject(error);
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();
});
}

Loading…
Cancel
Save