Browse Source

fix(electron): assign sqlitePlugin globally and improve error logging

- Assign CapacitorSQLite instance to the global sqlitePlugin variable so it is accessible in IPC handlers.
- Enhance error logging in the IPC handler to include JSON stringification and stack trace for better debugging.
- Reveal that the generic .handle() method is not available on the plugin, clarifying the next steps for correct IPC wiring.
pull/134/head
Matthew Raymer 1 week ago
parent
commit
a6d8f0eb8a
  1. 4
      package.json
  2. 4
      scripts/build-electron.js
  3. 84
      src/electron/main.ts
  4. 50
      src/main.electron.ts
  5. 124
      src/services/platforms/ElectronPlatformService.ts

4
package.json

@ -190,8 +190,8 @@
"to": "www" "to": "www"
}, },
{ {
"from": "dist-electron/resources/preload.mjs", "from": "dist-electron/resources/preload.js",
"to": "preload.mjs" "to": "preload.js"
} }
], ],
"linux": { "linux": {

4
scripts/build-electron.js

@ -49,8 +49,8 @@ indexContent = indexContent.replace(
fs.writeFileSync(finalIndexPath, indexContent); fs.writeFileSync(finalIndexPath, indexContent);
// Copy preload script to resources // Copy preload script to resources
const preloadSrc = path.join(electronDistPath, "preload.mjs"); const preloadSrc = path.join(electronDistPath, "preload.js");
const preloadDest = path.join(electronDistPath, "resources", "preload.mjs"); const preloadDest = path.join(electronDistPath, "resources", "preload.js");
// Ensure resources directory exists // Ensure resources directory exists
const resourcesDir = path.join(electronDistPath, "resources"); const resourcesDir = path.join(electronDistPath, "resources");

84
src/electron/main.ts

@ -8,6 +8,13 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
// Set global variables that the plugin needs
global.__dirname = __dirname;
global.__filename = __filename;
// Now import the plugin after setting up globals
import { CapacitorSQLite } from "@capacitor-community/sqlite/electron/dist/plugin.js";
// Simple logger implementation // Simple logger implementation
const logger = { const logger = {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -69,44 +76,35 @@ let sqlitePlugin: any = null;
async function initializeSQLite() { async function initializeSQLite() {
try { try {
// Import the plugin directly in the main process console.log("Initializing SQLite plugin...");
const plugin = await import("@capacitor-community/sqlite/electron/dist/plugin.js"); sqlitePlugin = new CapacitorSQLite();
sqlitePlugin = new plugin.CapacitorSQLite(); // Test the plugin
const echoResult = await sqlitePlugin.echo({ value: "test" });
// Test the plugin with a simple operation console.log("SQLite plugin echo test:", echoResult);
const result = await sqlitePlugin.echo({ value: "test" }); // Initialize database connection
if (result.value !== "test") { const db = await sqlitePlugin.createConnection({
throw new Error("SQLite plugin echo test failed"); database: "timesafari.db",
} version: 1,
});
logger.info("SQLite plugin initialized successfully"); console.log("SQLite plugin initialized successfully");
return db;
} catch (error) { } catch (error) {
logger.error("Failed to initialize SQLite plugin:", error); console.error("Failed to initialize SQLite plugin:", error);
throw error; throw error;
} }
} }
// Initialize SQLite plugin
initializeSQLite().catch(error => {
logger.error("Failed to initialize SQLite plugin:", error);
});
// Set up IPC handlers for SQLite operations
ipcMain.handle("check-sqlite-availability", () => {
return sqlitePlugin !== null;
});
ipcMain.handle("capacitor-sqlite", async (event, ...args) => {
if (!sqlitePlugin) {
throw new Error("SQLite plugin not initialized");
}
return sqlitePlugin.handle(event, ...args);
});
// Initialize app when ready // Initialize app when ready
app.whenReady().then(() => { app.whenReady().then(async () => {
logger.info("App is ready, creating window..."); logger.info("App is ready, initializing SQLite...");
createWindow(); try {
await initializeSQLite();
logger.info("SQLite initialized, creating window...");
createWindow();
} catch (error) {
logger.error("Failed to initialize app:", error);
app.quit();
}
}); });
// Check if running in dev mode // Check if running in dev mode
@ -115,8 +113,8 @@ const isDev = process.argv.includes("--inspect");
function createWindow(): void { function createWindow(): void {
// Resolve preload path based on environment // Resolve preload path based on environment
const preloadPath = app.isPackaged const preloadPath = app.isPackaged
? path.join(process.resourcesPath, "preload.mjs") ? path.join(process.resourcesPath, "preload.js")
: path.join(__dirname, "preload.mjs"); : path.join(__dirname, "preload.js");
logger.log("[Electron] Preload path:", preloadPath); logger.log("[Electron] Preload path:", preloadPath);
logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath)); logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath));
@ -331,3 +329,21 @@ app.on("activate", () => {
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error); logger.error("Uncaught Exception:", error);
}); });
// Set up IPC handlers for SQLite operations
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");
}
try {
return await sqlitePlugin.handle(event, ...args);
} catch (error) {
logger.error("Error handling SQLite IPC request:", error, JSON.stringify(error), (error as any)?.stack);
throw error;
}
});

50
src/main.electron.ts

