|
|
@ -93,13 +93,17 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
private isConnectionOpen = false; |
|
|
|
private operationQueue: Promise<unknown> = Promise.resolve(); |
|
|
|
private queueLock = false; |
|
|
|
private connectionState: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected'; |
|
|
|
private connectionState: |
|
|
|
| "disconnected" |
|
|
|
| "connecting" |
|
|
|
| "connected" |
|
|
|
| "error" = "disconnected"; |
|
|
|
private connectionPromise: Promise<void> | null = null; |
|
|
|
|
|
|
|
// SQLite initialization configuration
|
|
|
|
private static readonly SQLITE_CONFIG = { |
|
|
|
INITIALIZATION: { |
|
|
|
TIMEOUT_MS: 5000, // Increase timeout to 5 seconds
|
|
|
|
TIMEOUT_MS: 5000, // Increase timeout to 5 seconds
|
|
|
|
RETRY_ATTEMPTS: 3, |
|
|
|
RETRY_DELAY_MS: 1000, |
|
|
|
READY_CHECK_INTERVAL_MS: 100, |
|
|
@ -124,18 +128,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(); |
|
|
@ -144,7 +155,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; |
|
|
|
} |
|
|
|
}; |
|
|
@ -161,8 +175,14 @@ 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; |
|
|
|
} |
|
|
|
|
|
|
@ -171,7 +191,9 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
|
|
|
|
// 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"); |
|
|
@ -182,12 +204,16 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
// Set up ready signal handler BEFORE setting timeout
|
|
|
|
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; |
|
|
@ -197,43 +223,67 @@ 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); |
|
|
|
} |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
// Set timeout for this attempt AFTER setting up handlers
|
|
|
|
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); |
|
|
|
|
|
|
|
} 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); |
|
|
|
} |
|
|
|
}; |
|
|
@ -254,30 +304,30 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
// Use IPC bridge with specific methods
|
|
|
|
this.sqlite = { |
|
|
|
createConnection: async (options) => { |
|
|
|
await window.electron.ipcRenderer.invoke('sqlite-create-connection', { |
|
|
|
await window.electron.ipcRenderer.invoke("sqlite-create-connection", { |
|
|
|
...options, |
|
|
|
database: this.dbName |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
}, |
|
|
|
query: async (options) => { |
|
|
|
return await window.electron.ipcRenderer.invoke('sqlite-query', { |
|
|
|
return await window.electron.ipcRenderer.invoke("sqlite-query", { |
|
|
|
...options, |
|
|
|
database: this.dbName |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
}, |
|
|
|
run: async (options) => { |
|
|
|
return await window.electron.ipcRenderer.invoke('sqlite-run', { |
|
|
|
return await window.electron.ipcRenderer.invoke("sqlite-run", { |
|
|
|
...options, |
|
|
|
database: this.dbName |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
}, |
|
|
|
execute: async (options) => { |
|
|
|
await window.electron.ipcRenderer.invoke('sqlite-execute', { |
|
|
|
await window.electron.ipcRenderer.invoke("sqlite-execute", { |
|
|
|
...options, |
|
|
|
database: this.dbName, |
|
|
|
statements: [{ statement: options.statements }] |
|
|
|
statements: [{ statement: options.statements }], |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
} as SQLiteOperations; |
|
|
|
|
|
|
|
// Create the connection (idempotent)
|
|
|
@ -537,7 +587,7 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
try { |
|
|
|
// Acquire lock
|
|
|
|
while (this.queueLock) { |
|
|
|
await new Promise(resolve => setTimeout(resolve, 50)); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
|
} |
|
|
|
this.queueLock = true; |
|
|
|
|
|
|
@ -562,20 +612,20 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
} |
|
|
|
|
|
|
|
// If we're already connected, return immediately
|
|
|
|
if (this.connectionState === 'connected') { |
|
|
|
if (this.connectionState === "connected") { |
|
|
|
return Promise.resolve(); |
|
|
|
} |
|
|
|
|
|
|
|
// Create new connection promise
|
|
|
|
this.connectionPromise = (async () => { |
|
|
|
try { |
|
|
|
this.connectionState = 'connecting'; |
|
|
|
this.connectionState = "connecting"; |
|
|
|
|
|
|
|
// Wait for any existing operations
|
|
|
|
await this.operationQueue; |
|
|
|
|
|
|
|
// Create connection
|
|
|
|
await window.electron!.ipcRenderer.invoke('sqlite-create-connection', { |
|
|
|
await window.electron!.ipcRenderer.invoke("sqlite-create-connection", { |
|
|
|
database: this.dbName, |
|
|
|
encrypted: false, |
|
|
|
mode: "no-encryption", |
|
|
@ -583,23 +633,26 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
logger.debug("[ElectronPlatformService] Database connection created"); |
|
|
|
|
|
|
|
// Open database
|
|
|
|
await window.electron!.ipcRenderer.invoke('sqlite-open', { |
|
|
|
database: this.dbName |
|
|
|
await window.electron!.ipcRenderer.invoke("sqlite-open", { |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
logger.debug("[ElectronPlatformService] Database opened"); |
|
|
|
|
|
|
|
// Verify database is 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) { |
|
|
|
throw new Error('[ElectronPlatformService] Database failed to open'); |
|
|
|
throw new Error("[ElectronPlatformService] Database failed to open"); |
|
|
|
} |
|
|
|
|
|
|
|
this.connectionState = 'connected'; |
|
|
|
this.connectionState = "connected"; |
|
|
|
this.isConnectionOpen = true; |
|
|
|
} catch (error) { |
|
|
|
this.connectionState = 'error'; |
|
|
|
this.connectionState = "error"; |
|
|
|
this.connectionPromise = null; |
|
|
|
throw error; |
|
|
|
} |
|
|
@ -609,28 +662,31 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
} |
|
|
|
|
|
|
|
private async releaseConnection(): Promise<void> { |
|
|
|
if (this.connectionState !== 'connected') { |
|
|
|
if (this.connectionState !== "connected") { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// Close database
|
|
|
|
await window.electron!.ipcRenderer.invoke('sqlite-close', { |
|
|
|
database: this.dbName |
|
|
|
await window.electron!.ipcRenderer.invoke("sqlite-close", { |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
logger.debug("[ElectronPlatformService] Database closed"); |
|
|
|
|
|
|
|
// Close connection
|
|
|
|
await window.electron!.ipcRenderer.invoke('sqlite-close-connection', { |
|
|
|
database: this.dbName |
|
|
|
await window.electron!.ipcRenderer.invoke("sqlite-close-connection", { |
|
|
|
database: this.dbName, |
|
|
|
}); |
|
|
|
logger.debug("[ElectronPlatformService] Database connection closed"); |
|
|
|
|
|
|
|
this.connectionState = 'disconnected'; |
|
|
|
this.connectionState = "disconnected"; |
|
|
|
this.isConnectionOpen = false; |
|
|
|
} catch (error) { |
|
|
|
logger.error("[ElectronPlatformService] Failed to close database:", error); |
|
|
|
this.connectionState = 'error'; |
|
|
|
logger.error( |
|
|
|
"[ElectronPlatformService] Failed to close database:", |
|
|
|
error, |
|
|
|
); |
|
|
|
this.connectionState = "error"; |
|
|
|
} finally { |
|
|
|
this.connectionPromise = null; |
|
|
|
} |
|
|
@ -650,7 +706,9 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
params: unknown[] = [], |
|
|
|
): Promise<QueryExecResult<T>> { |
|
|
|
if (this.dbFatalError) { |
|
|
|
throw new Error("Database is in a fatal error state. Please restart the app."); |
|
|
|
throw new Error( |
|
|
|
"Database is in a fatal error state. Please restart the app.", |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return this.enqueueOperation(async () => { |
|
|
@ -659,23 +717,33 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
await this.getConnection(); |
|
|
|
|
|
|
|
// Execute query
|
|
|
|
const result = await window.electron!.ipcRenderer.invoke('sqlite-query', { |
|
|
|
database: this.dbName, |
|
|
|
statement: sql, |
|
|
|
values: params |
|
|
|
}) as SQLiteQueryResult; |
|
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Query executed successfully"); |
|
|
|
const result = (await window.electron!.ipcRenderer.invoke( |
|
|
|
"sqlite-query", |
|
|
|
{ |
|
|
|
database: this.dbName, |
|
|
|
statement: sql, |
|
|
|
values: params, |
|
|
|
}, |
|
|
|
)) as SQLiteQueryResult; |
|
|
|
logger.debug( |
|
|
|
"[ElectronPlatformService] [dbQuery] Query executed successfully", |
|
|
|
); |
|
|
|
|
|
|
|
// Process results
|
|
|
|
const columns = result.values?.[0] ? Object.keys(result.values[0]) : []; |
|
|
|
const processedResult = { |
|
|
|
columns, |
|
|
|
values: (result.values || []).map((row: Record<string, unknown>) => row as T) |
|
|
|
values: (result.values || []).map( |
|
|
|
(row: Record<string, unknown>) => row as T, |
|
|
|
), |
|
|
|
}; |
|
|
|
|
|
|
|
return processedResult; |
|
|
|
} catch (error) { |
|
|
|
logger.error("[ElectronPlatformService] [dbQuery] Query failed:", error); |
|
|
|
logger.error( |
|
|
|
"[ElectronPlatformService] [dbQuery] Query failed:", |
|
|
|
error, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} finally { |
|
|
|
// Release connection after query
|
|
|
@ -692,7 +760,9 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
params?: unknown[], |
|
|
|
): Promise<{ changes: number; lastId?: number }> { |
|
|
|
if (this.dbFatalError) { |
|
|
|
throw new Error("Database is in a fatal error state. Please restart the app."); |
|
|
|
throw new Error( |
|
|
|
"Database is in a fatal error state. Please restart the app.", |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return this.enqueueOperation(async () => { |
|
|
@ -701,12 +771,17 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
await this.getConnection(); |
|
|
|
|
|
|
|
// Execute query
|
|
|
|
const result = await window.electron!.ipcRenderer.invoke('sqlite-run', { |
|
|
|
database: this.dbName, |
|
|
|
statement: sql, |
|
|
|
values: params |
|
|
|
}) as SQLiteQueryResult; |
|
|
|
logger.debug("[ElectronPlatformService] [dbExec] Query executed successfully"); |
|
|
|
const result = (await window.electron!.ipcRenderer.invoke( |
|
|
|
"sqlite-run", |
|
|
|
{ |
|
|
|
database: this.dbName, |
|
|
|
statement: sql, |
|
|
|
values: params, |
|
|
|
}, |
|
|
|
)) as SQLiteQueryResult; |
|
|
|
logger.debug( |
|
|
|
"[ElectronPlatformService] [dbExec] Query executed successfully", |
|
|
|
); |
|
|
|
|
|
|
|
return { |
|
|
|
changes: result.changes?.changes || 0, |
|
|
|