refactor-initialize #221
@@ -4,76 +4,181 @@ import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
|
||||
* HMR-safe global singleton storage for PlatformService
|
||||
*
|
||||
* The factory determines which platform implementation to use based on the VITE_PLATFORM
|
||||
* environment variable. Supported platforms are:
|
||||
* - capacitor: Mobile platform using Capacitor
|
||||
* - electron: Desktop platform using Electron with Capacitor
|
||||
* - web: Default web platform (fallback)
|
||||
* Uses multiple fallbacks to ensure persistence across module reloads:
|
||||
* 1. globalThis (standard, works in most environments)
|
||||
* 2. window (browser fallback)
|
||||
* 3. self (web worker 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
|
||||
* ```typescript
|
||||
* const platformService = PlatformServiceFactory.getInstance();
|
||||
* await platformService.takePicture();
|
||||
* import { PlatformSvc } from "./services/PlatformServiceFactory";
|
||||
* await PlatformSvc.takePicture();
|
||||
* ```
|
||||
*/
|
||||
export class PlatformServiceFactory {
|
||||
private static instance: PlatformService | null = null;
|
||||
private static callCount = 0; // Debug counter
|
||||
private static creationLogged = false; // Only log creation once
|
||||
export const PlatformSvc = new Proxy({} as PlatformService, {
|
||||
get(_target, prop) {
|
||||
const svc = getPlatformSvc();
|
||||
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.
|
||||
* Creates the appropriate platform-specific implementation based on environment.
|
||||
*
|
||||
* @returns {PlatformService} The singleton instance of PlatformService
|
||||
* Gets the singleton instance of PlatformService.
|
||||
* @deprecated Use `PlatformSvc` directly instead
|
||||
*/
|
||||
public static getInstance(): PlatformService {
|
||||
PlatformServiceFactory.callCount++;
|
||||
|
||||
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;
|
||||
return PlatformSvc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug method to check singleton usage stats
|
||||
*/
|
||||
public static getStats(): { callCount: number; instanceExists: boolean } {
|
||||
const global = getGlobal();
|
||||
return {
|
||||
callCount: PlatformServiceFactory.callCount,
|
||||
instanceExists: PlatformServiceFactory.instance !== null,
|
||||
callCount: 0, // Deprecated - no longer tracking
|
||||
instanceExists: global.__PLATFORM_SERVICE_SINGLETON__ !== undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Share } from "@capacitor/share";
|
||||
import {
|
||||
SQLiteConnection,
|
||||
SQLiteDBConnection,
|
||||
CapacitorSQLite,
|
||||
DBSQLiteValues,
|
||||
} from "@capacitor-community/sqlite";
|
||||
import { SQLITE } from "./sqlite";
|
||||
|
||||
import { runMigrations } from "@/db-sql/migration";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
@@ -41,16 +40,23 @@ export class CapacitorPlatformService
|
||||
/** Current camera direction */
|
||||
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 dbName = "timesafari.sqlite";
|
||||
private initialized = false;
|
||||
private initializationPromise: Promise<void> | null = null;
|
||||
private operationQueue = new OperationQueue<SQLiteDBConnection>();
|
||||
|
||||
constructor() {
|
||||
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
|
||||
*/
|
||||
@@ -158,7 +184,10 @@ export class CapacitorPlatformService
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.sqlite.closeConnection(this.dbName, false);
|
||||
await SQLITE.closeConnection(
|
||||
CapacitorPlatformService.DB_NAME,
|
||||
CapacitorPlatformService.READ_ONLY,
|
||||
);
|
||||
} catch (closeConnError) {
|
||||
logger.debug(
|
||||
"[CapacitorPlatformService] Error closing connection during cleanup:",
|
||||
@@ -189,11 +218,7 @@ export class CapacitorPlatformService
|
||||
|
||||
this.initializationPromise = (async () => {
|
||||
const conn = await this.runPhase("connect", async () =>
|
||||
this._getOrCreateConnection(this.dbName, {
|
||||
version: 1,
|
||||
encrypted: false,
|
||||
mode: "no-encryption",
|
||||
}),
|
||||
this.getOrCreateConnection(),
|
||||
);
|
||||
|
||||
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(
|
||||
name: string,
|
||||
opts: {
|
||||
version?: number;
|
||||
encrypted?: boolean;
|
||||
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
|
||||
}
|
||||
private async getOrCreateConnection(): Promise<SQLiteDBConnection> {
|
||||
// 1) Reuse existing open connection
|
||||
if (await this.isDbOpen(this.db)) {
|
||||
this.connLog("reusing existing open connection");
|
||||
return this.db!;
|
||||
}
|
||||
|
||||
try {
|
||||
const has = await this.sqlite.isConnection(name, false);
|
||||
if (has.result) {
|
||||
const conn = await this.sqlite.retrieveConnection(name, false);
|
||||
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,
|
||||
// 2) Check manager state FIRST (before attempting create)
|
||||
const exists = await SQLITE.isConnection(
|
||||
CapacitorPlatformService.DB_NAME,
|
||||
CapacitorPlatformService.READ_ONLY,
|
||||
);
|
||||
this.db = conn;
|
||||
await conn.open();
|
||||
return conn;
|
||||
this.connLog(`isConnection? ${exists.result ? "yes" : "no"}`);
|
||||
|
||||
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> {
|
||||
@@ -443,6 +635,40 @@ export class CapacitorPlatformService
|
||||
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> {
|
||||
// If we have an initialization promise, wait for it
|
||||
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