Remove unused usePlatformService composable and document mixin architecture

- Delete unused usePlatformService.ts file
- Create architecture-decisions.md documenting mixin choice over composables
- Update README.md with architecture decision reference and usage guidance
- Document rationale: performance, consistency, developer experience
- Maintain established class-based component pattern with vue-facing-decorator
This commit is contained in:
Matthew Raymer
2025-07-02 11:27:18 +00:00
parent f4d2923916
commit 03e14371f6
5 changed files with 293 additions and 546 deletions

View File

@@ -1,389 +0,0 @@
/**
* 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>;