You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

970 lines
29 KiB

/**
* SQLite Initialization and Management for TimeSafari Electron
*
* This module handles the complete lifecycle of SQLite database initialization,
* connection management, and IPC communication in the TimeSafari Electron app.
*
* Key Features:
* - Database path management with proper permissions
* - Plugin initialization and state verification
* - Connection lifecycle management
* - PRAGMA configuration for optimal performance
* - Migration system integration with configurable logging
* - Error handling and recovery
* - IPC communication layer with proper cleanup
*
* Database Configuration:
* - Uses WAL journal mode for better concurrency
* - Configures optimal PRAGMA settings
* - Implements connection pooling
* - Handles encryption (when enabled)
*
* State Management:
* - Tracks plugin initialization state
* - Monitors connection health
* - Manages transaction state
* - Implements recovery mechanisms
*
* Error Handling:
* - Custom SQLiteError class for detailed error tracking
* - Comprehensive error logging
* - Automatic recovery attempts
* - State verification before operations
*
* Security:
* - Proper file permissions (0o755)
* - Write access verification
* - Connection state validation
* - Transaction safety
*
* Performance:
* - WAL mode for better concurrency
* - Optimized PRAGMA settings
* - Connection pooling
* - Efficient state management
*
* @author Matthew Raymer <matthew.raymer@anomalistdesign.com>
* @version 1.0.0
* @since 2025-06-01
*/
import { app, ipcMain } from 'electron';
import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js';
import * as SQLiteModule from '@capacitor-community/sqlite/electron/dist/plugin.js';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { runMigrations } from './sqlite-migrations';
import { logger } from './logger';
import { startDatabaseOperation, endDatabaseOperation } from '../setup';
// Types for state management
interface PluginState {
isInitialized: boolean;
isAvailable: boolean;
lastVerified: Date | null;
lastError: Error | null;
instance: any | null;
}
interface TransactionState {
isActive: boolean;
lastVerified: Date | null;
database: string | null;
}
// State tracking
let pluginState: PluginState = {
isInitialized: false,
isAvailable: false,
lastVerified: null,
lastError: null,
instance: null
};
let transactionState: TransactionState = {
isActive: false,
lastVerified: null,
database: null
};
// Constants
const MAX_RECOVERY_ATTEMPTS = 3;
const RECOVERY_DELAY_MS = 1000;
const VERIFICATION_TIMEOUT_MS = 5000;
// Type definitions for SQLite operations
interface SQLiteConnectionOptions {
database: string;
version?: number;
readOnly?: boolean;
readonly?: boolean;
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 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
};
}
}
// 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,
code,
error,
details
);
};
// Add delay utility with timeout
const delay = (ms: number, timeoutMs: number = VERIFICATION_TIMEOUT_MS): Promise<void> => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new SQLiteError('Operation timed out', 'delay'));
}, timeoutMs);
setTimeout(() => {
clearTimeout(timeout);
resolve();
}, ms);
});
};
// Plugin state verification
const verifyPluginState = async (): Promise<boolean> => {
if (!pluginState.instance || !pluginState.isInitialized) {
return false;
}
try {
// Test plugin responsiveness
const echoResult = await pluginState.instance.echo({ value: 'test' });
if (!echoResult || echoResult.value !== 'test') {
throw new SQLiteError('Plugin echo test failed', 'verifyPluginState');
}
pluginState.isAvailable = true;
pluginState.lastVerified = new Date();
pluginState.lastError = null;
return true;
} catch (error) {
pluginState.isAvailable = false;
pluginState.lastError = handleError(error, 'verifyPluginState');
return false;
}
};
// Transaction state verification
const verifyTransactionState = async (database: string): Promise<boolean> => {
if (!pluginState.instance || !pluginState.isAvailable) {
return false;
}
try {
// Check if we're in a transaction
const isActive = await pluginState.instance.isTransactionActive({ database });
// Only update state if it's different
if (isActive !== transactionState.isActive || transactionState.database !== database) {
transactionState.isActive = isActive;
transactionState.lastVerified = new Date();
transactionState.database = isActive ? database : null;
}
return true;
} catch (error) {
// Reset state on error
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
logger.error('Transaction state verification failed:', error);
return false;
}
};
// Plugin initialization
const initializePlugin = async (): Promise<boolean> => {
logger.info('Starting plugin initialization');
try {
// Create plugin instance
let rawPlugin;
if (SQLiteModule.default?.CapacitorSQLite) {
logger.debug('Using default export CapacitorSQLite');
rawPlugin = new SQLiteModule.default.CapacitorSQLite();
} else {
logger.debug('Using direct CapacitorSQLite class');
rawPlugin = new CapacitorSQLite();
}
// Verify instance
if (!rawPlugin || typeof rawPlugin !== 'object') {
throw new SQLiteError('Invalid plugin instance created', 'initializePlugin');
}
// Test plugin functionality
const echoResult = await rawPlugin.echo({ value: 'test' });
if (!echoResult || echoResult.value !== 'test') {
throw new SQLiteError('Plugin echo test failed', 'initializePlugin');
}
// Update state only after successful verification
pluginState = {
isInitialized: true,
isAvailable: true,
lastVerified: new Date(),
lastError: null,
instance: rawPlugin
};
logger.info('Plugin initialized successfully');
return true;
} catch (error) {
pluginState = {
isInitialized: false,
isAvailable: false,
lastVerified: new Date(),
lastError: handleError(error, 'initializePlugin'),
instance: null
};
logger.error('Plugin initialization failed:', {
error: pluginState.lastError,
timestamp: new Date().toISOString()
});
return false;
}
};
// Recovery mechanism
const recoverPluginState = async (attempt: number = 1): Promise<boolean> => {
logger.info(`Attempting plugin state recovery (attempt ${attempt}/${MAX_RECOVERY_ATTEMPTS})`);
if (attempt > MAX_RECOVERY_ATTEMPTS) {
logger.error('Max recovery attempts reached');
return false;
}
try {
// Cleanup existing connection if any
if (pluginState.instance) {
try {
await pluginState.instance.closeConnection({ database: 'timesafari' });
logger.debug('Closed existing database connection during recovery');
} catch (error) {
logger.warn('Error closing connection during recovery:', error);
}
}
// Reset state
pluginState = {
isInitialized: false,
isAvailable: false,
lastVerified: new Date(),
lastError: null,
instance: null
};
// Wait before retry with exponential backoff
const backoffDelay = RECOVERY_DELAY_MS * Math.pow(2, attempt - 1);
await delay(backoffDelay);
// Reinitialize
const success = await initializePlugin();
if (!success && attempt < MAX_RECOVERY_ATTEMPTS) {
return recoverPluginState(attempt + 1);
}
return success;
} catch (error) {
logger.error('Plugin recovery failed:', error);
if (attempt < MAX_RECOVERY_ATTEMPTS) {
return recoverPluginState(attempt + 1);
}
return false;
}
};
/**
* Initializes database paths and ensures proper permissions
*
* This function:
* 1. Creates the database directory if it doesn't exist
* 2. Sets proper permissions (0o755)
* 3. Verifies write access
* 4. Returns the absolute path to the database directory
*
* @returns {Promise<string>} Absolute path to database directory
* @throws {SQLiteError} If directory creation or permission setting fails
*/
const initializeDatabasePaths = async (): Promise<string> => {
try {
// Get the absolute app data directory
const appDataDir = path.join(os.homedir(), 'Databases', 'TimeSafari');
logger.info('App data directory:', appDataDir);
// Ensure directory exists with proper permissions
if (!fs.existsSync(appDataDir)) {
await fs.promises.mkdir(appDataDir, {
recursive: true,
mode: 0o755
});
} else {
await fs.promises.chmod(appDataDir, 0o755);
}
// Verify directory permissions
const stats = await fs.promises.stat(appDataDir);
logger.info('Directory permissions:', {
mode: stats.mode.toString(8),
uid: stats.uid,
gid: stats.gid,
isDirectory: stats.isDirectory(),
isWritable: !!(stats.mode & 0o200)
});
// Test write access
const testFile = path.join(appDataDir, '.write-test');
await fs.promises.writeFile(testFile, 'test');
await fs.promises.unlink(testFile);
return appDataDir;
} catch (error) {
throw handleError(error, 'initializeDatabasePaths');
}
};
/**
* Main SQLite initialization function
*
* Orchestrates the complete database initialization process:
* 1. Sets up database paths
* 2. Initializes the SQLite plugin
* 3. Creates and verifies database connection
* 4. Configures database PRAGMAs
* 5. Runs database migrations
* 6. Handles errors and recovery
*
* Database Configuration:
* - Uses WAL journal mode
* - Enables foreign keys
* - Sets optimal page size and cache
* - Configures busy timeout
*
* Error Recovery:
* - Implements exponential backoff
* - Verifies plugin state
* - Attempts connection recovery
* - Maintains detailed error logs
*
* @throws {SQLiteError} If initialization fails and recovery is unsuccessful
*/
export async function initializeSQLite(): Promise<void> {
logger.info('Starting SQLite initialization');
try {
// Initialize database paths
const dbDir = await initializeDatabasePaths();
const dbPath = path.join(dbDir, 'timesafariSQLite.db');
// Initialize plugin
if (!await initializePlugin()) {
throw new SQLiteError('Plugin initialization failed', 'initializeSQLite');
}
// Verify plugin state
if (!await verifyPluginState()) {
throw new SQLiteError('Plugin state verification failed', 'initializeSQLite');
}
// Set up database connection - simplified to match sacred-sql
const connectionOptions = {
database: 'timesafari',
version: 1,
readOnly: false,
useNative: true,
mode: 'rwc'
};
// Create and verify connection
logger.debug('Creating database connection:', connectionOptions);
await pluginState.instance.createConnection(connectionOptions);
await delay(500); // Wait for connection registration
const isRegistered = await pluginState.instance.isDatabase({
database: connectionOptions.database
});
if (!isRegistered) {
throw new SQLiteError('Database not registered', 'initializeSQLite');
}
// Open database
logger.debug('Opening database with options:', connectionOptions);
await pluginState.instance.open({
...connectionOptions,
mode: 'rwc'
});
// Set PRAGMAs to match sacred-sql approach
const pragmaStatements = [
'PRAGMA journal_mode = MEMORY;', // Changed to MEMORY mode to match sacred-sql
'PRAGMA foreign_keys = ON;',
'PRAGMA synchronous = NORMAL;',
'PRAGMA temp_store = MEMORY;',
'PRAGMA page_size = 4096;',
'PRAGMA cache_size = 2000;'
// Removed WAL-specific settings
];
logger.debug('Setting database PRAGMAs');
for (const statement of pragmaStatements) {
try {
logger.debug('Executing PRAGMA:', statement);
const result = await pluginState.instance.execute({
database: connectionOptions.database,
statements: statement,
transaction: false
});
logger.debug('PRAGMA result:', { statement, result });
} catch (error) {
logger.error('PRAGMA execution failed:', {
statement,
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
name: error.name
} : error
});
throw error;
}
}
// Run migrations with enhanced error logging
logger.info('Starting database migrations');
const migrationResults = await runMigrations(
pluginState.instance,
connectionOptions.database
);
// Check migration results with detailed logging
const failedMigrations = migrationResults.filter(r => !r.success);
if (failedMigrations.length > 0) {
logger.error('Migration failures:', {
totalMigrations: migrationResults.length,
failedCount: failedMigrations.length,
failures: failedMigrations.map(f => ({
version: f.version,
name: f.name,
error: f.error instanceof Error ? {
message: f.error.message,
stack: f.error.stack,
name: f.error.name
} : f.error,
state: f.state
}))
});
throw new SQLiteError(
`Database migrations failed: ${failedMigrations.map(f => f.name).join(', ')}`,
'initializeSQLite',
'SQLITE_MIGRATION_FAILED',
failedMigrations
);
}
logger.info('SQLite initialization completed successfully');
} catch (error) {
const sqliteError = handleError(error, 'initializeSQLite');
logger.error('SQLite initialization failed:', {
error: sqliteError,
pluginState: {
isInitialized: pluginState.isInitialized,
isAvailable: pluginState.isAvailable,
lastVerified: pluginState.lastVerified,
lastError: pluginState.lastError
}
});
// Attempt recovery
if (await recoverPluginState()) {
logger.info('Recovery successful, retrying initialization');
return initializeSQLite();
}
throw sqliteError;
}
}
// Add IPC handler tracking
const registeredHandlers = new Set<string>();
/**
* Removes all registered SQLite IPC handlers
* Called before re-registering handlers to prevent duplicates
*/
const cleanupSQLiteHandlers = (): void => {
logger.debug('Cleaning up SQLite IPC handlers');
for (const channel of registeredHandlers) {
try {
ipcMain.removeHandler(channel);
logger.debug(`Removed handler for channel: ${channel}`);
} catch (error) {
logger.warn(`Failed to remove handler for channel ${channel}:`, error);
}
}
registeredHandlers.clear();
};
/**
* Registers an IPC handler with tracking
* @param channel The IPC channel to register
* @param handler The handler function
*/
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
if (registeredHandlers.has(channel)) {
logger.debug(`Handler already registered for channel: ${channel}, removing first`);
ipcMain.removeHandler(channel);
}
ipcMain.handle(channel, handler);
registeredHandlers.add(channel);
logger.debug(`Registered handler for channel: ${channel}`);
};
/**
* Sets up IPC handlers for SQLite operations
* Registers handlers for all SQLite operations defined in VALID_CHANNELS.invoke
* Tracks database operations to prevent reloads during critical operations
* Implements proper handler cleanup to prevent duplicates
*
* @author Matthew Raymer
*/
export function setupSQLiteHandlers(): void {
logger.info('Setting up SQLite IPC handlers');
// Clean up any existing handlers first
cleanupSQLiteHandlers();
// Handler for checking SQLite availability
registerHandler('sqlite-is-available', async () => {
logger.debug('Checking SQLite availability');
try {
startDatabaseOperation();
const isAvailable = await verifyPluginState();
logger.debug('SQLite availability check:', { isAvailable });
return isAvailable;
} catch (error) {
logger.error('SQLite availability check failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for echo test
registerHandler('sqlite-echo', async (_event, { value }) => {
logger.debug('SQLite echo test:', { value });
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-echo');
}
const result = await pluginState.instance.echo({ value });
logger.debug('SQLite echo result:', result);
return result;
} catch (error) {
logger.error('SQLite echo failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for creating database connection
registerHandler('sqlite-create-connection', async (_event, options: SQLiteConnectionOptions) => {
logger.debug('Creating SQLite connection:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-create-connection');
}
const validatedOptions = validateConnectionOptions(options);
await pluginState.instance.createConnection(validatedOptions);
logger.debug('SQLite connection created successfully');
return true;
} catch (error) {
logger.error('SQLite connection creation failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for executing SQL statements
registerHandler('sqlite-execute', async (_event, options: SQLiteExecuteOptions) => {
logger.debug('Executing SQL statements:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-execute');
}
const result = await pluginState.instance.execute(options);
logger.debug('SQL execution result:', result);
return result;
} catch (error) {
logger.error('SQL execution failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for querying data
registerHandler('sqlite-query', async (_event, options: SQLiteQueryOptions) => {
logger.debug('Querying SQLite:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-query');
}
const result = await pluginState.instance.query(options);
logger.debug('SQL query result:', result);
return result;
} catch (error) {
logger.error('SQL query failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for running SQL statements
registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
logger.debug('Running SQL statement:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-run');
}
const result = await pluginState.instance.run(options);
logger.debug('SQL run result:', result);
return result;
} catch (error) {
logger.error('SQL run failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for closing database connection
registerHandler('sqlite-close-connection', async (_event, options: { database: string }) => {
logger.debug('Closing SQLite connection:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-close-connection');
}
await pluginState.instance.closeConnection(options);
logger.debug('SQLite connection closed successfully');
return true;
} catch (error) {
logger.error('SQLite connection close failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for opening database
registerHandler('sqlite-open', async (_event, options: SQLiteConnectionOptions) => {
logger.debug('Opening SQLite database:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-open');
}
await pluginState.instance.open(options);
logger.debug('SQLite database opened successfully');
return true;
} catch (error) {
logger.error('SQLite database open failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for closing database
registerHandler('sqlite-close', async (_event, options: { database: string }) => {
logger.debug('Closing SQLite database:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-close');
}
await pluginState.instance.close(options);
logger.debug('SQLite database closed successfully');
return true;
} catch (error) {
logger.error('SQLite database close failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for checking if database is open
registerHandler('sqlite-is-db-open', async (_event, options: { database: string }) => {
logger.debug('Checking if SQLite database is open:', options);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-is-db-open');
}
const isOpen = await pluginState.instance.isDBOpen(options);
logger.debug('SQLite database open check:', { isOpen });
return isOpen;
} catch (error) {
logger.error('SQLite database open check failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for getting database path
registerHandler('get-path', async () => {
logger.debug('Getting database path');
try {
startDatabaseOperation();
const dbDir = await initializeDatabasePaths();
const dbPath = path.join(dbDir, 'timesafariSQLite.db');
logger.debug('Database path:', dbPath);
return dbPath;
} catch (error) {
logger.error('Failed to get database path:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for getting base path
registerHandler('get-base-path', async () => {
logger.debug('Getting base path');
try {
startDatabaseOperation();
const dbDir = await initializeDatabasePaths();
logger.debug('Base path:', dbDir);
return dbDir;
} catch (error) {
logger.error('Failed to get base path:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Handler for SQLite status updates
registerHandler('sqlite-status', async (_event, status: { status: string; database: string; timestamp: number }) => {
logger.debug('SQLite status update:', status);
try {
startDatabaseOperation();
if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-status');
}
// Verify database is still open
const isOpen = await pluginState.instance.isDBOpen({ database: status.database });
if (!isOpen) {
throw new SQLiteError('Database not open', 'sqlite-status');
}
logger.info('SQLite status update processed:', {
status: status.status,
database: status.database,
timestamp: new Date(status.timestamp).toISOString()
});
return { success: true, isOpen };
} catch (error) {
logger.error('SQLite status update failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
logger.info('SQLite IPC handlers setup complete');
}
// Update transaction management to be more careful
const beginTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
throw new SQLiteError('Database not available', 'beginTransaction');
}
// Verify current state first
await verifyTransactionState(database);
if (transactionState.isActive) {
throw new SQLiteError('Transaction already active', 'beginTransaction');
}
try {
await pluginState.instance.beginTransaction({ database });
transactionState.isActive = true;
transactionState.lastVerified = new Date();
transactionState.database = database;
} catch (error) {
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
throw new SQLiteError('Failed to begin transaction', 'beginTransaction', error);
}
};
const commitTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
throw new SQLiteError('Database not available', 'commitTransaction');
}
// Verify current state first
await verifyTransactionState(database);
if (!transactionState.isActive || transactionState.database !== database) {
throw new SQLiteError('No active transaction', 'commitTransaction');
}
try {
await pluginState.instance.commitTransaction({ database });
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
} catch (error) {
// Don't reset state on error - let rollback handle it
throw new SQLiteError('Failed to commit transaction', 'commitTransaction', error);
}
};
const rollbackTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
return; // Just return if plugin not available
}
// Only attempt rollback if we think we're in a transaction
if (transactionState.isActive && transactionState.database === database) {
try {
await pluginState.instance.rollbackTransaction({ database });
} catch (error) {
logger.error('Rollback failed:', error);
}
}
// Always reset state after rollback attempt
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
};