forked from jsnbuchanan/crowd-funder-for-time-pwa
- 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.
365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
/**
|
|
* 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>;
|