Browse Source

refactor(electron): WIP - use window.CapacitorSQLite API for all DB ops in ElectronPlatformService

- Remove connection object and connection pool logic
- Call all database methods directly on window.CapacitorSQLite with db name
- Refactor migrations, queries, and exec to match Capacitor SQLite Electron API
- Ensure preload script exposes both window.electron and window.CapacitorSQLite
- Fixes runtime errors related to missing query/run methods on connection
- Improves security and cross-platform compatibility

Co-authored-by: Matthew Raymer
pull/136/head
Matthew Raymer 6 days ago
parent
commit
66929d9b14
  1. 79
      electron/src/index.ts
  2. 25
      electron/src/preload.ts
  3. 148
      src/electron/main.ts
  4. 41
      src/electron/preload.js
  5. 90
      src/main.electron.ts
  6. 21
      src/services/PlatformService.ts
  7. 286
      src/services/platforms/ElectronPlatformService.ts
  8. 27
      src/types/electron.d.ts

79
electron/src/index.ts

@ -40,52 +40,71 @@ if (electronIsDev) {
// Run Application // Run Application
(async () => { (async () => {
try { try {
// Wait for electron app to be ready. // Wait for electron app to be ready.
await app.whenReady(); await app.whenReady();
// Security - Set Content-Security-Policy based on whether or not we are in dev mode. // Security - Set Content-Security-Policy based on whether or not we are in dev mode.
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
// Initialize SQLite and register handlers BEFORE app initialization // Initialize our app, build windows, and load content first
console.log('[Main] Starting SQLite initialization...'); 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
const mainWindow = myCapacitorApp.getMainWindow();
if (!mainWindow) {
throw new Error('Main window not available after app initialization');
}
// Wait for window to be ready
await new Promise<void>((resolve) => {
if (mainWindow.isVisible()) {
resolve();
} else {
mainWindow.once('show', () => resolve());
}
});
// Now initialize SQLite after window is ready
console.log('[Electron Main Process] Starting SQLite initialization...');
try { try {
// Register handlers first to prevent "no handler" errors // Register handlers first to prevent "no handler" errors
setupSQLiteHandlers(); setupSQLiteHandlers();
console.log('[Main] SQLite handlers registered'); console.log('[Electron Main Process] SQLite handlers registered');
// Then initialize the plugin // Then initialize the plugin
await initializeSQLite(); await initializeSQLite();
console.log('[Main] SQLite plugin initialized successfully'); 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) { } catch (error) {
console.error('[Main] Failed to initialize SQLite:', 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 // Don't proceed with app initialization if SQLite fails
throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
// Initialize our app, build windows, and load content. // Check for updates if we are in a packaged app.
console.log('[Main] Starting app initialization...');
await myCapacitorApp.init();
console.log('[Main] App initialization complete');
// Check for updates if we are in a packaged app.
if (!electronIsDev) { if (!electronIsDev) {
console.log('[Main] Checking for updates...'); console.log('[Electron Main Process] Checking for updates...');
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
} }
} catch (error) { } catch (error) {
console.error('[Main] Fatal error during app initialization:', error); console.error('[Electron Main Process] Fatal error during initialization:', error);
// Ensure we notify the user before quitting app.quit();
const mainWindow = myCapacitorApp.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('app-error', {
type: 'initialization',
error: error instanceof Error ? error.message : 'Unknown error'
});
// Give the window time to show the error
setTimeout(() => app.quit(), 5000);
} else {
app.quit();
}
} }
})(); })();

25
electron/src/preload.ts

@ -41,12 +41,12 @@ const createSQLiteProxy = () => {
} catch (error) { } catch (error) {
lastError = error instanceof Error ? error : new Error(String(error)); lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < MAX_RETRIES) { if (attempt < MAX_RETRIES) {
logger.warn(`SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error); logger.warn(`[CapacitorSQLite] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
} }
} }
} }
throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`); throw new Error(`[CapacitorSQLite] SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
}; };
const wrapOperation = (method: string) => { const wrapOperation = (method: string) => {
@ -65,8 +65,8 @@ const createSQLiteProxy = () => {
} }
return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args); return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args);
} catch (error) { } catch (error) {
logger.error(`SQLite ${method} failed:`, error); logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, error);
throw new Error(`Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(`[CapacitorSQLite] Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
}; };
}; };
@ -85,13 +85,24 @@ const createSQLiteProxy = () => {
}; };
}; };
// Expose only the CapacitorSQLite proxy // 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
});
// Expose CapacitorSQLite proxy as before
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy()); contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
// Log startup // Log startup
logger.log('Script starting...'); logger.log('[CapacitorSQLite] Preload script starting...');
// Handle window load // Handle window load
window.addEventListener('load', () => { window.addEventListener('load', () => {
logger.log('Script complete'); logger.log('[CapacitorSQLite] Preload script complete');
}); });

148
src/electron/main.ts

