Browse Source

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
pull/142/head
Matthew Raymer 3 days ago
parent
commit
be61ba1bce
  1. 62
      src/components/TopMessage.vue
  2. 17
      src/db/databaseUtil.ts
  3. 8
      src/main.web.ts
  4. 4
      src/registerSQLWorker.js
  5. 6
      src/services/AbsurdSqlDatabaseService.ts
  6. 5
      src/services/PlatformService.ts
  7. 8
      src/services/PlatformServiceFactory.ts
  8. 200
      src/services/platforms/CapacitorPlatformService.ts
  9. 30
      src/services/platforms/WebPlatformService.ts
  10. 136
      src/utils/PlatformServiceMixin.ts
  11. 128
      src/utils/usePlatformService.ts

62
src/components/TopMessage.vue

@ -18,11 +18,23 @@ import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app"; 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"; import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil";
@Component @Component({
mixins: [PlatformServiceMixin],
})
export default class TopMessage extends Vue { 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; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop selected = ""; @Prop selected = "";
@ -59,24 +71,24 @@ export default class TopMessage extends Vue {
} }
/** /**
* Get settings for the active account using the platform service composable. * Get settings for the active account using the platform service mixin.
* This replaces the direct call to databaseUtil.retrieveSettingsForActiveAccount() * This demonstrates the concise mixin pattern with direct database access.
* and demonstrates the new composable pattern.
*/ */
private async getActiveAccountSettings() { private async getActiveAccountSettings() {
const { dbQuery } = usePlatformService(); // Declare defaultSettings outside try block for proper scope
let defaultSettings;
try { try {
// Get default settings first // Get default settings first
const defaultSettings = await this.getDefaultSettings(); defaultSettings = await this.getDefaultSettings();
// If no active DID, return defaults // If no active DID, return defaults
if (!defaultSettings.activeDid) { if (!defaultSettings.activeDid) {
return defaultSettings; return defaultSettings;
} }
// Get account-specific settings using the composable // Get account-specific settings using the mixin (much more concise!)
const result = await dbQuery( const result = await (this as any).dbQuery(
"SELECT * FROM settings WHERE accountDid = ?", "SELECT * FROM settings WHERE accountDid = ?",
[defaultSettings.activeDid], [defaultSettings.activeDid],
); );
@ -105,22 +117,29 @@ export default class TopMessage extends Vue {
return settings; return settings;
} catch (error) { } catch (error) {
console.error(`Failed to retrieve account settings for ${defaultSettings.activeDid}:`, error); console.error(
return defaultSettings; `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() { private async getDefaultSettings() {
const { dbQuery } = usePlatformService();
try { try {
const result = await dbQuery( // Direct database access via mixin - no destructuring needed!
"SELECT * FROM settings WHERE id = ?", const result = await (this as any).dbQuery("SELECT * FROM settings WHERE id = ?", [
[MASTER_SETTINGS_KEY], MASTER_SETTINGS_KEY,
); ]);
if (!result?.values?.length) { if (!result?.values?.length) {
return { 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 // Handle searchBoxes parsing
if (settings.searchBoxes) { if (settings.searchBoxes) {

17
src/db/databaseUtil.ts

@ -182,7 +182,7 @@ export async function logToDb(
message: string, message: string,
level: string = "info", level: string = "info",
): Promise<void> { ): Promise<void> {
// 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 // just log to console instead to break circular dependency
if (isLoggingToDatabase) { if (isLoggingToDatabase) {
console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`); console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`);
@ -210,12 +210,21 @@ export async function logToDb(
const sevenDaysAgo = new Date( const sevenDaysAgo = new Date(
new Date().getTime() - 7 * 24 * 60 * 60 * 1000, new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
).toDateString(); // Use date string to match schema ).toDateString(); // Use date string to match schema
memoryLogs = memoryLogs.filter((log) => log.split(" ")[0] > sevenDaysAgo); memoryLogs = memoryLogs.filter(
await platform.dbExec("DELETE FROM logs WHERE date < ?", [sevenDaysAgo]); (log) => log.split(" ")[0] > sevenDaysAgo,
);
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
sevenDaysAgo,
]);
lastCleanupDate = todayKey; lastCleanupDate = todayKey;
} }
} catch (error) { } 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 { } finally {
// Always reset the flag to prevent permanent blocking of database logging // Always reset the flag to prevent permanent blocking of database logging

8
src/main.web.ts

@ -5,9 +5,13 @@ const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
// Debug: Check SharedArrayBuffer availability // 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(`[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 // Only import service worker for web builds
if (pwa_enabled) { if (pwa_enabled) {

4
src/registerSQLWorker.js

@ -29,7 +29,9 @@ let databaseService = null;
async function getDatabaseService() { async function getDatabaseService() {
if (!databaseService) { if (!databaseService) {
// Dynamic import to prevent circular dependency // Dynamic import to prevent circular dependency
const { default: service } = await import("./services/AbsurdSqlDatabaseService"); const { default: service } = await import(
"./services/AbsurdSqlDatabaseService"
);
databaseService = service; databaseService = service;
} }
return databaseService; return databaseService;

6
src/services/AbsurdSqlDatabaseService.ts

@ -59,7 +59,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
await this.initializationPromise; await this.initializationPromise;
} catch (error) { } catch (error) {
// logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); // DISABLED // 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 this.initializationPromise = null; // Reset on failure
throw error; throw error;
} }
@ -153,7 +153,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
// " ... with params:", // " ... with params:",
// operation.params, // operation.params,
// ); // );
console.error( logger.error(
"Error while processing SQL queue:", "Error while processing SQL queue:",
error, error,
" ... for sql:", " ... for sql:",
@ -208,7 +208,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
// logger.error( // DISABLED // logger.error( // DISABLED
// `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, // `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`, `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
); );
throw new Error( throw new Error(

5
src/services/PlatformService.ts

@ -137,8 +137,5 @@ export interface PlatformService {
* @param params - The parameters to pass to the query * @param params - The parameters to pass to the query
* @returns Promise resolving to the first row as an array, or undefined if no results * @returns Promise resolving to the first row as an array, or undefined if no results
*/ */
dbGetOneRow( dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
} }

8
src/services/PlatformServiceFactory.ts

@ -38,9 +38,11 @@ export class PlatformServiceFactory {
// Only log when actually creating the instance // Only log when actually creating the instance
const platform = process.env.VITE_PLATFORM || "web"; const platform = process.env.VITE_PLATFORM || "web";
if (!PlatformServiceFactory.creationLogged) { if (!PlatformServiceFactory.creationLogged) {
console.log(`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`); console.log(
`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`,
);
PlatformServiceFactory.creationLogged = true; PlatformServiceFactory.creationLogged = true;
} }
@ -63,7 +65,7 @@ export class PlatformServiceFactory {
public static getStats(): { callCount: number; instanceExists: boolean } { public static getStats(): { callCount: number; instanceExists: boolean } {
return { return {
callCount: PlatformServiceFactory.callCount, callCount: PlatformServiceFactory.callCount,
instanceExists: PlatformServiceFactory.instance !== null instanceExists: PlatformServiceFactory.instance !== null,
}; };
} }
} }

200
src/services/platforms/CapacitorPlatformService.ts

@ -187,8 +187,11 @@ export class CapacitorPlatformService implements PlatformService {
params: unknown[] = [], params: unknown[] = [],
): Promise<R> { ): Promise<R> {
// Log incoming parameters for debugging (HIGH PRIORITY) // 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 // Convert parameters to SQLite-compatible types with robust serialization
const convertedParams = params.map((param, index) => { const convertedParams = params.map((param, index) => {
if (param === null || param === undefined) { if (param === null || param === undefined) {
@ -196,51 +199,75 @@ export class CapacitorPlatformService implements PlatformService {
} }
if (typeof param === "object" && param !== null) { if (typeof param === "object" && param !== null) {
// Enhanced debug logging for all objects (HIGH PRIORITY) // Enhanced debug logging for all objects (HIGH PRIORITY)
logger.warn(`[CapacitorPlatformService] Object param at index ${index}:`, { logger.warn(
type: typeof param, `[CapacitorPlatformService] Object param at index ${index}:`,
toString: param.toString(), {
constructorName: param.constructor?.name, type: typeof param,
isArray: Array.isArray(param), toString: param.toString(),
keys: Object.keys(param), constructorName: param.constructor?.name,
stringRep: String(param) 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") // Special handling for Proxy objects (common cause of "An object could not be cloned")
const isProxy = this.isProxyObject(param); 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 // AGGRESSIVE: If toString contains "Proxy", treat as Proxy even if isProxyObject returns false
const stringRep = String(param); const stringRep = String(param);
const forceProxyDetection = stringRep.includes('Proxy(') || stringRep.startsWith('Proxy'); const forceProxyDetection =
logger.warn(`[CapacitorPlatformService] Force proxy detection for index ${index}:`, forceProxyDetection); stringRep.includes("Proxy(") || stringRep.startsWith("Proxy");
logger.warn(
`[CapacitorPlatformService] Force proxy detection for index ${index}:`,
forceProxyDetection,
);
if (isProxy || 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 { try {
// AGGRESSIVE EXTRACTION: Try multiple methods to extract actual values // AGGRESSIVE EXTRACTION: Try multiple methods to extract actual values
if (Array.isArray(param)) { if (Array.isArray(param)) {
// Method 1: Array.from() to extract from Proxy(Array) // Method 1: Array.from() to extract from Proxy(Array)
const actualArray = Array.from(param); 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 // Method 2: Manual element extraction for safety
const manualArray: unknown[] = []; const manualArray: unknown[] = [];
for (let i = 0; i < param.length; i++) { for (let i = 0; i < param.length; i++) {
manualArray.push(param[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 // Use the manual extraction as it's more reliable
return manualArray; return manualArray;
} else { } else {
// For Proxy(Object), try to extract actual object // For Proxy(Object), try to extract actual object
const actualObject = Object.assign({}, param); const actualObject = Object.assign({}, param);
logger.info(`[CapacitorPlatformService] Extracted object from Proxy:`, actualObject); logger.info(
`[CapacitorPlatformService] Extracted object from Proxy:`,
actualObject,
);
return actualObject; return actualObject;
} }
} catch (proxyError) { } 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 // FALLBACK: Try to extract primitive values manually
if (Array.isArray(param)) { if (Array.isArray(param)) {
try { try {
@ -248,30 +275,42 @@ export class CapacitorPlatformService implements PlatformService {
for (let i = 0; i < param.length; i++) { for (let i = 0; i < param.length; i++) {
fallbackArray.push(param[i]); fallbackArray.push(param[i]);
} }
logger.info(`[CapacitorPlatformService] Fallback array extraction successful:`, fallbackArray); logger.info(
`[CapacitorPlatformService] Fallback array extraction successful:`,
fallbackArray,
);
return fallbackArray; return fallbackArray;
} catch (fallbackError) { } 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 Array - Could not extract]`;
} }
} }
return `[Proxy Object - Could not extract]`; return `[Proxy Object - Could not extract]`;
} }
} }
try { try {
// Safely convert objects and arrays to JSON strings // Safely convert objects and arrays to JSON strings
return JSON.stringify(param); return JSON.stringify(param);
} catch (error) { } catch (error) {
// Handle non-serializable objects // Handle non-serializable objects
logger.error(`[CapacitorPlatformService] Failed to serialize parameter at index ${index}:`, error); logger.error(
logger.error(`[CapacitorPlatformService] Problematic parameter:`, param); `[CapacitorPlatformService] Failed to serialize parameter at index ${index}:`,
error,
);
logger.error(
`[CapacitorPlatformService] Problematic parameter:`,
param,
);
// Fallback: Convert to string representation // Fallback: Convert to string representation
if (Array.isArray(param)) { if (Array.isArray(param)) {
return `[Array(${param.length})]`; return `[Array(${param.length})]`;
} }
return `[Object ${param.constructor?.name || 'Unknown'}]`; return `[Object ${param.constructor?.name || "Unknown"}]`;
} }
} }
if (typeof param === "boolean") { if (typeof param === "boolean") {
@ -280,12 +319,16 @@ export class CapacitorPlatformService implements PlatformService {
} }
if (typeof param === "function") { if (typeof param === "function") {
// Functions can't be serialized - convert to string representation // Functions can't be serialized - convert to string representation
logger.warn(`[CapacitorPlatformService] Function parameter detected and converted to string at index ${index}`); logger.warn(
return `[Function ${param.name || 'Anonymous'}]`; `[CapacitorPlatformService] Function parameter detected and converted to string at index ${index}`,
);
return `[Function ${param.name || "Anonymous"}]`;
} }
if (typeof param === "symbol") { if (typeof param === "symbol") {
// Symbols can't be serialized - convert to string representation // 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(); return param.toString();
} }
// Numbers, strings, bigints are supported, but ensure bigints are converted to strings // 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) // Log converted parameters for debugging (HIGH PRIORITY)
logger.warn(`[CapacitorPlatformService] Converted params:`, convertedParams); logger.warn(
`[CapacitorPlatformService] Converted params:`,
convertedParams,
);
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
// Create completely plain objects that Vue cannot make reactive // Create completely plain objects that Vue cannot make reactive
// Step 1: Deep clone the converted params to ensure they're plain objects // Step 1: Deep clone the converted params to ensure they're plain objects
const plainParams = JSON.parse(JSON.stringify(convertedParams)); const plainParams = JSON.parse(JSON.stringify(convertedParams));
// Step 2: Create operation object using Object.create(null) for no prototype // Step 2: Create operation object using Object.create(null) for no prototype
const operation = Object.create(null) as QueuedOperation; const operation = Object.create(null) as QueuedOperation;
operation.type = type; operation.type = type;
@ -310,16 +356,25 @@ export class CapacitorPlatformService implements PlatformService {
operation.params = plainParams; operation.params = plainParams;
operation.resolve = (value: unknown) => resolve(value as R); operation.resolve = (value: unknown) => resolve(value as R);
operation.reject = reject; operation.reject = reject;
// Step 3: Freeze everything to prevent modification // Step 3: Freeze everything to prevent modification
Object.freeze(operation.params); Object.freeze(operation.params);
Object.freeze(operation); Object.freeze(operation);
// Add enhanced logging to verify our fix // Add enhanced logging to verify our fix
logger.warn(`[CapacitorPlatformService] Final operation.params type:`, typeof operation.params); logger.warn(
logger.warn(`[CapacitorPlatformService] Final operation.params toString:`, operation.params.toString()); `[CapacitorPlatformService] Final operation.params type:`,
logger.warn(`[CapacitorPlatformService] Final operation.params constructor:`, operation.params.constructor?.name); 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); this.operationQueue.push(operation);
// If we're already initialized, start processing the queue // If we're already initialized, start processing the queue
@ -367,33 +422,42 @@ export class CapacitorPlatformService implements PlatformService {
try { try {
// Method 1: Check toString representation // Method 1: Check toString representation
const objString = obj.toString(); const objString = obj.toString();
if (objString.includes('Proxy(') || objString.startsWith('Proxy')) { if (objString.includes("Proxy(") || objString.startsWith("Proxy")) {
logger.debug("[CapacitorPlatformService] Proxy detected via toString:", objString); logger.debug(
"[CapacitorPlatformService] Proxy detected via toString:",
objString,
);
return true; return true;
} }
// Method 2: Check constructor name // Method 2: Check constructor name
const constructorName = obj.constructor?.name; const constructorName = obj.constructor?.name;
if (constructorName === 'Proxy') { if (constructorName === "Proxy") {
logger.debug("[CapacitorPlatformService] Proxy detected via constructor name"); logger.debug(
"[CapacitorPlatformService] Proxy detected via constructor name",
);
return true; return true;
} }
// Method 3: Check Object.prototype.toString // Method 3: Check Object.prototype.toString
const objToString = Object.prototype.toString.call(obj); const objToString = Object.prototype.toString.call(obj);
if (objToString.includes('Proxy')) { if (objToString.includes("Proxy")) {
logger.debug("[CapacitorPlatformService] Proxy detected via Object.prototype.toString"); logger.debug(
"[CapacitorPlatformService] Proxy detected via Object.prototype.toString",
);
return true; return true;
} }
// Method 4: Vue/Reactive Proxy detection - check for __v_ properties // 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 // Check for Vue reactive proxy indicators
const hasVueProxy = Object.getOwnPropertyNames(obj).some(prop => const hasVueProxy = Object.getOwnPropertyNames(obj).some(
prop.startsWith('__v_') || prop.startsWith('__r_') (prop) => prop.startsWith("__v_") || prop.startsWith("__r_"),
); );
if (hasVueProxy) { if (hasVueProxy) {
logger.debug("[CapacitorPlatformService] Vue reactive Proxy detected"); logger.debug(
"[CapacitorPlatformService] Vue reactive Proxy detected",
);
return true; return true;
} }
} }
@ -401,15 +465,24 @@ export class CapacitorPlatformService implements PlatformService {
// Method 5: Try JSON.stringify and check for Proxy in error or result // Method 5: Try JSON.stringify and check for Proxy in error or result
try { try {
const jsonString = JSON.stringify(obj); const jsonString = JSON.stringify(obj);
if (jsonString.includes('Proxy')) { if (jsonString.includes("Proxy")) {
logger.debug("[CapacitorPlatformService] Proxy detected in JSON serialization"); logger.debug(
"[CapacitorPlatformService] Proxy detected in JSON serialization",
);
return true; return true;
} }
} catch (jsonError) { } catch (jsonError) {
// If JSON.stringify fails, it might be a non-serializable Proxy // If JSON.stringify fails, it might be a non-serializable Proxy
const errorMessage = jsonError instanceof Error ? jsonError.message : String(jsonError); const errorMessage =
if (errorMessage.includes('Proxy') || errorMessage.includes('circular') || errorMessage.includes('clone')) { jsonError instanceof Error ? jsonError.message : String(jsonError);
logger.debug("[CapacitorPlatformService] Proxy detected via JSON serialization error"); if (
errorMessage.includes("Proxy") ||
errorMessage.includes("circular") ||
errorMessage.includes("clone")
) {
logger.debug(
"[CapacitorPlatformService] Proxy detected via JSON serialization error",
);
return true; return true;
} }
} }
@ -417,7 +490,10 @@ export class CapacitorPlatformService implements PlatformService {
return false; return false;
} catch (error) { } catch (error) {
// If we can't inspect the object, it might be a Proxy causing issues // 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 return true; // Assume it's a Proxy if we can't inspect it
} }
} }
@ -1268,8 +1344,12 @@ export class CapacitorPlatformService implements PlatformService {
params?: unknown[], params?: unknown[],
): Promise<unknown[] | undefined> { ): Promise<unknown[] | undefined> {
await this.waitForInitialization(); await this.waitForInitialization();
const result = await this.queueOperation<QueryExecResult>("query", sql, params || []); const result = await this.queueOperation<QueryExecResult>(
"query",
sql,
params || [],
);
// Return the first row from the result, or undefined if no results // Return the first row from the result, or undefined if no results
if (result && result.values && result.values.length > 0) { if (result && result.values && result.values.length > 0) {
return result.values[0]; return result.values[0];

30
src/services/platforms/WebPlatformService.ts

@ -47,14 +47,16 @@ export class WebPlatformService implements PlatformService {
constructor() { constructor() {
WebPlatformService.instanceCount++; WebPlatformService.instanceCount++;
// Only warn if multiple instances (which shouldn't happen with singleton) // Only warn if multiple instances (which shouldn't happen with singleton)
if (WebPlatformService.instanceCount > 1) { 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 { } else {
console.log(`[WebPlatformService] Initializing web platform service`); console.log(`[WebPlatformService] Initializing web platform service`);
} }
// Start worker initialization but don't await it in constructor // Start worker initialization but don't await it in constructor
this.workerInitPromise = this.initializeWorker(); this.workerInitPromise = this.initializeWorker();
} }
@ -74,19 +76,26 @@ export class WebPlatformService implements PlatformService {
// This is required for Safari compatibility with nested workers // This is required for Safari compatibility with nested workers
// It installs a handler that proxies web worker creation through the main thread // It installs a handler that proxies web worker creation through the main thread
// CRITICAL: Only call initBackend from main thread, not from worker context // CRITICAL: Only call initBackend from main thread, not from worker context
const isMainThread = typeof window !== 'undefined'; const isMainThread = typeof window !== "undefined";
if (isMainThread) { if (isMainThread) {
// We're in the main thread - safe to dynamically import and call initBackend // We're in the main thread - safe to dynamically import and call initBackend
try { try {
const { initBackend } = await import("absurd-sql/dist/indexeddb-main-thread"); const { initBackend } = await import(
"absurd-sql/dist/indexeddb-main-thread"
);
initBackend(this.worker); initBackend(this.worker);
} catch (error) { } catch (error) {
console.error("[WebPlatformService] Failed to import/call initBackend:", error); console.error(
"[WebPlatformService] Failed to import/call initBackend:",
error,
);
throw error; throw error;
} }
} else { } else {
// We're in a worker context - skip initBackend call // 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) => { this.worker.onmessage = (event) => {
@ -120,13 +129,16 @@ export class WebPlatformService implements PlatformService {
const { id, type } = message; const { id, type } = message;
// Handle absurd-sql internal messages (these are normal, don't log) // 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 return; // Internal absurd-sql message, ignore silently
} }
if (!id) { if (!id) {
// logger.warn("[WebPlatformService] Received message without ID:", message); // DISABLED // 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; return;
} }

136
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<any>;
dbExec(sql: string, params?: unknown[]): Promise<any>;
dbGetOneRow(sql: string, params?: unknown[]): Promise<any>;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
capabilities: any;
}

128
src/utils/usePlatformService.ts

@ -1,28 +1,28 @@
/** /**
* Platform Service Composable for TimeSafari * Platform Service Composable for TimeSafari
* *
* Provides centralized access to platform-specific services across Vue components. * Provides centralized access to platform-specific services across Vue components.
* This composable encapsulates the singleton pattern and provides a clean interface * This composable encapsulates the singleton pattern and provides a clean interface
* for components to access platform functionality without directly managing * for components to access platform functionality without directly managing
* the PlatformServiceFactory. * the PlatformServiceFactory.
* *
* Benefits: * Benefits:
* - Centralized service access * - Centralized service access
* - Better testability with easy mocking * - Better testability with easy mocking
* - Cleaner component code * - Cleaner component code
* - Type safety with TypeScript * - Type safety with TypeScript
* - Reactive capabilities if needed in the future * - Reactive capabilities if needed in the future
* *
* @author Matthew Raymer * @author Matthew Raymer
* @version 1.0.0 * @version 1.0.0
* @since 2025-07-02 * @since 2025-07-02
*/ */
import { ref, readonly } from 'vue'; import { ref, readonly } from "vue";
import { PlatformServiceFactory } from '@/services/PlatformServiceFactory'; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import type { PlatformService } from '@/services/PlatformService'; import type { PlatformService } from "@/services/PlatformService";
import * as databaseUtil from '@/db/databaseUtil'; import * as databaseUtil from "@/db/databaseUtil";
import { Contact } from '@/db/tables/contacts'; import { Contact } from "@/db/tables/contacts";
/** /**
* Reactive reference to the platform service instance * Reactive reference to the platform service instance
@ -48,27 +48,27 @@ function initializePlatformService(): PlatformService {
/** /**
* Platform Service Composable * Platform Service Composable
* *
* Provides access to platform-specific services in a composable pattern. * Provides access to platform-specific services in a composable pattern.
* This is the recommended way for Vue components to access platform functionality. * This is the recommended way for Vue components to access platform functionality.
* *
* @returns Object containing platform service and utility functions * @returns Object containing platform service and utility functions
* *
* @example * @example
* ```typescript * ```typescript
* // In a Vue component * // In a Vue component
* import { usePlatformService } from '@/utils/usePlatformService'; * import { usePlatformService } from '@/utils/usePlatformService';
* *
* export default { * export default {
* setup() { * setup() {
* const { platform, dbQuery, dbExec, takePicture } = usePlatformService(); * const { platform, dbQuery, dbExec, takePicture } = usePlatformService();
* *
* // Use platform methods directly * // Use platform methods directly
* const takePhoto = async () => { * const takePhoto = async () => {
* const result = await takePicture(); * const result = await takePicture();
* console.log('Photo taken:', result); * console.log('Photo taken:', result);
* }; * };
* *
* return { takePhoto }; * return { takePhoto };
* } * }
* }; * };
@ -85,46 +85,55 @@ export function usePlatformService() {
*/ */
const safeSerializeParams = (params?: unknown[]): unknown[] => { const safeSerializeParams = (params?: unknown[]): unknown[] => {
if (!params) return []; if (!params) return [];
console.log('[usePlatformService] Original params:', params); console.log("[usePlatformService] Original params:", params);
console.log('[usePlatformService] Params toString:', params.toString()); console.log("[usePlatformService] Params toString:", params.toString());
console.log('[usePlatformService] Params constructor:', params.constructor?.name); console.log(
"[usePlatformService] Params constructor:",
params.constructor?.name,
);
// Use the most aggressive approach: JSON round-trip + spread operator // Use the most aggressive approach: JSON round-trip + spread operator
try { try {
// Method 1: JSON round-trip to completely strip any Proxy // Method 1: JSON round-trip to completely strip any Proxy
const jsonSerialized = JSON.parse(JSON.stringify(params)); 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 // Method 2: Spread operator to create new array
const spreadArray = [...jsonSerialized]; const spreadArray = [...jsonSerialized];
console.log('[usePlatformService] Spread array:', spreadArray); console.log("[usePlatformService] Spread array:", spreadArray);
// Method 3: Force primitive extraction for each element // 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) { if (param === null || param === undefined) {
return param; return param;
} }
// Force convert to primitive value // Force convert to primitive value
if (typeof param === 'object') { if (typeof param === "object") {
if (Array.isArray(param)) { if (Array.isArray(param)) {
return [...param]; // Spread to new array return [...param]; // Spread to new array
} else { } else {
return { ...param }; // Spread to new object return { ...param }; // Spread to new object
} }
} }
return param; return param;
}); });
console.log('[usePlatformService] Final params:', finalParams); console.log("[usePlatformService] Final params:", finalParams);
console.log('[usePlatformService] Final params toString:', finalParams.toString()); console.log(
console.log('[usePlatformService] Final params constructor:', finalParams.constructor?.name); "[usePlatformService] Final params toString:",
finalParams.toString(),
);
console.log(
"[usePlatformService] Final params constructor:",
finalParams.constructor?.name,
);
return finalParams; return finalParams;
} catch (error) { } catch (error) {
console.error('[usePlatformService] Serialization error:', error); console.error("[usePlatformService] Serialization error:", error);
// Fallback: manual extraction // Fallback: manual extraction
const fallbackParams: unknown[] = []; const fallbackParams: unknown[] = [];
for (let i = 0; i < params.length; i++) { for (let i = 0; i < params.length; i++) {
@ -133,11 +142,16 @@ export function usePlatformService() {
const value = params[i]; const value = params[i];
fallbackParams.push(value); fallbackParams.push(value);
} catch (accessError) { } 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])); fallbackParams.push(String(params[i]));
} }
} }
console.log('[usePlatformService] Fallback params:', fallbackParams); console.log("[usePlatformService] Fallback params:", fallbackParams);
return fallbackParams; return fallbackParams;
} }
}; };
@ -194,12 +208,12 @@ export function usePlatformService() {
const capabilities = service.getCapabilities(); const capabilities = service.getCapabilities();
return !capabilities.isNativeApp; return !capabilities.isNativeApp;
}; };
const isCapacitor = () => { const isCapacitor = () => {
const capabilities = service.getCapabilities(); const capabilities = service.getCapabilities();
return capabilities.isNativeApp && capabilities.isMobile; return capabilities.isNativeApp && capabilities.isMobile;
}; };
const isElectron = () => { const isElectron = () => {
const capabilities = service.getCapabilities(); const capabilities = service.getCapabilities();
return capabilities.isNativeApp && !capabilities.isMobile; return capabilities.isNativeApp && !capabilities.isMobile;
@ -263,9 +277,13 @@ export function usePlatformService() {
* @param showBlocked Whether to include blocked contacts * @param showBlocked Whether to include blocked contacts
* @returns Promise<Contact[]> Filtered contacts * @returns Promise<Contact[]> Filtered contacts
*/ */
const getContactsWithFilter = async (showBlocked = true): Promise<Contact[]> => { const getContactsWithFilter = async (
showBlocked = true,
): Promise<Contact[]> => {
const contacts = await getContacts(); 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) => { const getAccount = async (did?: string) => {
if (!did) return null; 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); const mappedResults = databaseUtil.mapQueryResultToValues(result);
return mappedResults.length > 0 ? mappedResults[0] : null; return mappedResults.length > 0 ? mappedResults[0] : null;
}; };
@ -311,19 +332,22 @@ export function usePlatformService() {
*/ */
const logActivity = async (message: string) => { const logActivity = async (message: string) => {
const timestamp = new Date().toISOString(); 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 { return {
// Direct service access (for advanced use cases) // Direct service access (for advanced use cases)
platform: readonly(platformService), platform: readonly(platformService),
isInitialized: readonly(isInitialized), isInitialized: readonly(isInitialized),
// Database operations (low-level) // Database operations (low-level)
dbQuery, dbQuery,
dbExec, dbExec,
dbGetOneRow, dbGetOneRow,
// Database operations (high-level) // Database operations (high-level)
getContacts, getContacts,
getContactsWithFilter, getContactsWithFilter,
@ -332,34 +356,34 @@ export function usePlatformService() {
saveDidSettings, saveDidSettings,
getAccount, getAccount,
logActivity, logActivity,
// Media operations // Media operations
takePicture, takePicture,
pickImage, pickImage,
rotateCamera, rotateCamera,
// Platform detection // Platform detection
isWeb, isWeb,
isCapacitor, isCapacitor,
isElectron, isElectron,
getCapabilities, getCapabilities,
// File operations // File operations
readFile, readFile,
writeFile, writeFile,
deleteFile, deleteFile,
listFiles, listFiles,
writeAndShareFile, writeAndShareFile,
// Navigation // Navigation
handleDeepLink, handleDeepLink,
// Raw service access for cases not covered above // Raw service access for cases not covered above
service service,
}; };
} }
/** /**
* Type helper for the composable return type * Type helper for the composable return type
*/ */
export type PlatformServiceComposable = ReturnType<typeof usePlatformService>; export type PlatformServiceComposable = ReturnType<typeof usePlatformService>;

Loading…
Cancel
Save