Fix worker-only database architecture and Vue Proxy serialization

- Implement worker-only database access to eliminate double migrations
- Add parameter serialization in usePlatformService to prevent Capacitor "object could not be cloned" errors
- Fix infinite logging loop with circuit breaker in databaseUtil
- Use dynamic imports in WebPlatformService to prevent worker thread errors
- Add higher-level database methods (getContacts, getSettings) to composable
- Eliminate Vue Proxy objects through JSON serialization and Object.freeze protection

Resolves Proxy(Array) serialization failures and worker context conflicts across Web/Capacitor/Electron platforms.
This commit is contained in:
Matthew Raymer
2025-07-02 07:24:51 +00:00
parent d3e0cd1c9f
commit 7b1f891c63
19 changed files with 1790 additions and 121 deletions

View File

@@ -0,0 +1,365 @@
/**
* 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';
/**
* Reactive reference to the platform service instance
* This allows for potential reactive features in the future
*/
const platformService = ref<PlatformService | null>(null);
/**
* Flag to track if service has been initialized
*/
const isInitialized = ref(false);
/**
* Initialize the platform service if not already done
*/
function initializePlatformService(): PlatformService {
if (!platformService.value) {
platformService.value = PlatformServiceFactory.getInstance();
isInitialized.value = true;
}
return platformService.value;
}
/**
* 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 };
* }
* };
* ```
*/
export function usePlatformService() {
// Initialize service on first use
const service = initializePlatformService();
/**
* Safely serialize parameters to avoid Proxy objects in native bridges
* Vue's reactivity system can wrap arrays in Proxy objects which cause
* "An object could not be cloned" errors in Capacitor
*/
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);
// 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);
// Method 2: Spread operator to create new array
const spreadArray = [...jsonSerialized];
console.log('[usePlatformService] Spread array:', spreadArray);
// Method 3: Force primitive extraction for each element
const finalParams = spreadArray.map((param, index) => {
if (param === null || param === undefined) {
return param;
}
// Force convert to primitive value
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);
return finalParams;
} catch (error) {
console.error('[usePlatformService] Serialization error:', error);
// Fallback: manual extraction
const fallbackParams: unknown[] = [];
for (let i = 0; i < params.length; i++) {
try {
// Try to access the value directly
const value = params[i];
fallbackParams.push(value);
} catch (accessError) {
console.error('[usePlatformService] Access error for param', i, ':', accessError);
fallbackParams.push(String(params[i]));
}
}
console.log('[usePlatformService] Fallback params:', fallbackParams);
return fallbackParams;
}
};
/**
* Database query method with proper typing and safe parameter serialization
*/
const dbQuery = async (sql: string, params?: unknown[]) => {
const safeParams = safeSerializeParams(params);
return await service.dbQuery(sql, safeParams);
};
/**
* Database execution method with proper typing and safe parameter serialization
*/
const dbExec = async (sql: string, params?: unknown[]) => {
const safeParams = safeSerializeParams(params);
return await service.dbExec(sql, safeParams);
};
/**
* Get single row from database with proper typing and safe parameter serialization
*/
const dbGetOneRow = async (sql: string, params?: unknown[]) => {
const safeParams = safeSerializeParams(params);
return await service.dbGetOneRow(sql, safeParams);
};
/**
* Take picture with platform-specific implementation
*/
const takePicture = async () => {
return await service.takePicture();
};
/**
* Pick image from device with platform-specific implementation
*/
const pickImage = async () => {
return await service.pickImage();
};
/**
* Get platform capabilities
*/
const getCapabilities = () => {
return service.getCapabilities();
};
/**
* Platform detection methods using capabilities
*/
const isWeb = () => {
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;
};
/**
* File operations (where supported)
*/
const readFile = async (path: string) => {
return await service.readFile(path);
};
const writeFile = async (path: string, content: string) => {
return await service.writeFile(path, content);
};
const deleteFile = async (path: string) => {
return await service.deleteFile(path);
};
const listFiles = async (directory: string) => {
return await service.listFiles(directory);
};
/**
* Camera operations
*/
const rotateCamera = async () => {
return await service.rotateCamera();
};
/**
* Deep link handling
*/
const handleDeepLink = async (url: string) => {
return await service.handleDeepLink(url);
};
/**
* File sharing
*/
const writeAndShareFile = async (fileName: string, content: string) => {
return await service.writeAndShareFile(fileName, content);
};
// ========================================
// Higher-level database operations
// ========================================
/**
* Get all contacts from database with proper typing
* @returns Promise<Contact[]> Typed array of contacts
*/
const getContacts = async (): Promise<Contact[]> => {
const result = await dbQuery("SELECT * FROM contacts ORDER BY name");
return databaseUtil.mapQueryResultToValues(result) as Contact[];
};
/**
* Get contacts with content visibility filter
* @param showBlocked Whether to include blocked contacts
* @returns Promise<Contact[]> Filtered contacts
*/
const getContactsWithFilter = async (showBlocked = true): Promise<Contact[]> => {
const contacts = await getContacts();
return showBlocked ? contacts : contacts.filter(c => c.iViewContent !== false);
};
/**
* Get user settings with proper typing
* @returns Promise with all user settings
*/
const getSettings = async () => {
return await databaseUtil.retrieveSettingsForActiveAccount();
};
/**
* Save default settings
* @param settings Partial settings object to update
*/
const saveSettings = async (settings: Partial<any>) => {
return await databaseUtil.updateDefaultSettings(settings);
};
/**
* Save DID-specific settings
* @param did DID identifier
* @param settings Partial settings object to update
*/
const saveDidSettings = async (did: string, settings: Partial<any>) => {
return await databaseUtil.updateDidSpecificSettings(did, settings);
};
/**
* Get account by DID
* @param did DID identifier
* @returns Account data or null if not found
*/
const getAccount = async (did?: string) => {
if (!did) return null;
const result = await dbQuery("SELECT * FROM accounts WHERE did = ? LIMIT 1", [did]);
const mappedResults = databaseUtil.mapQueryResultToValues(result);
return mappedResults.length > 0 ? mappedResults[0] : null;
};
/**
* Log activity message to database
* @param message Activity message to log
*/
const logActivity = async (message: string) => {
const timestamp = new Date().toISOString();
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,
dbGetOneRow,
// Database operations (high-level)
getContacts,
getContactsWithFilter,
getSettings,
saveSettings,
saveDidSettings,
getAccount,
logActivity,
// Media operations
takePicture,
pickImage,
rotateCamera,
// Platform detection
isWeb,
isCapacitor,
isElectron,
getCapabilities,
// File operations
readFile,
writeFile,
deleteFile,
listFiles,
writeAndShareFile,
// Navigation
handleDeepLink,
// Raw service access for cases not covered above
service
};
}
/**
* Type helper for the composable return type
*/
export type PlatformServiceComposable = ReturnType<typeof usePlatformService>;