You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

775 lines
23 KiB

/**
* Enhanced PlatformService Mixin with ultra-concise database operations and caching
*
* Provides cached platform service access and utility methods for Vue components.
* Eliminates repetitive PlatformServiceFactory.getInstance() calls across components.
*
* Features:
* - Cached platform service instance (created once per component)
* - Enhanced database utility methods with comprehensive error handling
* - Ultra-concise database interaction methods ($db, $exec, $one, etc.)
* - Automatic query result mapping and JSON field parsing
* - Specialized shortcuts for common queries (contacts, settings)
* - Transaction support with automatic rollback on errors
* - Mixin pattern for easy integration with existing class components
* - Enhanced utility methods for common patterns
* - Robust error handling and logging
* - Ultra-concise database interaction methods
* - Smart caching layer with TTL for performance optimization
* - Settings shortcuts for ultra-frequent update patterns
*
* Benefits:
* - Eliminates repeated PlatformServiceFactory.getInstance() calls
* - Provides consistent error handling across components
* - Reduces boilerplate database code by up to 80%
* - Maintains type safety with TypeScript
* - Includes common database utility patterns
* - Enhanced error handling and logging
* - Ultra-concise method names for frequent operations
* - Automatic caching for settings and contacts (massive performance gain)
* - Settings update shortcuts reduce 90% of update boilerplate
*
* @author Matthew Raymer
* @version 4.0.0
* @since 2025-07-02
*/
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import type {
PlatformService,
PlatformCapabilities,
} from "@/services/PlatformService";
import { mapColumnsToValues, parseJsonField } from "@/db/databaseUtil";
import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings";
import * as databaseUtil from "@/db/databaseUtil";
import { logger } from "@/utils/logger";
import { Contact } from "@/db/tables/contacts";
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
// =================================================
// TYPESCRIPT INTERFACES
// =================================================
/**
* Cache entry interface for storing data with TTL
*/
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number; // milliseconds
}
/**
* Vue component interface that uses the PlatformServiceMixin
* This provides proper typing for the cache system
*/
interface VueComponentWithMixin {
_platformService: PlatformService | null;
$options: { name?: string };
activeDid?: string;
platformService(): PlatformService;
}
/**
* Global cache store for mixin instances
* Uses WeakMap to avoid memory leaks when components are destroyed
*/
const componentCaches = new WeakMap<
VueComponentWithMixin,
Map<string, CacheEntry<unknown>>
>();
/**
* Cache configuration constants
*/
const CACHE_DEFAULTS = {
settings: 30000, // 30 seconds TTL for settings
contacts: 60000, // 60 seconds TTL for contacts
accounts: 30000, // 30 seconds TTL for accounts
default: 15000, // 15 seconds default TTL
} as const;
/**
* Enhanced mixin that provides cached platform service access and utility methods
* with smart caching layer for ultimate performance optimization
*/
export const PlatformServiceMixin = {
data() {
return {
// Cache the platform service instance at component level
_platformService: null as PlatformService | null,
};
},
computed: {
/**
* Cached platform service instance
* Created once per component lifecycle
*/
platformService(): PlatformService {
if (!(this as unknown as VueComponentWithMixin)._platformService) {
(this as unknown as VueComponentWithMixin)._platformService =
PlatformServiceFactory.getInstance();
}
return (this as unknown as VueComponentWithMixin)._platformService!;
},
/**
* Platform detection utilities
*/
isCapacitor(): boolean {
return (this as unknown as VueComponentWithMixin)
.platformService()
.isCapacitor();
},
isWeb(): boolean {
return (this as unknown as VueComponentWithMixin)
.platformService()
.isWeb();
},
isElectron(): boolean {
return (this as unknown as VueComponentWithMixin)
.platformService()
.isElectron();
},
/**
* Platform capabilities
*/
capabilities() {
return (this as unknown as VueComponentWithMixin)
.platformService()
.getCapabilities();
},
},
methods: {
// =================================================
// CACHING UTILITY METHODS
// =================================================
/**
* Get or initialize cache for this component instance
*/
_getCache(): Map<string, CacheEntry<unknown>> {
let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
if (!cache) {
cache = new Map();
componentCaches.set(this as unknown as VueComponentWithMixin, cache);
}
return cache;
},
/**
* Check if cache entry is valid (not expired)
*/
_isCacheValid(entry: CacheEntry<unknown>): boolean {
return Date.now() - entry.timestamp < entry.ttl;
},
/**
* Get data from cache if valid, otherwise return null
*/
_getCached<T>(key: string): T | null {
const cache = this._getCache();
const entry = cache.get(key);
if (entry && this._isCacheValid(entry)) {
return entry.data as T;
}
cache.delete(key); // Clean up expired entries
return null;
},
/**
* Store data in cache with TTL
*/
_setCached<T>(key: string, data: T, ttl?: number): T {
const cache = this._getCache();
const actualTtl = ttl || CACHE_DEFAULTS.default;
cache.set(key, {
data,
timestamp: Date.now(),
ttl: actualTtl,
});
return data;
},
/**
* Invalidate specific cache entry
*/
_invalidateCache(key: string): void {
const cache = this._getCache();
cache.delete(key);
},
/**
* Clear all cache entries for this component
*/
_clearCache(): void {
const cache = this._getCache();
cache.clear();
},
// =================================================
// ENHANCED DATABASE METHODS (with error handling)
// =================================================
/**
* Enhanced database query method with error handling
*/
async $dbQuery(sql: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbQuery(sql, params);
} catch (error) {
logger.error(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
`[${(this as any).$options.name}] Database query failed:`,
{
sql,
params,
error,
},
);
throw error;
}
},
/**
* Enhanced database execution method with error handling
*/
async $dbExec(sql: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbExec(sql, params);
} catch (error) {
logger.error(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
`[${(this as any).$options.name}] Database exec failed:`,
{
sql,
params,
error,
},
);
throw error;
}
},
/**
* Enhanced database single row query method with error handling
*/
async $dbGetOneRow(sql: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
} catch (error) {
logger.error(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
`[${(this as any).$options.name}] Database single row query failed:`,
{
sql,
params,
error,
},
);
throw error;
}
},
/**
* Utility method for retrieving and parsing settings
* Common pattern used across many components
*/
async $getSettings(
key: string,
fallback: Settings | null = null,
): Promise<Settings | null> {
try {
const result = await this.$dbQuery(
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
[key, key],
);
if (!result?.values?.length) {
return fallback;
}
const mappedResults = mapColumnsToValues(result.columns, result.values);
if (!mappedResults.length) {
return fallback;
}
const settings = mappedResults[0] as Settings;
// Handle JSON field parsing
if (settings.searchBoxes) {
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
}
return settings;
} catch (error) {
logger.error(
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get settings:`,
{
key,
error,
},
);
return fallback;
}
},
/**
* Utility method for merging default and account-specific settings
* Handles the common pattern of layered settings
*/
async $getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback: Settings = {},
): Promise<Settings> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
defaultKey,
defaultFallback,
);
// If no account DID, return defaults
if (!accountDid) {
return defaultSettings || defaultFallback;
}
// Get account-specific overrides
const accountResult = await this.$dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (!accountResult?.values?.length) {
return defaultSettings || defaultFallback;
}
// Map and filter non-null overrides
const mappedResults = mapColumnsToValues(
accountResult.columns,
accountResult.values,
);
if (!mappedResults.length) {
return defaultSettings || defaultFallback;
}
const overrideSettings = mappedResults[0] as Settings;
const filteredOverrides = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
// Merge settings with overrides taking precedence
const mergedSettings = {
...defaultSettings,
...filteredOverrides,
} as Settings;
// Handle JSON field parsing
if (mergedSettings.searchBoxes) {
mergedSettings.searchBoxes = parseJsonField(
mergedSettings.searchBoxes,
[],
);
}
return mergedSettings;
} catch (error) {
logger.error(
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get merged settings:`,
{
defaultKey,
accountDid,
error,
},
);
return defaultFallback;
}
},
/**
* Transaction wrapper with automatic rollback on error
*/
async $withTransaction<T>(callback: () => Promise<T>): Promise<T> {
try {
await this.$dbExec("BEGIN TRANSACTION");
const result = await callback();
await this.$dbExec("COMMIT");
return result;
} catch (error) {
await this.$dbExec("ROLLBACK");
throw error;
}
},
// =================================================
// ULTRA-CONCISE DATABASE METHODS (shortest names)
// =================================================
/**
* Ultra-short database query - just $db()
* @param sql SQL query string
* @param params Query parameters
*/
async $db(
sql: string,
params: unknown[] = [],
): Promise<QueryExecResult | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbQuery(sql, params);
},
/**
* Ultra-short database exec - just $exec()
* @param sql SQL statement string
* @param params Statement parameters
*/
async $exec(
sql: string,
params: unknown[] = [],
): Promise<DatabaseExecResult> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbExec(sql, params);
},
/**
* Ultra-short single row query - just $one()
* @param sql SQL query string
* @param params Query parameters
*/
async $one(
sql: string,
params: unknown[] = [],
): Promise<unknown[] | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
},
// =================================================
// QUERY + MAPPING COMBO METHODS (ultimate conciseness)
// =================================================
/**
* Query with automatic result mapping - $query()
* Combines database query + mapping in one call
* @param sql SQL query string
* @param params Query parameters
* @returns Mapped array of results
*/
async $query<T = Record<string, unknown>>(
sql: string,
params: unknown[] = [],
): Promise<T[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (this as any).platformService.dbQuery(sql, params);
if (!result?.columns || !result?.values) {
return [];
}
const mappedResults = mapColumnsToValues(result.columns, result.values);
return mappedResults as T[];
},
/**
* Get first result with automatic mapping - $first()
* @param sql SQL query string
* @param params Query parameters
* @returns First mapped result or null
*/
async $first<T = Record<string, unknown>>(
sql: string,
params: unknown[] = [],
): Promise<T | null> {
const results = await this.$query(sql, params);
return results.length > 0 ? (results[0] as T) : null;
},
// =================================================
// CACHED SPECIALIZED SHORTCUTS (massive performance boost)
// =================================================
/**
* Load all contacts with caching - $contacts()
* Ultra-concise shortcut with 60s TTL for performance
* @returns Cached mapped array of all contacts
*/
async $contacts(): Promise<Contact[]> {
const cacheKey = "contacts_all";
const cached = this._getCached<Contact[]>(cacheKey);
if (cached) {
return cached;
}
const contacts = await this.$query(
"SELECT * FROM contacts ORDER BY name",
);
return this._setCached(
cacheKey,
contacts as Contact[],
CACHE_DEFAULTS.contacts,
);
},
/**
* Load settings with optional defaults and caching - $settings()
* Ultra-concise with 30s TTL for massive performance gain
* @param defaults Optional default values
* @returns Cached settings object
*/
async $settings(defaults: Settings = {}): Promise<Settings> {
const cacheKey = `settings_${String(MASTER_SETTINGS_KEY)}`;
const cached = this._getCached<Settings>(cacheKey);
if (cached) {
return { ...cached, ...defaults }; // Merge with any new defaults
}
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
if (!settings) {
return defaults;
}
// **ELECTRON-SPECIFIC FIX**: Apply platform-specific API server override
// This ensures Electron always uses production endpoints regardless of cached settings
if (process.env.VITE_PLATFORM === "electron") {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
return this._setCached(cacheKey, settings, CACHE_DEFAULTS.settings);
},
/**
* Load account-specific settings with caching - $accountSettings()
* @param did DID identifier (optional, uses current active DID)
* @param defaults Optional default values
* @returns Cached merged settings object
*/
async $accountSettings(
did?: string,
defaults: Settings = {},
): Promise<Settings> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = did || (this as any).activeDid;
const cacheKey = `account_settings_${currentDid || "default"}`;
const cached = this._getCached<Settings>(cacheKey);
if (cached) {
return { ...cached, ...defaults }; // Merge with any new defaults
}
let settings;
if (!currentDid) {
settings = await this.$settings(defaults);
} else {
settings = await this.$getMergedSettings(
MASTER_SETTINGS_KEY,
currentDid,
defaults,
);
}
return this._setCached(cacheKey, settings, CACHE_DEFAULTS.settings);
},
// =================================================
// SETTINGS UPDATE SHORTCUTS (eliminate 90% boilerplate)
// =================================================
/**
* Save default settings with cache invalidation - $saveSettings()
* Ultra-concise shortcut for updateDefaultSettings
* @param changes Settings changes to save
* @returns Promise<boolean> Success status
*/
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
const result = await databaseUtil.updateDefaultSettings(changes);
// Invalidate related caches
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
this._invalidateCache(`account_settings_default`);
return result;
},
/**
* Save user-specific settings with cache invalidation - $saveUserSettings()
* Ultra-concise shortcut for updateDidSpecificSettings
* @param did DID identifier
* @param changes Settings changes to save
* @returns Promise<boolean> Success status
*/
async $saveUserSettings(
did: string,
changes: Partial<Settings>,
): Promise<boolean> {
const result = await databaseUtil.updateDidSpecificSettings(did, changes);
// Invalidate related caches
this._invalidateCache(`account_settings_${did}`);
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
return result;
},
/**
* Save settings for current active user - $saveMySettings()
* Ultra-concise shortcut using activeDid from component
* @param changes Settings changes to save
* @returns Promise<boolean> Success status
*/
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = (this as any).activeDid;
if (!currentDid) {
return await this.$saveSettings(changes);
}
return await this.$saveUserSettings(currentDid, changes);
},
// =================================================
// CACHE MANAGEMENT METHODS
// =================================================
/**
* Manually refresh settings cache - $refreshSettings()
* Forces reload of settings from database
*/
async $refreshSettings(): Promise<Settings> {
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = (this as any).activeDid;
if (currentDid) {
this._invalidateCache(`account_settings_${currentDid}`);
}
return await this.$settings();
},
/**
* Manually refresh contacts cache - $refreshContacts()
* Forces reload of contacts from database
*/
async $refreshContacts(): Promise<Contact[]> {
this._invalidateCache("contacts_all");
return await this.$contacts();
},
/**
* Clear all caches for this component - $clearAllCaches()
* Useful for manual cache management
*/
$clearAllCaches(): void {
this._clearCache();
},
},
};
// =================================================
// TYPESCRIPT INTERFACES
// =================================================
/**
* Enhanced interface with caching utility methods
*/
export interface IPlatformServiceMixin {
platformService: PlatformService;
$dbQuery(
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined>;
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$getSettings(
key: string,
fallback?: Settings | null,
): Promise<Settings | null>;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback?: Settings,
): Promise<Settings>;
$withTransaction<T>(callback: () => Promise<T>): Promise<T>;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
capabilities: PlatformCapabilities;
}
// TypeScript declaration merging to eliminate (this as any) type assertions
declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
// Core platform service access
platformService: PlatformService;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
capabilities: PlatformCapabilities;
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
// Query + mapping combo methods
$query<T = Record<string, unknown>>(
sql: string,
params?: unknown[],
): Promise<T[]>;
$first<T = Record<string, unknown>>(
sql: string,
params?: unknown[],
): Promise<T | null>;
// Enhanced utility methods
$dbQuery(
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined>;
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$dbGetOneRow(
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
$getSettings(
key: string,
defaults?: Settings | null,
): Promise<Settings | null>;
$getMergedSettings(
key: string,
did?: string,
defaults?: Settings,
): Promise<Settings>;
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
// Cached specialized shortcuts (massive performance boost)
$contacts(): Promise<Contact[]>;
$settings(defaults?: Settings): Promise<Settings>;
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
// Settings update shortcuts (eliminate 90% boilerplate)
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
$saveUserSettings(
did: string,
changes: Partial<Settings>,
): Promise<boolean>;
$saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// Cache management methods
$refreshSettings(): Promise<Settings>;
$refreshContacts(): Promise<Contact[]>;
$clearAllCaches(): void;
}
}