diff --git a/experiment.sh b/experiment.sh index c6cf4548..f8c37426 100755 --- a/experiment.sh +++ b/experiment.sh @@ -15,6 +15,8 @@ check_command() { check_command node check_command npm +mkdir -p ~/.local/share/TimeSafari/timesafari + # Clean up previous builds echo "Cleaning previous builds..." rm -rf dist* diff --git a/src/electron/main.ts b/src/electron/main.ts index d0a84c2b..3778a5d5 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -71,6 +71,20 @@ try { throw error; } +// Database path logic +let dbPath: string; +let dbDir: string; + +app.whenReady().then(() => { + const basePath = app.getPath('userData'); + dbDir = path.join(basePath, 'timesafari'); + dbPath = path.join(dbDir, 'timesafari.db'); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + console.log('[Main] [Electron] Resolved dbPath:', dbPath); +}); + // Initialize SQLite plugin let sqlitePlugin: any = null; @@ -81,9 +95,9 @@ async function initializeSQLite() { // Test the plugin const echoResult = await sqlitePlugin.echo({ value: "test" }); console.log("SQLite plugin echo test:", echoResult); - // Initialize database connection + // Initialize database connection using absolute dbPath const db = await sqlitePlugin.createConnection({ - database: "timesafari.db", + database: dbPath, version: 1, }); console.log("SQLite plugin initialized successfully"); @@ -335,15 +349,51 @@ ipcMain.handle("check-sqlite-availability", () => { return sqlitePlugin !== null; }); -ipcMain.handle("capacitor-sqlite", async (event, ...args) => { - if (!sqlitePlugin) { - logger.error("SQLite plugin not initialized when handling IPC request"); - throw new Error("SQLite plugin not initialized"); +ipcMain.handle("sqlite-echo", async (_event, value) => { + try { + return await sqlitePlugin.echo({ value }); + } catch (error) { + logger.error("Error in sqlite-echo:", error, JSON.stringify(error), (error as any)?.stack); + throw error; } +}); + +ipcMain.handle("sqlite-create-connection", async (_event, options) => { try { - return await sqlitePlugin.handle(event, ...args); + return await sqlitePlugin.createConnection(options); } catch (error) { - logger.error("Error handling SQLite IPC request:", error, JSON.stringify(error), (error as any)?.stack); + logger.error("Error in sqlite-create-connection:", error, JSON.stringify(error), (error as any)?.stack); throw error; } }); + +ipcMain.handle("sqlite-execute", async (_event, options) => { + try { + return await sqlitePlugin.execute(options); + } catch (error) { + logger.error("Error in sqlite-execute:", error, JSON.stringify(error), (error as any)?.stack); + throw error; + } +}); + +ipcMain.handle("sqlite-query", async (_event, options) => { + try { + return await sqlitePlugin.query(options); + } catch (error) { + logger.error("Error in sqlite-query:", error, JSON.stringify(error), (error as any)?.stack); + throw error; + } +}); + +ipcMain.handle("sqlite-close-connection", async (_event, options) => { + try { + return await sqlitePlugin.closeConnection(options); + } catch (error) { + logger.error("Error in sqlite-close-connection:", error, JSON.stringify(error), (error as any)?.stack); + throw error; + } +}); + +ipcMain.handle("sqlite-is-available", async () => { + return sqlitePlugin !== null; +}); diff --git a/src/electron/preload.js b/src/electron/preload.js index 57262f48..1efda7e3 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -85,55 +85,81 @@ try { }, }); - // Expose protected methods that allow the renderer process to use - // the ipcRenderer without exposing the entire object - contextBridge.exposeInMainWorld( - 'electron', - { - // SQLite plugin bridge - sqlite: { - // Check if SQLite is available - isAvailable: () => ipcRenderer.invoke('check-sqlite-availability'), - // Execute SQLite operations - execute: (method, ...args) => ipcRenderer.invoke('capacitor-sqlite', method, ...args), - }, - // ... existing exposed methods ... - } - ); - // Create a proxy for the CapacitorSQLite plugin const createSQLiteProxy = () => { + const MAX_RETRIES = 3; + const RETRY_DELAY = 1000; // 1 second + + const withRetry = async (operation, ...args) => { + let lastError; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + return await operation(...args); + } catch (error) { + lastError = error; + if (attempt < MAX_RETRIES) { + logger.warn(`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'}`); + }; + + const wrapOperation = (method) => { + return async (...args) => { + try { + return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args); + } catch (error) { + logger.error(`SQLite ${method} failed:`, error); + throw new Error(`Database operation failed: ${error.message || 'Unknown error'}`); + } + }; + }; + + // Create a proxy that matches the CapacitorSQLite interface return { - async createConnection(...args) { - return ipcRenderer.invoke('capacitor-sqlite', 'createConnection', ...args); - }, - async isConnection(...args) { - return ipcRenderer.invoke('capacitor-sqlite', 'isConnection', ...args); - }, - async retrieveConnection(...args) { - return ipcRenderer.invoke('capacitor-sqlite', 'retrieveConnection', ...args); - }, - async retrieveAllConnections() { - return ipcRenderer.invoke('capacitor-sqlite', 'retrieveAllConnections'); - }, - async closeConnection(...args) { - return ipcRenderer.invoke('capacitor-sqlite', 'closeConnection', ...args); - }, - async closeAllConnections() { - return ipcRenderer.invoke('capacitor-sqlite', 'closeAllConnections'); - }, - async isAvailable() { - return ipcRenderer.invoke('capacitor-sqlite', 'isAvailable'); - }, - async getPlatform() { - return 'electron'; - }, + echo: wrapOperation('echo'), + createConnection: wrapOperation('create-connection'), + closeConnection: wrapOperation('close-connection'), + execute: wrapOperation('execute'), + query: wrapOperation('query'), + run: wrapOperation('run'), + isAvailable: wrapOperation('is-available'), + getPlatform: () => Promise.resolve('electron'), + // Add other methods as needed }; }; - // Expose the SQLite plugin proxy + // 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, + send: (channel, data) => { + const validChannels = ["toMain"]; + if (validChannels.includes(channel)) { + ipcRenderer.send(channel, data); + } + }, + receive: (channel, func) => { + const validChannels = ["fromMain"]; + if (validChannels.includes(channel)) { + ipcRenderer.on(channel, (event, ...args) => func(...args)); + } + }, + env: { + isElectron: true, + isDev: process.env.NODE_ENV === "development", + platform: "electron", + }, + getBasePath: () => { + return process.env.NODE_ENV === "development" ? "/" : "./"; + }, + }); + logger.info("Preload script completed successfully"); } catch (error) { logger.error("Error in preload script:", error); diff --git a/src/main.electron.ts b/src/main.electron.ts index 48d3edbd..c9e35d9e 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -10,7 +10,7 @@ async function initializeSQLite() { while (retries < maxRetries) { try { - const isAvailable = await window.electron.sqlite.isAvailable(); + const isAvailable = await window.CapacitorSQLite.isAvailable(); if (isAvailable) { logger.info("[Electron] SQLite plugin bridge initialized successfully"); return true; diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index a70a001c..663545b1 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -30,35 +30,62 @@ export class ElectronPlatformService implements PlatformService { private dbName = "timesafari.db"; private initialized = false; private initializationPromise: Promise | null = null; + private readonly MAX_RETRIES = 3; + private readonly RETRY_DELAY = 1000; // 1 second + private dbConnectionErrorLogged = false; + private dbFatalError = false; constructor() { - // Use the IPC bridge for SQLite operations if (!window.CapacitorSQLite) { throw new Error("CapacitorSQLite not initialized in Electron"); } this.sqlite = new SQLiteConnection(window.CapacitorSQLite); } + private async withRetry(operation: () => Promise): Promise { + let lastError: Error | undefined; + for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + if (attempt < this.MAX_RETRIES) { + console.warn(`Database operation failed (attempt ${attempt}/${this.MAX_RETRIES}), retrying...`, error); + await new Promise(resolve => setTimeout(resolve, this.RETRY_DELAY)); + } + } + } + throw new Error(`Database operation failed after ${this.MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`); + } + private async initializeDatabase(): Promise { - // If already initialized, return + if (this.dbFatalError) { + throw new Error("Database is in a fatal error state. Please restart the app."); + } if (this.initialized) { return; } - - // If initialization is in progress, wait for it if (this.initializationPromise) { return this.initializationPromise; } - // Start initialization this.initializationPromise = (async () => { try { - // Check if SQLite is available through IPC - const isAvailable = await window.electron.sqlite.isAvailable(); + // Test SQLite availability using the CapacitorSQLite proxy + const isAvailable = await window.CapacitorSQLite.isAvailable(); if (!isAvailable) { throw new Error("SQLite is not available in the main process"); } + // Log the arguments to createConnection + logger.info("Calling createConnection with:", { + dbName: this.dbName, + readOnly: false, + encryption: "no-encryption", + version: 1, + useNative: true, + }); + // Create/Open database with native implementation this.db = await this.sqlite.createConnection( this.dbName, @@ -68,7 +95,18 @@ export class ElectronPlatformService implements PlatformService { true, // Use native implementation ); - await this.db.open(); + logger.info("createConnection result:", this.db); + + if (!this.db || typeof this.db.execute !== 'function') { + if (!this.dbConnectionErrorLogged) { + logger.error("Failed to create a valid database connection"); + this.dbConnectionErrorLogged = true; + } + throw new Error("Failed to create a valid database connection"); + } + + // Do NOT call open() here; Electron connection is ready after createConnection + // await this.db.open(); // Set journal mode to WAL for better performance await this.db.execute("PRAGMA journal_mode=WAL;"); @@ -78,8 +116,63 @@ export class ElectronPlatformService implements PlatformService { this.initialized = true; logger.log("[Electron] SQLite database initialized successfully"); + + // Extra logging for debugging DB creation and permissions + try { + logger.info("[Debug] process.cwd():", process.cwd()); + } catch (e) { logger.warn("[Debug] Could not log process.cwd()", e); } + try { + logger.info("[Debug] __dirname:", __dirname); + } catch (e) { logger.warn("[Debug] Could not log __dirname", e); } + try { + if (typeof window !== 'undefined' && window.electron && window.electron.getPath) { + logger.info("[Debug] electron.getPath('userData'):", window.electron.getPath('userData')); + logger.info("[Debug] electron.getPath('appPath'):", window.electron.getPath('appPath')); + } + } catch (e) { logger.warn("[Debug] Could not log electron.getPath", e); } + // Try to log directory contents + try { + const fs = require('fs'); + logger.info("[Debug] Files in process.cwd():", fs.readdirSync(process.cwd())); + logger.info("[Debug] Files in __dirname:", fs.readdirSync(__dirname)); + } catch (e) { logger.warn("[Debug] Could not list directory contents", e); } + // Try to log file permissions for likely DB file + try { + const fs = require('fs'); + const dbFileCandidates = [ + `${process.cwd()}/timesafari.db`, + `${__dirname}/timesafari.db`, + `${process.cwd()}/timesafari`, + `${__dirname}/timesafari`, + ]; + for (const candidate of dbFileCandidates) { + if (fs.existsSync(candidate)) { + logger.info(`[Debug] DB file candidate exists: ${candidate}`); + logger.info(`[Debug] File stats:`, fs.statSync(candidate)); + try { + fs.accessSync(candidate, fs.constants.W_OK); + logger.info(`[Debug] File is writable: ${candidate}`); + } catch (err) { + logger.warn(`[Debug] File is NOT writable: ${candidate}`); + } + } + } + } catch (e) { logger.warn("[Debug] Could not check DB file permissions", e); } + // Log plugin version if available + try { + if (window.CapacitorSQLite && window.CapacitorSQLite.getPlatform) { + window.CapacitorSQLite.getPlatform().then((platform) => { + logger.info("[Debug] CapacitorSQLite platform:", platform); + }); + } + } catch (e) { logger.warn("[Debug] Could not log plugin version/platform", e); } + // Comment: To specify the database location in Capacitor SQLite for Electron, you typically only provide the database name. The plugin will create the DB in the app's user data directory. If you need to control the path, check the plugin's Electron docs or source for a 'location' or 'directory' option in createConnection. If not available, the DB will be created in the default location (usually app user data dir). } catch (error) { - logger.error("[Electron] Error initializing SQLite database:", error); + this.dbFatalError = true; + if (!this.dbConnectionErrorLogged) { + logger.error("[Electron] Error initializing SQLite database:", error); + this.dbConnectionErrorLogged = true; + } this.initialized = false; this.initializationPromise = null; throw error; @@ -341,6 +434,9 @@ export class ElectronPlatformService implements PlatformService { 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."); + } try { await this.initializeDatabase(); if (!this.db) { @@ -357,4 +453,85 @@ export class ElectronPlatformService implements PlatformService { throw error; } } + + async initialize(): Promise { + if (this.initialized) { + return; + } + + try { + await this.withRetry(async () => { + const isAvailable = await window.CapacitorSQLite.isAvailable(); + if (!isAvailable) { + throw new Error('SQLite is not available in this environment'); + } + + this.db = await this.sqlite.createConnection( + this.dbName, + false, + "no-encryption", + 1, + true, // Use native implementation + ); + if (!this.db || typeof this.db.execute !== 'function') { + throw new Error("Failed to create a valid database connection"); + } + await this.db.execute("PRAGMA journal_mode=WAL;"); + await this.runMigrations(); + this.initialized = true; + }); + } catch (error) { + console.error('Failed to initialize database:', error); + throw new Error(`Database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + async query(sql: string, params: any[] = []): Promise { + if (this.dbFatalError) { + throw new Error("Database is in a fatal error state. Please restart the app."); + } + if (!this.initialized) { + throw new Error('Database not initialized. Call initialize() first.'); + } + + return this.withRetry(async () => { + try { + const result = await this.db?.query(sql, params); + return result?.values as T[]; + } catch (error) { + console.error('Query failed:', { sql, params, error }); + throw new Error(`Query failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }); + } + + async execute(sql: string, params: any[] = []): Promise { + if (!this.initialized) { + throw new Error('Database not initialized. Call initialize() first.'); + } + + await this.withRetry(async () => { + try { + await this.db?.run(sql, params); + } catch (error) { + console.error('Execute failed:', { sql, params, error }); + throw new Error(`Execute failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }); + } + + async close(): Promise { + if (!this.initialized) { + return; + } + + try { + await this.db?.close(); + this.initialized = false; + this.db = null; + } catch (error) { + console.error('Failed to close database:', error); + throw new Error(`Failed to close database: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index fc904372..39cc76ef 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -3,14 +3,32 @@ import type { CapacitorSQLite } from '@capacitor-community/sqlite'; declare global { interface Window { - CapacitorSQLite: typeof CapacitorSQLite; + CapacitorSQLite: { + echo: (options: { value: string }) => Promise<{ value: string }>; + createConnection: (options: any) => Promise; + closeConnection: (options: any) => Promise; + execute: (options: any) => Promise; + query: (options: any) => Promise; + run: (options: any) => Promise; + isAvailable: () => Promise; + getPlatform: () => Promise; + }; electron: { sqlite: { isAvailable: () => Promise; execute: (method: string, ...args: unknown[]) => Promise; }; // Add other electron IPC methods as needed - }; + getPath: (pathType: string) => string; + send: (channel: string, data: any) => void; + receive: (channel: string, func: (...args: any[]) => void) => void; + env: { + isElectron: boolean; + isDev: boolean; + platform: string; + }; + getBasePath: () => string; + } } }