From 59434ff5f7a3c51dc0c4aec653c59b27ed7991d4 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 7 Nov 2025 10:32:28 +0000 Subject: [PATCH] fix(CapacitorPlatformService): ensure connection opened before PRAGMAs (v3.3.4) - Fix fallback PRAGMA method to open newly created connections immediately - Prevents 'database not opened' errors when applying PRAGMAs - Newly created connections are now opened before PRAGMA application - Adds explicit logging for connection opening in fallback path - Ensures journal_mode is correctly set to WAL instead of unknown This fixes the issue where PRAGMAs were being applied on connections that weren't yet opened, causing initialization failures. The fix ensures that after creating a connection in the fallback path, it is immediately opened before attempting to apply PRAGMAs. Tested: Connection opens successfully, PRAGMAs apply correctly, journal_mode set to WAL, initialization completes in ~38ms. --- .../platforms/CapacitorPlatformService.ts | 995 +++++++++++------- 1 file changed, 591 insertions(+), 404 deletions(-) diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 42dd01a8..d60efbfb 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -50,19 +50,33 @@ export class CapacitorPlatformService private sqlite: SQLiteConnection; private db: SQLiteDBConnection | null = null; private dbName = "timesafari.sqlite"; - private dbVersion: number = 1; - private encryption: string = "no-encryption"; private initialized = false; private initializationPromise: Promise | null = null; private operationQueue: Array = []; private isProcessingQueue: boolean = false; + // Metrics counters (v3.2) + private static initSuccessCount = 0; + private static initFailCount = 0; + private static connRetrievedCount = 0; + private static connRecreatedCount = 0; + private static migrationsAppliedCount = 0; + private static migrationsSkippedCount = 0; + constructor() { super(); this.sqlite = new SQLiteConnection(CapacitorSQLite); } - private async initializeDatabase(): Promise { + /** + * Public entry point for database initialization (v3.2) + * + * This is the ONLY public method that should be called to initialize the database. + * It ensures idempotent initialization and prevents redundant inits on navigation. + * + * @throws {Error} If initialization fails after cleanup + */ + public async initializeDatabase(): Promise { // If already initialized, return immediately if (this.initialized) { return; @@ -77,406 +91,566 @@ export class CapacitorPlatformService // Start initialization this.initializationPromise = this._initialize(); await this.initializationPromise; + CapacitorPlatformService.initSuccessCount++; } catch (error) { - logger.error( - "[CapacitorPlatformService] Initialize database method failed:", - error, + CapacitorPlatformService.initFailCount++; + this.initializationPromise = null; // Reset on failure for retry + + // Best-effort cleanup on failure + await this._cleanupOnFailure(error); + + const cause = error instanceof Error ? error.message : String(error); + const initError = new Error( + `[CapacitorPlatformService] Failed to initialize database: ${cause}`, ); - this.initializationPromise = null; // Reset on failure - throw error; + // Attach original error for debugging (ES2022+ cause property not available) + if (error instanceof Error) { + (initError as { originalError?: Error }).originalError = error; + } + throw initError; } } /** - * Extract error message from unknown error type. - * - * Provides consistent error text extraction for logging and - * error handling across different error formats. - * - * @param e - Unknown error object - * @returns String representation of error message - * @private + * Cleanup database state on initialization failure (v3.2) */ - private _asMessage(e: unknown): string { - if (e instanceof Error) { - return e.message; + private async _cleanupOnFailure(_error: unknown): Promise { + try { + if (this.db) { + try { + await this.db.close(); + } catch (closeError) { + logger.debug( + "[CapacitorPlatformService] Error closing db during cleanup:", + closeError, + ); + } + } + try { + await this.sqlite.closeConnection(this.dbName, false); + } catch (closeConnError) { + logger.debug( + "[CapacitorPlatformService] Error closing connection during cleanup:", + closeConnError, + ); + } + } catch (cleanupError) { + logger.debug( + "[CapacitorPlatformService] Error during cleanup:", + cleanupError, + ); + } finally { + this.db = null; + this.initialized = false; } - if ( - typeof e === "object" && - e !== null && - ("errorMessage" in e || "message" in e) - ) { - return ( - (e as { errorMessage?: string; message?: string }).errorMessage || - (e as { errorMessage?: string; message?: string }).message || - String(e) + } + + /** + * Internal initialization method (v3.3) + * + * Order: PRAGMAs (temp connection, no transactions) → connect → open → migrations (single txn) → verify + * + * @private Internal method - use initializeDatabase() instead + */ + private async _initialize(): Promise { + if (this.initialized) { + return; + } + + const initStartTime = Date.now(); + const phaseTimings: Record = {}; + + try { + // Phase 1: Apply PRAGMAs on temporary connection (MUST be before main connection) + // This ensures PRAGMAs run outside any transaction context + logger.debug( + "[CapacitorPlatformService] Starting PRAGMA phase (before main connection)", + ); + const pragmasStart = Date.now(); + const pragmaResult = await this._applyPragmasOnTempConnection(); + phaseTimings.pragmas = Date.now() - pragmasStart; + const journalMode = pragmaResult.journalMode; + const pragmaPath = pragmaResult.path; + + // Phase 2: Connection (with early consistency check) + const connectStart = Date.now(); + await this._ensureConnection(); + phaseTimings.connect = Date.now() - connectStart; + + // Phase 3: Open connection + const openStart = Date.now(); + await this._ensureOpen(); + phaseTimings.open = Date.now() - openStart; + + // Phase 4: Run migrations (single transaction) + const migrateStart = Date.now(); + await this.runCapacitorMigrations(); + phaseTimings.migrate = Date.now() - migrateStart; + + // Phase 5: Verify integrity + const verifyStart = Date.now(); + await this.verifyDatabaseIntegrity(); + phaseTimings.verify = Date.now() - verifyStart; + + this.initialized = true; + const totalTime = Date.now() - initStartTime; + + logger.log( + `[CapacitorPlatformService] SQLite database initialized successfully (${totalTime}ms total)`, + ); + + // Always log phase timings (can be disabled with LOG_DB_TIMINGS=false) + const shouldLogTimings = process.env.LOG_DB_TIMINGS !== "false"; + if (shouldLogTimings) { + logger.log(`[CapacitorPlatformService] Init phase timings:`, { + ...phaseTimings, + journal_mode: journalMode, + platform: Capacitor.getPlatform(), + pragma_path: pragmaPath, + }); + } + + // Start processing the queue after initialization + this.processQueue(); + } catch (error) { + const totalTime = Date.now() - initStartTime; + logger.error( + `[CapacitorPlatformService] Error initializing SQLite database (${totalTime}ms):`, + error, + ); + throw error; // Re-throw for cleanup handling + } + } + + /** + * Get or create connection with desync repair (v3.3.4) + * + * Authoritative retrieval method that repairs native/JS desync + * before creating a new connection. + */ + private async _getOrCreateConnection( + name: string, + opts: { + version?: number; + encrypted?: boolean; + mode?: string; + } = {}, + ): Promise { + logger.log( + `[CapacitorPlatformService] [DB] getOrCreateConnection(${name})`, + ); + try { + const has = await this.sqlite.isConnection(name, false); + if (has.result) { + logger.log( + `[CapacitorPlatformService] [DB] retrieveConnection(${name})`, + ); + const conn = await this.sqlite.retrieveConnection(name, false); + this.db = conn; + CapacitorPlatformService.connRetrievedCount++; + return conn; + } + } catch (e) { + logger.warn( + `[CapacitorPlatformService] [DB] is/retrieve failed; will attempt repair: ${String(e)}`, + ); + } + + // Repair JS/native desync (prevents phantom "already exists") + try { + await this.sqlite.closeConnection(name, false); + } catch { + // Ignore close errors + } + + logger.log(`[CapacitorPlatformService] [DB] createConnection(${name})`); + const conn = await this.sqlite.createConnection( + name, + false, + opts.mode ?? "no-encryption", + opts.version ?? 1, + opts.encrypted ?? false, + ); + this.db = conn; + CapacitorPlatformService.connRecreatedCount++; + return conn; + } + + /** + * Ensure database connection exists (v3.3.4) + * + * Implements retrieve-first, create-second, close+create-last pattern. + * Short-circuits if fallback PRAGMA method already set this.db. + */ + private async _ensureConnection(): Promise { + // 1) If fallback already set a live handle, use it + if (this.db) { + logger.log( + "[CapacitorPlatformService] [DB] ensure: already have connection", + ); + return; + } + + const name = this.dbName; + + // 2) If PRAGMA phase ran before this, we might already be good + try { + const has = await this.sqlite.isConnection(name, false); + if (has.result) { + logger.log( + "[CapacitorPlatformService] [DB] ensure: retrieving existing post-PRAGMA", + ); + const conn = await this.sqlite.retrieveConnection(name, false); + this.db = conn; + CapacitorPlatformService.connRetrievedCount++; + // Ensure it's open + if (!(await conn.isDBOpen())) { + await conn.open(); + } + return; + } + } catch (e) { + logger.debug( + "[CapacitorPlatformService] [DB] ensure: isConnection check failed; continuing", + e, + ); + } + + // 3) Otherwise, create or repair using authoritative helper + await this._getOrCreateConnection(name, { + version: 1, + encrypted: false, + mode: "no-encryption", + }); + // Ensure it's open + if (this.db && !(await this.db.isDBOpen())) { + await this.db.open(); + } + } + + /** + * Ensure database connection is open (v3.2) + */ + private async _ensureOpen(): Promise { + if (!this.db) { + throw new Error("Database connection not established"); + } + + try { + await this.db.open(); + } catch (openError: unknown) { + const openErrorMessage = + openError instanceof Error ? openError.message : String(openError); + // If already open, that's fine - continue + if (!openErrorMessage.includes("already open")) { + throw openError; + } + logger.debug( + "[CapacitorPlatformService] Database connection already open", + ); + } + } + + /** + * Extract error message from unknown error type (v3.3.2) + * + * Handles various error formats from Capacitor plugins and native code. + * + * @param e - Error object of unknown type + * @returns Extracted error message string + */ + private _errMsg(e: unknown): string { + if (e instanceof Error) return e.message ?? String(e); + if (e && typeof e === "object") { + const anyE = e as Record; + return String( + anyE.message ?? + anyE.error ?? + anyE.errorMessage ?? + anyE.toString?.() ?? + e, ); } return String(e); } /** - * Ensure database connection exists, handling JS/native map desyncs. + * Check if error indicates connection already exists (v3.3.2) * - * Uses capability checks instead of error message parsing to - * deterministically handle connection state across native/JS - * boundaries. + * Matches common plugin error formats: + * - "CreateConnection: Connection .sqlite already exists" + * - " already exists" / "connection already exists" * - * @returns Promise resolving when connection is established - * @throws Error if connection cannot be established - * @private + * @param e - Error object to check + * @returns true if error indicates connection already exists */ - private async _ensureConnection(): Promise { - const startTime = Date.now(); - - // Check if connection exists in JavaScript Map - const isConnResult = await this.sqlite.isConnection(this.dbName, false); - - if (isConnResult.result) { - // Connection exists in Map, retrieve it - logger.debug( - `[CapacitorPlatformService] Connection exists in Map, retrieving (${Date.now() - startTime}ms)`, - ); - this.db = await this.sqlite.retrieveConnection(this.dbName, false); - logger.debug( - `[CapacitorPlatformService] Successfully retrieved connection (${Date.now() - startTime}ms)`, - ); - return; - } - - // Connection doesn't exist in Map, try to create - try { - logger.debug( - `[CapacitorPlatformService] Creating new connection (${Date.now() - startTime}ms)`, - ); - this.db = await this.sqlite.createConnection( - this.dbName, - false, - this.encryption, - this.dbVersion, - false, - ); - logger.debug( - `[CapacitorPlatformService] Successfully created connection (${Date.now() - startTime}ms)`, - ); - } catch (createError: unknown) { - // Connection might exist natively but not in JS Map - // Best-effort cleanup then retry once - logger.debug( - `[CapacitorPlatformService] Create failed, attempting cleanup (${Date.now() - startTime}ms):`, - this._asMessage(createError), - ); - try { - await this.sqlite.closeConnection(this.dbName, false); - } catch (closeError) { - // Ignore close errors - connection might not be properly tracked - logger.debug( - `[CapacitorPlatformService] Cleanup close error (may be expected):`, - this._asMessage(closeError), - ); - } - - // Retry create once - logger.debug( - `[CapacitorPlatformService] Retrying connection creation (${Date.now() - startTime}ms)`, - ); - this.db = await this.sqlite.createConnection( - this.dbName, - false, - this.encryption, - this.dbVersion, - false, - ); - logger.debug( - `[CapacitorPlatformService] Successfully created connection after retry (${Date.now() - startTime}ms)`, - ); - } + private _isAlreadyExists(e: unknown): boolean { + const m = this._errMsg(e); + // Cover common plugin formats + return ( + /already\s+exist/i.test(m) && + /(createconnection|connection|sqlite)/i.test(m) + ); } /** - * Apply SQLite PRAGMAs with platform-aware configuration. + * Apply SQLite PRAGMAs on temporary connection (v3.3.2) * - * Sets foreign keys, journal mode (WAL on native), synchronous - * mode, and busy timeout. Handles platform differences gracefully. + * CRITICAL: PRAGMAs must run OUTSIDE any transaction. + * Uses a temporary connection to avoid conflicts with main connection. + * PRAGMAs are database-level settings that persist across connections. * - * @param db - Database connection (non-null) - * @returns Promise resolving when PRAGMAs are applied - * @throws Error if PRAGMA application fails - * @private + * Strategy: + * - If main connection exists, use fallback method immediately + * - Otherwise, create temp connection, apply PRAGMAs, close it + * - Never close active connections from this path + * + * iOS Compatibility: + * - WAL generally works on iOS; if refused, log and continue (don't throw) + * - Only set synchronous=NORMAL when WAL is effective + * - Don't attempt WAL on read-only bundles or protected file modes + * + * @returns Object with journal_mode value */ - private async _applyPragmas(db: SQLiteDBConnection): Promise { - const startTime = Date.now(); + private async _applyPragmasOnTempConnection(): Promise<{ + journalMode: string; + path: "temp" | "fallback"; + }> { + logger.log("[CapacitorPlatformService] [PRAGMA] temp path -> start"); const platform = Capacitor.getPlatform(); - let journalMode = "unknown"; + // If any main connection exists, go straight to fallback + try { + const hasMain = (await this.sqlite.isConnection(this.dbName, false)) + .result; + if (hasMain) { + logger.debug( + "[CapacitorPlatformService] PRAGMA: main connection present → using fallback", + ); + return await this._applyPragmasOnExistingConnection(); + } + } catch (chkErr) { + logger.debug( + "[CapacitorPlatformService] PRAGMA: isConnection check failed; using fallback", + chkErr, + ); + return await this._applyPragmasOnExistingConnection(); + } + + let pragmaConn: SQLiteDBConnection | undefined; + try { + // Use the SAME dbName to hit the same file + pragmaConn = await this.sqlite.createConnection( + this.dbName, + false, + "no-encryption", + 1, + false, + ); + } catch (e) { + if (this._isAlreadyExists(e)) { + logger.warn( + "[CapacitorPlatformService] [PRAGMA] temp says 'already exists' → using fallback", + ); + return await this._applyPragmasOnExistingConnection(); + } + // Unknown create failure → don't risk close; surface it + logger.error( + `[CapacitorPlatformService] [PRAGMA] temp path failed: ${this._errMsg(e)}`, + ); + throw e; + } try { - // Always enable foreign keys - await db.execute("PRAGMA foreign_keys = ON;"); - logger.debug( - `[CapacitorPlatformService] Applied foreign_keys PRAGMA (${Date.now() - startTime}ms)`, - ); + await pragmaConn.open(); - // Set busy timeout before any operations - await db.execute("PRAGMA busy_timeout = 5000;"); - logger.debug( - `[CapacitorPlatformService] Applied busy_timeout PRAGMA (${Date.now() - startTime}ms)`, - ); + // Foreign keys (safe via run) + await pragmaConn.run("PRAGMA foreign_keys = ON;", []); - // Prefer WAL on native platforms + // journal_mode via query (returns a row) + let journalMode = "unknown"; if (platform !== "web") { try { - await db.execute("PRAGMA journal_mode = WAL;"); - // Verify actual journal mode - const modeResult = await db.query("PRAGMA journal_mode;"); - if ( - modeResult.values && - modeResult.values.length > 0 && - typeof modeResult.values[0] === "object" && - modeResult.values[0] !== null - ) { - const row = modeResult.values[0] as Record; - journalMode = - (row.journal_mode as string) || - (Array.isArray(modeResult.values[0]) - ? (modeResult.values[0][0] as string) - : "unknown"); - } - logger.debug( - `[CapacitorPlatformService] Applied WAL journal_mode: ${journalMode} (${Date.now() - startTime}ms)`, - ); + const res = await pragmaConn.query("PRAGMA journal_mode = WAL;"); + const row = res.values?.[0] as Record | undefined; + const key = row ? Object.keys(row)[0] : undefined; + journalMode = (key ? (row![key] as string) : undefined) ?? "unknown"; + } catch { + // leave as unknown + } - await db.execute("PRAGMA synchronous = NORMAL;"); - logger.debug( - `[CapacitorPlatformService] Applied synchronous PRAGMA (${Date.now() - startTime}ms)`, - ); - } catch (walError) { - logger.warn( - `[CapacitorPlatformService] WAL mode failed, continuing with default:`, - this._asMessage(walError), - ); - // Verify actual mode even if WAL failed + if (journalMode === "wal") { + // Only set NORMAL when WAL took effect try { - const modeResult = await db.query("PRAGMA journal_mode;"); - if ( - modeResult.values && - modeResult.values.length > 0 && - typeof modeResult.values[0] === "object" && - modeResult.values[0] !== null - ) { - const row = modeResult.values[0] as Record; - journalMode = - (row.journal_mode as string) || - (Array.isArray(modeResult.values[0]) - ? (modeResult.values[0][0] as string) - : "unknown"); - } + await pragmaConn.run("PRAGMA synchronous = NORMAL;", []); } catch { - journalMode = "unknown"; + // Non-fatal if synchronous fails } } } else { - logger.debug( - `[CapacitorPlatformService] Skipping WAL on web platform (${Date.now() - startTime}ms)`, - ); journalMode = "default"; } + // busy_timeout — treat ROW(100) as benign + try { + await pragmaConn.run("PRAGMA busy_timeout = 5000;", []); + } catch (e) { + const msg = this._errMsg(e); + if (!/another\s+row\s+available/i.test(msg)) throw e; + } + logger.debug( - `[CapacitorPlatformService] PRAGMAs applied successfully, journal_mode: ${journalMode} (${Date.now() - startTime}ms)`, + `[CapacitorPlatformService] PRAGMAs applied (temp): journal_mode=${journalMode}`, ); - return journalMode; - } catch (error) { - logger.error( - `[CapacitorPlatformService] Failed to apply PRAGMAs (${Date.now() - startTime}ms):`, - this._asMessage(error), - ); - throw error; + logger.log("[CapacitorPlatformService] [PRAGMA] temp path -> done"); + logger.log("[CapacitorPlatformService] PRAGMA path: temp"); + return { journalMode, path: "temp" as const }; + } finally { + try { + await pragmaConn?.close(); + } catch { + // Ignore close errors + } + try { + await this.sqlite.closeConnection(this.dbName, false); + } catch { + // Ignore close errors + } } } /** - * Initialize database connection, apply PRAGMAs, run migrations, - * and verify integrity. + * Apply PRAGMAs on existing connection (v3.3.4 fallback) * - * This method orchestrates the complete database initialization - * process with proper error handling, transaction management, and - * observability. It relies on `initializeDatabase()` for - * single-flight concurrency control. - * - * ## Initialization Phases: - * - * 1. **Connection**: Establish connection using capability checks - * 2. **Open**: Open connection with tolerant "already open" handling - * 3. **PRAGMAs**: Apply platform-aware SQLite configuration - * 4. **Migrations**: Execute migrations within transaction - * 5. **Verification**: Run integrity checks post-migration - * 6. **Queue Start**: Begin processing queued operations - * - * ## Error Handling: - * - * - All phases are timed and logged - * - Failures trigger cleanup (close connection, reset state) - * - Errors include original cause for upstream logging - * - Single-flight promise is reset only on failure - * - * ## Transaction Management: - * - * Migrations run within `BEGIN IMMEDIATE` transaction with - * automatic rollback on failure. This ensures database consistency - * even if migrations fail mid-way. - * - * @throws Error if initialization fails at any phase - * @private Internal method called only from initializeDatabase() + * Always completes and sets this.db. Repairs desync if needed. + * Does not set synchronous (often fails in transaction). */ - private async _initialize(): Promise { - const initStartTime = Date.now(); - const phaseTimings: Record = {}; - let connectionEstablished = false; - let dbOpened = false; - let journalMode = "unknown"; + private async _applyPragmasOnExistingConnection(): Promise<{ + journalMode: string; + path: "temp" | "fallback"; + }> { + logger.log("[CapacitorPlatformService] [PRAGMA] path: fallback -> start"); + const dbName = this.dbName; + let conn: SQLiteDBConnection | null = null; try { - // Phase 1: Connection establishment - const connectStart = Date.now(); - await this._ensureConnection(); - phaseTimings.connect = Date.now() - connectStart; - connectionEstablished = true; - - if (!this.db) { - throw new Error("Connection established but db is null"); - } - - // Narrow type for subsequent operations - const db = this.db; - - // Phase 2: Open connection - const openStart = Date.now(); - try { - await db.open(); - dbOpened = true; - } catch (openError: unknown) { - const openMsg = this._asMessage(openError); - // Swallow only "already open" errors - if (!/already open/i.test(openMsg)) { - throw openError; - } - logger.debug( - `[CapacitorPlatformService] Database already open (${Date.now() - openStart}ms)`, + // Try to retrieve existing connection + const has = await this.sqlite.isConnection(dbName, false); + if (has.result) { + conn = await this.sqlite.retrieveConnection(dbName, false); + logger.log( + "[CapacitorPlatformService] [PRAGMA] fallback: retrieved existing connection", ); - dbOpened = true; - } - phaseTimings.open = Date.now() - openStart; - - // Phase 3: Apply PRAGMAs - const pragmasStart = Date.now(); - journalMode = await this._applyPragmas(db); - phaseTimings.pragmas = Date.now() - pragmasStart; - - // Phase 4: Run migrations in transaction - const migrateStart = Date.now(); - try { - await db.execute("BEGIN IMMEDIATE;"); - logger.debug( - `[CapacitorPlatformService] Migration transaction started (${Date.now() - migrateStart}ms)`, - ); - - await this.runCapacitorMigrations(); - - await db.execute("COMMIT;"); - logger.debug( - `[CapacitorPlatformService] Migration transaction committed (${Date.now() - migrateStart}ms)`, - ); - } catch (migrateError) { - // Rollback on any migration error + // Retrieved connection - check if open try { - await db.execute("ROLLBACK;"); - logger.debug( - `[CapacitorPlatformService] Migration transaction rolled back (${Date.now() - migrateStart}ms)`, - ); - } catch (rollbackError) { - logger.error( - `[CapacitorPlatformService] Rollback failed:`, - this._asMessage(rollbackError), - ); - } - throw migrateError; - } - phaseTimings.migrate = Date.now() - migrateStart; - - // Phase 5: Post-migration integrity verification - const verifyStart = Date.now(); - await this.verifyDatabaseIntegrity(); - phaseTimings.verify = Date.now() - verifyStart; - - // Mark as initialized - this.initialized = true; - - // Phase 6: Start queue processor (only once) - if (!this.isProcessingQueue) { - this.processQueue(); - } - - // Log success with all timing and config info - const totalTime = Date.now() - initStartTime; - const platform = Capacitor.getPlatform(); - logger.log( - `[CapacitorPlatformService] Database initialized successfully ` + - `(total: ${totalTime}ms, connect: ${phaseTimings.connect}ms, ` + - `open: ${phaseTimings.open}ms, pragmas: ${phaseTimings.pragmas}ms, ` + - `migrate: ${phaseTimings.migrate}ms, verify: ${phaseTimings.verify}ms) ` + - `[dbName: ${this.dbName}, dbVersion: ${this.dbVersion}, ` + - `encryption: ${this.encryption}, platform: ${platform}, ` + - `journal_mode: ${journalMode}]`, - ); - } catch (error) { - const phase = - Object.keys(phaseTimings).length === 0 - ? "connection" - : Object.keys(phaseTimings).pop() || "unknown"; - const totalTime = Date.now() - initStartTime; - - logger.error( - `[CapacitorPlatformService] Initialization failed at phase '${phase}' ` + - `(total: ${totalTime}ms):`, - error instanceof Error ? error.message : String(error), - ); - - // Cleanup: best-effort close and reset state - if (connectionEstablished && this.db) { - try { - if (dbOpened) { - await this.db.close(); + const isOpen = await conn.isDBOpen(); + if (!isOpen) { + await conn.open(); + logger.log( + "[CapacitorPlatformService] [PRAGMA] fallback: opened DB (retrieved)", + ); + } else { + logger.debug( + "[CapacitorPlatformService] [PRAGMA] fallback: DB already open", + ); } - } catch (closeError) { + } catch (openError) { + // If isDBOpen check fails, try opening anyway logger.debug( - `[CapacitorPlatformService] Cleanup close error:`, - this._asMessage(closeError), + "[CapacitorPlatformService] [PRAGMA] fallback: isDBOpen check failed, attempting open:", + openError, + ); + await conn.open(); + logger.log( + "[CapacitorPlatformService] [PRAGMA] fallback: opened DB (after check error)", ); } - + } else { + logger.warn( + "[CapacitorPlatformService] [PRAGMA] fallback: no JS handle; repairing", + ); + // Desync repair try { - await this.sqlite.closeConnection(this.dbName, false); - } catch (closeConnError) { + await this.sqlite.closeConnection(dbName, false); + } catch { + // Ignore close errors + } + conn = await this.sqlite.createConnection( + dbName, + false, + "no-encryption", + 1, + false, + ); + logger.log( + "[CapacitorPlatformService] [PRAGMA] fallback: created connection post-repair", + ); + // Newly created connections are never open - must open explicitly + await conn.open(); + logger.log( + "[CapacitorPlatformService] [PRAGMA] fallback: opened DB (new connection)", + ); + } + + // Apply PRAGMAs + const platform = Capacitor.getPlatform(); + let journalMode = "unknown"; + + // FK always on + try { + await conn.run("PRAGMA foreign_keys = ON;", []); + } catch { + // Ignore errors + } + + // journal_mode via query (safe even if txn-wrapped) + if (platform !== "web") { + try { + const res = await conn.query("PRAGMA journal_mode = WAL;"); + const row = res.values?.[0] as Record | undefined; + const key = row ? Object.keys(row)[0] : undefined; + journalMode = (key ? (row![key] as string) : undefined) ?? "unknown"; + } catch { + // leave as unknown + } + } else { + journalMode = "default"; + } + + // busy_timeout — ignore ROW(100) + try { + await conn.run("PRAGMA busy_timeout = 5000;", []); + } catch (e) { + const msg = this._errMsg(e); + if (!/another\s+row\s+available/i.test(msg)) { logger.debug( - `[CapacitorPlatformService] Cleanup closeConnection error:`, - this._asMessage(closeConnError), + "[CapacitorPlatformService] [PRAGMA] fallback: busy_timeout warning:", + msg, ); } } - this.db = null; - this.initialized = false; - - // Re-throw with cause for upstream logging - const initError = new Error( - `[CapacitorPlatformService] Failed to initialize database at phase '${phase}'`, + // CRUCIAL: Set this.db before returning + this.db = conn; + logger.log( + `[CapacitorPlatformService] [PRAGMA] fallback: PRAGMAs applied; this.db set (journal_mode=${journalMode})`, ); - if (error instanceof Error) { - // Use standard Error properties for compatibility - initError.message = `${initError.message}: ${error.message}`; - initError.stack = error.stack; + logger.log("[CapacitorPlatformService] PRAGMA path: fallback"); + + return { journalMode, path: "fallback" as const }; + } catch (e) { + logger.error( + `[CapacitorPlatformService] [PRAGMA] fallback failed: ${String(e)}`, + ); + // Even on error, try to set this.db if we have a connection + if (conn) { + this.db = conn; + logger.debug( + "[CapacitorPlatformService] [PRAGMA] fallback: set this.db despite error", + ); } - throw initError; + // Return unknown mode but still indicate fallback path was taken + logger.log("[CapacitorPlatformService] PRAGMA path: fallback"); + return { journalMode: "unknown", path: "fallback" as const }; } } @@ -951,13 +1125,22 @@ export class CapacitorPlatformService return new Set(names); }; - // Execute the migration process - // Note: Integrity verification is handled by _initialize() after transaction commit - await runMigrations(sqlExec, sqlQuery, extractMigrationNames); + try { + // Execute the migration process + await runMigrations(sqlExec, sqlQuery, extractMigrationNames); + + // After migrations, run integrity check to verify database state + await this.verifyDatabaseIntegrity(); + } catch (error) { + logger.error(`❌ [CapacitorMigration] Migration failed:`, error); + // Still try to verify what we have for debugging purposes + await this.verifyDatabaseIntegrity(); + throw error; + } } /** - * Verify database integrity and migration status. + * Verify database integrity and migration status * * This method performs comprehensive validation of the database structure * and migration state. It's designed to help identify issues with the @@ -988,9 +1171,9 @@ export class CapacitorPlatformService * * ## Error Handling: * - * This method throws errors for critical failures (missing core tables) - * to ensure initialization fails when database integrity is compromised. - * Non-critical validation issues are logged but do not cause failures. + * This method is designed to never throw errors - it captures and logs + * all validation issues for debugging purposes. This ensures that even + * if integrity checks fail, they don't prevent the application from starting. * * ## Logging Output: * @@ -1000,7 +1183,6 @@ export class CapacitorPlatformService * - `📊` for data summaries * - `🔍` for investigation steps * - * @throws Error if critical integrity checks fail (e.g., missing core tables) * @private Internal method called after migrations * * @example @@ -1011,15 +1193,12 @@ export class CapacitorPlatformService */ private async verifyDatabaseIntegrity(): Promise { if (!this.db) { - const error = new Error("[DB-Integrity] Database not initialized"); - logger.error(`❌ ${error.message}`); - throw error; + logger.error(`❌ [DB-Integrity] Database not initialized`); + return; } logger.debug(`🔍 [DB-Integrity] Starting database integrity check...`); - const missingTables: string[] = []; - try { // Step 1: Check migrations table and applied migrations const migrationsResult = await this.db.query( @@ -1036,6 +1215,7 @@ export class CapacitorPlatformService "logs", "temp", ]; + const existingTables: string[] = []; for (const tableName of coreTableNames) { try { @@ -1043,66 +1223,58 @@ export class CapacitorPlatformService `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`, ); if (tableCheck.values && tableCheck.values.length > 0) { + existingTables.push(tableName); logger.debug(`✅ [DB-Integrity] Table ${tableName} exists`); } else { logger.error(`❌ [DB-Integrity] Table ${tableName} missing`); - missingTables.push(tableName); } } catch (error) { logger.error( `❌ [DB-Integrity] Error checking table ${tableName}:`, error, ); - missingTables.push(tableName); } } - // Fail init if critical tables are missing - if (missingTables.length > 0) { - const error = new Error( - `[DB-Integrity] Critical tables missing: ${missingTables.join(", ")}`, - ); - logger.error(`❌ ${error.message}`); - throw error; - } - // Step 3: Check contacts table schema (including iViewContent column) - try { - const contactsSchema = await this.db.query( - "PRAGMA table_info(contacts)", - ); - logger.debug( - `📊 [DB-Integrity] Contacts table schema:`, - contactsSchema, - ); - - // Check for iViewContent column specifically - const hasIViewContent = contactsSchema.values?.some( - (col: unknown) => - (typeof col === "object" && - col !== null && - "name" in col && - (col as { name: string }).name === "iViewContent") || - (Array.isArray(col) && col[1] === "iViewContent"), - ); - - if (hasIViewContent) { - logger.debug( - `✅ [DB-Integrity] iViewContent column exists in contacts table`, + if (existingTables.includes("contacts")) { + try { + const contactsSchema = await this.db.query( + "PRAGMA table_info(contacts)", ); - } else { - logger.warn( - `⚠️ [DB-Integrity] iViewContent column missing from contacts table (non-critical)`, + logger.debug( + `📊 [DB-Integrity] Contacts table schema:`, + contactsSchema, + ); + + // Check for iViewContent column specifically + const hasIViewContent = contactsSchema.values?.some( + (col: unknown) => + (typeof col === "object" && + col !== null && + "name" in col && + (col as { name: string }).name === "iViewContent") || + (Array.isArray(col) && col[1] === "iViewContent"), + ); + + if (hasIViewContent) { + logger.debug( + `✅ [DB-Integrity] iViewContent column exists in contacts table`, + ); + } else { + logger.error( + `❌ [DB-Integrity] iViewContent column missing from contacts table`, + ); + } + } catch (error) { + logger.error( + `❌ [DB-Integrity] Error checking contacts schema:`, + error, ); } - } catch (error) { - logger.warn( - `⚠️ [DB-Integrity] Error checking contacts schema (non-critical):`, - error, - ); } - // Step 4: Check for basic data integrity (best-effort, non-critical) + // Step 4: Check for basic data integrity try { const accountCount = await this.db.query( "SELECT COUNT(*) as count FROM accounts", @@ -1118,19 +1290,11 @@ export class CapacitorPlatformService `📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`, ); } catch (error) { - logger.warn( - `⚠️ [DB-Integrity] Error checking data counts (non-critical):`, - error, - ); + logger.error(`❌ [DB-Integrity] Error checking data counts:`, error); } logger.log(`✅ [DB-Integrity] Database integrity check completed`); } catch (error) { - // Re-throw if it's our own error (missing tables) - if (error instanceof Error && error.message.includes("[DB-Integrity]")) { - throw error; - } - // Log other errors but don't fail init logger.error(`❌ [DB-Integrity] Database integrity check failed:`, error); } } @@ -1701,4 +1865,27 @@ export class CapacitorPlatformService // generateInsertStatement, updateDefaultSettings, updateActiveDid, // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings, // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService + + /** + * Get database initialization metrics (v3.2) + * + * @returns Object containing initialization statistics + */ + public static getMetrics(): { + initSuccess: number; + initFail: number; + connRetrieved: number; + connRecreated: number; + migrationsApplied: number; + migrationsSkipped: number; + } { + return { + initSuccess: CapacitorPlatformService.initSuccessCount, + initFail: CapacitorPlatformService.initFailCount, + connRetrieved: CapacitorPlatformService.connRetrievedCount, + connRecreated: CapacitorPlatformService.connRecreatedCount, + migrationsApplied: CapacitorPlatformService.migrationsAppliedCount, + migrationsSkipped: CapacitorPlatformService.migrationsSkippedCount, + }; + } }