Browse Source

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.
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
91a1c05473
  1. 97
      electron/src/index.ts
  2. 106
      electron/src/preload.ts
  3. 65
      electron/src/rt/sqlite-init.ts
  4. 18
      electron/src/setup.ts
  5. 81
      src/main.electron.ts
  6. 17
      src/router/index.ts

97
electron/src/index.ts

@ -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();

106
electron/src/preload.ts

@ -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...');

65
electron/src/rt/sqlite-init.ts

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

18
electron/src/setup.ts

@ -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);

81
src/main.electron.ts

@ -17,31 +17,56 @@ const app = initializeApp();
// Create a promise that resolves when SQLite is ready
const sqliteReady = new Promise<void>((resolve, reject) => {
if (!window.electron?.ipcRenderer) {
logger.error("[Main Electron] IPC renderer not available");
reject(new Error("IPC renderer not available"));
return;
}
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
logger.error("[Main Electron] SQLite initialization timeout");
reject(new Error("SQLite initialization timeout"));
}, 30000); // 30 second timeout
window.electron.ipcRenderer.once('sqlite-ready', () => {
clearTimeout(timeout);
logger.info("[Main Electron] Received SQLite ready signal");
resolve();
});
// Wait for electron bridge to be available
const checkElectronBridge = () => {
if (window.electron?.ipcRenderer) {
logger.info("[Main Electron] IPC renderer bridge available");
// Listen for SQLite ready signal
window.electron.ipcRenderer.once('sqlite-ready', () => {
clearTimeout(timeout);
logger.info("[Main Electron] Received SQLite ready signal");
resolve();
});
// Also listen for database errors
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
clearTimeout(timeout);
const status = args[0] as { status: string; error?: string };
if (status.status === 'error') {
logger.error("[Main Electron] Database error:", status.error);
reject(new Error(status.error || 'Database initialization failed'));
}
});
// Also listen for database errors
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
clearTimeout(timeout);
const status = args[0] as { status: string; error?: string };
if (status.status === 'error') {
reject(new Error(status.error || 'Database initialization failed'));
// Check if SQLite is already available
window.electron.ipcRenderer.invoke('sqlite-is-available')
.then((result: unknown) => {
const isAvailable = Boolean(result);
if (isAvailable) {
logger.info("[Main Electron] SQLite is already available");
// Don't resolve here - wait for the ready signal
// This prevents race conditions where the ready signal arrives after this check
}
})
.catch((error: Error) => {
logger.error("[Main Electron] Failed to check SQLite availability:", error);
// Don't reject here - wait for either ready signal or timeout
});
} else {
// Check again in 100ms if bridge isn't ready
setTimeout(checkElectronBridge, 100);
}
});
};
// Start checking for bridge
checkElectronBridge();
});
// Wait for SQLite to be ready before mounting
@ -49,17 +74,31 @@ sqliteReady
.then(() => {
logger.info("[Main Electron] SQLite ready, mounting app...");
app.mount("#app");
logger.info("[Main Electron] App mounted successfully");
})
.catch((error) => {
logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown error');
// Show error to user
// Show error to user with retry option
const errorDiv = document.createElement("div");
errorDiv.style.cssText =
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%;";
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;";
errorDiv.innerHTML = `
<h2>Failed to Initialize Application</h2>
<p>There was an error initializing the database. Please try restarting.</p>
<p>There was an error initializing the database. This could be due to:</p>
<ul style="text-align: left; margin: 10px 0;">
<li>Database file is locked by another process</li>
<li>Insufficient permissions to access the database</li>
<li>Database file is corrupted</li>
</ul>
<p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p>
<div style="margin-top: 15px;">
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
Retry
</button>
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;">
Reset Database
</button>
</div>
`;
document.body.appendChild(errorDiv);
});

17
src/router/index.ts

@ -277,18 +277,31 @@ const initialPath = isElectron
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
: window.location.pathname;
logger.info("[Router] Initializing router", { isElectron, initialPath });
const history = isElectron
? createMemoryHistory() // Memory history for Electron
: createWebHistory("/"); // Add base path for web apps
/** @type {*} */
const router = createRouter({
history,
routes,
});
// Set initial route
router.beforeEach((to, from, next) => {
logger.info("[Router] Navigation", { to: to.path, from: from.path });
next();
});
// Replace initial URL to start at `/` if necessary
router.replace(initialPath || "/");
if (initialPath === "/" || !initialPath) {
logger.info("[Router] Setting initial route to /");
router.replace("/");
} else {
logger.info("[Router] Setting initial route to", initialPath);
router.replace(initialPath);
}
const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any

Loading…
Cancel
Save