refactor: implement HMR-safe singleton and improve database connection recovery
- Refactor PlatformServiceFactory to use globalThis-based singleton storage - Store singleton in globalThis/window/self for persistence across module reloads - Add comprehensive debug logging for singleton lifecycle tracking - Implement Proxy-based lazy initialization to avoid circular dependencies - Add HMR support to preserve singleton across hot module replacement - Enhance CapacitorPlatformService database connection recovery - Improve getOrCreateConnection() with robust error handling - Add recovery path for 'already exists' desync scenarios - Wrap retrieveConnection and db.open() calls in try-catch blocks - Handle cases where retrieveConnection throws instead of returning null - Add 50ms delay after closeConnection to allow native cleanup - Add shared SQLite connection manager (sqlite.ts) - Centralize SQLiteConnection instance creation - Prevent connection desync issues across the application This addresses the issue where PlatformService singleton was being recreated on navigation, causing repeated database initialization attempts and connection desync errors.
This commit is contained in:
@@ -4,76 +4,181 @@ import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
|||||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating platform-specific service implementations.
|
* HMR-safe global singleton storage for PlatformService
|
||||||
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
|
|
||||||
*
|
*
|
||||||
* The factory determines which platform implementation to use based on the VITE_PLATFORM
|
* Uses multiple fallbacks to ensure persistence across module reloads:
|
||||||
* environment variable. Supported platforms are:
|
* 1. globalThis (standard, works in most environments)
|
||||||
* - capacitor: Mobile platform using Capacitor
|
* 2. window (browser fallback)
|
||||||
* - electron: Desktop platform using Electron with Capacitor
|
* 3. self (web worker fallback)
|
||||||
* - web: Default web platform (fallback)
|
*/
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __PLATFORM_SERVICE_SINGLETON__: PlatformService | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the global object for singleton storage
|
||||||
|
* Uses multiple fallbacks to ensure compatibility
|
||||||
|
*/
|
||||||
|
function getGlobal(): typeof globalThis {
|
||||||
|
if (typeof globalThis !== "undefined") return globalThis;
|
||||||
|
if (typeof window !== "undefined") return window as typeof globalThis;
|
||||||
|
if (typeof self !== "undefined") return self as typeof globalThis;
|
||||||
|
// Fallback for Node.js environments
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return {} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create platform-specific service implementation
|
||||||
|
*
|
||||||
|
* Uses console.log instead of logger to avoid circular dependency
|
||||||
|
* (logger imports PlatformServiceFactory)
|
||||||
|
*/
|
||||||
|
function create(): PlatformService {
|
||||||
|
const which = import.meta.env?.VITE_PLATFORM ?? "web";
|
||||||
|
|
||||||
|
// Use console.log to avoid circular dependency with logger
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`[PlatformServiceFactory] Creating singleton instance for platform: ${which}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (which === "capacitor") return new CapacitorPlatformService();
|
||||||
|
if (which === "electron") return new ElectronPlatformService();
|
||||||
|
return new WebPlatformService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the HMR-safe singleton instance of PlatformService
|
||||||
|
*
|
||||||
|
* Uses lazy initialization to avoid circular dependency issues at module load time.
|
||||||
|
*/
|
||||||
|
function getPlatformSvc(): PlatformService {
|
||||||
|
const global = getGlobal();
|
||||||
|
|
||||||
|
// Debug: Check if singleton exists (always log in dev to diagnose)
|
||||||
|
const exists = global.__PLATFORM_SERVICE_SINGLETON__ !== undefined;
|
||||||
|
const hasKey = Object.prototype.hasOwnProperty.call(
|
||||||
|
global,
|
||||||
|
"__PLATFORM_SERVICE_SINGLETON__",
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`[PlatformServiceFactory] Singleton check: ${exists ? "EXISTS" : "MISSING"} (global type: ${typeof global}, has key: ${hasKey})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[PlatformServiceFactory] Creating new singleton instance");
|
||||||
|
global.__PLATFORM_SERVICE_SINGLETON__ = create();
|
||||||
|
// Verify it was stored
|
||||||
|
if (global.__PLATFORM_SERVICE_SINGLETON__) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
"[PlatformServiceFactory] Singleton created and stored successfully",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
"[PlatformServiceFactory] ERROR: Singleton creation failed - storage returned undefined",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Log singleton reuse
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[PlatformServiceFactory] Reusing existing singleton instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard: ensure singleton exists (should never be undefined at this point)
|
||||||
|
const singleton = global.__PLATFORM_SERVICE_SINGLETON__;
|
||||||
|
if (!singleton) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
"[PlatformServiceFactory] CRITICAL: Singleton is undefined after creation/retrieval",
|
||||||
|
);
|
||||||
|
// Fallback: create a new one
|
||||||
|
global.__PLATFORM_SERVICE_SINGLETON__ = create();
|
||||||
|
return global.__PLATFORM_SERVICE_SINGLETON__;
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMR-safe singleton instance of PlatformService
|
||||||
|
*
|
||||||
|
* This is the ONLY way to access PlatformService throughout the application.
|
||||||
|
* Do not create new instances of platform services directly.
|
||||||
|
*
|
||||||
|
* Uses lazy initialization via Proxy to avoid circular dependency issues at module load time.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* const platformService = PlatformServiceFactory.getInstance();
|
* import { PlatformSvc } from "./services/PlatformServiceFactory";
|
||||||
* await platformService.takePicture();
|
* await PlatformSvc.takePicture();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class PlatformServiceFactory {
|
export const PlatformSvc = new Proxy({} as PlatformService, {
|
||||||
private static instance: PlatformService | null = null;
|
get(_target, prop) {
|
||||||
private static callCount = 0; // Debug counter
|
const svc = getPlatformSvc();
|
||||||
private static creationLogged = false; // Only log creation once
|
const value = (svc as unknown as Record<string, unknown>)[prop as string];
|
||||||
|
// Bind methods to maintain 'this' context
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return value.bind(svc);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preserve singleton across Vite HMR
|
||||||
|
if (import.meta?.hot) {
|
||||||
|
if (import.meta.env?.MODE === "development") {
|
||||||
|
// Defer logging until singleton is actually accessed
|
||||||
|
setTimeout(() => {
|
||||||
|
const global = getGlobal();
|
||||||
|
if (global.__PLATFORM_SERVICE_SINGLETON__) {
|
||||||
|
// Use console.log to avoid circular dependency with logger
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[PlatformSvc] HMR active; singleton pinned to global");
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
import.meta.hot.accept(() => {
|
||||||
|
// Don't recreate on HMR - keep existing instance
|
||||||
|
const global = getGlobal();
|
||||||
|
if (!global.__PLATFORM_SERVICE_SINGLETON__) {
|
||||||
|
// Restore singleton if it was lost during HMR
|
||||||
|
global.__PLATFORM_SERVICE_SINGLETON__ = getPlatformSvc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
import.meta.hot.dispose(() => {
|
||||||
|
// Don't delete - keep the global instance
|
||||||
|
// The singleton will persist in globalThis/window/self
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy factory class for backward compatibility
|
||||||
|
* @deprecated Use `PlatformSvc` directly instead
|
||||||
|
*/
|
||||||
|
export class PlatformServiceFactory {
|
||||||
/**
|
/**
|
||||||
* Gets or creates the singleton instance of PlatformService.
|
* Gets the singleton instance of PlatformService.
|
||||||
* Creates the appropriate platform-specific implementation based on environment.
|
* @deprecated Use `PlatformSvc` directly instead
|
||||||
*
|
|
||||||
* @returns {PlatformService} The singleton instance of PlatformService
|
|
||||||
*/
|
*/
|
||||||
public static getInstance(): PlatformService {
|
public static getInstance(): PlatformService {
|
||||||
PlatformServiceFactory.callCount++;
|
return PlatformSvc;
|
||||||
|
|
||||||
if (PlatformServiceFactory.instance) {
|
|
||||||
// Normal case - return existing instance silently
|
|
||||||
return PlatformServiceFactory.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only log when actually creating the instance
|
|
||||||
const platform = process.env.VITE_PLATFORM || "web";
|
|
||||||
|
|
||||||
if (!PlatformServiceFactory.creationLogged) {
|
|
||||||
// Use console for critical startup message to avoid circular dependency
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`,
|
|
||||||
);
|
|
||||||
PlatformServiceFactory.creationLogged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case "capacitor":
|
|
||||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
|
||||||
break;
|
|
||||||
case "electron":
|
|
||||||
// Use a specialized electron service that extends CapacitorPlatformService
|
|
||||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
|
||||||
break;
|
|
||||||
case "web":
|
|
||||||
default:
|
|
||||||
PlatformServiceFactory.instance = new WebPlatformService();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlatformServiceFactory.instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug method to check singleton usage stats
|
* Debug method to check singleton usage stats
|
||||||
*/
|
*/
|
||||||
public static getStats(): { callCount: number; instanceExists: boolean } {
|
public static getStats(): { callCount: number; instanceExists: boolean } {
|
||||||
|
const global = getGlobal();
|
||||||
return {
|
return {
|
||||||
callCount: PlatformServiceFactory.callCount,
|
callCount: 0, // Deprecated - no longer tracking
|
||||||
instanceExists: PlatformServiceFactory.instance !== null,
|
instanceExists: global.__PLATFORM_SERVICE_SINGLETON__ !== undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import {
|
|||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { Share } from "@capacitor/share";
|
import { Share } from "@capacitor/share";
|
||||||
import {
|
import {
|
||||||
SQLiteConnection,
|
|
||||||
SQLiteDBConnection,
|
SQLiteDBConnection,
|
||||||
CapacitorSQLite,
|
|
||||||
DBSQLiteValues,
|
DBSQLiteValues,
|
||||||
} from "@capacitor-community/sqlite";
|
} from "@capacitor-community/sqlite";
|
||||||
|
import { SQLITE } from "./sqlite";
|
||||||
|
|
||||||
import { runMigrations } from "@/db-sql/migration";
|
import { runMigrations } from "@/db-sql/migration";
|
||||||
import { QueryExecResult } from "@/interfaces/database";
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
@@ -41,16 +40,23 @@ export class CapacitorPlatformService
|
|||||||
/** Current camera direction */
|
/** Current camera direction */
|
||||||
private currentDirection: CameraDirection = CameraDirection.Rear;
|
private currentDirection: CameraDirection = CameraDirection.Rear;
|
||||||
|
|
||||||
private sqlite: SQLiteConnection;
|
// Connection parameters - must be 100% consistent across all plugin calls
|
||||||
|
private static readonly DB_NAME = "timesafari.sqlite";
|
||||||
|
private static readonly READ_ONLY = false;
|
||||||
|
private static readonly VERSION = 1;
|
||||||
|
private static readonly ENCRYPTED = false;
|
||||||
|
private static readonly MODE = "no-encryption";
|
||||||
|
private static readonly LOG_CONN =
|
||||||
|
import.meta.env?.VITE_LOG_DB_CONN === "true";
|
||||||
|
|
||||||
private db: SQLiteDBConnection | null = null;
|
private db: SQLiteDBConnection | null = null;
|
||||||
private dbName = "timesafari.sqlite";
|
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private initializationPromise: Promise<void> | null = null;
|
private initializationPromise: Promise<void> | null = null;
|
||||||
private operationQueue = new OperationQueue<SQLiteDBConnection>();
|
private operationQueue = new OperationQueue<SQLiteDBConnection>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
// Use shared SQLITE instance - do not create new SQLiteConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -71,6 +77,26 @@ export class CapacitorPlatformService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if database connection is open
|
||||||
|
*/
|
||||||
|
private async isDbOpen(db?: SQLiteDBConnection | null): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return !!db && (await db.isDBOpen()).result === true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection lifecycle logging (only when VITE_LOG_DB_CONN=true)
|
||||||
|
*/
|
||||||
|
private connLog(msg: string, extra?: unknown): void {
|
||||||
|
if (CapacitorPlatformService.LOG_CONN) {
|
||||||
|
logger.info(`[CPS][conn] ${msg}`, extra ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply SQLite PRAGMAs - single vetted sequence
|
* Apply SQLite PRAGMAs - single vetted sequence
|
||||||
*/
|
*/
|
||||||
@@ -158,7 +184,10 @@ export class CapacitorPlatformService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.sqlite.closeConnection(this.dbName, false);
|
await SQLITE.closeConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
} catch (closeConnError) {
|
} catch (closeConnError) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[CapacitorPlatformService] Error closing connection during cleanup:",
|
"[CapacitorPlatformService] Error closing connection during cleanup:",
|
||||||
@@ -189,11 +218,7 @@ export class CapacitorPlatformService
|
|||||||
|
|
||||||
this.initializationPromise = (async () => {
|
this.initializationPromise = (async () => {
|
||||||
const conn = await this.runPhase("connect", async () =>
|
const conn = await this.runPhase("connect", async () =>
|
||||||
this._getOrCreateConnection(this.dbName, {
|
this.getOrCreateConnection(),
|
||||||
version: 1,
|
|
||||||
encrypted: false,
|
|
||||||
mode: "no-encryption",
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.runPhase("pragmas", async () => this.applyPragmas(conn));
|
await this.runPhase("pragmas", async () => this.applyPragmas(conn));
|
||||||
@@ -213,60 +238,227 @@ export class CapacitorPlatformService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create connection with desync repair (v3.5 - Minimal)
|
* Get or create connection with deterministic reuse (v3.6)
|
||||||
|
*
|
||||||
|
* Order: reuse in-memory → check manager → retrieve → create
|
||||||
|
* Never calls createConnection when connection already exists in native plugin
|
||||||
*/
|
*/
|
||||||
private async _getOrCreateConnection(
|
private async getOrCreateConnection(): Promise<SQLiteDBConnection> {
|
||||||
name: string,
|
// 1) Reuse existing open connection
|
||||||
opts: {
|
if (await this.isDbOpen(this.db)) {
|
||||||
version?: number;
|
this.connLog("reusing existing open connection");
|
||||||
encrypted?: boolean;
|
return this.db!;
|
||||||
mode?: string;
|
|
||||||
} = {},
|
|
||||||
): Promise<SQLiteDBConnection> {
|
|
||||||
// Check if we already have an open connection
|
|
||||||
if (this.db) {
|
|
||||||
try {
|
|
||||||
const isOpen = await this.db.isDBOpen();
|
|
||||||
if (isOpen.result) {
|
|
||||||
return this.db;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue to retrieve/create
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 2) Check manager state FIRST (before attempting create)
|
||||||
const has = await this.sqlite.isConnection(name, false);
|
const exists = await SQLITE.isConnection(
|
||||||
if (has.result) {
|
CapacitorPlatformService.DB_NAME,
|
||||||
const conn = await this.sqlite.retrieveConnection(name, false);
|
CapacitorPlatformService.READ_ONLY,
|
||||||
this.db = conn;
|
|
||||||
const isOpen = await conn.isDBOpen();
|
|
||||||
if (!isOpen.result) {
|
|
||||||
await conn.open();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Will attempt repair below
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repair JS/native desync
|
|
||||||
try {
|
|
||||||
await this.sqlite.closeConnection(name, false);
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
const conn = await this.sqlite.createConnection(
|
|
||||||
name,
|
|
||||||
false,
|
|
||||||
opts.mode ?? "no-encryption",
|
|
||||||
opts.version ?? 1,
|
|
||||||
opts.encrypted ?? false,
|
|
||||||
);
|
);
|
||||||
this.db = conn;
|
this.connLog(`isConnection? ${exists.result ? "yes" : "no"}`);
|
||||||
await conn.open();
|
|
||||||
return conn;
|
if (exists.result) {
|
||||||
|
// 3) Retrieve and ensure open (before assigning to this.db)
|
||||||
|
try {
|
||||||
|
const db = await SQLITE.retrieveConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
if (db) {
|
||||||
|
try {
|
||||||
|
// Always open retrieved handles before assigning
|
||||||
|
if (!(await this.isDbOpen(db))) {
|
||||||
|
await db.open();
|
||||||
|
}
|
||||||
|
this.db = db;
|
||||||
|
this.connLog("retrieved+open");
|
||||||
|
return db;
|
||||||
|
} catch (openErr: unknown) {
|
||||||
|
const openMsg =
|
||||||
|
openErr instanceof Error ? openErr.message : String(openErr);
|
||||||
|
this.connLog(
|
||||||
|
`retrieved connection but open() failed: ${openMsg} → close stale`,
|
||||||
|
);
|
||||||
|
// Fall through to close + create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Stale registry entry; clean and fall through to create
|
||||||
|
this.connLog("retrieve returned null → close stale");
|
||||||
|
} catch (retrieveErr: unknown) {
|
||||||
|
const retrieveMsg =
|
||||||
|
retrieveErr instanceof Error
|
||||||
|
? retrieveErr.message
|
||||||
|
: String(retrieveErr);
|
||||||
|
// Retrieve threw error - treat as stale registry entry
|
||||||
|
this.connLog(
|
||||||
|
`retrieveConnection threw error: ${retrieveMsg} → close stale`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await SQLITE.closeConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Ignore close errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Create NEW (only if not found in manager)
|
||||||
|
// Self-heal: if create throws "already exists", try retrieve instead
|
||||||
|
try {
|
||||||
|
const db = await SQLITE.createConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.ENCRYPTED,
|
||||||
|
CapacitorPlatformService.MODE,
|
||||||
|
CapacitorPlatformService.VERSION,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
this.db = db;
|
||||||
|
this.connLog("created+open");
|
||||||
|
return db;
|
||||||
|
} catch (openErr: unknown) {
|
||||||
|
const openMsg =
|
||||||
|
openErr instanceof Error ? openErr.message : String(openErr);
|
||||||
|
// If open fails on a newly created connection, treat as "already exists" case
|
||||||
|
if (openMsg.includes("does not exist") || openMsg.includes("already exists")) {
|
||||||
|
this.connLog(
|
||||||
|
`create succeeded but open() failed: ${openMsg} → treating as 'already exists'`,
|
||||||
|
);
|
||||||
|
// Fall through to "already exists" recovery path
|
||||||
|
throw new Error("already exists");
|
||||||
|
}
|
||||||
|
// Re-throw other open errors
|
||||||
|
throw openErr;
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
|
if (errorMessage.includes("already exists")) {
|
||||||
|
// Self-heal: connection exists but wasn't found by isConnection
|
||||||
|
this.connLog("create threw 'already exists' → trying retrieve");
|
||||||
|
try {
|
||||||
|
const db = await SQLITE.retrieveConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
if (db) {
|
||||||
|
try {
|
||||||
|
if (!(await this.isDbOpen(db))) {
|
||||||
|
await db.open();
|
||||||
|
}
|
||||||
|
this.db = db;
|
||||||
|
this.connLog("retrieved+open (after create-throw)");
|
||||||
|
return db;
|
||||||
|
} catch (openErr: unknown) {
|
||||||
|
const openMsg =
|
||||||
|
openErr instanceof Error ? openErr.message : String(openErr);
|
||||||
|
this.connLog(
|
||||||
|
`retrieved connection but open() failed: ${openMsg} → closing stale + recreate`,
|
||||||
|
);
|
||||||
|
// Fall through to close + recreate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (retrieveErr: unknown) {
|
||||||
|
const retrieveMsg =
|
||||||
|
retrieveErr instanceof Error
|
||||||
|
? retrieveErr.message
|
||||||
|
: String(retrieveErr);
|
||||||
|
this.connLog(
|
||||||
|
`retrieveConnection threw error: ${retrieveMsg} → closing stale + recreate`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Retrieve failed after "already exists" - close stale and recreate
|
||||||
|
this.connLog(
|
||||||
|
"retrieve after create-throw failed → closing stale + recreate",
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await SQLITE.closeConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
// Small delay to allow native cleanup
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
} catch (closeErr) {
|
||||||
|
// Log but continue - connection might already be closed
|
||||||
|
this.connLog("closeConnection error (may be expected)", closeErr);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const fresh = await SQLITE.createConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.ENCRYPTED,
|
||||||
|
CapacitorPlatformService.MODE,
|
||||||
|
CapacitorPlatformService.VERSION,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await fresh.open();
|
||||||
|
this.db = fresh;
|
||||||
|
this.connLog("created+open (after stale close)");
|
||||||
|
return fresh;
|
||||||
|
} catch (freshOpenErr: unknown) {
|
||||||
|
const freshOpenMsg =
|
||||||
|
freshOpenErr instanceof Error
|
||||||
|
? freshOpenErr.message
|
||||||
|
: String(freshOpenErr);
|
||||||
|
this.connLog(
|
||||||
|
`recreate succeeded but open() failed: ${freshOpenMsg} → final retrieve attempt`,
|
||||||
|
);
|
||||||
|
// Fall through to final retrieve attempt
|
||||||
|
throw freshOpenErr;
|
||||||
|
}
|
||||||
|
} catch (recreateErr: unknown) {
|
||||||
|
const recreateMsg =
|
||||||
|
recreateErr instanceof Error
|
||||||
|
? recreateErr.message
|
||||||
|
: String(recreateErr);
|
||||||
|
// If recreate fails, try one more retrieve attempt
|
||||||
|
this.connLog(
|
||||||
|
`recreate failed: ${recreateMsg} → final retrieve attempt`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const finalDb = await SQLITE.retrieveConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
if (finalDb) {
|
||||||
|
try {
|
||||||
|
if (!(await this.isDbOpen(finalDb))) {
|
||||||
|
await finalDb.open();
|
||||||
|
}
|
||||||
|
this.db = finalDb;
|
||||||
|
this.connLog("retrieved+open (final attempt)");
|
||||||
|
return finalDb;
|
||||||
|
} catch (finalOpenErr: unknown) {
|
||||||
|
const finalOpenMsg =
|
||||||
|
finalOpenErr instanceof Error
|
||||||
|
? finalOpenErr.message
|
||||||
|
: String(finalOpenErr);
|
||||||
|
this.connLog(
|
||||||
|
`final retrieve succeeded but open() failed: ${finalOpenMsg}`,
|
||||||
|
);
|
||||||
|
// Fall through to throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (finalRetrieveErr: unknown) {
|
||||||
|
const finalRetrieveMsg =
|
||||||
|
finalRetrieveErr instanceof Error
|
||||||
|
? finalRetrieveErr.message
|
||||||
|
: String(finalRetrieveErr);
|
||||||
|
this.connLog(
|
||||||
|
`final retrieve attempt also failed: ${finalRetrieveMsg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// All recovery attempts failed
|
||||||
|
throw new Error(
|
||||||
|
`Failed to establish database connection after recovery attempts: ${recreateMsg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Re-throw if it's not an "already exists" error
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processQueue(): Promise<void> {
|
private async processQueue(): Promise<void> {
|
||||||
@@ -443,6 +635,40 @@ export class CapacitorPlatformService
|
|||||||
await this.initializeDatabase();
|
await this.initializeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicit teardown for database connection (v3.6)
|
||||||
|
*
|
||||||
|
* Use this ONLY on explicit teardown scenarios:
|
||||||
|
* - Sign-out
|
||||||
|
* - App shutdown
|
||||||
|
* - Corruption recovery
|
||||||
|
*
|
||||||
|
* DO NOT call on route changes or component unmounts.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public async teardown(): Promise<void> {
|
||||||
|
if (await this.isDbOpen(this.db)) {
|
||||||
|
try {
|
||||||
|
await this.db?.close();
|
||||||
|
} catch {
|
||||||
|
// Ignore close errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await SQLITE.closeConnection(
|
||||||
|
CapacitorPlatformService.DB_NAME,
|
||||||
|
CapacitorPlatformService.READ_ONLY,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Ignore close errors
|
||||||
|
}
|
||||||
|
this.db = null;
|
||||||
|
this.initialized = false;
|
||||||
|
this.initializationPromise = null;
|
||||||
|
this.connLog("teardown complete");
|
||||||
|
}
|
||||||
|
|
||||||
private async waitForInitialization(): Promise<void> {
|
private async waitForInitialization(): Promise<void> {
|
||||||
// If we have an initialization promise, wait for it
|
// If we have an initialization promise, wait for it
|
||||||
if (this.initializationPromise) {
|
if (this.initializationPromise) {
|
||||||
|
|||||||
20
src/services/platforms/sqlite.ts
Normal file
20
src/services/platforms/sqlite.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Shared SQLite connection manager for Capacitor platform
|
||||||
|
*
|
||||||
|
* Ensures only one SQLiteConnection instance exists across the application,
|
||||||
|
* preventing connection desync issues and unnecessary connection recreation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CapacitorSQLite, SQLiteConnection } from "@capacitor-community/sqlite";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native Capacitor SQLite plugin instance
|
||||||
|
* This is the bridge to the native SQLite implementation
|
||||||
|
*/
|
||||||
|
export const CAP_SQLITE = CapacitorSQLite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared SQLite connection manager
|
||||||
|
* Use this instance throughout the application - do not create new SQLiteConnection instances
|
||||||
|
*/
|
||||||
|
export const SQLITE = new SQLiteConnection(CAP_SQLITE);
|
||||||
Reference in New Issue
Block a user