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 {
// From records.ts
PlanSummaryRecord,
GiveSummaryRecord,
} from "./records";
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");
}
let appIsMounted = false;
// Initialize app and SQLite
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
const sqliteReady = new Promise<void>((resolve, reject) => {
let retryCount = 0;
let initializationTimeout: NodeJS.Timeout;
const attemptInitialization = () => {
// Clear any existing timeout
if (initializationTimeout) {
clearTimeout(initializationTimeout);
}
// Set timeout for this attempt
initializationTimeout = setTimeout(() => {
if (retryCount < 3) { // Use same retry count as ElectronPlatformService
if (retryCount < 3) {
// Use same retry count as ElectronPlatformService
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
} 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"));
}
}, 10000); // Use same timeout as ElectronPlatformService
@ -71,134 +51,198 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
setTimeout(checkElectronBridge, 100);
return;
}
// At this point we know ipcRenderer exists
const ipcRenderer = window.electron.ipcRenderer;
logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available");
// Listen for SQLite ready signal
logger.debug("[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal");
ipcRenderer.once('sqlite-ready', () => {
logger.debug(
"[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal",
);
ipcRenderer.once("sqlite-ready", () => {
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();
});
// Also listen for database errors
logger.debug("[Main Electron] [IPC:database-status] Registering listener for database status");
ipcRenderer.once('database-status', (...args: unknown[]) => {
logger.debug(
"[Main Electron] [IPC:database-status] Registering listener for database status",
);
ipcRenderer.once("database-status", (...args: unknown[]) => {
clearTimeout(initializationTimeout);
const status = args[0] as { status: string; error?: string };
if (status.status === 'error') {
logger.error("[Main Electron] [IPC:database-status] Database error:", {
error: status.error,
channel: 'database-status'
});
reject(new Error(status.error || 'Database initialization failed'));
if (status.status === "error") {
logger.error(
"[Main Electron] [IPC:database-status] Database error:",
{
error: status.error,
channel: "database-status",
},
);
reject(new Error(status.error || "Database initialization failed"));
}
});
// Check if SQLite is already available
logger.debug("[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability");
ipcRenderer.invoke('sqlite-is-available')
logger.debug(
"[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability",
);
ipcRenderer
.invoke("sqlite-is-available")
.then(async (result: unknown) => {
const isAvailable = Boolean(result);
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 {
// First create a database connection
logger.debug("[Main Electron] [IPC:get-path] Requesting database path");
const dbPath = await ipcRenderer.invoke('get-path');
logger.info("[Main Electron] [IPC:get-path] Database path received:", { dbPath });
logger.debug(
"[Main Electron] [IPC:get-path] Requesting database path",
);
const dbPath = await ipcRenderer.invoke("get-path");
logger.info(
"[Main Electron] [IPC:get-path] Database path received:",
{ dbPath },
);
// Create the database connection
logger.debug("[Main Electron] [IPC:sqlite-create-connection] Creating database connection");
await ipcRenderer.invoke('sqlite-create-connection', {
database: 'timesafari',
version: 1
logger.debug(
"[Main Electron] [IPC:sqlite-create-connection] Creating database connection",
);
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
logger.debug("[Main Electron] [IPC:sqlite-open] Opening database");
await ipcRenderer.invoke('sqlite-open', {
database: 'timesafari'
logger.debug(
"[Main Electron] [IPC:sqlite-open] Opening database",
);
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
logger.debug("[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open");
const isOpen = await ipcRenderer.invoke('sqlite-is-db-open', {
database: 'timesafari'
logger.debug(
"[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open",
);
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) {
throw new Error('Database failed to open');
throw new Error("Database failed to open");
}
// Now execute the test query
logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query");
const testQuery = await ipcRenderer.invoke('sqlite-query', {
database: 'timesafari',
statement: 'SELECT 1 as test;' // Safe test query
}) as SQLiteQueryResult;
logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", {
hasResults: Boolean(testQuery?.values),
resultCount: testQuery?.values?.length
});
logger.debug(
"[Main Electron] [IPC:sqlite-query] Executing test query",
);
const testQuery = (await ipcRenderer.invoke("sqlite-query", {
database: "timesafari",
statement: "SELECT 1 as test;", // Safe test query
})) as SQLiteQueryResult;
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
logger.debug("[Main Electron] [IPC:sqlite-status] Sending SQLite ready status");
await ipcRenderer.invoke('sqlite-status', {
status: 'ready',
database: 'timesafari',
timestamp: Date.now()
logger.debug(
"[Main Electron] [IPC:sqlite-status] Sending SQLite ready status",
);
await ipcRenderer.invoke("sqlite-status", {
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
// Database will be closed during app shutdown
} catch (error) {
logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", {
error,
lastOperation: 'sqlite-test-query',
database: 'timesafari'
});
logger.error(
"[Main Electron] [IPC:*] SQLite test operation failed:",
{
error,
lastOperation: "sqlite-test-query",
database: "timesafari",
},
);
// Try to close everything if anything was opened
try {
logger.debug("[Main Electron] [IPC:cleanup] Attempting database cleanup after error");
await ipcRenderer.invoke('sqlite-close', {
database: 'timesafari'
}).catch((closeError) => {
logger.warn("[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:", closeError);
});
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");
logger.debug(
"[Main Electron] [IPC:cleanup] Attempting database cleanup after error",
);
await ipcRenderer
.invoke("sqlite-close", {
database: "timesafari",
})
.catch((closeError) => {
logger.warn(
"[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:",
closeError,
);
});
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) {
logger.error("[Main Electron] [IPC:cleanup] Failed to cleanup database:", {
error: closeError,
database: 'timesafari'
});
logger.error(
"[Main Electron] [IPC:cleanup] Failed to cleanup database:",
{
error: closeError,
database: "timesafari",
},
);
}
// Don't reject here - we still want to wait for the ready signal
}
}
})
.catch((error: Error) => {
logger.error("[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:", {
error,
channel: 'sqlite-is-available'
});
logger.error(
"[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:",
{
error,
channel: "sqlite-is-available",
},
);
// Don't reject here - wait for either ready signal or timeout
});
};
@ -215,20 +259,22 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
sqliteReady
.then(async () => {
logger.info("[Main Electron] SQLite ready, initializing router...");
// 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);
logger.info("[Main Electron] Router initialized");
// Now mount the app
logger.info("[Main Electron] Mounting app...");
app.mount("#app");
appIsMounted = true;
logger.info("[Main Electron] App mounted successfully");
})
.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
const errorDiv = document.createElement("div");
errorDiv.style.cssText =
@ -241,7 +287,7 @@ sqliteReady
<li>Insufficient permissions to access the database</li>
<li>Database file is corrupted</li>
</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;">
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
Retry

5
src/services/PlatformService.ts

@ -115,7 +115,10 @@ export interface PlatformService {
* @param params Query parameters
* @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.

34
src/services/database/ConnectionPool.ts

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

267
src/services/platforms/ElectronPlatformService.ts

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

160
src/utils/debug-electron.ts

@ -1,123 +1,169 @@
/**
* Debug utilities for Electron integration
* Helps verify the context bridge and SQLite functionality
*
* @author Matthew Raymer
*/
const debugLogger = {
log: (...args: unknown[]) => console.log('[Debug]', ...args),
error: (...args: unknown[]) => console.error('[Debug]', ...args),
info: (...args: unknown[]) => console.info('[Debug]', ...args),
warn: (...args: unknown[]) => console.warn('[Debug]', ...args),
debug: (...args: unknown[]) => console.debug('[Debug]', ...args)
};
import { logger } from "./logger";
// Define the SQLite interface
export interface SQLiteAPI {
isAvailable: () => Promise<boolean>;
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> {
debugLogger.info('Verifying Electron API exposure...');
logger.info("[Debug] Verifying Electron API exposure...");
// Check if window.electron exists
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
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:', {
hasOn: typeof window.electron.ipcRenderer.on === 'function',
hasOnce: typeof window.electron.ipcRenderer.once === 'function',
hasSend: typeof window.electron.ipcRenderer.send === 'function',
hasInvoke: typeof window.electron.ipcRenderer.invoke === 'function'
logger.info("[Debug] IPC renderer is available with methods:", {
hasOn: typeof window.electron.ipcRenderer.on === "function",
hasOnce: typeof window.electron.ipcRenderer.once === "function",
hasSend: typeof window.electron.ipcRenderer.send === "function",
hasInvoke: typeof window.electron.ipcRenderer.invoke === "function",
});
// Verify SQLite API
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:', {
hasIsAvailable: typeof window.electron.sqlite.isAvailable === 'function',
hasEcho: typeof window.electron.sqlite.echo === 'function',
hasCreateConnection: typeof window.electron.sqlite.createConnection === 'function',
hasCloseConnection: typeof window.electron.sqlite.closeConnection === 'function',
hasQuery: typeof window.electron.sqlite.query === 'function',
hasRun: typeof window.electron.sqlite.run === 'function',
hasExecute: typeof window.electron.sqlite.execute === 'function',
hasGetPlatform: typeof window.electron.sqlite.getPlatform === 'function'
logger.info("[Debug] SQLite API is available with methods:", {
hasIsAvailable: typeof window.electron.sqlite.isAvailable === "function",
hasEcho: typeof window.electron.sqlite.echo === "function",
hasCreateConnection:
typeof window.electron.sqlite.createConnection === "function",
hasCloseConnection:
typeof window.electron.sqlite.closeConnection === "function",
hasQuery: typeof window.electron.sqlite.query === "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
try {
const isAvailable = await window.electron.sqlite.isAvailable();
debugLogger.info('SQLite availability check:', { isAvailable });
logger.info("[Debug] SQLite availability check:", { isAvailable });
} catch (error) {
debugLogger.error('SQLite availability check failed:', error);
logger.error("[Debug] SQLite availability check failed:", error);
}
// Test echo functionality
try {
const echoResult = await window.electron.sqlite.echo('test');
debugLogger.info('SQLite echo test:', echoResult);
const echoResult = await window.electron.sqlite.echo("test");
logger.info("[Debug] SQLite echo test:", echoResult);
} catch (error) {
debugLogger.error('SQLite echo test failed:', error);
logger.error("[Debug] SQLite echo test failed:", error);
}
// Verify environment
debugLogger.info('Environment:', {
logger.info("[Debug] Environment:", {
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 async function testSQLiteOperations(): Promise<void> {
debugLogger.info('Testing SQLite operations...');
logger.info("[Debug] Testing SQLite operations...");
try {
// Test connection creation
debugLogger.info('Creating test connection...');
logger.info("[Debug] Creating test connection...");
await window.electron.sqlite.createConnection({
database: 'test',
database: "test",
version: 1,
readOnly: false
readOnly: false,
});
debugLogger.info('Test connection created successfully');
logger.info("[Debug] Test connection created successfully");
// Test query
debugLogger.info('Testing query operation...');
logger.info("[Debug] Testing query operation...");
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
debugLogger.info('Testing run operation...');
logger.info("[Debug] Testing run operation...");
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
debugLogger.info('Testing execute operation...');
logger.info("[Debug] Testing execute operation...");
const executeResult = await window.electron.sqlite.execute({
statements: [
{ statement: 'INSERT INTO test_table (id) VALUES (1)' },
{ statement: 'SELECT * FROM test_table' }
]
{ statement: "INSERT INTO test_table (id) VALUES (1)" },
{ statement: "SELECT * FROM test_table" },
],
});
debugLogger.info('Execute test result:', executeResult);
logger.info("[Debug] Execute test result:", executeResult);
// Clean up
debugLogger.info('Closing test connection...');
await window.electron.sqlite.closeConnection({ database: 'test' });
debugLogger.info('Test connection closed');
logger.info("[Debug] Closing test connection...");
await window.electron.sqlite.closeConnection({ database: "test" });
logger.info("[Debug] Test connection closed");
} catch (error) {
debugLogger.error('SQLite operation test failed:', error);
logger.error("[Debug] SQLite operation test failed:", 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 * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../types";
import { GiveRecordWithContactInfo } from "../interfaces/give";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Claim {
@ -610,7 +610,10 @@ export default class HomeView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logConsoleAndDb("[initializeIdentity] Error retrieving settings or feed: " + err, true);
logConsoleAndDb(
"[initializeIdentity] Error retrieving settings or feed: " + err,
true,
);
this.$notify(
{
group: "alert",
@ -1687,7 +1690,7 @@ export default class HomeView extends Vue {
* @param event Event object
* @param imageUrl URL of image to cache
*/
async cacheImageData(event: Event, imageUrl: string) {
async cacheImageData(_event: Event, imageUrl: string) {
try {
// For images that might fail CORS, just store the URL
// The Web Share API will handle sharing the URL appropriately
@ -1748,7 +1751,7 @@ export default class HomeView extends Vue {
this.axios,
);
if (result.type === "success") {
if (result.success) {
this.$notify(
{
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.)",
onYes: async () => {
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`DELETE FROM accounts WHERE id = ?`,
[id],
);
await platformService.dbExec(`DELETE FROM accounts WHERE id = ?`, [
id,
]);
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;

Loading…
Cancel
Save