@ -3,11 +3,29 @@ import { logger } from "./utils/logger";
async function initializeSQLite() { async function initializeSQLite() {
try { try {
const isAvailable = await window.electron.sqlite.isAvailable(); // Wait for SQLite to be available in the main process
if (!isAvailable) { let retries = 0;
throw new Error("SQLite plugin not available in main process"); const maxRetries = 5;
const retryDelay = 1000; // 1 second
while (retries < maxRetries) {
try {
const isAvailable = await window.electron.sqlite.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));
}
} }
logger.info("[Electron] SQLite plugin bridge initialized");
throw new Error("SQLite plugin not available after maximum retries");
} catch (error) { } catch (error) {
logger.error("[Electron] Failed to initialize SQLite plugin bridge:", error); logger.error("[Electron] Failed to initialize SQLite plugin bridge:", error);
throw error; throw error;
@ -27,8 +45,22 @@ if (pwa_enabled) {
// Initialize app and SQLite // Initialize app and SQLite
const app = initializeApp(); const app = initializeApp();
initializeSQLite().then(() => {
app.mount("#app"); // Initialize SQLite first, then mount the app
}).catch(error => { initializeSQLite()
logger.error("[Electron] Failed to initialize app:", error); .then(() => {
}); logger.info("[Electron] SQLite initialized, mounting app...");
app.mount("#app");
})
.catch(error => {
logger.error("[Electron] Failed to initialize app:", error);
// Show error to user
const errorDiv = document.createElement("div");
errorDiv.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%;";
errorDiv.innerHTML = `
<h2>Failed to Initialize Database</h2>
<p>There was an error initializing the database. Please try restarting the application.</p>
<p>Error details: ${error.message}</p>
`;
document.body.appendChild(errorDiv);
});

124
src/services/platforms/ElectronPlatformService.ts

@ -4,11 +4,10 @@ import {
PlatformCapabilities, PlatformCapabilities,
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { QueryExecResult, SqlValue } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
import { import {
SQLiteConnection, SQLiteConnection,
SQLiteDBConnection, SQLiteDBConnection,
Changes,
} from "@capacitor-community/sqlite"; } from "@capacitor-community/sqlite";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
@ -30,6 +29,7 @@ export class ElectronPlatformService implements PlatformService {
private db: SQLiteDBConnection | null = null; private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.db"; private dbName = "timesafari.db";
private initialized = false; private initialized = false;
private initializationPromise: Promise<void> | null = null;
constructor() { constructor() {
// Use the IPC bridge for SQLite operations // Use the IPC bridge for SQLite operations
@ -40,40 +40,53 @@ export class ElectronPlatformService implements PlatformService {
} }
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
// If already initialized, return
if (this.initialized) { if (this.initialized) {
return; return;
} }
try { // If initialization is in progress, wait for it
// Check if SQLite is available through IPC if (this.initializationPromise) {
const isAvailable = await window.electron.sqlite.isAvailable(); return this.initializationPromise;
if (!isAvailable) { }
throw new Error("SQLite is not available in the main process");
}
// Create/Open database with native implementation
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
true, // Use native implementation
);
await this.db.open();
// Set journal mode to WAL for better performance
await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations // Start initialization
await this.runMigrations(); this.initializationPromise = (async () => {
try {
// Check if SQLite is available through IPC
const isAvailable = await window.electron.sqlite.isAvailable();
if (!isAvailable) {
throw new Error("SQLite is not available in the main process");
}
// Create/Open database with native implementation
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
true, // Use native implementation
);
await this.db.open();
// Set journal mode to WAL for better performance
await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations
await this.runMigrations();
this.initialized = true;
logger.log("[Electron] SQLite database initialized successfully");
} catch (error) {
logger.error("[Electron] Error initializing SQLite database:", error);
this.initialized = false;
this.initializationPromise = null;
throw error;
}
})();
this.initialized = true; return this.initializationPromise;
logger.log("SQLite database initialized successfully");
} catch (error) {
logger.error("Error initializing SQLite database:", error);
throw new Error("Failed to initialize database");
}
} }
private async runMigrations(): Promise<void> { private async runMigrations(): Promise<void> {
@ -300,24 +313,24 @@ export class ElectronPlatformService implements PlatformService {
/** /**
* @see PlatformService.dbQuery * @see PlatformService.dbQuery
*/ */
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> { async dbQuery(
await this.initializeDatabase(); sql: string,
if (!this.db) { params?: unknown[],
throw new Error("Database not initialized"); ): Promise<QueryExecResult | undefined> {
}
try { try {
const result = await this.db.query(sql, params || []); await this.initializeDatabase();
const values = result.values || []; if (!this.db) {
throw new Error("Database not initialized");
}
const result = await this.db.query(sql, params);
// Convert SQLite plugin result to QueryExecResult format
return { return {
columns: [], // SQLite plugin doesn't provide column names in query result columns: [], // SQLite plugin doesn't provide column names
values: values as SqlValue[][], values: result.values || [],
}; };
} catch (error) { } catch (error) {
logger.error("Error executing query:", error); logger.error("[Electron] Database query error:", error);
throw new Error( throw error;
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
);
} }
} }
@ -328,23 +341,20 @@ export class ElectronPlatformService implements PlatformService {
sql: string, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }> { ): Promise<{ changes: number; lastId?: number }> {
await this.initializeDatabase();
if (!this.db) {
throw new Error("Database not initialized");
}
try { try {
const result = await this.db.run(sql, params || []); await this.initializeDatabase();
const changes = result.changes as Changes; if (!this.db) {
throw new Error("Database not initialized");
}
const result = await this.db.run(sql, params);
// Convert SQLite plugin result to expected format
return { return {
changes: changes?.changes || 0, changes: result.changes?.changes || 0,
lastId: changes?.lastId, lastId: result.changes?.lastId,
}; };
} catch (error) { } catch (error) {
logger.error("Error executing statement:", error); logger.error("[Electron] Database execution error:", error);
throw new Error( throw error;
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
);
} }
} }
} }

Loading…
Cancel
Save