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:
@@ -5,7 +5,15 @@ import {
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
import databaseService from "../AbsurdSqlDatabaseService";
|
||||
// Dynamic import of initBackend to prevent worker context errors
|
||||
import type {
|
||||
WorkerRequest,
|
||||
WorkerResponse,
|
||||
QueryRequest,
|
||||
ExecRequest,
|
||||
QueryResult,
|
||||
GetOneRowRequest,
|
||||
} from "@/interfaces/worker-messages";
|
||||
|
||||
/**
|
||||
* Platform service implementation for web browser platform.
|
||||
@@ -16,11 +24,215 @@ import databaseService from "../AbsurdSqlDatabaseService";
|
||||
* - Image capture using the browser's file input
|
||||
* - Image selection from local filesystem
|
||||
* - Image processing and conversion
|
||||
* - Database operations via worker thread messaging
|
||||
*
|
||||
* Note: File system operations are not available in the web platform
|
||||
* due to browser security restrictions. These methods throw appropriate errors.
|
||||
*/
|
||||
export class WebPlatformService implements PlatformService {
|
||||
private static instanceCount = 0; // Debug counter
|
||||
private worker: Worker | null = null;
|
||||
private workerReady = false;
|
||||
private workerInitPromise: Promise<void> | null = null;
|
||||
private pendingMessages = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (_value: unknown) => void;
|
||||
reject: (_reason: unknown) => void;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
>();
|
||||
private messageIdCounter = 0;
|
||||
private readonly messageTimeout = 30000; // 30 seconds
|
||||
|
||||
constructor() {
|
||||
WebPlatformService.instanceCount++;
|
||||
|
||||
// Only warn if multiple instances (which shouldn't happen with singleton)
|
||||
if (WebPlatformService.instanceCount > 1) {
|
||||
console.error(`[WebPlatformService] ERROR: Multiple instances created! Count: ${WebPlatformService.instanceCount}`);
|
||||
} else {
|
||||
console.log(`[WebPlatformService] Initializing web platform service`);
|
||||
}
|
||||
|
||||
// Start worker initialization but don't await it in constructor
|
||||
this.workerInitPromise = this.initializeWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SQL worker for database operations
|
||||
*/
|
||||
private async initializeWorker(): Promise<void> {
|
||||
try {
|
||||
// logger.log("[WebPlatformService] Initializing SQL worker..."); // DISABLED
|
||||
|
||||
this.worker = new Worker(
|
||||
new URL("../../registerSQLWorker.js", import.meta.url),
|
||||
{ type: "module" },
|
||||
);
|
||||
|
||||
// This is required for Safari compatibility with nested workers
|
||||
// It installs a handler that proxies web worker creation through the main thread
|
||||
// CRITICAL: Only call initBackend from main thread, not from worker context
|
||||
const isMainThread = typeof window !== 'undefined';
|
||||
if (isMainThread) {
|
||||
// We're in the main thread - safe to dynamically import and call initBackend
|
||||
try {
|
||||
const { initBackend } = await import("absurd-sql/dist/indexeddb-main-thread");
|
||||
initBackend(this.worker);
|
||||
} catch (error) {
|
||||
console.error("[WebPlatformService] Failed to import/call initBackend:", error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// We're in a worker context - skip initBackend call
|
||||
console.log("[WebPlatformService] Skipping initBackend call in worker context");
|
||||
}
|
||||
|
||||
this.worker.onmessage = (event) => {
|
||||
this.handleWorkerMessage(event.data);
|
||||
};
|
||||
|
||||
this.worker.onerror = (error) => {
|
||||
// logger.error("[WebPlatformService] Worker error:", error); // DISABLED
|
||||
console.error("[WebPlatformService] Worker error:", error);
|
||||
this.workerReady = false;
|
||||
};
|
||||
|
||||
// Send ping to verify worker is ready
|
||||
await this.sendWorkerMessage({ type: "ping" });
|
||||
this.workerReady = true;
|
||||
|
||||
// logger.log("[WebPlatformService] SQL worker initialized successfully"); // DISABLED
|
||||
} catch (error) {
|
||||
// logger.error("[WebPlatformService] Failed to initialize worker:", error); // DISABLED
|
||||
console.error("[WebPlatformService] Failed to initialize worker:", error);
|
||||
this.workerReady = false;
|
||||
this.workerInitPromise = null;
|
||||
throw new Error("Failed to initialize database worker");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages received from the worker
|
||||
*/
|
||||
private handleWorkerMessage(message: WorkerResponse): void {
|
||||
const { id, type } = message;
|
||||
|
||||
// Handle absurd-sql internal messages (these are normal, don't log)
|
||||
if (!id && message.type?.startsWith('__absurd:')) {
|
||||
return; // Internal absurd-sql message, ignore silently
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
// logger.warn("[WebPlatformService] Received message without ID:", message); // DISABLED
|
||||
console.warn("[WebPlatformService] Received message without ID:", message);
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = this.pendingMessages.get(id);
|
||||
if (!pending) {
|
||||
// logger.warn( // DISABLED
|
||||
// "[WebPlatformService] Received response for unknown message ID:",
|
||||
// id,
|
||||
// );
|
||||
console.warn(
|
||||
"[WebPlatformService] Received response for unknown message ID:",
|
||||
id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear timeout and remove from pending
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingMessages.delete(id);
|
||||
|
||||
switch (type) {
|
||||
case "success":
|
||||
pending.resolve(message.data);
|
||||
break;
|
||||
|
||||
case "error": {
|
||||
const error = new Error(message.error.message);
|
||||
if (message.error.stack) {
|
||||
error.stack = message.error.stack;
|
||||
}
|
||||
pending.reject(error);
|
||||
break;
|
||||
}
|
||||
|
||||
case "init-complete":
|
||||
pending.resolve(true);
|
||||
break;
|
||||
|
||||
case "pong":
|
||||
pending.resolve(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
// logger.warn("[WebPlatformService] Unknown response type:", type); // DISABLED
|
||||
console.warn("[WebPlatformService] Unknown response type:", type);
|
||||
pending.resolve(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the worker and wait for response
|
||||
*/
|
||||
private async sendWorkerMessage<T>(
|
||||
request: Omit<WorkerRequest, "id">,
|
||||
): Promise<T> {
|
||||
if (!this.worker) {
|
||||
throw new Error("Worker not initialized");
|
||||
}
|
||||
|
||||
const id = `msg_${++this.messageIdCounter}_${Date.now()}`;
|
||||
const fullRequest: WorkerRequest = { id, ...request } as WorkerRequest;
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingMessages.delete(id);
|
||||
reject(new Error(`Worker message timeout for ${request.type} (${id})`));
|
||||
}, this.messageTimeout);
|
||||
|
||||
this.pendingMessages.set(id, {
|
||||
resolve: resolve as (value: unknown) => void,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
|
||||
// logger.log( // DISABLED
|
||||
// `[WebPlatformService] Sending message: ${request.type} (${id})`,
|
||||
// );
|
||||
this.worker!.postMessage(fullRequest);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for worker to be ready
|
||||
*/
|
||||
private async ensureWorkerReady(): Promise<void> {
|
||||
// Wait for initial initialization to complete
|
||||
if (this.workerInitPromise) {
|
||||
await this.workerInitPromise;
|
||||
}
|
||||
|
||||
if (this.workerReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to ping the worker if not ready
|
||||
try {
|
||||
await this.sendWorkerMessage<boolean>({ type: "ping" });
|
||||
this.workerReady = true;
|
||||
} catch (error) {
|
||||
// logger.error("[WebPlatformService] Worker not ready:", error); // DISABLED
|
||||
console.error("[WebPlatformService] Worker not ready:", error);
|
||||
throw new Error("Database worker not ready");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the web platform
|
||||
* @returns Platform capabilities object
|
||||
@@ -358,30 +570,43 @@ export class WebPlatformService implements PlatformService {
|
||||
/**
|
||||
* @see PlatformService.dbQuery
|
||||
*/
|
||||
dbQuery(
|
||||
async dbQuery(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<QueryExecResult | undefined> {
|
||||
return databaseService.query(sql, params).then((result) => result[0]);
|
||||
await this.ensureWorkerReady();
|
||||
return this.sendWorkerMessage<QueryResult>({
|
||||
type: "query",
|
||||
sql,
|
||||
params,
|
||||
} as QueryRequest).then((result) => result.result[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbExec
|
||||
*/
|
||||
dbExec(
|
||||
async dbExec(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<{ changes: number; lastId?: number }> {
|
||||
return databaseService.run(sql, params);
|
||||
await this.ensureWorkerReady();
|
||||
return this.sendWorkerMessage<{ changes: number; lastId?: number }>({
|
||||
type: "exec",
|
||||
sql,
|
||||
params,
|
||||
} as ExecRequest);
|
||||
}
|
||||
|
||||
async dbGetOneRow(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<unknown[] | undefined> {
|
||||
return databaseService
|
||||
.query(sql, params)
|
||||
.then((result: QueryExecResult[]) => result[0]?.values[0]);
|
||||
await this.ensureWorkerReady();
|
||||
return this.sendWorkerMessage<unknown[] | undefined>({
|
||||
type: "getOneRow",
|
||||
sql,
|
||||
params,
|
||||
} as GetOneRowRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user