fix(electron): consolidate SQLite initialization and IPC handling

- Consolidate preload script with secure IPC bridge and channel validation
- Ensure single initialization path in main process
- Add robust error handling and user feedback
- Fix race conditions in window creation and SQLite ready signal

Current state:
- SQLite initializes successfully in main process
- IPC bridge is established and events are transmitted
- Window creation and loading sequence is correct
- Renderer receives ready signal and mounts app
- Database operations still fail in renderer due to connection issues

Known issues:
- SQLite proxy not properly initialized in renderer
- Database connection not established in renderer
- Error logging attempts to use database before ready
- Connection state management needs improvement

This commit represents a stable point where IPC communication
is working but database operations need to be fixed.
This commit is contained in:
Matthew Raymer
2025-06-03 02:52:17 +00:00
parent 66929d9b14
commit 91a1c05473
6 changed files with 275 additions and 109 deletions

View File

@@ -40,68 +40,79 @@ if (electronIsDev) {
// Run Application
(async () => {
try {
// Wait for electron app to be ready.
// Wait for electron app to be ready first
await app.whenReady();
// Security - Set Content-Security-Policy based on whether or not we are in dev mode.
console.log('[Electron Main Process] App is ready');
// Initialize SQLite plugin and handlers BEFORE creating any windows
console.log('[Electron Main Process] Initializing SQLite...');
setupSQLiteHandlers();
await initializeSQLite();
console.log('[Electron Main Process] SQLite initialization complete');
// Security - Set Content-Security-Policy
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
// Initialize our app, build windows, and load content first
// Initialize our app and create window
console.log('[Electron Main Process] Starting app initialization...');
await myCapacitorApp.init();
console.log('[Electron Main Process] App initialization complete');
// Get the main window and wait for it to be ready
// Get the main window
const mainWindow = myCapacitorApp.getMainWindow();
if (!mainWindow) {
throw new Error('Main window not available after app initialization');
}
// Wait for window to be ready
// Wait for window to be ready and loaded
await new Promise<void>((resolve) => {
if (mainWindow.isVisible()) {
resolve();
} else {
mainWindow.once('show', () => resolve());
}
const handleReady = () => {
console.log('[Electron Main Process] Window ready to show');
mainWindow.show();
// Wait for window to finish loading
mainWindow.webContents.once('did-finish-load', () => {
console.log('[Electron Main Process] Window finished loading');
// Send SQLite ready signal after window is fully loaded
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('sqlite-ready');
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
} else {
console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
}
resolve();
});
};
// Always use the event since isReadyToShow is not reliable
mainWindow.once('ready-to-show', handleReady);
});
// Now initialize SQLite after window is ready
console.log('[Electron Main Process] Starting SQLite initialization...');
try {
// Register handlers first to prevent "no handler" errors
setupSQLiteHandlers();
console.log('[Electron Main Process] SQLite handlers registered');
// Then initialize the plugin
await initializeSQLite();
console.log('[Electron Main Process] SQLite plugin initialized successfully');
// Send SQLite ready signal since window is ready
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('sqlite-ready');
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
} else {
console.warn('[Electron Main Process] Could not send SQLite ready signal - window was destroyed');
}
} catch (error) {
console.error('[Electron Main Process] Failed to initialize SQLite:', error);
// Notify renderer about database status
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('database-status', {
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
// Don't proceed with app initialization if SQLite fails
throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Check for updates if we are in a packaged app.
// Check for updates if we are in a packaged app
if (!electronIsDev) {
console.log('[Electron Main Process] Checking for updates...');
autoUpdater.checkForUpdatesAndNotify();
}
// Handle window close
mainWindow.on('closed', () => {
console.log('[Electron Main Process] Main window closed');
});
// Handle window close request
mainWindow.on('close', (event) => {
console.log('[Electron Main Process] Window close requested');
if (mainWindow.webContents.isLoading()) {
event.preventDefault();
console.log('[Electron Main Process] Deferring window close due to loading state');
mainWindow.webContents.once('did-finish-load', () => {
mainWindow.close();
});
}
});
} catch (error) {
console.error('[Electron Main Process] Fatal error during initialization:', error);
app.quit();

View File

@@ -28,50 +28,81 @@ interface SQLiteConnectionOptions {
[key: string]: unknown; // Allow other properties
}
// Create a proxy for the CapacitorSQLite plugin
// Define valid channels for security
const VALID_CHANNELS = {
send: ['toMain', 'sqlite-status'],
receive: ['fromMain', 'sqlite-ready', 'database-status'],
invoke: ['sqlite-is-available', 'sqlite-echo', 'sqlite-create-connection', 'sqlite-execute', 'sqlite-query', 'sqlite-run', 'sqlite-close-connection', 'get-path', 'get-base-path']
};
// Create a secure IPC bridge
const createSecureIPCBridge = () => {
return {
send: (channel: string, data: unknown) => {
if (VALID_CHANNELS.send.includes(channel)) {
ipcRenderer.send(channel, data);
} else {
logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`);
}
},
receive: (channel: string, func: (...args: unknown[]) => void) => {
if (VALID_CHANNELS.receive.includes(channel)) {
ipcRenderer.on(channel, (_event, ...args) => func(...args));
} else {
logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`);
}
},
once: (channel: string, func: (...args: unknown[]) => void) => {
if (VALID_CHANNELS.receive.includes(channel)) {
ipcRenderer.once(channel, (_event, ...args) => func(...args));
} else {
logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`);
}
},
invoke: async (channel: string, ...args: unknown[]) => {
if (VALID_CHANNELS.invoke.includes(channel)) {
return await ipcRenderer.invoke(channel, ...args);
} else {
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
throw new Error(`Invalid channel: ${channel}`);
}
}
};
};
// Create SQLite proxy with retry logic
const createSQLiteProxy = () => {
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second
const RETRY_DELAY = 1000;
const withRetry = async <T>(operation: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T> => {
let lastError: Error | null = null;
const withRetry = async (operation: (...args: unknown[]) => Promise<unknown>, ...args: unknown[]) => {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
lastError = error;
if (attempt < MAX_RETRIES) {
logger.warn(`[CapacitorSQLite] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
logger.warn(`[Preload] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
}
}
}
throw new Error(`[CapacitorSQLite] SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
};
const wrapOperation = (method: string) => {
return async (...args: unknown[]): Promise<unknown> => {
return async (...args: unknown[]) => {
try {
// For createConnection, ensure readOnly is false
if (method === 'create-connection') {
const options = args[0] as SQLiteConnectionOptions;
if (options && typeof options === 'object') {
// Set readOnly to false and ensure mode is rwc
options.readOnly = false;
options.mode = 'rwc';
// Remove any lowercase readonly property if it exists
delete options.readonly;
}
}
return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args);
const handlerName = `sqlite-${method}`;
return await withRetry(ipcRenderer.invoke, handlerName, ...args);
} catch (error) {
logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, error);
throw new Error(`[CapacitorSQLite] Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
logger.error(`[Preload] SQLite ${method} failed:`, error);
throw error;
}
};
};
// Create a proxy that matches the CapacitorSQLite interface
return {
echo: wrapOperation('echo'),
createConnection: wrapOperation('create-connection'),
@@ -80,24 +111,25 @@ const createSQLiteProxy = () => {
query: wrapOperation('query'),
run: wrapOperation('run'),
isAvailable: wrapOperation('is-available'),
getPlatform: () => Promise.resolve('electron'),
// Add other methods as needed
getPlatform: () => Promise.resolve('electron')
};
};
// Expose the Electron IPC API
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
on: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
once: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.once(channel, (event, ...args) => func(...args)),
send: (channel: string, data: unknown) => ipcRenderer.send(channel, data),
invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args),
},
// Add other APIs as needed
});
try {
// Expose the secure IPC bridge and SQLite proxy
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: createSecureIPCBridge(),
sqlite: createSQLiteProxy(),
env: {
platform: 'electron',
isDev: process.env.NODE_ENV === 'development'
}
});
// Expose CapacitorSQLite proxy as before
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
} catch (error) {
logger.error('[Preload] Failed to initialize IPC bridge:', error);
}
// Log startup
logger.log('[CapacitorSQLite] Preload script starting...');

View File

@@ -531,6 +531,7 @@ export function setupSQLiteHandlers(): void {
'sqlite-create-connection',
'sqlite-execute',
'sqlite-query',
'sqlite-run',
'sqlite-close-connection',
'sqlite-get-error'
];
@@ -578,7 +579,69 @@ export function setupSQLiteHandlers(): void {
}
});
// ... other handlers ...
// Add the sqlite-run handler
ipcMain.handle('sqlite-run', async (_event, options) => {
try {
if (!await verifyPluginState()) {
throw new SQLiteError('Plugin not available', 'sqlite-run');
}
const { statement, values } = options;
if (!statement) {
throw new SQLiteError('No statement provided', 'sqlite-run');
}
// Add database name to options
const runOptions = {
database: 'timesafari', // Use the same database name as in initialization
statement,
values: values || []
};
logger.debug('Running SQL statement:', runOptions);
const result = await pluginState.instance.run(runOptions);
if (!result) {
throw new SQLiteError('Run operation returned no result', 'sqlite-run');
}
return result;
} catch (error) {
throw handleError(error, 'sqlite-run');
}
});
// Add the sqlite-query handler
ipcMain.handle('sqlite-query', async (_event, options) => {
try {
if (!await verifyPluginState()) {
throw new SQLiteError('Plugin not available', 'sqlite-query');
}
const { statement, values } = options;
if (!statement) {
throw new SQLiteError('No statement provided', 'sqlite-query');
}
// Add database name to options
const queryOptions = {
database: 'timesafari', // Use the same database name as in initialization
statement,
values: values || []
};
logger.debug('Executing SQL query:', queryOptions);
const result = await pluginState.instance.query(queryOptions);
if (!result) {
throw new SQLiteError('Query operation returned no result', 'sqlite-query');
}
return result;
} catch (error) {
throw handleError(error, 'sqlite-query');
}
});
logger.info('SQLite IPC handlers registered successfully');
}

View File

@@ -105,8 +105,15 @@ export class ElectronCapacitorApp {
defaultWidth: 1000,
defaultHeight: 800,
});
// Setup preload script path and construct our main window.
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
// Setup preload script path based on environment
const preloadPath = app.isPackaged
? join(process.resourcesPath, 'preload.js')
: join(__dirname, 'preload.js');
console.log('[Electron Main Process] Preload path:', preloadPath);
console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath));
this.MainWindow = new BrowserWindow({
icon,
show: false,
@@ -115,11 +122,12 @@ export class ElectronCapacitorApp {
width: this.mainWindowState.width,
height: this.mainWindowState.height,
webPreferences: {
nodeIntegration: true,
nodeIntegration: false,
contextIsolation: true,
// Use preload to inject the electron varriant overrides for capacitor plugins.
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
sandbox: false,
preload: preloadPath,
webSecurity: true,
allowRunningInsecureContent: false,
},
});
this.mainWindowState.manage(this.MainWindow);