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
This commit is contained in:
@@ -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');
|
||||
throw new SQLiteError(
|
||||
'Plugin not available',
|
||||
'sqlite-run',
|
||||
'SQLITE_PLUGIN_UNAVAILABLE',
|
||||
null,
|
||||
{ pluginState }
|
||||
);
|
||||
}
|
||||
|
||||
const { statement, values } = options;
|
||||
if (!statement) {
|
||||
throw new SQLiteError('No statement provided', 'sqlite-run');
|
||||
}
|
||||
|
||||
// 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 }
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
// Validate result structure
|
||||
if (!('changes' in result)) {
|
||||
throw new SQLiteError(
|
||||
'Invalid run result structure',
|
||||
'sqlite-run',
|
||||
'SQLITE_INVALID_RESULT',
|
||||
null,
|
||||
{ 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
vendored
54
src/types/electron.d.ts
vendored
@@ -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>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user