forked from jsnbuchanan/crowd-funder-for-time-pwa
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:
365
src/utils/usePlatformService.ts
Normal file
365
src/utils/usePlatformService.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user