Browse Source

refactor: enhance SQLite error handling and type safety

Current State:
- SQLite initialization completes successfully
- API exposure and IPC bridge working correctly
- Type definitions and interfaces properly implemented
- Enhanced error handling with specific error codes
- Comprehensive logging system in place

Critical Issue Identified:
SQLite initialization timeout causing cascading failures:
- Components attempting database operations before initialization complete
- Error logging failing due to database unavailability
- Multiple components affected (HomeView, AccountView, ImageMethodDialog)
- User experience impacted with cache clear prompts

Changes Made:
- Added proper TypeScript interfaces for SQLite operations
- Enhanced SQLiteError class with error codes and context
- Implemented input validation utilities
- Added detailed logging with timestamps
- Improved error categorization and handling
- Added result structure validation

Type Definitions Added:
- SQLiteConnectionOptions
- SQLiteQueryOptions
- SQLiteExecuteOptions
- SQLiteResult
- SQLiteEchoResult

Error Codes Implemented:
- SQLITE_BUSY
- SQLITE_NO_TABLE
- SQLITE_SYNTAX_ERROR
- SQLITE_PLUGIN_UNAVAILABLE
- SQLITE_INVALID_OPTIONS
- SQLITE_MIGRATION_FAILED
- SQLITE_INVALID_RESULT
- SQLITE_ECHO_MISMATCH

Next Steps:
1. Implement initialization synchronization
2. Add component loading states
3. Improve error recovery mechanisms
4. Add proper error boundaries
5. Implement fallback UI states

Affected Files:
- electron/src/rt/sqlite-init.ts
- src/types/electron.d.ts

Note: This is a transitional commit. While the structure and type safety
are improved, the initialization timeout issue needs to be addressed in
the next commit to prevent cascading failures.

Testing Required:
- SQLite initialization timing
- Component loading sequences
- Error recovery scenarios
- Database operation retries
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
ec74fff892
  1. 298
      electron/src/rt/sqlite-init.ts
  2. 54
      src/types/electron.d.ts

298
electron/src/rt/sqlite-init.ts

