From be61ba1bce39f8fddc569b9095719b22cf286b16 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 2 Jul 2025 09:58:07 +0000 Subject: [PATCH] Fix Vue property conflicts in PlatformServiceMixin implementation - Remove duplicate property declarations from TopMessage component - Use (this as any) type assertion for mixin methods - Resolves 'Data property already defined' warnings - Fixes 'this.dbQuery is not a function' runtime errors --- src/components/TopMessage.vue | 62 ++++-- src/db/databaseUtil.ts | 17 +- src/main.web.ts | 8 +- src/registerSQLWorker.js | 4 +- src/services/AbsurdSqlDatabaseService.ts | 6 +- src/services/PlatformService.ts | 5 +- src/services/PlatformServiceFactory.ts | 8 +- .../platforms/CapacitorPlatformService.ts | 200 ++++++++++++------ src/services/platforms/WebPlatformService.ts | 30 ++- src/utils/PlatformServiceMixin.ts | 136 ++++++++++++ src/utils/usePlatformService.ts | 128 ++++++----- 11 files changed, 446 insertions(+), 158 deletions(-) create mode 100644 src/utils/PlatformServiceMixin.ts diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index da60b559..51e088e4 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -18,11 +18,23 @@ import { Component, Vue, Prop } from "vue-facing-decorator"; import { AppString, NotificationIface } from "../constants/app"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app"; -import { usePlatformService } from "../utils/usePlatformService"; +import { + PlatformServiceMixin, + IPlatformServiceMixin, +} from "../utils/PlatformServiceMixin"; import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil"; -@Component +@Component({ + mixins: [PlatformServiceMixin], +}) export default class TopMessage extends Vue { + // NOTE: This component uses PlatformServiceMixin which provides: + // - this.dbQuery(), this.dbExec(), this.dbGetOneRow() methods + // - this.platformService computed property + // - this.isCapacitor, this.isWeb, this.isElectron computed properties + // - this.capabilities computed property + // TypeScript requires (this as any) for mixin methods due to compile-time limitations + $notify!: (notification: NotificationIface, timeout?: number) => void; @Prop selected = ""; @@ -59,24 +71,24 @@ export default class TopMessage extends Vue { } /** - * Get settings for the active account using the platform service composable. - * This replaces the direct call to databaseUtil.retrieveSettingsForActiveAccount() - * and demonstrates the new composable pattern. + * Get settings for the active account using the platform service mixin. + * This demonstrates the concise mixin pattern with direct database access. */ private async getActiveAccountSettings() { - const { dbQuery } = usePlatformService(); + // Declare defaultSettings outside try block for proper scope + let defaultSettings; try { // Get default settings first - const defaultSettings = await this.getDefaultSettings(); - + defaultSettings = await this.getDefaultSettings(); + // If no active DID, return defaults if (!defaultSettings.activeDid) { return defaultSettings; } - // Get account-specific settings using the composable - const result = await dbQuery( + // Get account-specific settings using the mixin (much more concise!) + const result = await (this as any).dbQuery( "SELECT * FROM settings WHERE accountDid = ?", [defaultSettings.activeDid], ); @@ -105,22 +117,29 @@ export default class TopMessage extends Vue { return settings; } catch (error) { - console.error(`Failed to retrieve account settings for ${defaultSettings.activeDid}:`, error); - return defaultSettings; + console.error( + `Failed to retrieve account settings for ${defaultSettings?.activeDid}:`, + error, + ); + return ( + defaultSettings || { + id: MASTER_SETTINGS_KEY, + activeDid: undefined, + apiServer: DEFAULT_ENDORSER_API_SERVER, + } + ); } } /** - * Get default settings using the platform service composable + * Get default settings using the platform service mixin */ private async getDefaultSettings() { - const { dbQuery } = usePlatformService(); - try { - const result = await dbQuery( - "SELECT * FROM settings WHERE id = ?", - [MASTER_SETTINGS_KEY], - ); + // Direct database access via mixin - no destructuring needed! + const result = await (this as any).dbQuery("SELECT * FROM settings WHERE id = ?", [ + MASTER_SETTINGS_KEY, + ]); if (!result?.values?.length) { return { @@ -130,7 +149,10 @@ export default class TopMessage extends Vue { }; } - const settings = mapColumnsToValues(result.columns, result.values)[0] as any; + const settings = mapColumnsToValues( + result.columns, + result.values, + )[0] as any; // Handle searchBoxes parsing if (settings.searchBoxes) { diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index 2f5c427c..66a8ac4c 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -182,7 +182,7 @@ export async function logToDb( message: string, level: string = "info", ): Promise { - // Prevent infinite logging loops - if we're already trying to log to database, + // Prevent infinite logging loops - if we're already trying to log to database, // just log to console instead to break circular dependency if (isLoggingToDatabase) { console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`); @@ -210,12 +210,21 @@ export async function logToDb( const sevenDaysAgo = new Date( new Date().getTime() - 7 * 24 * 60 * 60 * 1000, ).toDateString(); // Use date string to match schema - memoryLogs = memoryLogs.filter((log) => log.split(" ")[0] > sevenDaysAgo); - await platform.dbExec("DELETE FROM logs WHERE date < ?", [sevenDaysAgo]); + memoryLogs = memoryLogs.filter( + (log) => log.split(" ")[0] > sevenDaysAgo, + ); + await platform.dbExec("DELETE FROM logs WHERE date < ?", [ + sevenDaysAgo, + ]); lastCleanupDate = todayKey; } } catch (error) { - console.error("Error logging to database:", error, " ... for original message:", message); + console.error( + "Error logging to database:", + error, + " ... for original message:", + message, + ); } } finally { // Always reset the flag to prevent permanent blocking of database logging diff --git a/src/main.web.ts b/src/main.web.ts index 2c328736..e98ee819 100644 --- a/src/main.web.ts +++ b/src/main.web.ts @@ -5,9 +5,13 @@ const platform = process.env.VITE_PLATFORM; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; // Debug: Check SharedArrayBuffer availability -console.log(`[SharedArrayBuffer] Available: ${typeof SharedArrayBuffer !== 'undefined'}`); +console.log( + `[SharedArrayBuffer] Available: ${typeof SharedArrayBuffer !== "undefined"}`, +); console.log(`[Browser] User Agent: ${navigator.userAgent}`); -console.log(`[Headers] Check COOP/COEP in Network tab if SharedArrayBuffer is false`); +console.log( + `[Headers] Check COOP/COEP in Network tab if SharedArrayBuffer is false`, +); // Only import service worker for web builds if (pwa_enabled) { diff --git a/src/registerSQLWorker.js b/src/registerSQLWorker.js index e2992504..7fb67b9d 100644 --- a/src/registerSQLWorker.js +++ b/src/registerSQLWorker.js @@ -29,7 +29,9 @@ let databaseService = null; async function getDatabaseService() { if (!databaseService) { // Dynamic import to prevent circular dependency - const { default: service } = await import("./services/AbsurdSqlDatabaseService"); + const { default: service } = await import( + "./services/AbsurdSqlDatabaseService" + ); databaseService = service; } return databaseService; diff --git a/src/services/AbsurdSqlDatabaseService.ts b/src/services/AbsurdSqlDatabaseService.ts index 8cfc7a25..a4471880 100644 --- a/src/services/AbsurdSqlDatabaseService.ts +++ b/src/services/AbsurdSqlDatabaseService.ts @@ -59,7 +59,7 @@ class AbsurdSqlDatabaseService implements DatabaseService { await this.initializationPromise; } catch (error) { // logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); // DISABLED - console.error(`AbsurdSqlDatabaseService initialize method failed:`, error); + logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); this.initializationPromise = null; // Reset on failure throw error; } @@ -153,7 +153,7 @@ class AbsurdSqlDatabaseService implements DatabaseService { // " ... with params:", // operation.params, // ); - console.error( + logger.error( "Error while processing SQL queue:", error, " ... for sql:", @@ -208,7 +208,7 @@ class AbsurdSqlDatabaseService implements DatabaseService { // logger.error( // DISABLED // `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, // ); - console.error( + logger.error( `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, ); throw new Error( diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 1fe35b48..6221cad8 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -137,8 +137,5 @@ export interface PlatformService { * @param params - The parameters to pass to the query * @returns Promise resolving to the first row as an array, or undefined if no results */ - dbGetOneRow( - sql: string, - params?: unknown[], - ): Promise; + dbGetOneRow(sql: string, params?: unknown[]): Promise; } diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts index e1d66ecb..108d95c2 100644 --- a/src/services/PlatformServiceFactory.ts +++ b/src/services/PlatformServiceFactory.ts @@ -38,9 +38,11 @@ export class PlatformServiceFactory { // Only log when actually creating the instance const platform = process.env.VITE_PLATFORM || "web"; - + if (!PlatformServiceFactory.creationLogged) { - console.log(`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`); + console.log( + `[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`, + ); PlatformServiceFactory.creationLogged = true; } @@ -63,7 +65,7 @@ export class PlatformServiceFactory { public static getStats(): { callCount: number; instanceExists: boolean } { return { callCount: PlatformServiceFactory.callCount, - instanceExists: PlatformServiceFactory.instance !== null + instanceExists: PlatformServiceFactory.instance !== null, }; } } diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index ff446012..267d2ad0 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -187,8 +187,11 @@ export class CapacitorPlatformService implements PlatformService { params: unknown[] = [], ): Promise { // Log incoming parameters for debugging (HIGH PRIORITY) - logger.warn(`[CapacitorPlatformService] queueOperation - SQL: ${sql}, Params:`, params); - + logger.warn( + `[CapacitorPlatformService] queueOperation - SQL: ${sql}, Params:`, + params, + ); + // Convert parameters to SQLite-compatible types with robust serialization const convertedParams = params.map((param, index) => { if (param === null || param === undefined) { @@ -196,51 +199,75 @@ export class CapacitorPlatformService implements PlatformService { } if (typeof param === "object" && param !== null) { // Enhanced debug logging for all objects (HIGH PRIORITY) - logger.warn(`[CapacitorPlatformService] Object param at index ${index}:`, { - type: typeof param, - toString: param.toString(), - constructorName: param.constructor?.name, - isArray: Array.isArray(param), - keys: Object.keys(param), - stringRep: String(param) - }); - + logger.warn( + `[CapacitorPlatformService] Object param at index ${index}:`, + { + type: typeof param, + toString: param.toString(), + constructorName: param.constructor?.name, + isArray: Array.isArray(param), + keys: Object.keys(param), + stringRep: String(param), + }, + ); + // Special handling for Proxy objects (common cause of "An object could not be cloned") const isProxy = this.isProxyObject(param); - logger.warn(`[CapacitorPlatformService] isProxy result for index ${index}:`, isProxy); - + logger.warn( + `[CapacitorPlatformService] isProxy result for index ${index}:`, + isProxy, + ); + // AGGRESSIVE: If toString contains "Proxy", treat as Proxy even if isProxyObject returns false const stringRep = String(param); - const forceProxyDetection = stringRep.includes('Proxy(') || stringRep.startsWith('Proxy'); - logger.warn(`[CapacitorPlatformService] Force proxy detection for index ${index}:`, forceProxyDetection); - + const forceProxyDetection = + stringRep.includes("Proxy(") || stringRep.startsWith("Proxy"); + logger.warn( + `[CapacitorPlatformService] Force proxy detection for index ${index}:`, + forceProxyDetection, + ); + if (isProxy || forceProxyDetection) { - logger.warn(`[CapacitorPlatformService] Proxy object detected at index ${index} (method: ${isProxy ? 'isProxyObject' : 'stringDetection'}), toString: ${stringRep}`); + logger.warn( + `[CapacitorPlatformService] Proxy object detected at index ${index} (method: ${isProxy ? "isProxyObject" : "stringDetection"}), toString: ${stringRep}`, + ); try { // AGGRESSIVE EXTRACTION: Try multiple methods to extract actual values if (Array.isArray(param)) { // Method 1: Array.from() to extract from Proxy(Array) const actualArray = Array.from(param); - logger.info(`[CapacitorPlatformService] Extracted array from Proxy via Array.from():`, actualArray); - + logger.info( + `[CapacitorPlatformService] Extracted array from Proxy via Array.from():`, + actualArray, + ); + // Method 2: Manual element extraction for safety const manualArray: unknown[] = []; for (let i = 0; i < param.length; i++) { manualArray.push(param[i]); } - logger.info(`[CapacitorPlatformService] Manual array extraction:`, manualArray); - + logger.info( + `[CapacitorPlatformService] Manual array extraction:`, + manualArray, + ); + // Use the manual extraction as it's more reliable return manualArray; } else { // For Proxy(Object), try to extract actual object const actualObject = Object.assign({}, param); - logger.info(`[CapacitorPlatformService] Extracted object from Proxy:`, actualObject); + logger.info( + `[CapacitorPlatformService] Extracted object from Proxy:`, + actualObject, + ); return actualObject; } } catch (proxyError) { - logger.error(`[CapacitorPlatformService] Failed to extract from Proxy at index ${index}:`, proxyError); - + logger.error( + `[CapacitorPlatformService] Failed to extract from Proxy at index ${index}:`, + proxyError, + ); + // FALLBACK: Try to extract primitive values manually if (Array.isArray(param)) { try { @@ -248,30 +275,42 @@ export class CapacitorPlatformService implements PlatformService { for (let i = 0; i < param.length; i++) { fallbackArray.push(param[i]); } - logger.info(`[CapacitorPlatformService] Fallback array extraction successful:`, fallbackArray); + logger.info( + `[CapacitorPlatformService] Fallback array extraction successful:`, + fallbackArray, + ); return fallbackArray; } catch (fallbackError) { - logger.error(`[CapacitorPlatformService] Fallback array extraction failed:`, fallbackError); + logger.error( + `[CapacitorPlatformService] Fallback array extraction failed:`, + fallbackError, + ); return `[Proxy Array - Could not extract]`; } } return `[Proxy Object - Could not extract]`; } } - + try { // Safely convert objects and arrays to JSON strings return JSON.stringify(param); } catch (error) { // Handle non-serializable objects - logger.error(`[CapacitorPlatformService] Failed to serialize parameter at index ${index}:`, error); - logger.error(`[CapacitorPlatformService] Problematic parameter:`, param); - + logger.error( + `[CapacitorPlatformService] Failed to serialize parameter at index ${index}:`, + error, + ); + logger.error( + `[CapacitorPlatformService] Problematic parameter:`, + param, + ); + // Fallback: Convert to string representation if (Array.isArray(param)) { return `[Array(${param.length})]`; } - return `[Object ${param.constructor?.name || 'Unknown'}]`; + return `[Object ${param.constructor?.name || "Unknown"}]`; } } if (typeof param === "boolean") { @@ -280,12 +319,16 @@ export class CapacitorPlatformService implements PlatformService { } if (typeof param === "function") { // Functions can't be serialized - convert to string representation - logger.warn(`[CapacitorPlatformService] Function parameter detected and converted to string at index ${index}`); - return `[Function ${param.name || 'Anonymous'}]`; + logger.warn( + `[CapacitorPlatformService] Function parameter detected and converted to string at index ${index}`, + ); + return `[Function ${param.name || "Anonymous"}]`; } if (typeof param === "symbol") { // Symbols can't be serialized - convert to string representation - logger.warn(`[CapacitorPlatformService] Symbol parameter detected and converted to string at index ${index}`); + logger.warn( + `[CapacitorPlatformService] Symbol parameter detected and converted to string at index ${index}`, + ); return param.toString(); } // Numbers, strings, bigints are supported, but ensure bigints are converted to strings @@ -296,13 +339,16 @@ export class CapacitorPlatformService implements PlatformService { }); // Log converted parameters for debugging (HIGH PRIORITY) - logger.warn(`[CapacitorPlatformService] Converted params:`, convertedParams); + logger.warn( + `[CapacitorPlatformService] Converted params:`, + convertedParams, + ); return new Promise((resolve, reject) => { // Create completely plain objects that Vue cannot make reactive // Step 1: Deep clone the converted params to ensure they're plain objects const plainParams = JSON.parse(JSON.stringify(convertedParams)); - + // Step 2: Create operation object using Object.create(null) for no prototype const operation = Object.create(null) as QueuedOperation; operation.type = type; @@ -310,16 +356,25 @@ export class CapacitorPlatformService implements PlatformService { operation.params = plainParams; operation.resolve = (value: unknown) => resolve(value as R); operation.reject = reject; - + // Step 3: Freeze everything to prevent modification Object.freeze(operation.params); Object.freeze(operation); - + // Add enhanced logging to verify our fix - logger.warn(`[CapacitorPlatformService] Final operation.params type:`, typeof operation.params); - logger.warn(`[CapacitorPlatformService] Final operation.params toString:`, operation.params.toString()); - logger.warn(`[CapacitorPlatformService] Final operation.params constructor:`, operation.params.constructor?.name); - + logger.warn( + `[CapacitorPlatformService] Final operation.params type:`, + typeof operation.params, + ); + logger.warn( + `[CapacitorPlatformService] Final operation.params toString:`, + operation.params.toString(), + ); + logger.warn( + `[CapacitorPlatformService] Final operation.params constructor:`, + operation.params.constructor?.name, + ); + this.operationQueue.push(operation); // If we're already initialized, start processing the queue @@ -367,33 +422,42 @@ export class CapacitorPlatformService implements PlatformService { try { // Method 1: Check toString representation const objString = obj.toString(); - if (objString.includes('Proxy(') || objString.startsWith('Proxy')) { - logger.debug("[CapacitorPlatformService] Proxy detected via toString:", objString); + if (objString.includes("Proxy(") || objString.startsWith("Proxy")) { + logger.debug( + "[CapacitorPlatformService] Proxy detected via toString:", + objString, + ); return true; } // Method 2: Check constructor name const constructorName = obj.constructor?.name; - if (constructorName === 'Proxy') { - logger.debug("[CapacitorPlatformService] Proxy detected via constructor name"); + if (constructorName === "Proxy") { + logger.debug( + "[CapacitorPlatformService] Proxy detected via constructor name", + ); return true; } // Method 3: Check Object.prototype.toString const objToString = Object.prototype.toString.call(obj); - if (objToString.includes('Proxy')) { - logger.debug("[CapacitorPlatformService] Proxy detected via Object.prototype.toString"); + if (objToString.includes("Proxy")) { + logger.debug( + "[CapacitorPlatformService] Proxy detected via Object.prototype.toString", + ); return true; } // Method 4: Vue/Reactive Proxy detection - check for __v_ properties - if (typeof obj === 'object' && obj !== null) { + if (typeof obj === "object" && obj !== null) { // Check for Vue reactive proxy indicators - const hasVueProxy = Object.getOwnPropertyNames(obj).some(prop => - prop.startsWith('__v_') || prop.startsWith('__r_') + const hasVueProxy = Object.getOwnPropertyNames(obj).some( + (prop) => prop.startsWith("__v_") || prop.startsWith("__r_"), ); if (hasVueProxy) { - logger.debug("[CapacitorPlatformService] Vue reactive Proxy detected"); + logger.debug( + "[CapacitorPlatformService] Vue reactive Proxy detected", + ); return true; } } @@ -401,15 +465,24 @@ export class CapacitorPlatformService implements PlatformService { // Method 5: Try JSON.stringify and check for Proxy in error or result try { const jsonString = JSON.stringify(obj); - if (jsonString.includes('Proxy')) { - logger.debug("[CapacitorPlatformService] Proxy detected in JSON serialization"); + if (jsonString.includes("Proxy")) { + logger.debug( + "[CapacitorPlatformService] Proxy detected in JSON serialization", + ); return true; } } catch (jsonError) { // If JSON.stringify fails, it might be a non-serializable Proxy - const errorMessage = jsonError instanceof Error ? jsonError.message : String(jsonError); - if (errorMessage.includes('Proxy') || errorMessage.includes('circular') || errorMessage.includes('clone')) { - logger.debug("[CapacitorPlatformService] Proxy detected via JSON serialization error"); + const errorMessage = + jsonError instanceof Error ? jsonError.message : String(jsonError); + if ( + errorMessage.includes("Proxy") || + errorMessage.includes("circular") || + errorMessage.includes("clone") + ) { + logger.debug( + "[CapacitorPlatformService] Proxy detected via JSON serialization error", + ); return true; } } @@ -417,7 +490,10 @@ export class CapacitorPlatformService implements PlatformService { return false; } catch (error) { // If we can't inspect the object, it might be a Proxy causing issues - logger.warn("[CapacitorPlatformService] Could not inspect object for Proxy detection:", error); + logger.warn( + "[CapacitorPlatformService] Could not inspect object for Proxy detection:", + error, + ); return true; // Assume it's a Proxy if we can't inspect it } } @@ -1268,8 +1344,12 @@ export class CapacitorPlatformService implements PlatformService { params?: unknown[], ): Promise { await this.waitForInitialization(); - const result = await this.queueOperation("query", sql, params || []); - + const result = await this.queueOperation( + "query", + sql, + params || [], + ); + // Return the first row from the result, or undefined if no results if (result && result.values && result.values.length > 0) { return result.values[0]; diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 17103880..cabba801 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -47,14 +47,16 @@ export class WebPlatformService implements PlatformService { constructor() { WebPlatformService.instanceCount++; - + // Only warn if multiple instances (which shouldn't happen with singleton) if (WebPlatformService.instanceCount > 1) { - console.error(`[WebPlatformService] ERROR: Multiple instances created! Count: ${WebPlatformService.instanceCount}`); + console.error( + `[WebPlatformService] ERROR: Multiple instances created! Count: ${WebPlatformService.instanceCount}`, + ); } else { console.log(`[WebPlatformService] Initializing web platform service`); } - + // Start worker initialization but don't await it in constructor this.workerInitPromise = this.initializeWorker(); } @@ -74,19 +76,26 @@ export class WebPlatformService implements PlatformService { // This is required for Safari compatibility with nested workers // It installs a handler that proxies web worker creation through the main thread // CRITICAL: Only call initBackend from main thread, not from worker context - const isMainThread = typeof window !== 'undefined'; + const isMainThread = typeof window !== "undefined"; if (isMainThread) { // We're in the main thread - safe to dynamically import and call initBackend try { - const { initBackend } = await import("absurd-sql/dist/indexeddb-main-thread"); + const { initBackend } = await import( + "absurd-sql/dist/indexeddb-main-thread" + ); initBackend(this.worker); } catch (error) { - console.error("[WebPlatformService] Failed to import/call initBackend:", error); + console.error( + "[WebPlatformService] Failed to import/call initBackend:", + error, + ); throw error; } } else { // We're in a worker context - skip initBackend call - console.log("[WebPlatformService] Skipping initBackend call in worker context"); + console.log( + "[WebPlatformService] Skipping initBackend call in worker context", + ); } this.worker.onmessage = (event) => { @@ -120,13 +129,16 @@ export class WebPlatformService implements PlatformService { const { id, type } = message; // Handle absurd-sql internal messages (these are normal, don't log) - if (!id && message.type?.startsWith('__absurd:')) { + if (!id && message.type?.startsWith("__absurd:")) { return; // Internal absurd-sql message, ignore silently } if (!id) { // logger.warn("[WebPlatformService] Received message without ID:", message); // DISABLED - console.warn("[WebPlatformService] Received message without ID:", message); + console.warn( + "[WebPlatformService] Received message without ID:", + message, + ); return; } diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts new file mode 100644 index 00000000..b6a020da --- /dev/null +++ b/src/utils/PlatformServiceMixin.ts @@ -0,0 +1,136 @@ +/** + * Platform Service Mixin for Vue Components + * + * Provides class-level caching of platform service instances to avoid + * repeated PlatformServiceFactory.getInstance() calls throughout components. + * + * This mixin implements a hybrid approach combining: + * - Class-level service caching for performance + * - Vue composition API patterns for modern development + * - Mixin pattern for easy integration with existing class components + * + * Benefits: + * - Eliminates repeated PlatformServiceFactory.getInstance() calls + * - Provides consistent service access pattern across components + * - Improves performance with cached instances + * - Maintains type safety with TypeScript + * + * @author Matthew Raymer + * @version 1.0.0 + * @since 2025-07-02 + */ + +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import type { PlatformService } from "@/services/PlatformService"; + +/** + * Mixin that provides cached platform service access to Vue components + * + * Usage: + * ```typescript + * @Component({ + * mixins: [PlatformServiceMixin] + * }) + * export default class MyComponent extends Vue { + * async someMethod() { + * // Access cached platform service directly + * const result = await this.platformService.dbQuery('SELECT * FROM users'); + * } + * } + * ``` + */ +export const PlatformServiceMixin = { + data() { + return { + _platformService: null as PlatformService | null, + }; + }, + + computed: { + /** + * Get the cached platform service instance + * Creates and caches the instance on first access + */ + platformService(): PlatformService { + if (!(this as any)._platformService) { + (this as any)._platformService = PlatformServiceFactory.getInstance(); + } + return (this as any)._platformService; + }, + + /** + * Check if running on Capacitor platform + */ + isCapacitor(): boolean { + const service = (this as any).platformService as any; + return typeof service.isCapacitor === "function" + ? service.isCapacitor() + : false; + }, + + /** + * Check if running on web platform + */ + isWeb(): boolean { + const service = (this as any).platformService as any; + return typeof service.isWeb === "function" ? service.isWeb() : false; + }, + + /** + * Check if running on Electron platform + */ + isElectron(): boolean { + const service = (this as any).platformService as any; + return typeof service.isElectron === "function" + ? service.isElectron() + : false; + }, + + /** + * Get platform capabilities + */ + capabilities() { + return (this as any).platformService.getCapabilities(); + }, + }, + + methods: { + /** + * Convenient database query method + * Shorthand for this.platformService.dbQuery() + */ + async dbQuery(sql: string, params?: unknown[]) { + return await (this as any).platformService.dbQuery(sql, params); + }, + + /** + * Convenient database execution method + * Shorthand for this.platformService.dbExec() + */ + async dbExec(sql: string, params?: unknown[]) { + return await (this as any).platformService.dbExec(sql, params); + }, + + /** + * Convenient database single row method + * Shorthand for this.platformService.dbGetOneRow() + */ + async dbGetOneRow(sql: string, params?: unknown[]) { + return await (this as any).platformService.dbGetOneRow(sql, params); + }, + }, +}; + +/** + * Type-only export for components that need to declare the mixin interface + */ +export interface IPlatformServiceMixin { + platformService: PlatformService; + dbQuery(sql: string, params?: unknown[]): Promise; + dbExec(sql: string, params?: unknown[]): Promise; + dbGetOneRow(sql: string, params?: unknown[]): Promise; + isCapacitor: boolean; + isWeb: boolean; + isElectron: boolean; + capabilities: any; +} diff --git a/src/utils/usePlatformService.ts b/src/utils/usePlatformService.ts index 2e59172e..a92f41fa 100644 --- a/src/utils/usePlatformService.ts +++ b/src/utils/usePlatformService.ts @@ -1,28 +1,28 @@ /** * Platform Service Composable for TimeSafari - * + * * Provides centralized access to platform-specific services across Vue components. * This composable encapsulates the singleton pattern and provides a clean interface * for components to access platform functionality without directly managing * the PlatformServiceFactory. - * + * * Benefits: * - Centralized service access * - Better testability with easy mocking * - Cleaner component code * - Type safety with TypeScript * - Reactive capabilities if needed in the future - * + * * @author Matthew Raymer * @version 1.0.0 * @since 2025-07-02 */ -import { ref, readonly } from 'vue'; -import { PlatformServiceFactory } from '@/services/PlatformServiceFactory'; -import type { PlatformService } from '@/services/PlatformService'; -import * as databaseUtil from '@/db/databaseUtil'; -import { Contact } from '@/db/tables/contacts'; +import { ref, readonly } from "vue"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import type { PlatformService } from "@/services/PlatformService"; +import * as databaseUtil from "@/db/databaseUtil"; +import { Contact } from "@/db/tables/contacts"; /** * Reactive reference to the platform service instance @@ -48,27 +48,27 @@ function initializePlatformService(): PlatformService { /** * Platform Service Composable - * + * * Provides access to platform-specific services in a composable pattern. * This is the recommended way for Vue components to access platform functionality. - * + * * @returns Object containing platform service and utility functions - * + * * @example * ```typescript * // In a Vue component * import { usePlatformService } from '@/utils/usePlatformService'; - * + * * export default { * setup() { * const { platform, dbQuery, dbExec, takePicture } = usePlatformService(); - * + * * // Use platform methods directly * const takePhoto = async () => { * const result = await takePicture(); * console.log('Photo taken:', result); * }; - * + * * return { takePhoto }; * } * }; @@ -85,46 +85,55 @@ export function usePlatformService() { */ const safeSerializeParams = (params?: unknown[]): unknown[] => { if (!params) return []; - - console.log('[usePlatformService] Original params:', params); - console.log('[usePlatformService] Params toString:', params.toString()); - console.log('[usePlatformService] Params constructor:', params.constructor?.name); - + + console.log("[usePlatformService] Original params:", params); + console.log("[usePlatformService] Params toString:", params.toString()); + console.log( + "[usePlatformService] Params constructor:", + params.constructor?.name, + ); + // Use the most aggressive approach: JSON round-trip + spread operator try { // Method 1: JSON round-trip to completely strip any Proxy const jsonSerialized = JSON.parse(JSON.stringify(params)); - console.log('[usePlatformService] JSON serialized:', jsonSerialized); - + console.log("[usePlatformService] JSON serialized:", jsonSerialized); + // Method 2: Spread operator to create new array const spreadArray = [...jsonSerialized]; - console.log('[usePlatformService] Spread array:', spreadArray); - + console.log("[usePlatformService] Spread array:", spreadArray); + // Method 3: Force primitive extraction for each element - const finalParams = spreadArray.map((param, index) => { + const finalParams = spreadArray.map((param, _index) => { if (param === null || param === undefined) { return param; } - + // Force convert to primitive value - if (typeof param === 'object') { + if (typeof param === "object") { if (Array.isArray(param)) { return [...param]; // Spread to new array } else { return { ...param }; // Spread to new object } } - + return param; }); - - console.log('[usePlatformService] Final params:', finalParams); - console.log('[usePlatformService] Final params toString:', finalParams.toString()); - console.log('[usePlatformService] Final params constructor:', finalParams.constructor?.name); - + + console.log("[usePlatformService] Final params:", finalParams); + console.log( + "[usePlatformService] Final params toString:", + finalParams.toString(), + ); + console.log( + "[usePlatformService] Final params constructor:", + finalParams.constructor?.name, + ); + return finalParams; } catch (error) { - console.error('[usePlatformService] Serialization error:', error); + console.error("[usePlatformService] Serialization error:", error); // Fallback: manual extraction const fallbackParams: unknown[] = []; for (let i = 0; i < params.length; i++) { @@ -133,11 +142,16 @@ export function usePlatformService() { const value = params[i]; fallbackParams.push(value); } catch (accessError) { - console.error('[usePlatformService] Access error for param', i, ':', accessError); + console.error( + "[usePlatformService] Access error for param", + i, + ":", + accessError, + ); fallbackParams.push(String(params[i])); } } - console.log('[usePlatformService] Fallback params:', fallbackParams); + console.log("[usePlatformService] Fallback params:", fallbackParams); return fallbackParams; } }; @@ -194,12 +208,12 @@ export function usePlatformService() { const capabilities = service.getCapabilities(); return !capabilities.isNativeApp; }; - + const isCapacitor = () => { const capabilities = service.getCapabilities(); return capabilities.isNativeApp && capabilities.isMobile; }; - + const isElectron = () => { const capabilities = service.getCapabilities(); return capabilities.isNativeApp && !capabilities.isMobile; @@ -263,9 +277,13 @@ export function usePlatformService() { * @param showBlocked Whether to include blocked contacts * @returns Promise Filtered contacts */ - const getContactsWithFilter = async (showBlocked = true): Promise => { + const getContactsWithFilter = async ( + showBlocked = true, + ): Promise => { const contacts = await getContacts(); - return showBlocked ? contacts : contacts.filter(c => c.iViewContent !== false); + return showBlocked + ? contacts + : contacts.filter((c) => c.iViewContent !== false); }; /** @@ -300,7 +318,10 @@ export function usePlatformService() { */ const getAccount = async (did?: string) => { if (!did) return null; - const result = await dbQuery("SELECT * FROM accounts WHERE did = ? LIMIT 1", [did]); + const result = await dbQuery( + "SELECT * FROM accounts WHERE did = ? LIMIT 1", + [did], + ); const mappedResults = databaseUtil.mapQueryResultToValues(result); return mappedResults.length > 0 ? mappedResults[0] : null; }; @@ -311,19 +332,22 @@ export function usePlatformService() { */ const logActivity = async (message: string) => { const timestamp = new Date().toISOString(); - await dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [timestamp, message]); + await dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ + timestamp, + message, + ]); }; return { // Direct service access (for advanced use cases) platform: readonly(platformService), isInitialized: readonly(isInitialized), - + // Database operations (low-level) dbQuery, - dbExec, + dbExec, dbGetOneRow, - + // Database operations (high-level) getContacts, getContactsWithFilter, @@ -332,34 +356,34 @@ export function usePlatformService() { saveDidSettings, getAccount, logActivity, - + // Media operations takePicture, pickImage, rotateCamera, - + // Platform detection isWeb, - isCapacitor, + isCapacitor, isElectron, getCapabilities, - + // File operations readFile, writeFile, deleteFile, listFiles, writeAndShareFile, - + // Navigation handleDeepLink, - + // Raw service access for cases not covered above - service + service, }; } /** * Type helper for the composable return type */ -export type PlatformServiceComposable = ReturnType; \ No newline at end of file +export type PlatformServiceComposable = ReturnType;