From 66929d9b141240896a13710aac9eed6eafa95194 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 2 Jun 2025 13:17:48 +0000 Subject: [PATCH] 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 --- electron/src/index.ts | 79 +++-- electron/src/preload.ts | 25 +- src/electron/main.ts | 148 +++++---- src/electron/preload.js | 41 +-- src/main.electron.ts | 90 +++--- src/services/PlatformService.ts | 21 +- .../platforms/ElectronPlatformService.ts | 286 +++++++----------- src/types/electron.d.ts | 27 ++ 8 files changed, 367 insertions(+), 350 deletions(-) create mode 100644 src/types/electron.d.ts diff --git a/electron/src/index.ts b/electron/src/index.ts index eb1fd59a..87c50e3e 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -40,52 +40,71 @@ if (electronIsDev) { // Run Application (async () => { try { - // Wait for electron app to be ready. - await app.whenReady(); + // Wait for electron app to be ready. + await app.whenReady(); - // Security - Set Content-Security-Policy based on whether or not we are in dev mode. - setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); + // Security - Set Content-Security-Policy based on whether or not we are in dev mode. + setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); - // Initialize SQLite and register handlers BEFORE app initialization - console.log('[Main] Starting SQLite initialization...'); + // Initialize our app, build windows, and load content first + 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((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 { // Register handlers first to prevent "no handler" errors setupSQLiteHandlers(); - console.log('[Main] SQLite handlers registered'); + console.log('[Electron Main Process] SQLite handlers registered'); // Then initialize the plugin 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) { - 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 throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } - // Initialize our app, build windows, and load content. - 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. + // Check for updates if we are in a packaged app. if (!electronIsDev) { - console.log('[Main] Checking for updates...'); - autoUpdater.checkForUpdatesAndNotify(); + console.log('[Electron Main Process] Checking for updates...'); + autoUpdater.checkForUpdatesAndNotify(); } } catch (error) { - console.error('[Main] Fatal error during app initialization:', error); - // Ensure we notify the user before quitting - 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(); - } + console.error('[Electron Main Process] Fatal error during initialization:', error); + app.quit(); } })(); diff --git a/electron/src/preload.ts b/electron/src/preload.ts index f05a3225..290801ac 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -41,12 +41,12 @@ const createSQLiteProxy = () => { } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); 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)); } } } - 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) => { @@ -65,8 +65,8 @@ const createSQLiteProxy = () => { } return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args); } catch (error) { - logger.error(`SQLite ${method} failed:`, error); - throw new Error(`Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, 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()); // Log startup -logger.log('Script starting...'); +logger.log('[CapacitorSQLite] Preload script starting...'); // Handle window load window.addEventListener('load', () => { - logger.log('Script complete'); + logger.log('[CapacitorSQLite] Preload script complete'); }); diff --git a/src/electron/main.ts b/src/electron/main.ts index 797f5f0f..f799ed14 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -83,20 +83,20 @@ const getAppDataPath = async (): Promise => { if (linuxPath) { // Expand ~ to home directory 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; } // Fallback to app.getPath if config path is not available 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; } 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 const userDataPath = app.getPath("userData"); logger.info( - "[Electron] Using fallback user data path after error:", + "[Electron Main] Using fallback user data path after error:", userDataPath, ); return userDataPath; @@ -123,7 +123,7 @@ const validateAndNormalizePath = async (filePath: string): Promise => { // Normalize the path const normalizedPath = path.normalize(resolvedPath); - logger.info("[Electron] Validated database path:", { + logger.info("[Electron Main] Validated database path:", { original: filePath, resolved: resolvedPath, normalized: normalizedPath, @@ -142,7 +142,7 @@ const ensureDirectoryExists = async (dirPath: string): Promise => { // Check if directory exists 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 }); } @@ -153,11 +153,11 @@ const ensureDirectoryExists = async (dirPath: string): Promise => { fs.constants.R_OK | fs.constants.W_OK, ); logger.info( - "[Electron] Database directory permissions verified:", + "[Electron Main] Database directory permissions verified:", normalizedPath, ); } 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}`); } @@ -167,16 +167,16 @@ const ensureDirectoryExists = async (dirPath: string): Promise => { await fs.promises.writeFile(testFile, "test"); await fs.promises.unlink(testFile); logger.info( - "[Electron] Database directory write test passed:", + "[Electron Main] Database directory write test passed:", normalizedPath, ); } 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}`); } } catch (error) { logger.error( - "[Electron] Failed to ensure database directory exists:", + "[Electron Main] Failed to ensure database directory exists:", error, ); throw error; @@ -203,7 +203,7 @@ const initializeDatabasePaths = async (): Promise => { try { // Get the base directory from config dbDir = await getAppDataPath(); - logger.info("[Electron] Database directory:", dbDir); + logger.info("[Electron Main] Database directory:", dbDir); // Ensure the directory exists and is writable await ensureDirectoryExists(dbDir); @@ -212,7 +212,7 @@ const initializeDatabasePaths = async (): Promise => { dbPath = await validateAndNormalizePath( 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 if (fs.existsSync(dbPath)) { @@ -222,18 +222,18 @@ const initializeDatabasePaths = async (): Promise => { fs.constants.R_OK | fs.constants.W_OK, ); logger.info( - "[Electron] Existing database file permissions verified:", + "[Electron Main] Existing database file permissions verified:", dbPath, ); } 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}`); } } dbPathInitialized = true; } catch (error) { - logger.error("[Electron] Failed to initialize database paths:", error); + logger.error("[Electron Main] Failed to initialize database paths:", error); throw error; } finally { dbPathInitializationPromise = null; @@ -260,7 +260,7 @@ async function initializeSQLite() { sqliteInitializationPromise = (async () => { try { - logger.info("[Electron] Initializing SQLite plugin..."); + logger.info("[Electron Main] Initializing SQLite plugin..."); sqlitePlugin = new CapacitorSQLite(); // Initialize database paths first @@ -272,7 +272,7 @@ async function initializeSQLite() { // Test the plugin 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 const connectionOptions = { @@ -285,14 +285,14 @@ async function initializeSQLite() { }; logger.info( - "[Electron] Creating initial connection with options:", + "[Electron Main] Creating initial connection with options:", connectionOptions, ); // Log the actual path being used - logger.info("[Electron] Using database path:", dbPath); - logger.info("[Electron] Path exists:", fs.existsSync(dbPath)); - logger.info("[Electron] Path is absolute:", path.isAbsolute(dbPath)); + logger.info("[Electron Main] Using database path:", dbPath); + logger.info("[Electron Main] Path exists:", fs.existsSync(dbPath)); + logger.info("[Electron Main] Path is absolute:", path.isAbsolute(dbPath)); const db = await sqlitePlugin.createConnection(connectionOptions); @@ -308,19 +308,19 @@ async function initializeSQLite() { // Verify the connection is working try { 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) { logger.error( - "[Electron] Database connection verification failed:", + "[Electron Main] Database connection verification failed:", error, ); throw error; } sqliteInitialized = true; - logger.info("[Electron] SQLite plugin initialized successfully"); + logger.info("[Electron Main] SQLite plugin initialized successfully"); } catch (error) { - logger.error("[Electron] Failed to initialize SQLite plugin:", error); + logger.error("[Electron Main] Failed to initialize SQLite plugin:", error); throw error; } finally { sqliteInitializationPromise = null; @@ -337,33 +337,53 @@ app.whenReady().then(async () => { // Create window first const mainWindow = createWindow(); - // Initialize database in background - initializeSQLite().catch((error) => { + // Wait for window to be ready + await new Promise((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( - "[Electron] Database initialization failed, but continuing:", + "[Electron Main] Database initialization failed:", error, ); // Notify renderer about database status if (!mainWindow.isDestroyed()) { mainWindow.webContents.send("database-status", { status: "error", - error: error.message, + error: error instanceof Error ? error.message : 'Unknown error', }); } - }); + } // Handle window close mainWindow.on("closed", () => { - logger.info("[Electron] Main window closed"); + logger.info("[Electron Main] Main window closed"); }); // Handle window close request 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 if (mainWindow.webContents.isLoading()) { 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.close(); }); @@ -380,15 +400,15 @@ function createWindow(): BrowserWindow { ? path.join(process.resourcesPath, "preload.js") : path.join(__dirname, "preload.js"); - logger.log("[Electron] Preload path:", preloadPath); - logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath)); + logger.log("[Electron Main] Preload path:", preloadPath); + logger.log("[Electron Main] Preload exists:", fs.existsSync(preloadPath)); // Log environment and paths - logger.log("[Electron] process.cwd():", process.cwd()); - logger.log("[Electron] __dirname:", __dirname); - logger.log("[Electron] app.getAppPath():", app.getAppPath()); - logger.log("[Electron] app.isPackaged:", app.isPackaged); - logger.log("[Electron] process.resourcesPath:", process.resourcesPath); + logger.log("[Electron Main] process.cwd():", process.cwd()); + logger.log("[Electron Main] __dirname:", __dirname); + logger.log("[Electron Main] app.getAppPath():", app.getAppPath()); + logger.log("[Electron Main] app.isPackaged:", app.isPackaged); + logger.log("[Electron Main] process.resourcesPath:", process.resourcesPath); // List files in __dirname and __dirname/www try { @@ -420,24 +440,24 @@ function createWindow(): BrowserWindow { // Show window when ready mainWindow.once("ready-to-show", () => { - logger.info("[Electron] Window ready to show"); + logger.info("[Electron Main] Window ready to show"); mainWindow.show(); }); // Handle window errors 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( "did-fail-load", (_event, errorCode, errorDescription) => { logger.error( - "[Electron] Page failed to load:", + "[Electron Main] Page failed to load:", errorCode, 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"); fileUrl = `file://${indexPath}`; logger.info( - "[Electron] App is packaged. Using process.resourcesPath for index.html", + "[Electron Main] App is packaged. Using process.resourcesPath for index.html", ); } else { indexPath = path.resolve(__dirname, "www", "index.html"); fileUrl = `file://${indexPath}`; 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] Using file URL:", fileUrl); + logger.info("[Electron Main] Resolved index.html path:", indexPath); + logger.info("[Electron Main] Using file URL:", fileUrl); // Load the index.html with retry logic const loadIndexHtml = async (retryCount = 0): Promise => { try { if (mainWindow.isDestroyed()) { logger.error( - "[Electron] Window was destroyed before loading index.html", + "[Electron Main] Window was destroyed before loading index.html", ); return; } 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) { 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 const stats = fs.statSync(indexPath); - logger.info("[Electron] index.html stats:", { + logger.info("[Electron Main] index.html stats:", { size: stats.size, mode: stats.mode, uid: stats.uid, @@ -490,27 +510,27 @@ function createWindow(): BrowserWindow { // Try loadURL first 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); - logger.info("[Electron] Successfully loaded index.html via loadURL"); + logger.info("[Electron Main] Successfully loaded index.html via loadURL"); } catch (loadUrlError) { logger.warn( - "[Electron] loadURL failed, trying loadFile:", + "[Electron Main] loadURL failed, trying loadFile:", loadUrlError, ); // Fallback to loadFile 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) { const errorMessage = 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 if (retryCount < 3 && !mainWindow.isDestroyed()) { 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 return loadIndexHtml(retryCount + 1); @@ -536,7 +556,7 @@ function createWindow(): BrowserWindow { // Start loading the index.html 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 @@ -607,7 +627,7 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => { }; logger.info( - "[Electron] Creating database connection with options:", + "[Electron Main] Creating database connection with options:", 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") { 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"); } } catch (queryError) { - logger.error("[Electron] Error verifying connection:", queryError); + logger.error("[Electron Main] Error verifying connection:", queryError); throw queryError; } - logger.info("[Electron] Database connection created successfully"); + logger.info("[Electron Main] Database connection created successfully"); return result; } catch (error) { - logger.error("[Electron] Error in sqlite-create-connection:", error); + logger.error("[Electron Main] Error in sqlite-create-connection:", error); throw error; } }); diff --git a/src/electron/preload.js b/src/electron/preload.js index c7ffdffc..7168d4f0 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -5,27 +5,27 @@ const logger = { // Always log in development, log with context in production if (process.env.NODE_ENV !== "production") { /* eslint-disable no-console */ - console.log(`[Preload] ${message}`, ...args); + console.log(`[Electron Preload] ${message}`, ...args); /* eslint-enable no-console */ } }, warn: (message, ...args) => { // Always log warnings /* eslint-disable no-console */ - console.warn(`[Preload] ${message}`, ...args); + console.warn(`[Electron Preload] ${message}`, ...args); /* eslint-enable no-console */ }, error: (message, ...args) => { // Always log errors /* eslint-disable no-console */ - console.error(`[Preload] ${message}`, ...args); + console.error(`[Electron Preload] ${message}`, ...args); /* eslint-enable no-console */ }, info: (message, ...args) => { // Always log info in development, log with context in production if (process.env.NODE_ENV !== "production") { /* eslint-disable no-console */ - console.info(`[Preload] ${message}`, ...args); + console.info(`[Electron Preload] ${message}`, ...args); /* eslint-enable no-console */ } }, @@ -143,32 +143,33 @@ try { }; // Expose only the CapacitorSQLite proxy - contextBridge.exposeInMainWorld("CapacitorSQLite", createSQLiteProxy()); - - // Remove the duplicate electron.sqlite bridge contextBridge.exposeInMainWorld("electron", { - // Keep other electron APIs but remove sqlite - getPath, + sqlite: createSQLiteProxy(), + getPath: (pathType) => ipcRenderer.invoke("get-path", pathType), send: (channel, data) => { - const validChannels = ["toMain"]; - if (validChannels.includes(channel)) { - ipcRenderer.send(channel, data); - } + ipcRenderer.send(channel, data); }, receive: (channel, func) => { - const validChannels = ["fromMain"]; - if (validChannels.includes(channel)) { + ipcRenderer.on(channel, (event, ...args) => func(...args)); + }, + ipcRenderer: { + on: (channel, func) => { 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: { - isElectron: true, - isDev: process.env.NODE_ENV === "development", platform: "electron", }, - getBasePath: () => { - return process.env.NODE_ENV === "development" ? "/" : "./"; - }, + getBasePath: () => ipcRenderer.invoke("get-base-path"), }); logger.info("Preload script completed successfully"); diff --git a/src/main.electron.ts b/src/main.electron.ts index e7f3cceb..56bf9b39 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -1,75 +1,65 @@ import { initializeApp } from "./main.common"; 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 pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; -logger.info("[Electron] Initializing app"); -logger.info("[Electron] Platform:", { platform }); -logger.info("[Electron] PWA enabled:", { pwa_enabled }); +logger.info("[Main Electron] Initializing app"); +logger.info("[Main Electron] Platform:", { platform }); +logger.info("[Main Electron] PWA enabled:", { 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 const app = initializeApp(); -// Initialize SQLite first, then mount the app -initializeSQLite() +// Create a promise that resolves when SQLite is ready +const sqliteReady = new Promise((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(() => { - logger.info("[Electron] SQLite initialized, mounting app..."); + logger.info("[Main Electron] SQLite ready, mounting app..."); app.mount("#app"); }) .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 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%;"; errorDiv.innerHTML = ` -

Failed to Initialize Database

-

There was an error initializing the database. Please try restarting the application.

-

Error details: ${error.message}

+

Failed to Initialize Application

+

There was an error initializing the database. Please try restarting.

+

Error details: ${error instanceof Error ? error.message : 'Unknown error'}

`; document.body.appendChild(errorDiv); }); diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 28ba5f7a..65a61574 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -1,5 +1,13 @@ import { QueryExecResult } from "@/interfaces/database"; +/** + * Query execution result interface + */ +export interface QueryExecResult { + columns: string[]; + values: T[]; +} + /** * Represents the result of an image capture or selection operation. * Contains both the image data as a Blob and the associated filename. @@ -102,15 +110,12 @@ export interface PlatformService { handleDeepLink(url: string): Promise; /** - * Executes a SQL query on the database. - * @param sql - The SQL query to execute - * @param params - The parameters to pass to the query - * @returns Promise resolving to the query result + * Execute a database query and return the results + * @param sql SQL query to execute + * @param params Query parameters + * @returns Query results with columns and values */ - dbQuery( - sql: string, - params?: unknown[], - ): Promise; + dbQuery(sql: string, params?: unknown[]): Promise>; /** * Executes a create/update/delete on the database. diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 3ba1be0c..5caafa8a 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -2,13 +2,37 @@ import { ImageResult, PlatformService, PlatformCapabilities, + QueryExecResult, } from "../PlatformService"; 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 { 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; + }; + sqlite: { + isAvailable: () => Promise; + execute: (method: string, ...args: unknown[]) => Promise; + }; + getPath: (pathType: string) => Promise; + send: (channel: string, data: unknown) => void; + receive: (channel: string, func: (...args: unknown[]) => void) => void; + env: { + platform: string; + }; + getBasePath: () => Promise; + }; + } +} + interface Migration { name: string; sql: string; @@ -24,121 +48,78 @@ interface Migration { */ export class ElectronPlatformService implements PlatformService { private sqlite: any; - private connection: SQLiteDBConnection | null = null; - private connectionPool: DatabaseConnectionPool; - private initializationPromise: Promise | null = null; + private dbName = "timesafari"; + private isInitialized = false; private dbFatalError = false; + private sqliteReadyPromise: Promise | null = null; constructor() { - this.connectionPool = DatabaseConnectionPool.getInstance(); - if (!window.CapacitorSQLite) { - throw new Error("CapacitorSQLite not initialized in Electron"); - } - this.sqlite = window.CapacitorSQLite; + this.sqliteReadyPromise = new Promise((resolve, reject) => { + if (!window.electron?.ipcRenderer) { + logger.warn('[ElectronPlatformService] IPC renderer not available'); + reject(new Error('IPC renderer not available')); + 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 { - // If we already have a connection, return immediately - if (this.connection) { - return; - } - - // If initialization is in progress, wait for it - if (this.initializationPromise) { - return this.initializationPromise; - } - - // Start initialization - this.initializationPromise = (async () => { - try { - if (!this.sqlite) { - logger.debug("[ElectronPlatformService] SQLite plugin not available, checking..."); - this.sqlite = await import("@capacitor-community/sqlite"); - } - - if (!this.sqlite) { - 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((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; + if (this.isInitialized) return; + if (this.sqliteReadyPromise) await this.sqliteReadyPromise; + this.sqlite = window.CapacitorSQLite; + if (!this.sqlite) throw new Error("CapacitorSQLite not available"); + // Create the connection (idempotent) + await this.sqlite.createConnection({ + database: this.dbName, + encrypted: false, + mode: "no-encryption", + readOnly: false, + }); + // Optionally, test the connection + await this.sqlite.query({ + database: this.dbName, + statement: "SELECT 1" + }); + // Run migrations if needed + await this.runMigrations(); + logger.info("[ElectronPlatformService] Database initialized successfully"); } private async runMigrations(): Promise { - if (!this.connection) { - throw new Error("Database not initialized"); - } - // Create migrations table if it doesn't exist - await this.connection.execute(` - CREATE TABLE IF NOT EXISTS migrations ( + await this.sqlite.execute({ + database: this.dbName, + statements: `CREATE TABLE IF NOT EXISTS migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - + );` + }); // 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( - result.values?.map((row) => row[0]) || [], + (result.values as unknown[][])?.map((row: unknown[]) => row[0] as string) || [] ); - // Run pending migrations in order const migrations: Migration[] = [ { @@ -226,13 +207,17 @@ export class ElectronPlatformService implements PlatformService { `, }, ]; - for (const migration of migrations) { if (!executedMigrations.has(migration.name)) { - await this.connection.execute(migration.sql); - await this.connection.run("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); + await this.sqlite.execute({ + database: this.dbName, + 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`); } } @@ -343,43 +328,32 @@ export class ElectronPlatformService implements PlatformService { /** * @see PlatformService.dbQuery */ - async dbQuery( - sql: string, - params?: unknown[], - ): Promise { - if (this.dbFatalError) { - throw new Error("Database is in a fatal error state. Please restart the app."); - } - + async dbQuery(sql: string, params: unknown[] = []): Promise> { await this.initializeDatabase(); - if (!this.connection) { - throw new Error("Database not initialized"); - } - - const result = await this.connection.query(sql, params); + if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app."); + const result = await this.sqlite.query({ + database: this.dbName, + statement: sql, + values: params + }); + const columns = result.values?.[0] ? Object.keys(result.values[0]) : []; return { - columns: [], // SQLite plugin doesn't provide column names - values: result.values || [], + columns, + values: (result.values || []).map((row: Record) => row as T) }; } /** * @see PlatformService.dbExec */ - async dbExec( - 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."); - } - + async dbExec(sql: string, params?: unknown[]): Promise<{ changes: number; lastId?: number }> { await this.initializeDatabase(); - if (!this.connection) { - throw new Error("Database not initialized"); - } - - const result = await this.connection.run(sql, params); + if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app."); + const result = await this.sqlite.run({ + database: this.dbName, + statement: sql, + values: params + }); return { changes: result.changes?.changes || 0, lastId: result.changes?.lastId, @@ -387,50 +361,20 @@ export class ElectronPlatformService implements PlatformService { } async initialize(): Promise { - if (this.dbFatalError) { - throw new Error("Database is in a fatal error state. Please restart the app."); - } await this.initializeDatabase(); } - async query(sql: string, params: any[] = []): Promise { - 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 { - 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"); - } - - await this.connection.run(sql, params); + if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app."); + await this.sqlite.run({ + database: this.dbName, + statement: sql, + values: params + }); } async close(): Promise { - if (!this.connection) { - return; - } - - try { - await this.connectionPool.releaseConnection("timesafari"); - this.connection = null; - } catch (error) { - logger.error("Failed to close database:", error); - throw error; - } + // Optionally implement close logic if needed } } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts new file mode 100644 index 00000000..3096a809 --- /dev/null +++ b/src/types/electron.d.ts @@ -0,0 +1,27 @@ +interface ElectronAPI { + sqlite: { + isAvailable: () => Promise; + execute: (method: string, ...args: unknown[]) => Promise; + }; + getPath: (pathType: string) => Promise; + 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; + }; + env: { + platform: string; + }; + getBasePath: () => Promise; +} + +declare global { + interface Window { + electron: ElectronAPI; + } +} + +export {}; \ No newline at end of file