@ -83,20 +83,20 @@ const getAppDataPath = async (): Promise<string> => {
if (linuxPath) { if (linuxPath) {
// Expand ~ to home directory // Expand ~ to home directory
const expandedPath = linuxPath.replace(/^~/, process.env.HOME || ""); const expandedPath = linuxPath.replace(/^~/, process.env.HOME || "");
logger.info("[Electron] Using configured database path:", expandedPath); logger.info("[Electron Main] Using configured database path:", expandedPath);
return expandedPath; return expandedPath;
} }
// Fallback to app.getPath if config path is not available // Fallback to app.getPath if config path is not available
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
logger.info("[Electron] Using fallback user data path:", userDataPath); logger.info("[Electron Main] Using fallback user data path:", userDataPath);
return userDataPath; return userDataPath;
} catch (error) { } catch (error) {
logger.error("[Electron] Error getting app data path:", error); logger.error("[Electron Main] Error getting app data path:", error);
// Fallback to app.getPath if anything fails // Fallback to app.getPath if anything fails
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
logger.info( logger.info(
"[Electron] Using fallback user data path after error:", "[Electron Main] Using fallback user data path after error:",
userDataPath, userDataPath,
); );
return userDataPath; return userDataPath;
@ -123,7 +123,7 @@ const validateAndNormalizePath = async (filePath: string): Promise<string> => {
// Normalize the path // Normalize the path
const normalizedPath = path.normalize(resolvedPath); const normalizedPath = path.normalize(resolvedPath);
logger.info("[Electron] Validated database path:", { logger.info("[Electron Main] Validated database path:", {
original: filePath, original: filePath,
resolved: resolvedPath, resolved: resolvedPath,
normalized: normalizedPath, normalized: normalizedPath,
@ -142,7 +142,7 @@ const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
// Check if directory exists // Check if directory exists
if (!fs.existsSync(normalizedPath)) { if (!fs.existsSync(normalizedPath)) {
logger.info("[Electron] Creating database directory:", normalizedPath); logger.info("[Electron Main] Creating database directory:", normalizedPath);
await fs.promises.mkdir(normalizedPath, { recursive: true }); await fs.promises.mkdir(normalizedPath, { recursive: true });
} }
@ -153,11 +153,11 @@ const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
fs.constants.R_OK | fs.constants.W_OK, fs.constants.R_OK | fs.constants.W_OK,
); );
logger.info( logger.info(
"[Electron] Database directory permissions verified:", "[Electron Main] Database directory permissions verified:",
normalizedPath, normalizedPath,
); );
} catch (error) { } catch (error) {
logger.error("[Electron] Database directory permission error:", error); logger.error("[Electron Main] Database directory permission error:", error);
throw new Error(`Database directory not accessible: ${normalizedPath}`); throw new Error(`Database directory not accessible: ${normalizedPath}`);
} }
@ -167,16 +167,16 @@ const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
await fs.promises.writeFile(testFile, "test"); await fs.promises.writeFile(testFile, "test");
await fs.promises.unlink(testFile); await fs.promises.unlink(testFile);
logger.info( logger.info(
"[Electron] Database directory write test passed:", "[Electron Main] Database directory write test passed:",
normalizedPath, normalizedPath,
); );
} catch (error) { } catch (error) {
logger.error("[Electron] Database directory write test failed:", error); logger.error("[Electron Main] Database directory write test failed:", error);
throw new Error(`Database directory not writable: ${normalizedPath}`); throw new Error(`Database directory not writable: ${normalizedPath}`);
} }
} catch (error) { } catch (error) {
logger.error( logger.error(
"[Electron] Failed to ensure database directory exists:", "[Electron Main] Failed to ensure database directory exists:",
error, error,
); );
throw error; throw error;
@ -203,7 +203,7 @@ const initializeDatabasePaths = async (): Promise<void> => {
try { try {
// Get the base directory from config // Get the base directory from config
dbDir = await getAppDataPath(); dbDir = await getAppDataPath();
logger.info("[Electron] Database directory:", dbDir); logger.info("[Electron Main] Database directory:", dbDir);
// Ensure the directory exists and is writable // Ensure the directory exists and is writable
await ensureDirectoryExists(dbDir); await ensureDirectoryExists(dbDir);
@ -212,7 +212,7 @@ const initializeDatabasePaths = async (): Promise<void> => {
dbPath = await validateAndNormalizePath( dbPath = await validateAndNormalizePath(
path.join(dbDir, "timesafari.db"), path.join(dbDir, "timesafari.db"),
); );
logger.info("[Electron] Database path initialized:", dbPath); logger.info("[Electron Main] Database path initialized:", dbPath);
// Verify the database file if it exists // Verify the database file if it exists
if (fs.existsSync(dbPath)) { if (fs.existsSync(dbPath)) {
@ -222,18 +222,18 @@ const initializeDatabasePaths = async (): Promise<void> => {
fs.constants.R_OK | fs.constants.W_OK, fs.constants.R_OK | fs.constants.W_OK,
); );
logger.info( logger.info(
"[Electron] Existing database file permissions verified:", "[Electron Main] Existing database file permissions verified:",
dbPath, dbPath,
); );
} catch (error) { } catch (error) {
logger.error("[Electron] Database file permission error:", error); logger.error("[Electron Main] Database file permission error:", error);
throw new Error(`Database file not accessible: ${dbPath}`); throw new Error(`Database file not accessible: ${dbPath}`);
} }
} }
dbPathInitialized = true; dbPathInitialized = true;
} catch (error) { } catch (error) {
logger.error("[Electron] Failed to initialize database paths:", error); logger.error("[Electron Main] Failed to initialize database paths:", error);
throw error; throw error;
} finally { } finally {
dbPathInitializationPromise = null; dbPathInitializationPromise = null;
@ -260,7 +260,7 @@ async function initializeSQLite() {
sqliteInitializationPromise = (async () => { sqliteInitializationPromise = (async () => {
try { try {
logger.info("[Electron] Initializing SQLite plugin..."); logger.info("[Electron Main] Initializing SQLite plugin...");
sqlitePlugin = new CapacitorSQLite(); sqlitePlugin = new CapacitorSQLite();
// Initialize database paths first // Initialize database paths first
@ -272,7 +272,7 @@ async function initializeSQLite() {
// Test the plugin // Test the plugin
const echoResult = await sqlitePlugin.echo({ value: "test" }); const echoResult = await sqlitePlugin.echo({ value: "test" });
logger.info("[Electron] SQLite plugin echo test:", echoResult); logger.info("[Electron Main] SQLite plugin echo test:", echoResult);
// Initialize database connection using validated dbPath // Initialize database connection using validated dbPath
const connectionOptions = { const connectionOptions = {
@ -285,14 +285,14 @@ async function initializeSQLite() {
}; };
logger.info( logger.info(
"[Electron] Creating initial connection with options:", "[Electron Main] Creating initial connection with options:",
connectionOptions, connectionOptions,
); );
// Log the actual path being used // Log the actual path being used
logger.info("[Electron] Using database path:", dbPath); logger.info("[Electron Main] Using database path:", dbPath);
logger.info("[Electron] Path exists:", fs.existsSync(dbPath)); logger.info("[Electron Main] Path exists:", fs.existsSync(dbPath));
logger.info("[Electron] Path is absolute:", path.isAbsolute(dbPath)); logger.info("[Electron Main] Path is absolute:", path.isAbsolute(dbPath));
const db = await sqlitePlugin.createConnection(connectionOptions); const db = await sqlitePlugin.createConnection(connectionOptions);
@ -308,19 +308,19 @@ async function initializeSQLite() {
// Verify the connection is working // Verify the connection is working
try { try {
const result = await db.query("PRAGMA journal_mode;"); const result = await db.query("PRAGMA journal_mode;");
logger.info("[Electron] Database connection verified:", result); logger.info("[Electron Main] Database connection verified:", result);
} catch (error) { } catch (error) {
logger.error( logger.error(
"[Electron] Database connection verification failed:", "[Electron Main] Database connection verification failed:",
error, error,
); );
throw error; throw error;
} }
sqliteInitialized = true; sqliteInitialized = true;
logger.info("[Electron] SQLite plugin initialized successfully"); logger.info("[Electron Main] SQLite plugin initialized successfully");
} catch (error) { } catch (error) {
logger.error("[Electron] Failed to initialize SQLite plugin:", error); logger.error("[Electron Main] Failed to initialize SQLite plugin:", error);
throw error; throw error;
} finally { } finally {
sqliteInitializationPromise = null; sqliteInitializationPromise = null;
@ -337,33 +337,53 @@ app.whenReady().then(async () => {
// Create window first // Create window first
const mainWindow = createWindow(); const mainWindow = createWindow();
// Initialize database in background // Wait for window to be ready
initializeSQLite().catch((error) => { await new Promise<void>((resolve) => {
mainWindow.once('ready-to-show', () => {
logger.info("[Electron Main] Window ready to show");
mainWindow.show();
resolve();
});
});
// Initialize database after window is ready
try {
await initializeSQLite();
logger.info("[Electron Main] SQLite plugin initialized successfully");
// Now send the ready signal since window is ready
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('sqlite-ready');
logger.info("[Electron Main] Sent SQLite ready signal to renderer");
} else {
logger.error("[Electron Main] Window was destroyed before sending SQLite ready signal");
}
} catch (error) {
logger.error( logger.error(
"[Electron] Database initialization failed, but continuing:", "[Electron Main] Database initialization failed:",
error, error,
); );
// Notify renderer about database status // Notify renderer about database status
if (!mainWindow.isDestroyed()) { if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send("database-status", { mainWindow.webContents.send("database-status", {
status: "error", status: "error",
error: error.message, error: error instanceof Error ? error.message : 'Unknown error',
}); });
} }
}); }
// Handle window close // Handle window close
mainWindow.on("closed", () => { mainWindow.on("closed", () => {
logger.info("[Electron] Main window closed"); logger.info("[Electron Main] Main window closed");
}); });
// Handle window close request // Handle window close request
mainWindow.on("close", (event) => { mainWindow.on("close", (event) => {
logger.info("[Electron] Window close requested"); logger.info("[Electron Main] Window close requested");
// Prevent immediate close if we're in the middle of something // Prevent immediate close if we're in the middle of something
if (mainWindow.webContents.isLoading()) { if (mainWindow.webContents.isLoading()) {
event.preventDefault(); event.preventDefault();
logger.info("[Electron] Deferring window close due to loading state"); logger.info("[Electron Main] Deferring window close due to loading state");
mainWindow.webContents.once("did-finish-load", () => { mainWindow.webContents.once("did-finish-load", () => {
mainWindow.close(); mainWindow.close();
}); });
@ -380,15 +400,15 @@ function createWindow(): BrowserWindow {
? path.join(process.resourcesPath, "preload.js") ? path.join(process.resourcesPath, "preload.js")
: path.join(__dirname, "preload.js"); : path.join(__dirname, "preload.js");
logger.log("[Electron] Preload path:", preloadPath); logger.log("[Electron Main] Preload path:", preloadPath);
logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath)); logger.log("[Electron Main] Preload exists:", fs.existsSync(preloadPath));
// Log environment and paths // Log environment and paths
logger.log("[Electron] process.cwd():", process.cwd()); logger.log("[Electron Main] process.cwd():", process.cwd());
logger.log("[Electron] __dirname:", __dirname); logger.log("[Electron Main] __dirname:", __dirname);
logger.log("[Electron] app.getAppPath():", app.getAppPath()); logger.log("[Electron Main] app.getAppPath():", app.getAppPath());
logger.log("[Electron] app.isPackaged:", app.isPackaged); logger.log("[Electron Main] app.isPackaged:", app.isPackaged);
logger.log("[Electron] process.resourcesPath:", process.resourcesPath); logger.log("[Electron Main] process.resourcesPath:", process.resourcesPath);
// List files in __dirname and __dirname/www // List files in __dirname and __dirname/www
try { try {
@ -420,24 +440,24 @@ function createWindow(): BrowserWindow {
// Show window when ready // Show window when ready
mainWindow.once("ready-to-show", () => { mainWindow.once("ready-to-show", () => {
logger.info("[Electron] Window ready to show"); logger.info("[Electron Main] Window ready to show");
mainWindow.show(); mainWindow.show();
}); });
// Handle window errors // Handle window errors
mainWindow.webContents.on("render-process-gone", (_event, details) => { mainWindow.webContents.on("render-process-gone", (_event, details) => {
logger.error("[Electron] Render process gone:", details); logger.error("[Electron Main] Render process gone:", details);
}); });
mainWindow.webContents.on( mainWindow.webContents.on(
"did-fail-load", "did-fail-load",
(_event, errorCode, errorDescription) => { (_event, errorCode, errorDescription) => {
logger.error( logger.error(
"[Electron] Page failed to load:", "[Electron Main] Page failed to load:",
errorCode, errorCode,
errorDescription, errorDescription,
); );
logger.error("[Electron] Failed URL:", mainWindow.webContents.getURL()); logger.error("[Electron Main] Failed URL:", mainWindow.webContents.getURL());
}, },
); );
@ -449,31 +469,31 @@ function createWindow(): BrowserWindow {
indexPath = path.join(process.resourcesPath, "www", "index.html"); indexPath = path.join(process.resourcesPath, "www", "index.html");
fileUrl = `file://${indexPath}`; fileUrl = `file://${indexPath}`;
logger.info( logger.info(
"[Electron] App is packaged. Using process.resourcesPath for index.html", "[Electron Main] App is packaged. Using process.resourcesPath for index.html",
); );
} else { } else {
indexPath = path.resolve(__dirname, "www", "index.html"); indexPath = path.resolve(__dirname, "www", "index.html");
fileUrl = `file://${indexPath}`; fileUrl = `file://${indexPath}`;
logger.info( logger.info(
"[Electron] App is not packaged. Using __dirname for index.html", "[Electron Main] App is not packaged. Using __dirname for index.html",
); );
} }
logger.info("[Electron] Resolved index.html path:", indexPath); logger.info("[Electron Main] Resolved index.html path:", indexPath);
logger.info("[Electron] Using file URL:", fileUrl); logger.info("[Electron Main] Using file URL:", fileUrl);
// Load the index.html with retry logic // Load the index.html with retry logic
const loadIndexHtml = async (retryCount = 0): Promise<void> => { const loadIndexHtml = async (retryCount = 0): Promise<void> => {
try { try {
if (mainWindow.isDestroyed()) { if (mainWindow.isDestroyed()) {
logger.error( logger.error(
"[Electron] Window was destroyed before loading index.html", "[Electron Main] Window was destroyed before loading index.html",
); );
return; return;
} }
const exists = fs.existsSync(indexPath); const exists = fs.existsSync(indexPath);
logger.info(`[Electron] fs.existsSync for index.html: ${exists}`); logger.info(`[Electron Main] fs.existsSync for index.html: ${exists}`);
if (!exists) { if (!exists) {
throw new Error(`index.html not found at path: ${indexPath}`); throw new Error(`index.html not found at path: ${indexPath}`);
@ -481,7 +501,7 @@ function createWindow(): BrowserWindow {
// Try to read the file to verify it's accessible // Try to read the file to verify it's accessible
const stats = fs.statSync(indexPath); const stats = fs.statSync(indexPath);
logger.info("[Electron] index.html stats:", { logger.info("[Electron Main] index.html stats:", {
size: stats.size, size: stats.size,
mode: stats.mode, mode: stats.mode,
uid: stats.uid, uid: stats.uid,
@ -490,27 +510,27 @@ function createWindow(): BrowserWindow {
// Try loadURL first // Try loadURL first
try { try {
logger.info("[Electron] Attempting to load index.html via loadURL"); logger.info("[Electron Main] Attempting to load index.html via loadURL");
await mainWindow.loadURL(fileUrl); await mainWindow.loadURL(fileUrl);
logger.info("[Electron] Successfully loaded index.html via loadURL"); logger.info("[Electron Main] Successfully loaded index.html via loadURL");
} catch (loadUrlError) { } catch (loadUrlError) {
logger.warn( logger.warn(
"[Electron] loadURL failed, trying loadFile:", "[Electron Main] loadURL failed, trying loadFile:",
loadUrlError, loadUrlError,
); );
// Fallback to loadFile // Fallback to loadFile
await mainWindow.loadFile(indexPath); await mainWindow.loadFile(indexPath);
logger.info("[Electron] Successfully loaded index.html via loadFile"); logger.info("[Electron Main] Successfully loaded index.html via loadFile");
} }
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred"; error instanceof Error ? error.message : "Unknown error occurred";
logger.error("[Electron] Error loading index.html:", errorMessage); logger.error("[Electron Main] Error loading index.html:", errorMessage);
// Retry logic // Retry logic
if (retryCount < 3 && !mainWindow.isDestroyed()) { if (retryCount < 3 && !mainWindow.isDestroyed()) {
logger.info( logger.info(
`[Electron] Retrying index.html load (attempt ${retryCount + 1})`, `[Electron Main] Retrying index.html load (attempt ${retryCount + 1})`,
); );
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
return loadIndexHtml(retryCount + 1); return loadIndexHtml(retryCount + 1);
@ -536,7 +556,7 @@ function createWindow(): BrowserWindow {
// Start loading the index.html // Start loading the index.html
loadIndexHtml().catch((error: unknown) => { loadIndexHtml().catch((error: unknown) => {
logger.error("[Electron] Fatal error loading index.html:", error); logger.error("[Electron Main] Fatal error loading index.html:", error);
}); });
// Only open DevTools if not in production // Only open DevTools if not in production
@ -607,7 +627,7 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => {
}; };
logger.info( logger.info(
"[Electron] Creating database connection with options:", "[Electron Main] Creating database connection with options:",
connectionOptions, connectionOptions,
); );
const result = await sqlitePlugin.createConnection(connectionOptions); const result = await sqlitePlugin.createConnection(connectionOptions);
@ -628,19 +648,19 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => {
}); });
if (testResult?.values?.[0]?.journal_mode === "off") { if (testResult?.values?.[0]?.journal_mode === "off") {
logger.error( logger.error(
"[Electron] Connection opened in read-only mode despite options", "[Electron Main] Connection opened in read-only mode despite options",
); );
throw new Error("Database connection opened in read-only mode"); throw new Error("Database connection opened in read-only mode");
} }
} catch (queryError) { } catch (queryError) {
logger.error("[Electron] Error verifying connection:", queryError); logger.error("[Electron Main] Error verifying connection:", queryError);
throw queryError; throw queryError;
} }
logger.info("[Electron] Database connection created successfully"); logger.info("[Electron Main] Database connection created successfully");
return result; return result;
} catch (error) { } catch (error) {
logger.error("[Electron] Error in sqlite-create-connection:", error); logger.error("[Electron Main] Error in sqlite-create-connection:", error);
throw error; throw error;
} }
}); });

41
src/electron/preload.js

@ -5,27 +5,27 @@ const logger = {
// Always log in development, log with context in production // Always log in development, log with context in production
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(`[Preload] ${message}`, ...args); console.log(`[Electron Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
} }
}, },
warn: (message, ...args) => { warn: (message, ...args) => {
// Always log warnings // Always log warnings
/* eslint-disable no-console */ /* eslint-disable no-console */
console.warn(`[Preload] ${message}`, ...args); console.warn(`[Electron Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
}, },
error: (message, ...args) => { error: (message, ...args) => {
// Always log errors // Always log errors
/* eslint-disable no-console */ /* eslint-disable no-console */
console.error(`[Preload] ${message}`, ...args); console.error(`[Electron Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
}, },
info: (message, ...args) => { info: (message, ...args) => {
// Always log info in development, log with context in production // Always log info in development, log with context in production
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.info(`[Preload] ${message}`, ...args); console.info(`[Electron Preload] ${message}`, ...args);
/* eslint-enable no-console */ /* eslint-enable no-console */
} }
}, },
@ -143,32 +143,33 @@ try {
}; };
// Expose only the CapacitorSQLite proxy // Expose only the CapacitorSQLite proxy
contextBridge.exposeInMainWorld("CapacitorSQLite", createSQLiteProxy());
// Remove the duplicate electron.sqlite bridge
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {
// Keep other electron APIs but remove sqlite sqlite: createSQLiteProxy(),
getPath, getPath: (pathType) => ipcRenderer.invoke("get-path", pathType),
send: (channel, data) => { send: (channel, data) => {
const validChannels = ["toMain"]; ipcRenderer.send(channel, data);
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
}, },
receive: (channel, func) => { receive: (channel, func) => {
const validChannels = ["fromMain"]; ipcRenderer.on(channel, (event, ...args) => func(...args));
if (validChannels.includes(channel)) { },
ipcRenderer: {
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args)); ipcRenderer.on(channel, (event, ...args) => func(...args));
},
once: (channel, func) => {
ipcRenderer.once(channel, (event, ...args) => func(...args));
},
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
invoke: (channel, ...args) => {
return ipcRenderer.invoke(channel, ...args);
} }
}, },
env: { env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
platform: "electron", platform: "electron",
}, },
getBasePath: () => { getBasePath: () => ipcRenderer.invoke("get-base-path"),
return process.env.NODE_ENV === "development" ? "/" : "./";
},
}); });
logger.info("Preload script completed successfully"); logger.info("Preload script completed successfully");

90
src/main.electron.ts

@ -1,75 +1,65 @@
import { initializeApp } from "./main.common"; import { initializeApp } from "./main.common";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
async function initializeSQLite() {
try {
// Wait for SQLite to be available in the main process
let retries = 0;
const maxRetries = 5;
const retryDelay = 1000; // 1 second
while (retries < maxRetries) {
try {
const isAvailable = await window.CapacitorSQLite.isAvailable();
if (isAvailable) {
logger.info(
"[Electron] SQLite plugin bridge initialized successfully",
);
return true;
}
} catch (error) {
logger.warn(
`[Electron] SQLite not available yet (attempt ${retries + 1}/${maxRetries}):`,
error,
);
}
retries++;
if (retries < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
throw new Error("SQLite plugin not available after maximum retries");
} catch (error) {
logger.error(
"[Electron] Failed to initialize SQLite plugin bridge:",
error,
);
throw error;
}
}
const platform = process.env.VITE_PLATFORM; const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.info("[Electron] Initializing app"); logger.info("[Main Electron] Initializing app");
logger.info("[Electron] Platform:", { platform }); logger.info("[Main Electron] Platform:", { platform });
logger.info("[Electron] PWA enabled:", { pwa_enabled }); logger.info("[Main Electron] PWA enabled:", { pwa_enabled });
if (pwa_enabled) { if (pwa_enabled) {
logger.warn("[Electron] PWA is enabled, but not supported in electron"); logger.warn("[Main Electron] PWA is enabled, but not supported in electron");
} }
// Initialize app and SQLite // Initialize app and SQLite
const app = initializeApp(); const app = initializeApp();
// Initialize SQLite first, then mount the app // Create a promise that resolves when SQLite is ready
initializeSQLite() 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(() => {
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();
});
// 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'));
}
});
});
// Wait for SQLite to be ready before mounting
sqliteReady
.then(() => { .then(() => {
logger.info("[Electron] SQLite initialized, mounting app..."); logger.info("[Main Electron] SQLite ready, mounting app...");
app.mount("#app"); app.mount("#app");
}) })
.catch((error) => { .catch((error) => {
logger.error("[Electron] Failed to initialize app:", error); logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown error');
// Show error to user // Show error to user
const errorDiv = document.createElement("div"); const errorDiv = document.createElement("div");
errorDiv.style.cssText = 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%;";
errorDiv.innerHTML = ` errorDiv.innerHTML = `
<h2>Failed to Initialize Database</h2> <h2>Failed to Initialize Application</h2>
<p>There was an error initializing the database. Please try restarting the application.</p> <p>There was an error initializing the database. Please try restarting.</p>
<p>Error details: ${error.message}</p> <p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p>
`; `;
document.body.appendChild(errorDiv); document.body.appendChild(errorDiv);
}); });

21
src/services/PlatformService.ts

@ -1,5 +1,13 @@
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
/**
* Query execution result interface
*/
export interface QueryExecResult<T = unknown> {
columns: string[];
values: T[];
}
/** /**
* Represents the result of an image capture or selection operation. * Represents the result of an image capture or selection operation.
* Contains both the image data as a Blob and the associated filename. * Contains both the image data as a Blob and the associated filename.
@ -102,15 +110,12 @@ export interface PlatformService {
handleDeepLink(url: string): Promise<void>; handleDeepLink(url: string): Promise<void>;
/** /**
* Executes a SQL query on the database. * Execute a database query and return the results
* @param sql - The SQL query to execute * @param sql SQL query to execute
* @param params - The parameters to pass to the query * @param params Query parameters
* @returns Promise resolving to the query result * @returns Query results with columns and values
*/ */
dbQuery( dbQuery<T = unknown>(sql: string, params?: unknown[]): Promise<QueryExecResult<T>>;
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined>;
/** /**
* Executes a create/update/delete on the database. * Executes a create/update/delete on the database.

286
src/services/platforms/ElectronPlatformService.ts

@ -2,13 +2,37 @@ import {
ImageResult, ImageResult,
PlatformService, PlatformService,
PlatformCapabilities, PlatformCapabilities,
QueryExecResult,
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
import { SQLiteDBConnection } from "@capacitor-community/sqlite";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { DatabaseConnectionPool } from "../database/ConnectionPool"; import { DatabaseConnectionPool } from "../database/ConnectionPool";
// Type for the electron window object
declare global {
interface Window {
electron: {
ipcRenderer?: {
on: (channel: string, func: (...args: unknown[]) => void) => void;
once: (channel: string, func: (...args: unknown[]) => void) => void;
send: (channel: string, data: unknown) => void;
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
};
sqlite: {
isAvailable: () => Promise<boolean>;
execute: (method: string, ...args: unknown[]) => Promise<unknown>;
};
getPath: (pathType: string) => Promise<string>;
send: (channel: string, data: unknown) => void;
receive: (channel: string, func: (...args: unknown[]) => void) => void;
env: {
platform: string;
};
getBasePath: () => Promise<string>;
};
}
}
interface Migration { interface Migration {
name: string; name: string;
sql: string; sql: string;
@ -24,121 +48,78 @@ interface Migration {
*/ */
export class ElectronPlatformService implements PlatformService { export class ElectronPlatformService implements PlatformService {
private sqlite: any; private sqlite: any;
private connection: SQLiteDBConnection | null = null; private dbName = "timesafari";
private connectionPool: DatabaseConnectionPool; private isInitialized = false;
private initializationPromise: Promise<void> | null = null;
private dbFatalError = false; private dbFatalError = false;
private sqliteReadyPromise: Promise<void> | null = null;
constructor() { constructor() {
this.connectionPool = DatabaseConnectionPool.getInstance(); this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
if (!window.CapacitorSQLite) { if (!window.electron?.ipcRenderer) {
throw new Error("CapacitorSQLite not initialized in Electron"); logger.warn('[ElectronPlatformService] IPC renderer not available');
} reject(new Error('IPC renderer not available'));
this.sqlite = window.CapacitorSQLite; return;
}
const timeout = setTimeout(() => {
reject(new Error('SQLite initialization timeout'));
}, 30000);
window.electron.ipcRenderer.once('sqlite-ready', () => {
clearTimeout(timeout);
logger.info('[ElectronPlatformService] Received SQLite ready signal');
this.isInitialized = true;
resolve();
});
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'));
}
});
});
} }
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
// If we already have a connection, return immediately if (this.isInitialized) return;
if (this.connection) { if (this.sqliteReadyPromise) await this.sqliteReadyPromise;
return; this.sqlite = window.CapacitorSQLite;
} if (!this.sqlite) throw new Error("CapacitorSQLite not available");
// Create the connection (idempotent)
// If initialization is in progress, wait for it await this.sqlite.createConnection({
if (this.initializationPromise) { database: this.dbName,
return this.initializationPromise; encrypted: false,
} mode: "no-encryption",
readOnly: false,
// Start initialization });
this.initializationPromise = (async () => { // Optionally, test the connection
try { await this.sqlite.query({
if (!this.sqlite) { database: this.dbName,
logger.debug("[ElectronPlatformService] SQLite plugin not available, checking..."); statement: "SELECT 1"
this.sqlite = await import("@capacitor-community/sqlite"); });
} // Run migrations if needed
await this.runMigrations();
if (!this.sqlite) { logger.info("[ElectronPlatformService] Database initialized successfully");
throw new Error("SQLite plugin not available");
}
// Get connection from pool
this.connection = await this.connectionPool.getConnection("timesafari", async () => {
// Create the connection
const connection = await this.sqlite.createConnection({
database: "timesafari",
encrypted: false,
mode: "no-encryption",
readonly: false,
});
// Wait for the connection to be fully initialized
await new Promise<void>((resolve, reject) => {
const checkConnection = async () => {
try {
// Try a simple query to verify the connection is ready
const result = await connection.query("SELECT 1");
if (result && result.values) {
resolve();
} else {
reject(new Error("Connection query returned invalid result"));
}
} catch (error) {
// If the error is that query is not a function, the connection isn't ready yet
if (error instanceof Error && error.message.includes("query is not a function")) {
setTimeout(checkConnection, 100);
} else {
reject(error);
}
}
};
checkConnection();
});
// Verify write access
const result = await connection.query("PRAGMA journal_mode");
const journalMode = result.values?.[0]?.journal_mode;
if (journalMode !== "wal") {
throw new Error(`Database is not writable. Journal mode: ${journalMode}`);
}
return connection;
});
// Run migrations if needed
await this.runMigrations();
logger.info("[ElectronPlatformService] Database initialized successfully");
} catch (error) {
logger.error("[ElectronPlatformService] Database initialization failed:", error);
this.connection = null;
throw error;
} finally {
this.initializationPromise = null;
}
})();
return this.initializationPromise;
} }
private async runMigrations(): Promise<void> { private async runMigrations(): Promise<void> {
if (!this.connection) {
throw new Error("Database not initialized");
}
// Create migrations table if it doesn't exist // Create migrations table if it doesn't exist
await this.connection.execute(` await this.sqlite.execute({
CREATE TABLE IF NOT EXISTS migrations ( database: this.dbName,
statements: `CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );`
`); });
// Get list of executed migrations // Get list of executed migrations
const result = await this.connection.query("SELECT name FROM migrations;"); const result = await this.sqlite.query({
database: this.dbName,
statement: "SELECT name FROM migrations;"
});
const executedMigrations = new Set( const executedMigrations = new Set(
result.values?.map((row) => row[0]) || [], (result.values as unknown[][])?.map((row: unknown[]) => row[0] as string) || []
); );
// Run pending migrations in order // Run pending migrations in order
const migrations: Migration[] = [ const migrations: Migration[] = [
{ {
@ -226,13 +207,17 @@ export class ElectronPlatformService implements PlatformService {
`, `,
}, },
]; ];
for (const migration of migrations) { for (const migration of migrations) {
if (!executedMigrations.has(migration.name)) { if (!executedMigrations.has(migration.name)) {
await this.connection.execute(migration.sql); await this.sqlite.execute({
await this.connection.run("INSERT INTO migrations (name) VALUES (?)", [ database: this.dbName,
migration.name, statements: migration.sql
]); });
await this.sqlite.run({
database: this.dbName,
statement: "INSERT INTO migrations (name) VALUES (?)",
values: [migration.name]
});
logger.log(`Migration ${migration.name} executed successfully`); logger.log(`Migration ${migration.name} executed successfully`);
} }
} }
@ -343,43 +328,32 @@ export class ElectronPlatformService implements PlatformService {
/** /**
* @see PlatformService.dbQuery * @see PlatformService.dbQuery
*/ */
async dbQuery( async dbQuery<T = unknown>(sql: string, params: unknown[] = []): Promise<QueryExecResult<T>> {
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined> {
if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app.");
}
await this.initializeDatabase(); await this.initializeDatabase();
if (!this.connection) { if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app.");
throw new Error("Database not initialized"); const result = await this.sqlite.query({
} database: this.dbName,
statement: sql,
const result = await this.connection.query(sql, params); values: params
});
const columns = result.values?.[0] ? Object.keys(result.values[0]) : [];
return { return {
columns: [], // SQLite plugin doesn't provide column names columns,
values: result.values || [], values: (result.values || []).map((row: Record<string, unknown>) => row as T)
}; };
} }
/** /**
* @see PlatformService.dbExec * @see PlatformService.dbExec
*/ */
async dbExec( async dbExec(sql: string, params?: unknown[]): Promise<{ changes: number; lastId?: number }> {
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app.");
}
await this.initializeDatabase(); await this.initializeDatabase();
if (!this.connection) { if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app.");
throw new Error("Database not initialized"); const result = await this.sqlite.run({
} database: this.dbName,
statement: sql,
const result = await this.connection.run(sql, params); values: params
});
return { return {
changes: result.changes?.changes || 0, changes: result.changes?.changes || 0,
lastId: result.changes?.lastId, lastId: result.changes?.lastId,
@ -387,50 +361,20 @@ export class ElectronPlatformService implements PlatformService {
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app.");
}
await this.initializeDatabase(); await this.initializeDatabase();
} }
async query<T>(sql: string, params: any[] = []): Promise<T[]> {
if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app.");
}
await this.initializeDatabase();
if (!this.connection) {
throw new Error("Database not initialized");
}
const result = await this.connection.query(sql, params);
return (result.values || []) as T[];
}
async execute(sql: string, params: any[] = []): Promise<void> { async execute(sql: string, params: any[] = []): Promise<void> {
if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app.");
}
await this.initializeDatabase(); await this.initializeDatabase();
if (!this.connection) { if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app.");
throw new Error("Database not initialized"); await this.sqlite.run({
} database: this.dbName,
statement: sql,
await this.connection.run(sql, params); values: params
});
} }
async close(): Promise<void> { async close(): Promise<void> {
if (!this.connection) { // Optionally implement close logic if needed
return;
}
try {
await this.connectionPool.releaseConnection("timesafari");
this.connection = null;
} catch (error) {
logger.error("Failed to close database:", error);
throw error;
}
} }
} }

27
src/types/electron.d.ts

@ -0,0 +1,27 @@
interface ElectronAPI {
sqlite: {
isAvailable: () => Promise<boolean>;
execute: (method: string, ...args: unknown[]) => Promise<unknown>;
};
getPath: (pathType: string) => Promise<string>;
send: (channel: string, data: unknown) => void;
receive: (channel: string, func: (...args: unknown[]) => void) => void;
ipcRenderer: {
on: (channel: string, func: (...args: unknown[]) => void) => void;
once: (channel: string, func: (...args: unknown[]) => void) => void;
send: (channel: string, data: unknown) => void;
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
};
env: {
platform: string;
};
getBasePath: () => Promise<string>;
}
declare global {
interface Window {
electron: ElectronAPI;
}
}
export {};
Loading…
Cancel
Save