@ -92,34 +92,129 @@ const MAX_RECOVERY_ATTEMPTS = 3;
const RECOVERY_DELAY_MS = 1000;
const VERIFICATION_TIMEOUT_MS = 5000;
// Error handling
// Type definitions for SQLite operations
interface SQLiteConnectionOptions {
database: string;
version?: number;
readOnly?: boolean;
readonly?: boolean;
encryption?: string;
mode?: string;
useNative?: boolean;
[key: string]: unknown;
}
interface SQLiteQueryOptions {
statement: string;
values?: unknown[];
}
interface SQLiteExecuteOptions {
statements: SQLiteQueryOptions[];
transaction?: boolean;
}
interface SQLiteResult {
changes?: { changes: number; lastId?: number };
values?: Record<string, unknown>[];
}
interface SQLiteEchoResult {
value: string;
}
// Enhanced error types
class SQLiteError extends Error {
constructor(
message: string,
public context: string,
public originalError?: unknown
public code: string = 'SQLITE_ERROR',
public originalError?: unknown,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'SQLiteError';
}
toJSON() {
return {
name: this.name,
message: this.message,
context: this.context,
code: this.code,
details: this.details,
stack: this.stack,
originalError: this.originalError instanceof Error ? {
name: this.originalError.name,
message: this.originalError.message,
stack: this.originalError.stack
} : this.originalError
};
}
}
const handleError = (error: unknown, context: string): SQLiteError => {
const errorMessage = error instanceof Error
? error.message
: 'Unknown error occurred';
const errorStack = error instanceof Error
? error.stack
: undefined;
// Validation utilities
const validateConnectionOptions = (options: unknown): SQLiteConnectionOptions => {
if (!options || typeof options !== 'object') {
throw new SQLiteError('Invalid connection options', 'validation', 'SQLITE_INVALID_OPTIONS');
}
const opts = options as SQLiteConnectionOptions;
if (!opts.database || typeof opts.database !== 'string') {
throw new SQLiteError('Database name is required', 'validation', 'SQLITE_INVALID_DATABASE');
}
return opts;
};
const validateQueryOptions = (options: unknown): SQLiteQueryOptions => {
if (!options || typeof options !== 'object') {
throw new SQLiteError('Invalid query options', 'validation', 'SQLITE_INVALID_OPTIONS');
}
const opts = options as SQLiteQueryOptions;
if (!opts.statement || typeof opts.statement !== 'string') {
throw new SQLiteError('SQL statement is required', 'validation', 'SQLITE_INVALID_STATEMENT');
}
if (opts.values && !Array.isArray(opts.values)) {
throw new SQLiteError('Values must be an array', 'validation', 'SQLITE_INVALID_VALUES');
}
return opts;
};
// Enhanced error handler with more context
const handleError = (error: unknown, context: string, details?: Record<string, unknown>): SQLiteError => {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
const errorStack = error instanceof Error ? error.stack : undefined;
// Determine error code based on context and message
let code = 'SQLITE_ERROR';
if (errorMessage.includes('database is locked')) {
code = 'SQLITE_BUSY';
} else if (errorMessage.includes('no such table')) {
code = 'SQLITE_NO_TABLE';
} else if (errorMessage.includes('syntax error')) {
code = 'SQLITE_SYNTAX_ERROR';
}
logger.error(`Error in ${context}:`, {
message: errorMessage,
stack: errorStack,
context,
code,
details,
timestamp: new Date().toISOString()
});
return new SQLiteError(`${context} failed: ${errorMessage}`, context, error);
return new SQLiteError(
`${context} failed: ${errorMessage}`,
context,
code,
error,
details
);
};
// Add delay utility with timeout
@ -472,8 +567,9 @@ export async function initializeSQLite(): Promise<void> {
}))
});
throw new SQLiteError(
'Database migrations failed',
`Database migrations failed: ${failedMigrations.map(f => f.name).join(', ')}`,
'initializeSQLite',
'SQLITE_MIGRATION_FAILED',
failedMigrations
);
}
@ -555,6 +651,69 @@ export function setupSQLiteHandlers(): void {
return false;
}
});
// Enhanced sqlite-echo handler
ipcMain.handle('sqlite-echo', async (_event, options) => {
try {
if (!await verifyPluginState()) {
throw new SQLiteError(
'Plugin not available',
'sqlite-echo',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
}
if (!options || typeof options !== 'object' || !('value' in options)) {
throw new SQLiteError(
'Invalid echo options',
'sqlite-echo',
'SQLITE_INVALID_OPTIONS',
null,
{ options }
);
}
const { value } = options as { value: unknown };
if (typeof value !== 'string') {
throw new SQLiteError(
'Echo value must be a string',
'sqlite-echo',
'SQLITE_INVALID_VALUE',
null,
{ value, type: typeof value }
);
}
logger.debug('Echo test:', { value, timestamp: new Date().toISOString() });
const result = await pluginState.instance.echo({ value });
if (!result || typeof result !== 'object' || !('value' in result)) {
throw new SQLiteError(
'Invalid echo result',
'sqlite-echo',
'SQLITE_INVALID_RESULT',
null,
{ result }
);
}
if (result.value !== value) {
throw new SQLiteError(
'Echo test failed - value mismatch',
'sqlite-echo',
'SQLITE_ECHO_MISMATCH',
null,
{ expected: value, received: result.value }
);
}
return result as SQLiteEchoResult;
} catch (error) {
throw handleError(error, 'sqlite-echo', { options });
}
});
ipcMain.handle('sqlite-get-error', async () => {
return pluginState.lastError ? {
@ -579,69 +738,114 @@ export function setupSQLiteHandlers(): void {
}
});
// Add the sqlite-run handler
// Enhanced sqlite-run handler
ipcMain.handle('sqlite-run', async (_event, options) => {
try {
if (!await verifyPluginState()) {
throw new SQLiteError('Plugin not available', 'sqlite-run');
}
const { statement, values } = options;
if (!statement) {
throw new SQLiteError('No statement provided', 'sqlite-run');
throw new SQLiteError(
'Plugin not available',
'sqlite-run',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
}
// Add database name to options
const runOptions = {
database: 'timesafari', // Use the same database name as in initialization
statement,
values: values || []
const runOptions = validateQueryOptions(options);
const runWithDb = {
database: 'timesafari',
...runOptions
};
logger.debug('Running SQL statement:', runOptions);
const result = await pluginState.instance.run(runOptions);
logger.debug('Running SQL statement:', {
...runWithDb,
timestamp: new Date().toISOString()
});
const result = await pluginState.instance.run(runWithDb);
if (!result) {
throw new SQLiteError('Run operation returned no result', 'sqlite-run');
throw new SQLiteError(
'Run operation returned no result',
'sqlite-run',
'SQLITE_NO_RESULT',
null,
{ run: runWithDb }
);
}
// Validate result structure
if (!('changes' in result)) {
throw new SQLiteError(
'Invalid run result structure',
'sqlite-run',
'SQLITE_INVALID_RESULT',
null,
{ result }
);
}
return result;
return result as SQLiteResult;
} catch (error) {
throw handleError(error, 'sqlite-run');
throw handleError(error, 'sqlite-run', { options });
}
});
// Add the sqlite-query handler
// Enhanced sqlite-query handler
ipcMain.handle('sqlite-query', async (_event, options) => {
try {
if (!await verifyPluginState()) {
throw new SQLiteError('Plugin not available', 'sqlite-query');
throw new SQLiteError(
'Plugin not available',
'sqlite-query',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
}
const { statement, values } = options;
if (!statement) {
throw new SQLiteError('No statement provided', 'sqlite-query');
}
// Add database name to options
const queryOptions = {
database: 'timesafari', // Use the same database name as in initialization
statement,
values: values || []
const queryOptions = validateQueryOptions(options);
const queryWithDb = {
database: 'timesafari',
...queryOptions
};
logger.debug('Executing SQL query:', queryOptions);
const result = await pluginState.instance.query(queryOptions);
logger.debug('Executing SQL query:', {
...queryWithDb,
timestamp: new Date().toISOString()
});
const result = await pluginState.instance.query(queryWithDb);
if (!result) {
throw new SQLiteError('Query operation returned no result', 'sqlite-query');
throw new SQLiteError(
'Query operation returned no result',
'sqlite-query',
'SQLITE_NO_RESULT',
null,
{ query: queryWithDb }
);
}
return result;
// Validate result structure
if (!('values' in result) && !('changes' in result)) {
throw new SQLiteError(
'Invalid query result structure',
'sqlite-query',
'SQLITE_INVALID_RESULT',
null,
{ result }
);
}
return result as SQLiteResult;
} catch (error) {
throw handleError(error, 'sqlite-query');
throw handleError(error, 'sqlite-query', { options });
}
});
logger.info('SQLite IPC handlers registered successfully');
logger.info('SQLite IPC handlers registered successfully', {
handlers: handlers.join(', '),
timestamp: new Date().toISOString()
});
}

54
src/types/electron.d.ts

@ -13,9 +13,16 @@ interface ElectronAPI {
[key: string]: unknown;
}) => Promise<void>;
closeConnection: (options: { database: string }) => Promise<void>;
query: (options: { statement: string; values?: unknown[] }) => Promise<unknown>;
run: (options: { statement: string; values?: unknown[] }) => Promise<unknown>;
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<unknown>;
query: (options: { statement: string; values?: unknown[] }) => Promise<{
values?: Record<string, unknown>[];
changes?: { changes: number; lastId?: number };
}>;
run: (options: { statement: string; values?: unknown[] }) => Promise<{
changes?: { changes: number; lastId?: number };
}>;
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{
changes?: { changes: number; lastId?: number }[];
}>;
getPlatform: () => Promise<string>;
};
ipcRenderer: {
@ -34,7 +41,46 @@ interface ElectronAPI {
declare global {
interface Window {
electron: ElectronAPI;
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>;
echo: (value: string) => Promise<{ value: string }>;
createConnection: (options: {
database: string;
version?: number;
readOnly?: boolean;
readonly?: boolean;
encryption?: string;
mode?: string;
useNative?: boolean;
[key: string]: unknown;
}) => Promise<void>;
closeConnection: (options: { database: string }) => Promise<void>;
query: (options: { statement: string; values?: unknown[] }) => Promise<{
values?: Record<string, unknown>[];
changes?: { changes: number; lastId?: number };
}>;
run: (options: { statement: string; values?: unknown[] }) => Promise<{
changes?: { changes: number; lastId?: number };
}>;
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{
changes?: { changes: number; lastId?: number }[];
}>;
getPlatform: () => Promise<string>;
};
env: {
platform: string;
isDev: boolean;
};
getPath: (pathType: string) => Promise<string>;
getBasePath: () => Promise<string>;
};
}
}

Loading…
Cancel
Save