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
This commit is contained in:
@@ -46,47 +46,66 @@ if (electronIsDev) {
|
|||||||
// 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.
|
|
||||||
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) {
|
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
|
|
||||||
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();
|
app.quit();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Handle when all of our windows are close (platforms have their own expectations).
|
// Handle when all of our windows are close (platforms have their own expectations).
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"];
|
|
||||||
if (validChannels.includes(channel)) {
|
|
||||||
ipcRenderer.send(channel, data);
|
ipcRenderer.send(channel, data);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
receive: (channel, func) => {
|
receive: (channel, func) => {
|
||||||
const validChannels = ["fromMain"];
|
|
||||||
if (validChannels.includes(channel)) {
|
|
||||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
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: {
|
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");
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.sqlite = window.CapacitorSQLite;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
encrypted: false,
|
||||||
mode: "no-encryption",
|
mode: "no-encryption",
|
||||||
readonly: false,
|
readOnly: false,
|
||||||
});
|
});
|
||||||
|
// Optionally, test the connection
|
||||||
// Wait for the connection to be fully initialized
|
await this.sqlite.query({
|
||||||
await new Promise<void>((resolve, reject) => {
|
database: this.dbName,
|
||||||
const checkConnection = async () => {
|
statement: "SELECT 1"
|
||||||
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
|
// Run migrations if needed
|
||||||
await this.runMigrations();
|
await this.runMigrations();
|
||||||
|
|
||||||
logger.info("[ElectronPlatformService] Database initialized successfully");
|
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
vendored
Normal file
27
src/types/electron.d.ts
vendored
Normal file
@@ -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 {};
|
||||||
Reference in New Issue
Block a user