Browse Source

WIP: Refactor Electron SQLite initialization and database path handling

- Add logic in main process to resolve and create the correct database directory and file path using Electron's app, path, and fs modules
- Pass absolute dbPath to CapacitorSQLite plugin for reliable database creation
- Add extensive logging for debugging database location, permissions, and initialization
- Remove redundant open() call after createConnection in Electron platform service
- Add IPC handlers for essential SQLite operations (echo, createConnection, execute, query, closeConnection, isAvailable)
- Improve error handling and logging throughout initialization and IPC
- Still investigating database file creation and permissions issues
pull/134/head
Matthew Raymer 1 week ago
parent
commit
a5a9e15ece
  1. 2
      experiment.sh
  2. 66
      src/electron/main.ts
  3. 108
      src/electron/preload.js
  4. 2
      src/main.electron.ts
  5. 195
      src/services/platforms/ElectronPlatformService.ts
  6. 22
      src/types/global.d.ts

2
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*

66
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;
});

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

2
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;

195
src/services/platforms/ElectronPlatformService.ts

@ -30,35 +30,62 @@ export class ElectronPlatformService implements PlatformService {
private dbName = "timesafari.db";
private initialized = false;
private initializationPromise: Promise<void> | 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<T>(operation: () => Promise<T>): Promise<T> {
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<void> {
// 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<void> {
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<T>(sql: string, params: any[] = []): Promise<T[]> {
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<void> {
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<void> {
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'}`);
}
}
}

22
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<any>;
closeConnection: (options: any) => Promise<any>;
execute: (options: any) => Promise<any>;
query: (options: any) => Promise<any>;
run: (options: any) => Promise<any>;
isAvailable: () => Promise<boolean>;
getPlatform: () => Promise<string>;
};
electron: {
sqlite: {
isAvailable: () => Promise<boolean>;
execute: (method: string, ...args: unknown[]) => Promise<unknown>;
};
// 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;
}
}
}

Loading…
Cancel
Save