Browse Source

refactor: consolidate Electron API type definitions

- Create unified ElectronAPI interface in debug-electron.ts
- Export SQLiteAPI, IPCRenderer, and ElectronEnv interfaces
- Update Window.electron type declarations to use shared interface
- Fix type conflicts between debug-electron.ts and ElectronPlatformService.ts

This change improves type safety and maintainability by centralizing
Electron API type definitions in a single location.
sql-absurd-sql-further
Matthew Raymer 3 weeks ago
parent
commit
98b3a35e3c
  1. 1
      src/interfaces/index.ts
  2. 286
      src/main.electron.ts
  3. 5
      src/services/PlatformService.ts
  4. 34
      src/services/database/ConnectionPool.ts
  5. 267
      src/services/platforms/ElectronPlatformService.ts
  6. 160
      src/utils/debug-electron.ts
  7. 11
      src/views/HomeView.vue
  8. 7
      src/views/IdentitySwitcherView.vue

1
src/interfaces/index.ts

@ -26,6 +26,7 @@ export type {
export type { export type {
// From records.ts // From records.ts
PlanSummaryRecord, PlanSummaryRecord,
GiveSummaryRecord,
} from "./records"; } from "./records";
export type { export type {

286
src/main.electron.ts

@ -13,53 +13,33 @@ if (pwa_enabled) {
logger.warn("[Main Electron] PWA is enabled, but not supported in electron"); logger.warn("[Main Electron] PWA is enabled, but not supported in electron");
} }
let appIsMounted = false;
// Initialize app and SQLite // Initialize app and SQLite
const app = initializeApp(); const app = initializeApp();
/**
* SQLite initialization configuration
* Defines timeouts and retry settings for database operations
*
* @author Matthew Raymer
*/
const SQLITE_CONFIG = {
INITIALIZATION: {
TIMEOUT_MS: 10000, // 10 seconds for initial setup
RETRY_ATTEMPTS: 3,
RETRY_DELAY_MS: 1000
},
OPERATIONS: {
TIMEOUT_MS: 5000, // 5 seconds for regular operations
RETRY_ATTEMPTS: 2,
RETRY_DELAY_MS: 500
},
CONNECTION: {
MAX_CONNECTIONS: 5,
IDLE_TIMEOUT_MS: 30000
}
};
// Create a promise that resolves when SQLite is ready // Create a promise that resolves when SQLite is ready
const sqliteReady = new Promise<void>((resolve, reject) => { const sqliteReady = new Promise<void>((resolve, reject) => {
let retryCount = 0; let retryCount = 0;
let initializationTimeout: NodeJS.Timeout; let initializationTimeout: NodeJS.Timeout;
const attemptInitialization = () => { const attemptInitialization = () => {
// Clear any existing timeout // Clear any existing timeout
if (initializationTimeout) { if (initializationTimeout) {
clearTimeout(initializationTimeout); clearTimeout(initializationTimeout);
} }
// Set timeout for this attempt // Set timeout for this attempt
initializationTimeout = setTimeout(() => { initializationTimeout = setTimeout(() => {
if (retryCount < 3) { // Use same retry count as ElectronPlatformService if (retryCount < 3) {
// Use same retry count as ElectronPlatformService
retryCount++; retryCount++;
logger.warn(`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`); logger.warn(
`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`,
);
setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService
} else { } else {
logger.error("[Main Electron] SQLite initialization failed after all retries"); logger.error(
"[Main Electron] SQLite initialization failed after all retries",
);
reject(new Error("SQLite initialization timeout after all retries")); reject(new Error("SQLite initialization timeout after all retries"));
} }
}, 10000); // Use same timeout as ElectronPlatformService }, 10000); // Use same timeout as ElectronPlatformService
@ -71,134 +51,198 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
setTimeout(checkElectronBridge, 100); setTimeout(checkElectronBridge, 100);
return; return;
} }
// At this point we know ipcRenderer exists // At this point we know ipcRenderer exists
const ipcRenderer = window.electron.ipcRenderer; const ipcRenderer = window.electron.ipcRenderer;
logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available"); logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available");
// Listen for SQLite ready signal // Listen for SQLite ready signal
logger.debug("[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal"); logger.debug(
ipcRenderer.once('sqlite-ready', () => { "[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal",
);
ipcRenderer.once("sqlite-ready", () => {
clearTimeout(initializationTimeout); clearTimeout(initializationTimeout);
logger.info("[Main Electron] [IPC:sqlite-ready] Received SQLite ready signal"); logger.info(
"[Main Electron] [IPC:sqlite-ready] Received SQLite ready signal",
);
resolve(); resolve();
}); });
// Also listen for database errors // Also listen for database errors
logger.debug("[Main Electron] [IPC:database-status] Registering listener for database status"); logger.debug(
ipcRenderer.once('database-status', (...args: unknown[]) => { "[Main Electron] [IPC:database-status] Registering listener for database status",
);
ipcRenderer.once("database-status", (...args: unknown[]) => {
clearTimeout(initializationTimeout); clearTimeout(initializationTimeout);
const status = args[0] as { status: string; error?: string }; const status = args[0] as { status: string; error?: string };
if (status.status === 'error') { if (status.status === "error") {
logger.error("[Main Electron] [IPC:database-status] Database error:", { logger.error(
error: status.error, "[Main Electron] [IPC:database-status] Database error:",
channel: 'database-status' {
}); error: status.error,
reject(new Error(status.error || 'Database initialization failed')); channel: "database-status",
},
);
reject(new Error(status.error || "Database initialization failed"));
} }
}); });
// Check if SQLite is already available // Check if SQLite is already available
logger.debug("[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability"); logger.debug(
ipcRenderer.invoke('sqlite-is-available') "[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability",
);
ipcRenderer
.invoke("sqlite-is-available")
.then(async (result: unknown) => { .then(async (result: unknown) => {
const isAvailable = Boolean(result); const isAvailable = Boolean(result);
if (isAvailable) { if (isAvailable) {
logger.info("[Main Electron] [IPC:sqlite-is-available] SQLite is available"); logger.info(
"[Main Electron] [IPC:sqlite-is-available] SQLite is available",
);
try { try {
// First create a database connection // First create a database connection
logger.debug("[Main Electron] [IPC:get-path] Requesting database path"); logger.debug(
const dbPath = await ipcRenderer.invoke('get-path'); "[Main Electron] [IPC:get-path] Requesting database path",
logger.info("[Main Electron] [IPC:get-path] Database path received:", { dbPath }); );
const dbPath = await ipcRenderer.invoke("get-path");
logger.info(
"[Main Electron] [IPC:get-path] Database path received:",
{ dbPath },
);
// Create the database connection // Create the database connection
logger.debug("[Main Electron] [IPC:sqlite-create-connection] Creating database connection"); logger.debug(
await ipcRenderer.invoke('sqlite-create-connection', { "[Main Electron] [IPC:sqlite-create-connection] Creating database connection",
database: 'timesafari', );
version: 1 await ipcRenderer.invoke("sqlite-create-connection", {
database: "timesafari",
version: 1,
}); });
logger.info("[Main Electron] [IPC:sqlite-create-connection] Database connection created"); logger.info(
"[Main Electron] [IPC:sqlite-create-connection] Database connection created",
);
// Explicitly open the database // Explicitly open the database
logger.debug("[Main Electron] [IPC:sqlite-open] Opening database"); logger.debug(
await ipcRenderer.invoke('sqlite-open', { "[Main Electron] [IPC:sqlite-open] Opening database",
database: 'timesafari' );
await ipcRenderer.invoke("sqlite-open", {
database: "timesafari",
}); });
logger.info("[Main Electron] [IPC:sqlite-open] Database opened successfully"); logger.info(
"[Main Electron] [IPC:sqlite-open] Database opened successfully",
);
// Verify the database is open // Verify the database is open
logger.debug("[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open"); logger.debug(
const isOpen = await ipcRenderer.invoke('sqlite-is-db-open', { "[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open",
database: 'timesafari' );
const isOpen = await ipcRenderer.invoke("sqlite-is-db-open", {
database: "timesafari",
}); });
logger.info("[Main Electron] [IPC:sqlite-is-db-open] Database open status:", { isOpen }); logger.info(
"[Main Electron] [IPC:sqlite-is-db-open] Database open status:",
{ isOpen },
);
if (!isOpen) { if (!isOpen) {
throw new Error('Database failed to open'); throw new Error("Database failed to open");
} }
// Now execute the test query // Now execute the test query
logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query"); logger.debug(
const testQuery = await ipcRenderer.invoke('sqlite-query', { "[Main Electron] [IPC:sqlite-query] Executing test query",
database: 'timesafari', );
statement: 'SELECT 1 as test;' // Safe test query const testQuery = (await ipcRenderer.invoke("sqlite-query", {
}) as SQLiteQueryResult; database: "timesafari",
logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", { statement: "SELECT 1 as test;", // Safe test query
hasResults: Boolean(testQuery?.values), })) as SQLiteQueryResult;
resultCount: testQuery?.values?.length logger.info(
}); "[Main Electron] [IPC:sqlite-query] Test query successful:",
{
hasResults: Boolean(testQuery?.values),
resultCount: testQuery?.values?.length,
},
);
// Signal that SQLite is ready - database stays open // Signal that SQLite is ready - database stays open
logger.debug("[Main Electron] [IPC:sqlite-status] Sending SQLite ready status"); logger.debug(
await ipcRenderer.invoke('sqlite-status', { "[Main Electron] [IPC:sqlite-status] Sending SQLite ready status",
status: 'ready', );
database: 'timesafari', await ipcRenderer.invoke("sqlite-status", {
timestamp: Date.now() status: "ready",
database: "timesafari",
timestamp: Date.now(),
}); });
logger.info("[Main Electron] SQLite ready status sent, database connection maintained"); logger.info(
"[Main Electron] SQLite ready status sent, database connection maintained",
);
// Remove the close operations - database stays open for component use // Remove the close operations - database stays open for component use
// Database will be closed during app shutdown // Database will be closed during app shutdown
} catch (error) { } catch (error) {
logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", { logger.error(
error, "[Main Electron] [IPC:*] SQLite test operation failed:",
lastOperation: 'sqlite-test-query', {
database: 'timesafari' error,
}); lastOperation: "sqlite-test-query",
database: "timesafari",
},
);
// Try to close everything if anything was opened // Try to close everything if anything was opened
try { try {
logger.debug("[Main Electron] [IPC:cleanup] Attempting database cleanup after error"); logger.debug(
await ipcRenderer.invoke('sqlite-close', { "[Main Electron] [IPC:cleanup] Attempting database cleanup after error",
database: 'timesafari' );
}).catch((closeError) => { await ipcRenderer
logger.warn("[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:", closeError); .invoke("sqlite-close", {
}); database: "timesafari",
})
await ipcRenderer.invoke('sqlite-close-connection', { .catch((closeError) => {
database: 'timesafari' logger.warn(
}).catch((closeError) => { "[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:",
logger.warn("[Main Electron] [IPC:sqlite-close-connection] Failed to close connection during cleanup:", closeError); closeError,
}); );
});
logger.info("[Main Electron] [IPC:cleanup] Database cleanup completed after error");
await ipcRenderer
.invoke("sqlite-close-connection", {
database: "timesafari",
})
.catch((closeError) => {
logger.warn(
"[Main Electron] [IPC:sqlite-close-connection] Failed to close connection during cleanup:",
closeError,
);
});
logger.info(
"[Main Electron] [IPC:cleanup] Database cleanup completed after error",
);
} catch (closeError) { } catch (closeError) {
logger.error("[Main Electron] [IPC:cleanup] Failed to cleanup database:", { logger.error(
error: closeError, "[Main Electron] [IPC:cleanup] Failed to cleanup database:",
database: 'timesafari' {
}); error: closeError,
database: "timesafari",
},
);
} }
// Don't reject here - we still want to wait for the ready signal // Don't reject here - we still want to wait for the ready signal
} }
} }
}) })
.catch((error: Error) => { .catch((error: Error) => {
logger.error("[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:", { logger.error(
error, "[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:",
channel: 'sqlite-is-available' {
}); error,
channel: "sqlite-is-available",
},
);
// Don't reject here - wait for either ready signal or timeout // Don't reject here - wait for either ready signal or timeout
}); });
}; };
@ -215,20 +259,22 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
sqliteReady sqliteReady
.then(async () => { .then(async () => {
logger.info("[Main Electron] SQLite ready, initializing router..."); logger.info("[Main Electron] SQLite ready, initializing router...");
// Initialize router after SQLite is ready // Initialize router after SQLite is ready
const router = await import('./router').then(m => m.default); const router = await import("./router").then((m) => m.default);
app.use(router); app.use(router);
logger.info("[Main Electron] Router initialized"); logger.info("[Main Electron] Router initialized");
// Now mount the app // Now mount the app
logger.info("[Main Electron] Mounting app..."); logger.info("[Main Electron] Mounting app...");
app.mount("#app"); app.mount("#app");
appIsMounted = true;
logger.info("[Main Electron] App mounted successfully"); logger.info("[Main Electron] App mounted successfully");
}) })
.catch((error) => { .catch((error) => {
logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown error'); logger.error(
"[Main Electron] Failed to initialize SQLite:",
error instanceof Error ? error.message : "Unknown error",
);
// Show error to user with retry option // Show error to user with retry option
const errorDiv = document.createElement("div"); const errorDiv = document.createElement("div");
errorDiv.style.cssText = errorDiv.style.cssText =
@ -241,7 +287,7 @@ sqliteReady
<li>Insufficient permissions to access the database</li> <li>Insufficient permissions to access the database</li>
<li>Database file is corrupted</li> <li>Database file is corrupted</li>
</ul> </ul>
<p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p> <p>Error details: ${error instanceof Error ? error.message : "Unknown error"}</p>
<div style="margin-top: 15px;"> <div style="margin-top: 15px;">
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;"> <button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
Retry Retry

5
src/services/PlatformService.ts

@ -115,7 +115,10 @@ export interface PlatformService {
* @param params Query parameters * @param params Query parameters
* @returns Query results with columns and values * @returns Query results with columns and values
*/ */
dbQuery<T = unknown>(sql: string, params?: unknown[]): Promise<QueryExecResult<T>>; dbQuery<T = unknown>(
sql: string,
params?: unknown[],
): Promise<QueryExecResult<T>>;
/** /**
* Executes a create/update/delete on the database. * Executes a create/update/delete on the database.

34
src/services/database/ConnectionPool.ts

@ -17,7 +17,10 @@ export class DatabaseConnectionPool {
private constructor() { private constructor() {
// Start cleanup interval // Start cleanup interval
this.cleanupInterval = setInterval(() => this.cleanup(), this.CLEANUP_INTERVAL); this.cleanupInterval = setInterval(
() => this.cleanup(),
this.CLEANUP_INTERVAL,
);
} }
public static getInstance(): DatabaseConnectionPool { public static getInstance(): DatabaseConnectionPool {
@ -29,14 +32,16 @@ export class DatabaseConnectionPool {
public async getConnection( public async getConnection(
dbName: string, dbName: string,
createConnection: () => Promise<SQLiteDBConnection> createConnection: () => Promise<SQLiteDBConnection>,
): Promise<SQLiteDBConnection> { ): Promise<SQLiteDBConnection> {
// Check if we have an existing connection // Check if we have an existing connection
const existing = this.connections.get(dbName); const existing = this.connections.get(dbName);
if (existing && !existing.inUse) { if (existing && !existing.inUse) {
existing.inUse = true; existing.inUse = true;
existing.lastUsed = Date.now(); existing.lastUsed = Date.now();
logger.debug(`[ConnectionPool] Reusing existing connection for ${dbName}`); logger.debug(
`[ConnectionPool] Reusing existing connection for ${dbName}`,
);
return existing.connection; return existing.connection;
} }
@ -52,12 +57,15 @@ export class DatabaseConnectionPool {
this.connections.set(dbName, { this.connections.set(dbName, {
connection, connection,
lastUsed: Date.now(), lastUsed: Date.now(),
inUse: true inUse: true,
}); });
logger.debug(`[ConnectionPool] Created new connection for ${dbName}`); logger.debug(`[ConnectionPool] Created new connection for ${dbName}`);
return connection; return connection;
} catch (error) { } catch (error) {
logger.error(`[ConnectionPool] Failed to create connection for ${dbName}:`, error); logger.error(
`[ConnectionPool] Failed to create connection for ${dbName}:`,
error,
);
throw error; throw error;
} }
} }
@ -89,9 +97,14 @@ export class DatabaseConnectionPool {
try { try {
await state.connection.close(); await state.connection.close();
this.connections.delete(dbName); this.connections.delete(dbName);
logger.debug(`[ConnectionPool] Cleaned up idle connection for ${dbName}`); logger.debug(
`[ConnectionPool] Cleaned up idle connection for ${dbName}`,
);
} catch (error) { } catch (error) {
logger.warn(`[ConnectionPool] Error closing idle connection for ${dbName}:`, error); logger.warn(
`[ConnectionPool] Error closing idle connection for ${dbName}:`,
error,
);
} }
} }
} }
@ -108,9 +121,12 @@ export class DatabaseConnectionPool {
await state.connection.close(); await state.connection.close();
logger.debug(`[ConnectionPool] Closed connection for ${dbName}`); logger.debug(`[ConnectionPool] Closed connection for ${dbName}`);
} catch (error) { } catch (error) {
logger.warn(`[ConnectionPool] Error closing connection for ${dbName}:`, error); logger.warn(
`[ConnectionPool] Error closing connection for ${dbName}:`,
error,
);
} }
} }
this.connections.clear(); this.connections.clear();
} }
} }

267
src/services/platforms/ElectronPlatformService.ts

@ -6,31 +6,17 @@ import {
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { ElectronAPI } from "../../utils/debug-electron";
import { verifyElectronAPI, testSQLiteOperations } from "../../utils/debug-electron"; import {
verifyElectronAPI,
testSQLiteOperations,
} from "../../utils/debug-electron";
// Type for the electron window object // Extend the global Window interface
declare global { declare global {
interface Window { interface Window {
electron: { electron: ElectronAPI;
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>;
};
} }
} }
@ -47,7 +33,7 @@ export interface SQLiteQueryResult {
/** /**
* Shared SQLite initialization state * Shared SQLite initialization state
* Used to coordinate initialization between main and service * Used to coordinate initialization between main and service
* *
* @author Matthew Raymer * @author Matthew Raymer
*/ */
export interface SQLiteInitState { export interface SQLiteInitState {
@ -61,9 +47,32 @@ export interface SQLiteInitState {
const sqliteInitState: SQLiteInitState = { const sqliteInitState: SQLiteInitState = {
isReady: false, isReady: false,
isInitializing: false, isInitializing: false,
lastReadyCheck: 0 lastReadyCheck: 0,
}; };
/**
* Interface defining SQLite database operations
* @author Matthew Raymer
*/
interface SQLiteOperations {
createConnection: (options: {
database: string;
encrypted: boolean;
mode: string;
}) => Promise<void>;
query: (options: {
database: string;
statement: string;
values?: unknown[];
}) => Promise<{ values?: unknown[] }>;
execute: (options: { database: string; statements: string }) => Promise<void>;
run: (options: {
database: string;
statement: string;
values?: unknown[];
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
}
/** /**
* Platform service implementation for Electron (desktop) platform. * Platform service implementation for Electron (desktop) platform.
* Provides native desktop functionality through Electron and Capacitor plugins for: * Provides native desktop functionality through Electron and Capacitor plugins for:
@ -71,11 +80,11 @@ const sqliteInitState: SQLiteInitState = {
* - Camera integration (TODO) * - Camera integration (TODO)
* - SQLite database operations * - SQLite database operations
* - System-level features (TODO) * - System-level features (TODO)
* *
* @author Matthew Raymer * @author Matthew Raymer
*/ */
export class ElectronPlatformService implements PlatformService { export class ElectronPlatformService implements PlatformService {
private sqlite: any; private sqlite: SQLiteOperations | null = null;
private dbName = "timesafari"; private dbName = "timesafari";
private isInitialized = false; private isInitialized = false;
private dbFatalError = false; private dbFatalError = false;
@ -85,17 +94,17 @@ export class ElectronPlatformService implements PlatformService {
// SQLite initialization configuration // SQLite initialization configuration
private static readonly SQLITE_CONFIG = { private static readonly SQLITE_CONFIG = {
INITIALIZATION: { INITIALIZATION: {
TIMEOUT_MS: 10000, // 10 seconds for initial setup TIMEOUT_MS: 10000, // 10 seconds for initial setup
RETRY_ATTEMPTS: 3, RETRY_ATTEMPTS: 3,
RETRY_DELAY_MS: 1000, RETRY_DELAY_MS: 1000,
READY_CHECK_INTERVAL_MS: 100 // How often to check if SQLite is already ready READY_CHECK_INTERVAL_MS: 100, // How often to check if SQLite is already ready
} },
}; };
constructor() { constructor() {
this.sqliteReadyPromise = new Promise<void>((resolve, reject) => { this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
let retryCount = 0; let retryCount = 0;
const cleanup = () => { const cleanup = () => {
if (this.initializationTimeout) { if (this.initializationTimeout) {
clearTimeout(this.initializationTimeout); clearTimeout(this.initializationTimeout);
@ -110,18 +119,25 @@ export class ElectronPlatformService implements PlatformService {
} }
// Check if SQLite is already available // Check if SQLite is already available
const isAvailable = await window.electron.ipcRenderer.invoke('sqlite-is-available'); const isAvailable = await window.electron.ipcRenderer.invoke(
"sqlite-is-available",
);
if (!isAvailable) { if (!isAvailable) {
return false; return false;
} }
// Check if database is already open // Check if database is already open
const isOpen = await window.electron.ipcRenderer.invoke('sqlite-is-db-open', { const isOpen = await window.electron.ipcRenderer.invoke(
database: this.dbName "sqlite-is-db-open",
}); {
database: this.dbName,
},
);
if (isOpen) { if (isOpen) {
logger.info('[ElectronPlatformService] SQLite is already ready and database is open'); logger.info(
"[ElectronPlatformService] SQLite is already ready and database is open",
);
sqliteInitState.isReady = true; sqliteInitState.isReady = true;
sqliteInitState.isInitializing = false; sqliteInitState.isInitializing = false;
sqliteInitState.lastReadyCheck = Date.now(); sqliteInitState.lastReadyCheck = Date.now();
@ -130,7 +146,10 @@ export class ElectronPlatformService implements PlatformService {
return false; return false;
} catch (error) { } catch (error) {
logger.warn('[ElectronPlatformService] Error checking existing readiness:', error); logger.warn(
"[ElectronPlatformService] Error checking existing readiness:",
error,
);
return false; return false;
} }
}; };
@ -147,49 +166,75 @@ export class ElectronPlatformService implements PlatformService {
// If someone else is initializing, wait for them // If someone else is initializing, wait for them
if (sqliteInitState.isInitializing) { if (sqliteInitState.isInitializing) {
logger.info('[ElectronPlatformService] Another initialization in progress, waiting...'); logger.info(
setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.READY_CHECK_INTERVAL_MS); "[ElectronPlatformService] Another initialization in progress, waiting...",
);
setTimeout(
attemptInitialization,
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
.READY_CHECK_INTERVAL_MS,
);
return; return;
} }
try { try {
sqliteInitState.isInitializing = true; sqliteInitState.isInitializing = true;
// Verify Electron API exposure first // Verify Electron API exposure first
await verifyElectronAPI(); await verifyElectronAPI();
logger.info('[ElectronPlatformService] Electron API verification successful'); logger.info(
"[ElectronPlatformService] Electron API verification successful",
);
if (!window.electron?.ipcRenderer) { if (!window.electron?.ipcRenderer) {
logger.warn('[ElectronPlatformService] IPC renderer not available'); logger.warn("[ElectronPlatformService] IPC renderer not available");
reject(new Error('IPC renderer not available')); reject(new Error("IPC renderer not available"));
return; return;
} }
// Set timeout for this attempt // Set timeout for this attempt
this.initializationTimeout = setTimeout(() => { this.initializationTimeout = setTimeout(() => {
if (retryCount < ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) { if (
retryCount <
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
.RETRY_ATTEMPTS
) {
retryCount++; retryCount++;
logger.warn(`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`); logger.warn(
setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS); `[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`,
);
setTimeout(
attemptInitialization,
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
.RETRY_DELAY_MS,
);
} else { } else {
cleanup(); cleanup();
sqliteInitState.isInitializing = false; sqliteInitState.isInitializing = false;
sqliteInitState.error = new Error('SQLite initialization timeout after all retries'); sqliteInitState.error = new Error(
logger.error('[ElectronPlatformService] SQLite initialization failed after all retries'); "SQLite initialization timeout after all retries",
);
logger.error(
"[ElectronPlatformService] SQLite initialization failed after all retries",
);
reject(sqliteInitState.error); reject(sqliteInitState.error);
} }
}, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS); }, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS);
// Set up ready signal handler // Set up ready signal handler
window.electron.ipcRenderer.once('sqlite-ready', async () => { window.electron.ipcRenderer.once("sqlite-ready", async () => {
cleanup(); cleanup();
logger.info('[ElectronPlatformService] Received SQLite ready signal'); logger.info(
"[ElectronPlatformService] Received SQLite ready signal",
);
try { try {
// Test SQLite operations after receiving ready signal // Test SQLite operations after receiving ready signal
await testSQLiteOperations(); await testSQLiteOperations();
logger.info('[ElectronPlatformService] SQLite operations test successful'); logger.info(
"[ElectronPlatformService] SQLite operations test successful",
);
this.isInitialized = true; this.isInitialized = true;
sqliteInitState.isReady = true; sqliteInitState.isReady = true;
sqliteInitState.isInitializing = false; sqliteInitState.isInitializing = false;
@ -198,28 +243,38 @@ export class ElectronPlatformService implements PlatformService {
} catch (error) { } catch (error) {
sqliteInitState.error = error as Error; sqliteInitState.error = error as Error;
sqliteInitState.isInitializing = false; sqliteInitState.isInitializing = false;
logger.error('[ElectronPlatformService] SQLite operations test failed:', error); logger.error(
"[ElectronPlatformService] SQLite operations test failed:",
error,
);
reject(error); reject(error);
} }
}); });
// Set up error handler // Set up error handler
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => { window.electron.ipcRenderer.once(
cleanup(); "database-status",
const status = args[0] as { status: string; error?: string }; (...args: unknown[]) => {
if (status.status === 'error') { cleanup();
this.dbFatalError = true; const status = args[0] as { status: string; error?: string };
sqliteInitState.error = new Error(status.error || 'Database initialization failed'); if (status.status === "error") {
sqliteInitState.isInitializing = false; this.dbFatalError = true;
reject(sqliteInitState.error); sqliteInitState.error = new Error(
} status.error || "Database initialization failed",
}); );
sqliteInitState.isInitializing = false;
reject(sqliteInitState.error);
}
},
);
} catch (error) { } catch (error) {
cleanup(); cleanup();
sqliteInitState.error = error as Error; sqliteInitState.error = error as Error;
sqliteInitState.isInitializing = false; sqliteInitState.isInitializing = false;
logger.error('[ElectronPlatformService] Initialization failed:', error); logger.error(
"[ElectronPlatformService] Initialization failed:",
error,
);
reject(error); reject(error);
} }
}; };
@ -232,26 +287,36 @@ export class ElectronPlatformService implements PlatformService {
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
if (this.isInitialized) return; if (this.isInitialized) return;
if (this.sqliteReadyPromise) await this.sqliteReadyPromise; if (this.sqliteReadyPromise) await this.sqliteReadyPromise;
this.sqlite = window.CapacitorSQLite;
if (!this.sqlite) throw new Error("CapacitorSQLite not available"); if (!window.CapacitorSQLite) {
throw new Error("CapacitorSQLite not available");
}
this.sqlite = window.CapacitorSQLite as unknown as SQLiteOperations;
// Create the connection (idempotent) // Create the connection (idempotent)
await this.sqlite.createConnection({ await this.sqlite.createConnection({
database: this.dbName, database: this.dbName,
encrypted: false, encrypted: false,
mode: "no-encryption", mode: "no-encryption",
readOnly: false,
}); });
// Optionally, test the connection // Optionally, test the connection
await this.sqlite.query({ await this.sqlite.query({
database: this.dbName, database: this.dbName,
statement: "SELECT 1" statement: "SELECT 1",
}); });
// 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");
} }
private async runMigrations(): Promise<void> { private async runMigrations(): Promise<void> {
if (!this.sqlite) {
throw new Error("SQLite not initialized");
}
// Create migrations table if it doesn't exist // Create migrations table if it doesn't exist
await this.sqlite.execute({ await this.sqlite.execute({
database: this.dbName, database: this.dbName,
@ -259,15 +324,18 @@ export class ElectronPlatformService implements PlatformService {
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.sqlite.query({ const result = await this.sqlite.query({
database: this.dbName, database: this.dbName,
statement: "SELECT name FROM migrations;" statement: "SELECT name FROM migrations;",
}); });
const executedMigrations = new Set( const executedMigrations = new Set(
(result.values as unknown[][])?.map((row: unknown[]) => row[0] as string) || [] (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[] = [
@ -360,12 +428,12 @@ export class ElectronPlatformService implements PlatformService {
if (!executedMigrations.has(migration.name)) { if (!executedMigrations.has(migration.name)) {
await this.sqlite.execute({ await this.sqlite.execute({
database: this.dbName, database: this.dbName,
statements: migration.sql statements: migration.sql,
}); });
await this.sqlite.run({ await this.sqlite.run({
database: this.dbName, database: this.dbName,
statement: "INSERT INTO migrations (name) VALUES (?)", statement: "INSERT INTO migrations (name) VALUES (?)",
values: [migration.name] values: [migration.name],
}); });
logger.log(`Migration ${migration.name} executed successfully`); logger.log(`Migration ${migration.name} executed successfully`);
} }
@ -477,23 +545,29 @@ export class ElectronPlatformService implements PlatformService {
/** /**
* Executes a database query with proper connection lifecycle management. * Executes a database query with proper connection lifecycle management.
* Opens connection, executes query, and ensures proper cleanup. * Opens connection, executes query, and ensures proper cleanup.
* *
* @param sql - SQL query to execute * @param sql - SQL query to execute
* @param params - Optional parameters for the query * @param params - Optional parameters for the query
* @returns Promise resolving to query results * @returns Promise resolving to query results
* @throws Error if database operations fail * @throws Error if database operations fail
*/ */
async dbQuery<T = unknown>(sql: string, params: unknown[] = []): Promise<QueryExecResult<T>> { async dbQuery<T = unknown>(
logger.debug("[ElectronPlatformService] [dbQuery] TEMPORARY TEST: Returning empty result for query:", { sql: string,
sql, params: unknown[] = [],
params, ): Promise<QueryExecResult<T>> {
timestamp: new Date().toISOString() logger.debug(
}); "[ElectronPlatformService] [dbQuery] TEMPORARY TEST: Returning empty result for query:",
{
sql,
params,
timestamp: new Date().toISOString(),
},
);
// TEMPORARY TEST: Return empty result // TEMPORARY TEST: Return empty result
return { return {
columns: [], columns: [],
values: [] values: [],
}; };
// Original implementation commented out for testing // Original implementation commented out for testing
@ -579,13 +653,23 @@ export class ElectronPlatformService implements PlatformService {
/** /**
* @see PlatformService.dbExec * @see PlatformService.dbExec
*/ */
async dbExec(sql: string, params?: unknown[]): Promise<{ changes: number; lastId?: number }> { async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.initializeDatabase(); await this.initializeDatabase();
if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app."); if (this.dbFatalError) {
throw new Error(
"Database is in a fatal error state. Please restart the app.",
);
}
if (!this.sqlite) {
throw new Error("SQLite not initialized");
}
const result = await this.sqlite.run({ const result = await this.sqlite.run({
database: this.dbName, database: this.dbName,
statement: sql, statement: sql,
values: params values: params,
}); });
return { return {
changes: result.changes?.changes || 0, changes: result.changes?.changes || 0,
@ -597,13 +681,20 @@ export class ElectronPlatformService implements PlatformService {
await this.initializeDatabase(); await this.initializeDatabase();
} }
async execute(sql: string, params: any[] = []): Promise<void> { async execute(sql: string, params: unknown[] = []): Promise<void> {
await this.initializeDatabase(); await this.initializeDatabase();
if (this.dbFatalError) throw new Error("Database is in a fatal error state. Please restart the app."); if (this.dbFatalError) {
throw new Error(
"Database is in a fatal error state. Please restart the app.",
);
}
if (!this.sqlite) {
throw new Error("SQLite not initialized");
}
await this.sqlite.run({ await this.sqlite.run({
database: this.dbName, database: this.dbName,
statement: sql, statement: sql,
values: params values: params,
}); });
} }

160
src/utils/debug-electron.ts

@ -1,123 +1,169 @@
/** /**
* Debug utilities for Electron integration * Debug utilities for Electron integration
* Helps verify the context bridge and SQLite functionality * Helps verify the context bridge and SQLite functionality
*
* @author Matthew Raymer
*/ */
const debugLogger = { import { logger } from "./logger";
log: (...args: unknown[]) => console.log('[Debug]', ...args),
error: (...args: unknown[]) => console.error('[Debug]', ...args), // Define the SQLite interface
info: (...args: unknown[]) => console.info('[Debug]', ...args), export interface SQLiteAPI {
warn: (...args: unknown[]) => console.warn('[Debug]', ...args), isAvailable: () => Promise<boolean>;
debug: (...args: unknown[]) => console.debug('[Debug]', ...args) echo: (value: string) => Promise<string>;
}; createConnection: (options: {
database: string;
version: number;
readOnly: boolean;
}) => Promise<void>;
closeConnection: (options: { database: string }) => Promise<void>;
query: (options: { statement: string }) => Promise<unknown>;
run: (options: { statement: string }) => Promise<unknown>;
execute: (options: {
statements: Array<{ statement: string }>;
}) => Promise<unknown>;
getPlatform: () => Promise<string>;
}
// Define the IPC renderer interface
export interface IPCRenderer {
on: (channel: string, func: (...args: unknown[]) => void) => void;
once: (channel: string, func: (...args: unknown[]) => void) => void;
send: (channel: string, ...args: unknown[]) => void;
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
}
// Define the environment interface
export interface ElectronEnv {
platform: string;
isDev: boolean;
}
// Define the complete electron interface
export interface ElectronAPI {
sqlite: SQLiteAPI;
ipcRenderer: IPCRenderer;
env: ElectronEnv;
getPath: (pathType: string) => Promise<string>;
getBasePath: () => Promise<string>;
}
// Define the window.electron interface
declare global {
interface Window {
electron: ElectronAPI;
}
}
export async function verifyElectronAPI(): Promise<void> { export async function verifyElectronAPI(): Promise<void> {
debugLogger.info('Verifying Electron API exposure...'); logger.info("[Debug] Verifying Electron API exposure...");
// Check if window.electron exists // Check if window.electron exists
if (!window.electron) { if (!window.electron) {
throw new Error('window.electron is not defined'); throw new Error("window.electron is not defined");
} }
debugLogger.info('window.electron is available'); logger.info("[Debug] window.electron is available");
// Verify IPC renderer // Verify IPC renderer
if (!window.electron.ipcRenderer) { if (!window.electron.ipcRenderer) {
throw new Error('IPC renderer is not available'); throw new Error("IPC renderer is not available");
} }
debugLogger.info('IPC renderer is available with methods:', { logger.info("[Debug] IPC renderer is available with methods:", {
hasOn: typeof window.electron.ipcRenderer.on === 'function', hasOn: typeof window.electron.ipcRenderer.on === "function",
hasOnce: typeof window.electron.ipcRenderer.once === 'function', hasOnce: typeof window.electron.ipcRenderer.once === "function",
hasSend: typeof window.electron.ipcRenderer.send === 'function', hasSend: typeof window.electron.ipcRenderer.send === "function",
hasInvoke: typeof window.electron.ipcRenderer.invoke === 'function' hasInvoke: typeof window.electron.ipcRenderer.invoke === "function",
}); });
// Verify SQLite API // Verify SQLite API
if (!window.electron.sqlite) { if (!window.electron.sqlite) {
throw new Error('SQLite API is not available'); throw new Error("SQLite API is not available");
} }
debugLogger.info('SQLite API is available with methods:', { logger.info("[Debug] SQLite API is available with methods:", {
hasIsAvailable: typeof window.electron.sqlite.isAvailable === 'function', hasIsAvailable: typeof window.electron.sqlite.isAvailable === "function",
hasEcho: typeof window.electron.sqlite.echo === 'function', hasEcho: typeof window.electron.sqlite.echo === "function",
hasCreateConnection: typeof window.electron.sqlite.createConnection === 'function', hasCreateConnection:
hasCloseConnection: typeof window.electron.sqlite.closeConnection === 'function', typeof window.electron.sqlite.createConnection === "function",
hasQuery: typeof window.electron.sqlite.query === 'function', hasCloseConnection:
hasRun: typeof window.electron.sqlite.run === 'function', typeof window.electron.sqlite.closeConnection === "function",
hasExecute: typeof window.electron.sqlite.execute === 'function', hasQuery: typeof window.electron.sqlite.query === "function",
hasGetPlatform: typeof window.electron.sqlite.getPlatform === 'function' hasRun: typeof window.electron.sqlite.run === "function",
hasExecute: typeof window.electron.sqlite.execute === "function",
hasGetPlatform: typeof window.electron.sqlite.getPlatform === "function",
}); });
// Test SQLite availability // Test SQLite availability
try { try {
const isAvailable = await window.electron.sqlite.isAvailable(); const isAvailable = await window.electron.sqlite.isAvailable();
debugLogger.info('SQLite availability check:', { isAvailable }); logger.info("[Debug] SQLite availability check:", { isAvailable });
} catch (error) { } catch (error) {
debugLogger.error('SQLite availability check failed:', error); logger.error("[Debug] SQLite availability check failed:", error);
} }
// Test echo functionality // Test echo functionality
try { try {
const echoResult = await window.electron.sqlite.echo('test'); const echoResult = await window.electron.sqlite.echo("test");
debugLogger.info('SQLite echo test:', echoResult); logger.info("[Debug] SQLite echo test:", echoResult);
} catch (error) { } catch (error) {
debugLogger.error('SQLite echo test failed:', error); logger.error("[Debug] SQLite echo test failed:", error);
} }
// Verify environment // Verify environment
debugLogger.info('Environment:', { logger.info("[Debug] Environment:", {
platform: window.electron.env.platform, platform: window.electron.env.platform,
isDev: window.electron.env.isDev isDev: window.electron.env.isDev,
}); });
debugLogger.info('Electron API verification complete'); logger.info("[Debug] Electron API verification complete");
} }
// Export a function to test SQLite operations // Export a function to test SQLite operations
export async function testSQLiteOperations(): Promise<void> { export async function testSQLiteOperations(): Promise<void> {
debugLogger.info('Testing SQLite operations...'); logger.info("[Debug] Testing SQLite operations...");
try { try {
// Test connection creation // Test connection creation
debugLogger.info('Creating test connection...'); logger.info("[Debug] Creating test connection...");
await window.electron.sqlite.createConnection({ await window.electron.sqlite.createConnection({
database: 'test', database: "test",
version: 1, version: 1,
readOnly: false readOnly: false,
}); });
debugLogger.info('Test connection created successfully'); logger.info("[Debug] Test connection created successfully");
// Test query // Test query
debugLogger.info('Testing query operation...'); logger.info("[Debug] Testing query operation...");
const queryResult = await window.electron.sqlite.query({ const queryResult = await window.electron.sqlite.query({
statement: 'SELECT 1 as test' statement: "SELECT 1 as test",
}); });
debugLogger.info('Query test result:', queryResult); logger.info("[Debug] Query test result:", queryResult);
// Test run // Test run
debugLogger.info('Testing run operation...'); logger.info("[Debug] Testing run operation...");
const runResult = await window.electron.sqlite.run({ const runResult = await window.electron.sqlite.run({
statement: 'CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)' statement:
"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)",
}); });
debugLogger.info('Run test result:', runResult); logger.info("[Debug] Run test result:", runResult);
// Test execute // Test execute
debugLogger.info('Testing execute operation...'); logger.info("[Debug] Testing execute operation...");
const executeResult = await window.electron.sqlite.execute({ const executeResult = await window.electron.sqlite.execute({
statements: [ statements: [
{ statement: 'INSERT INTO test_table (id) VALUES (1)' }, { statement: "INSERT INTO test_table (id) VALUES (1)" },
{ statement: 'SELECT * FROM test_table' } { statement: "SELECT * FROM test_table" },
] ],
}); });
debugLogger.info('Execute test result:', executeResult); logger.info("[Debug] Execute test result:", executeResult);
// Clean up // Clean up
debugLogger.info('Closing test connection...'); logger.info("[Debug] Closing test connection...");
await window.electron.sqlite.closeConnection({ database: 'test' }); await window.electron.sqlite.closeConnection({ database: "test" });
debugLogger.info('Test connection closed'); logger.info("[Debug] Test connection closed");
} catch (error) { } catch (error) {
debugLogger.error('SQLite operation test failed:', error); logger.error("[Debug] SQLite operation test failed:", error);
throw error; throw error;
} }
debugLogger.info('SQLite operations test complete'); logger.info("[Debug] SQLite operations test complete");
} }

11
src/views/HomeView.vue

@ -350,7 +350,7 @@ import {
import { GiveSummaryRecord } from "../interfaces"; import { GiveSummaryRecord } from "../interfaces";
import * as serverUtil from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../types"; import { GiveRecordWithContactInfo } from "../interfaces/give";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Claim { interface Claim {
@ -610,7 +610,10 @@ export default class HomeView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logConsoleAndDb("[initializeIdentity] Error retrieving settings or feed: " + err, true); logConsoleAndDb(
"[initializeIdentity] Error retrieving settings or feed: " + err,
true,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1687,7 +1690,7 @@ export default class HomeView extends Vue {
* @param event Event object * @param event Event object
* @param imageUrl URL of image to cache * @param imageUrl URL of image to cache
*/ */
async cacheImageData(event: Event, imageUrl: string) { async cacheImageData(_event: Event, imageUrl: string) {
try { try {
// For images that might fail CORS, just store the URL // For images that might fail CORS, just store the URL
// The Web Share API will handle sharing the URL appropriately // The Web Share API will handle sharing the URL appropriately
@ -1748,7 +1751,7 @@ export default class HomeView extends Vue {
this.axios, this.axios,
); );
if (result.type === "success") { if (result.success) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

7
src/views/IdentitySwitcherView.vue

@ -187,10 +187,9 @@ export default class IdentitySwitcherView extends Vue {
text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)", text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
onYes: async () => { onYes: async () => {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec( await platformService.dbExec(`DELETE FROM accounts WHERE id = ?`, [
`DELETE FROM accounts WHERE id = ?`, id,
[id], ]);
);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage // one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise; const accountsDB = await accountsDBPromise;

Loading…
Cancel
Save