forked from trent_larson/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:
@@ -167,6 +167,13 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
"[CapacitorPlatformService] Error while processing SQL queue:",
|
||||
error,
|
||||
);
|
||||
logger.error(
|
||||
`[CapacitorPlatformService] Failed operation - Type: ${operation.type}, SQL: ${operation.sql}`,
|
||||
);
|
||||
logger.error(
|
||||
`[CapacitorPlatformService] Failed operation - Params:`,
|
||||
operation.params,
|
||||
);
|
||||
operation.reject(error);
|
||||
}
|
||||
}
|
||||
@@ -179,31 +186,140 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
sql: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<R> {
|
||||
// Convert parameters to SQLite-compatible types
|
||||
const convertedParams = params.map((param) => {
|
||||
// Log incoming parameters for debugging (HIGH PRIORITY)
|
||||
logger.warn(`[CapacitorPlatformService] queueOperation - SQL: ${sql}, Params:`, params);
|
||||
|
||||
// Convert parameters to SQLite-compatible types with robust serialization
|
||||
const convertedParams = params.map((param, index) => {
|
||||
if (param === null || param === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (typeof param === "object" && param !== null) {
|
||||
// Convert objects and arrays to JSON strings
|
||||
return JSON.stringify(param);
|
||||
// Enhanced debug logging for all objects (HIGH PRIORITY)
|
||||
logger.warn(`[CapacitorPlatformService] Object param at index ${index}:`, {
|
||||
type: typeof param,
|
||||
toString: param.toString(),
|
||||
constructorName: param.constructor?.name,
|
||||
isArray: Array.isArray(param),
|
||||
keys: Object.keys(param),
|
||||
stringRep: String(param)
|
||||
});
|
||||
|
||||
// Special handling for Proxy objects (common cause of "An object could not be cloned")
|
||||
const isProxy = this.isProxyObject(param);
|
||||
logger.warn(`[CapacitorPlatformService] isProxy result for index ${index}:`, isProxy);
|
||||
|
||||
// AGGRESSIVE: If toString contains "Proxy", treat as Proxy even if isProxyObject returns false
|
||||
const stringRep = String(param);
|
||||
const forceProxyDetection = stringRep.includes('Proxy(') || stringRep.startsWith('Proxy');
|
||||
logger.warn(`[CapacitorPlatformService] Force proxy detection for index ${index}:`, forceProxyDetection);
|
||||
|
||||
if (isProxy || forceProxyDetection) {
|
||||
logger.warn(`[CapacitorPlatformService] Proxy object detected at index ${index} (method: ${isProxy ? 'isProxyObject' : 'stringDetection'}), toString: ${stringRep}`);
|
||||
try {
|
||||
// AGGRESSIVE EXTRACTION: Try multiple methods to extract actual values
|
||||
if (Array.isArray(param)) {
|
||||
// Method 1: Array.from() to extract from Proxy(Array)
|
||||
const actualArray = Array.from(param);
|
||||
logger.info(`[CapacitorPlatformService] Extracted array from Proxy via Array.from():`, actualArray);
|
||||
|
||||
// Method 2: Manual element extraction for safety
|
||||
const manualArray: unknown[] = [];
|
||||
for (let i = 0; i < param.length; i++) {
|
||||
manualArray.push(param[i]);
|
||||
}
|
||||
logger.info(`[CapacitorPlatformService] Manual array extraction:`, manualArray);
|
||||
|
||||
// Use the manual extraction as it's more reliable
|
||||
return manualArray;
|
||||
} else {
|
||||
// For Proxy(Object), try to extract actual object
|
||||
const actualObject = Object.assign({}, param);
|
||||
logger.info(`[CapacitorPlatformService] Extracted object from Proxy:`, actualObject);
|
||||
return actualObject;
|
||||
}
|
||||
} catch (proxyError) {
|
||||
logger.error(`[CapacitorPlatformService] Failed to extract from Proxy at index ${index}:`, proxyError);
|
||||
|
||||
// FALLBACK: Try to extract primitive values manually
|
||||
if (Array.isArray(param)) {
|
||||
try {
|
||||
const fallbackArray: unknown[] = [];
|
||||
for (let i = 0; i < param.length; i++) {
|
||||
fallbackArray.push(param[i]);
|
||||
}
|
||||
logger.info(`[CapacitorPlatformService] Fallback array extraction successful:`, fallbackArray);
|
||||
return fallbackArray;
|
||||
} catch (fallbackError) {
|
||||
logger.error(`[CapacitorPlatformService] Fallback array extraction failed:`, fallbackError);
|
||||
return `[Proxy Array - Could not extract]`;
|
||||
}
|
||||
}
|
||||
return `[Proxy Object - Could not extract]`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Safely convert objects and arrays to JSON strings
|
||||
return JSON.stringify(param);
|
||||
} catch (error) {
|
||||
// Handle non-serializable objects
|
||||
logger.error(`[CapacitorPlatformService] Failed to serialize parameter at index ${index}:`, error);
|
||||
logger.error(`[CapacitorPlatformService] Problematic parameter:`, param);
|
||||
|
||||
// Fallback: Convert to string representation
|
||||
if (Array.isArray(param)) {
|
||||
return `[Array(${param.length})]`;
|
||||
}
|
||||
return `[Object ${param.constructor?.name || 'Unknown'}]`;
|
||||
}
|
||||
}
|
||||
if (typeof param === "boolean") {
|
||||
// Convert boolean to integer (0 or 1)
|
||||
return param ? 1 : 0;
|
||||
}
|
||||
// Numbers, strings, bigints, and buffers are already supported
|
||||
if (typeof param === "function") {
|
||||
// Functions can't be serialized - convert to string representation
|
||||
logger.warn(`[CapacitorPlatformService] Function parameter detected and converted to string at index ${index}`);
|
||||
return `[Function ${param.name || 'Anonymous'}]`;
|
||||
}
|
||||
if (typeof param === "symbol") {
|
||||
// Symbols can't be serialized - convert to string representation
|
||||
logger.warn(`[CapacitorPlatformService] Symbol parameter detected and converted to string at index ${index}`);
|
||||
return param.toString();
|
||||
}
|
||||
// Numbers, strings, bigints are supported, but ensure bigints are converted to strings
|
||||
if (typeof param === "bigint") {
|
||||
return param.toString();
|
||||
}
|
||||
return param;
|
||||
});
|
||||
|
||||
// Log converted parameters for debugging (HIGH PRIORITY)
|
||||
logger.warn(`[CapacitorPlatformService] Converted params:`, convertedParams);
|
||||
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
const operation: QueuedOperation = {
|
||||
type,
|
||||
sql,
|
||||
params: convertedParams,
|
||||
resolve: (value: unknown) => resolve(value as R),
|
||||
reject,
|
||||
};
|
||||
// Create completely plain objects that Vue cannot make reactive
|
||||
// Step 1: Deep clone the converted params to ensure they're plain objects
|
||||
const plainParams = JSON.parse(JSON.stringify(convertedParams));
|
||||
|
||||
// Step 2: Create operation object using Object.create(null) for no prototype
|
||||
const operation = Object.create(null) as QueuedOperation;
|
||||
operation.type = type;
|
||||
operation.sql = sql;
|
||||
operation.params = plainParams;
|
||||
operation.resolve = (value: unknown) => resolve(value as R);
|
||||
operation.reject = reject;
|
||||
|
||||
// Step 3: Freeze everything to prevent modification
|
||||
Object.freeze(operation.params);
|
||||
Object.freeze(operation);
|
||||
|
||||
// Add enhanced logging to verify our fix
|
||||
logger.warn(`[CapacitorPlatformService] Final operation.params type:`, typeof operation.params);
|
||||
logger.warn(`[CapacitorPlatformService] Final operation.params toString:`, operation.params.toString());
|
||||
logger.warn(`[CapacitorPlatformService] Final operation.params constructor:`, operation.params.constructor?.name);
|
||||
|
||||
this.operationQueue.push(operation);
|
||||
|
||||
// If we're already initialized, start processing the queue
|
||||
@@ -237,6 +353,75 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if an object is a Proxy object that cannot be serialized
|
||||
* Proxy objects cause "An object could not be cloned" errors in Capacitor
|
||||
* @param obj - Object to test
|
||||
* @returns true if the object appears to be a Proxy
|
||||
*/
|
||||
private isProxyObject(obj: unknown): boolean {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Method 1: Check toString representation
|
||||
const objString = obj.toString();
|
||||
if (objString.includes('Proxy(') || objString.startsWith('Proxy')) {
|
||||
logger.debug("[CapacitorPlatformService] Proxy detected via toString:", objString);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 2: Check constructor name
|
||||
const constructorName = obj.constructor?.name;
|
||||
if (constructorName === 'Proxy') {
|
||||
logger.debug("[CapacitorPlatformService] Proxy detected via constructor name");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 3: Check Object.prototype.toString
|
||||
const objToString = Object.prototype.toString.call(obj);
|
||||
if (objToString.includes('Proxy')) {
|
||||
logger.debug("[CapacitorPlatformService] Proxy detected via Object.prototype.toString");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 4: Vue/Reactive Proxy detection - check for __v_ properties
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
// Check for Vue reactive proxy indicators
|
||||
const hasVueProxy = Object.getOwnPropertyNames(obj).some(prop =>
|
||||
prop.startsWith('__v_') || prop.startsWith('__r_')
|
||||
);
|
||||
if (hasVueProxy) {
|
||||
logger.debug("[CapacitorPlatformService] Vue reactive Proxy detected");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Method 5: Try JSON.stringify and check for Proxy in error or result
|
||||
try {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
if (jsonString.includes('Proxy')) {
|
||||
logger.debug("[CapacitorPlatformService] Proxy detected in JSON serialization");
|
||||
return true;
|
||||
}
|
||||
} catch (jsonError) {
|
||||
// If JSON.stringify fails, it might be a non-serializable Proxy
|
||||
const errorMessage = jsonError instanceof Error ? jsonError.message : String(jsonError);
|
||||
if (errorMessage.includes('Proxy') || errorMessage.includes('circular') || errorMessage.includes('clone')) {
|
||||
logger.debug("[CapacitorPlatformService] Proxy detected via JSON serialization error");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// If we can't inspect the object, it might be a Proxy causing issues
|
||||
logger.warn("[CapacitorPlatformService] Could not inspect object for Proxy detection:", error);
|
||||
return true; // Assume it's a Proxy if we can't inspect it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute database migrations for the Capacitor platform
|
||||
*
|
||||
@@ -1074,4 +1259,21 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
params || [],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbGetOneRow
|
||||
*/
|
||||
async dbGetOneRow(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<unknown[] | undefined> {
|
||||
await this.waitForInitialization();
|
||||
const result = await this.queueOperation<QueryExecResult>("query", sql, params || []);
|
||||
|
||||
// Return the first row from the result, or undefined if no results
|
||||
if (result && result.values && result.values.length > 0) {
|
||||
return result.values[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user