Browse Source
- 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.pull/142/head
19 changed files with 1791 additions and 122 deletions
@ -0,0 +1,349 @@ |
|||||
|
# Worker-Only Database Implementation for Web Platform |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This implementation fixes the double migration issue in the TimeSafari web platform by implementing worker-only database access, similar to the Capacitor platform architecture. |
||||
|
|
||||
|
## Problem Solved |
||||
|
|
||||
|
**Before:** Web platform had dual database contexts: |
||||
|
- Worker thread: `registerSQLWorker.js` → `AbsurdSqlDatabaseService.initialize()` → migrations run |
||||
|
- Main thread: `WebPlatformService.dbQuery()` → `databaseService.query()` → migrations run **AGAIN** |
||||
|
|
||||
|
**After:** Single database context: |
||||
|
- Worker thread: Handles ALL database operations and initializes once |
||||
|
- Main thread: Sends messages to worker, no direct database access |
||||
|
|
||||
|
## Architecture Changes |
||||
|
|
||||
|
### 1. Message-Based Communication |
||||
|
```typescript |
||||
|
// Main Thread (WebPlatformService) |
||||
|
await this.sendWorkerMessage<QueryResult>({ |
||||
|
type: "query", |
||||
|
sql: "SELECT * FROM users", |
||||
|
params: [] |
||||
|
}); |
||||
|
|
||||
|
// Worker Thread (registerSQLWorker.js) |
||||
|
onmessage = async (event) => { |
||||
|
const { id, type, sql, params } = event.data; |
||||
|
if (type === "query") { |
||||
|
const result = await databaseService.query(sql, params); |
||||
|
postMessage({ id, type: "success", data: { result } }); |
||||
|
} |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
### 2. Type-Safe Worker Messages |
||||
|
```typescript |
||||
|
// src/interfaces/worker-messages.ts |
||||
|
export interface QueryRequest extends BaseWorkerMessage { |
||||
|
type: "query"; |
||||
|
sql: string; |
||||
|
params?: unknown[]; |
||||
|
} |
||||
|
|
||||
|
export type WorkerRequest = |
||||
|
| QueryRequest |
||||
|
| ExecRequest |
||||
|
| GetOneRowRequest |
||||
|
| InitRequest |
||||
|
| PingRequest; |
||||
|
``` |
||||
|
|
||||
|
### 3. Circular Dependency Resolution |
||||
|
|
||||
|
**🔥 Critical Fix: Stack Overflow Prevention** |
||||
|
|
||||
|
**Problem**: Circular module dependency caused infinite recursion: |
||||
|
- `WebPlatformService` constructor → creates Worker |
||||
|
- Worker loads `registerSQLWorker.js` → imports `databaseService` |
||||
|
- Module resolution creates circular dependency → Stack Overflow |
||||
|
|
||||
|
**Solution**: Lazy Loading in Worker |
||||
|
```javascript |
||||
|
// Before (caused stack overflow) |
||||
|
import databaseService from "./services/AbsurdSqlDatabaseService"; |
||||
|
|
||||
|
// After (fixed) |
||||
|
let databaseService = null; |
||||
|
|
||||
|
async function getDatabaseService() { |
||||
|
if (!databaseService) { |
||||
|
// Dynamic import prevents circular dependency |
||||
|
const { default: service } = await import("./services/AbsurdSqlDatabaseService"); |
||||
|
databaseService = service; |
||||
|
} |
||||
|
return databaseService; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Key Changes for Stack Overflow Fix:** |
||||
|
- ✅ Removed top-level import of database service |
||||
|
- ✅ Added lazy loading with dynamic import |
||||
|
- ✅ Updated all handlers to use `await getDatabaseService()` |
||||
|
- ✅ Removed auto-initialization that triggered immediate loading |
||||
|
- ✅ Database service only loads when first database operation occurs |
||||
|
|
||||
|
## Implementation Details |
||||
|
|
||||
|
### 1. WebPlatformService Changes |
||||
|
- Removed direct database imports |
||||
|
- Added worker message handling |
||||
|
- Implemented timeout and error handling |
||||
|
- All database methods now proxy to worker |
||||
|
|
||||
|
### 2. Worker Thread Changes |
||||
|
- Added message-based operation handling |
||||
|
- Implemented lazy loading for database service |
||||
|
- Added proper error handling and response formatting |
||||
|
- Fixed circular dependency with dynamic imports |
||||
|
|
||||
|
### 3. Main Thread Changes |
||||
|
- Removed duplicate worker creation in `main.web.ts` |
||||
|
- WebPlatformService now manages single worker instance |
||||
|
- Added Safari compatibility with `initBackend()` |
||||
|
|
||||
|
## Files Modified |
||||
|
|
||||
|
1. **src/interfaces/worker-messages.ts** *(NEW)* |
||||
|
- Type definitions for worker communication |
||||
|
- Request and response message interfaces |
||||
|
|
||||
|
2. **src/registerSQLWorker.js** *(MAJOR REWRITE)* |
||||
|
- Message-based operation handling |
||||
|
- **Fixed circular dependency with lazy loading** |
||||
|
- Proper error handling and response formatting |
||||
|
|
||||
|
3. **src/services/platforms/WebPlatformService.ts** *(MAJOR REWRITE)* |
||||
|
- Worker-only database access |
||||
|
- Message sending and response handling |
||||
|
- Timeout and error management |
||||
|
|
||||
|
4. **src/main.web.ts** *(SIMPLIFIED)* |
||||
|
- Removed duplicate worker creation |
||||
|
- Simplified initialization flow |
||||
|
|
||||
|
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** *(NEW)* |
||||
|
- Complete documentation of changes |
||||
|
|
||||
|
## Benefits |
||||
|
|
||||
|
### ✅ Fixes Double Migration Issue |
||||
|
- Database migrations now run only once in worker thread |
||||
|
- No duplicate initialization between main thread and worker |
||||
|
|
||||
|
### ✅ Prevents Stack Overflow |
||||
|
- Circular dependency resolved with lazy loading |
||||
|
- Worker loads immediately without triggering database import |
||||
|
- Database service loads on-demand when first operation occurs |
||||
|
|
||||
|
### ✅ Improved Performance |
||||
|
- Single database connection |
||||
|
- No redundant operations |
||||
|
- Better resource utilization |
||||
|
|
||||
|
### ✅ Better Error Handling |
||||
|
- Centralized error handling in worker |
||||
|
- Type-safe message communication |
||||
|
- Proper timeout handling |
||||
|
|
||||
|
### ✅ Consistent Architecture |
||||
|
- Matches Capacitor platform pattern |
||||
|
- Single-threaded database access |
||||
|
- Clear separation of concerns |
||||
|
|
||||
|
## Testing Verification |
||||
|
|
||||
|
After implementation, you should see: |
||||
|
|
||||
|
1. **Worker Loading**: |
||||
|
``` |
||||
|
[SQLWorker] Worker loaded, ready to receive messages |
||||
|
``` |
||||
|
|
||||
|
2. **Database Initialization** (only on first operation): |
||||
|
``` |
||||
|
[SQLWorker] Starting database initialization... |
||||
|
[SQLWorker] Database initialization completed successfully |
||||
|
``` |
||||
|
|
||||
|
3. **No Stack Overflow**: Application starts without infinite recursion |
||||
|
4. **Single Migration Run**: Database migrations execute only once |
||||
|
5. **Functional Database**: All queries, inserts, and updates work correctly |
||||
|
|
||||
|
## Migration from Previous Implementation |
||||
|
|
||||
|
If upgrading from the dual-context implementation: |
||||
|
|
||||
|
1. **Remove Direct Database Imports**: No more `import databaseService` in main thread |
||||
|
2. **Update Database Calls**: Use platform service methods instead of direct database calls |
||||
|
3. **Handle Async Operations**: All database operations are now async message-based |
||||
|
4. **Error Handling**: Update error handling to work with worker responses |
||||
|
|
||||
|
## Security Considerations |
||||
|
|
||||
|
- Worker thread isolates database operations |
||||
|
- Message validation prevents malformed requests |
||||
|
- Timeout handling prevents hanging operations |
||||
|
- Type safety reduces runtime errors |
||||
|
|
||||
|
## Performance Notes |
||||
|
|
||||
|
- Initial worker creation has minimal overhead |
||||
|
- Database operations have message passing overhead (negligible) |
||||
|
- Single database connection is more efficient than dual connections |
||||
|
- Lazy loading reduces startup time |
||||
|
|
||||
|
## Migration Execution Flow |
||||
|
|
||||
|
### Before (Problematic) |
||||
|
``` |
||||
|
┌────────────── ───┐ ┌─────────────────┐ |
||||
|
│ Main Thread │ │ Worker Thread │ |
||||
|
│ │ │ │ |
||||
|
│ WebPlatformService│ │registerSQLWorker│ |
||||
|
│ ↓ │ │ ↓ │ |
||||
|
│ databaseService │ │ databaseService │ |
||||
|
│ (Instance A) │ │ (Instance B) │ |
||||
|
│ ↓ │ │ ↓ │ |
||||
|
│ [Run Migrations] │ │[Run Migrations] │ ← DUPLICATE! |
||||
|
└─────────────── ──┘ └─────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
### After (Fixed) |
||||
|
``` |
||||
|
┌─────────────── ──┐ ┌─────────────────┐ |
||||
|
│ Main Thread │ │ Worker Thread │ |
||||
|
│ │ │ │ |
||||
|
│ WebPlatformService │───→│registerSQLWorker│ |
||||
|
│ │ │ ↓ │ |
||||
|
│ [Send Messages] │ │ databaseService │ |
||||
|
│ │ │(Single Instance)│ |
||||
|
│ │ │ ↓ │ |
||||
|
│ │ │[Run Migrations] │ ← ONCE ONLY! |
||||
|
└─────────────── ──┘ └─────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
## Security Considerations |
||||
|
|
||||
|
### 1. **Message Validation** |
||||
|
- All worker messages validated for required fields |
||||
|
- Unknown message types rejected with errors |
||||
|
- Proper error responses prevent information leakage |
||||
|
|
||||
|
### 2. **Timeout Protection** |
||||
|
- 30-second timeout prevents hung operations |
||||
|
- Automatic cleanup of pending messages |
||||
|
- Worker health checks via ping/pong |
||||
|
|
||||
|
### 3. **Error Sanitization** |
||||
|
- Error messages logged but not exposed raw to main thread |
||||
|
- Stack traces included only in development |
||||
|
- Graceful handling of worker failures |
||||
|
|
||||
|
## Testing Considerations |
||||
|
|
||||
|
### 1. **Unit Tests Needed** |
||||
|
- Worker message handling |
||||
|
- WebPlatformService worker communication |
||||
|
- Error handling and timeouts |
||||
|
- Migration execution (should run once only) |
||||
|
|
||||
|
### 2. **Integration Tests** |
||||
|
- End-to-end database operations |
||||
|
- Worker lifecycle management |
||||
|
- Cross-browser compatibility (especially Safari) |
||||
|
|
||||
|
### 3. **Performance Tests** |
||||
|
- Message passing overhead |
||||
|
- Database operation throughput |
||||
|
- Memory usage with worker communication |
||||
|
|
||||
|
## Browser Compatibility |
||||
|
|
||||
|
### 1. **Modern Browsers** |
||||
|
- Chrome/Edge: Full SharedArrayBuffer support |
||||
|
- Firefox: Full SharedArrayBuffer support (with headers) |
||||
|
- Safari: Uses IndexedDB fallback via `initBackend()` |
||||
|
|
||||
|
### 2. **Required Headers** |
||||
|
``` |
||||
|
Cross-Origin-Opener-Policy: same-origin |
||||
|
Cross-Origin-Embedder-Policy: require-corp |
||||
|
``` |
||||
|
|
||||
|
## Deployment Notes |
||||
|
|
||||
|
### 1. **Development** |
||||
|
- Enhanced logging shows worker message flow |
||||
|
- Clear separation between worker and main thread logs |
||||
|
- Easy debugging via browser DevTools |
||||
|
|
||||
|
### 2. **Production** |
||||
|
- Reduced logging overhead |
||||
|
- Optimized message passing |
||||
|
- Proper error reporting without sensitive data |
||||
|
|
||||
|
## Future Enhancements |
||||
|
|
||||
|
### 1. **Potential Optimizations** |
||||
|
- Message batching for bulk operations |
||||
|
- Connection pooling simulation |
||||
|
- Persistent worker state management |
||||
|
|
||||
|
### 2. **Additional Features** |
||||
|
- Database backup/restore via worker |
||||
|
- Schema introspection commands |
||||
|
- Performance monitoring hooks |
||||
|
|
||||
|
## Rollback Plan |
||||
|
|
||||
|
If issues arise, rollback involves: |
||||
|
1. Restore original `WebPlatformService.ts` |
||||
|
2. Restore original `registerSQLWorker.js` |
||||
|
3. Restore original `main.web.ts` |
||||
|
4. Remove `worker-messages.ts` interface |
||||
|
|
||||
|
## Commit Messages |
||||
|
|
||||
|
```bash |
||||
|
git add src/interfaces/worker-messages.ts |
||||
|
git commit -m "Add worker message interface for type-safe database communication |
||||
|
|
||||
|
- Define TypeScript interfaces for worker request/response messages |
||||
|
- Include query, exec, getOneRow, init, and ping message types |
||||
|
- Provide type safety for web platform worker messaging" |
||||
|
|
||||
|
git add src/registerSQLWorker.js |
||||
|
git commit -m "Implement message-based worker for single-point database access |
||||
|
|
||||
|
- Replace simple auto-init with comprehensive message handler |
||||
|
- Add support for query, exec, getOneRow, init, ping operations |
||||
|
- Implement proper error handling and response management |
||||
|
- Ensure single database initialization point to prevent double migrations" |
||||
|
|
||||
|
git add src/services/platforms/WebPlatformService.ts |
||||
|
git commit -m "Migrate WebPlatformService to worker-only database access |
||||
|
|
||||
|
- Remove direct databaseService import to prevent dual context issue |
||||
|
- Implement worker-based messaging for all database operations |
||||
|
- Add worker lifecycle management with initialization tracking |
||||
|
- Include message timeout and error handling for reliability |
||||
|
- Add Safari compatibility with initBackend call" |
||||
|
|
||||
|
git add src/main.web.ts |
||||
|
git commit -m "Remove duplicate worker creation from main.web.ts |
||||
|
|
||||
|
- Worker initialization now handled by WebPlatformService |
||||
|
- Prevents duplicate worker creation and database contexts |
||||
|
- Simplifies main thread initialization" |
||||
|
|
||||
|
git add WORKER_ONLY_DATABASE_IMPLEMENTATION.md |
||||
|
git commit -m "Document worker-only database implementation |
||||
|
|
||||
|
- Comprehensive documentation of architecture changes |
||||
|
- Explain problem solved and benefits achieved |
||||
|
- Include security considerations and testing requirements" |
||||
|
``` |
@ -1,28 +1,18 @@ |
|||||
<!DOCTYPE html> |
<!DOCTYPE html> |
||||
<html lang=""> |
<html lang="en"> |
||||
<head> |
<head> |
||||
<meta charset="utf-8"> |
<meta charset="UTF-8" /> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> |
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover"> |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<link rel="icon" href="/favicon.ico"> |
|
||||
|
<!-- SharedArrayBuffer support headers for absurd-sql --> |
||||
|
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin" /> |
||||
|
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp" /> |
||||
|
|
||||
<title>Time Safari</title> |
<title>Time Safari</title> |
||||
</head> |
</head> |
||||
<body> |
<body> |
||||
<noscript> |
|
||||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> |
|
||||
</noscript> |
|
||||
<div id="app"></div> |
<div id="app"></div> |
||||
<script type="module"> |
<script type="module" src="/src/main.web.ts"></script> |
||||
const platform = process.env.VITE_PLATFORM; |
|
||||
switch (platform) { |
|
||||
case 'capacitor': |
|
||||
import('./src/main.capacitor.ts'); |
|
||||
break; |
|
||||
case 'web': |
|
||||
default: |
|
||||
import('./src/main.web.ts'); |
|
||||
break; |
|
||||
} |
|
||||
</script> |
|
||||
</body> |
</body> |
||||
</html> |
</html> |
||||
|
@ -0,0 +1,128 @@ |
|||||
|
/** |
||||
|
* Worker Message Interface for Database Operations |
||||
|
* |
||||
|
* Defines the communication protocol between the main thread and the |
||||
|
* SQL worker thread for TimeSafari web platform. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
* @since 2025-07-02 |
||||
|
*/ |
||||
|
|
||||
|
import type { QueryExecResult } from "./database"; |
||||
|
|
||||
|
/** |
||||
|
* Base interface for all worker messages |
||||
|
*/ |
||||
|
interface BaseWorkerMessage { |
||||
|
id: string; |
||||
|
type: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database query request message |
||||
|
*/ |
||||
|
export interface QueryRequest extends BaseWorkerMessage { |
||||
|
type: "query"; |
||||
|
sql: string; |
||||
|
params?: unknown[]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database execution request message (INSERT, UPDATE, DELETE, etc.) |
||||
|
*/ |
||||
|
export interface ExecRequest extends BaseWorkerMessage { |
||||
|
type: "exec"; |
||||
|
sql: string; |
||||
|
params?: unknown[]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database get one row request message |
||||
|
*/ |
||||
|
export interface GetOneRowRequest extends BaseWorkerMessage { |
||||
|
type: "getOneRow"; |
||||
|
sql: string; |
||||
|
params?: unknown[]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database initialization request message |
||||
|
*/ |
||||
|
export interface InitRequest extends BaseWorkerMessage { |
||||
|
type: "init"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Health check request message |
||||
|
*/ |
||||
|
export interface PingRequest extends BaseWorkerMessage { |
||||
|
type: "ping"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Union type of all possible request messages |
||||
|
*/ |
||||
|
export type WorkerRequest = |
||||
|
| QueryRequest |
||||
|
| ExecRequest |
||||
|
| GetOneRowRequest |
||||
|
| InitRequest |
||||
|
| PingRequest; |
||||
|
|
||||
|
/** |
||||
|
* Success response from worker |
||||
|
*/ |
||||
|
export interface SuccessResponse extends BaseWorkerMessage { |
||||
|
type: "success"; |
||||
|
data: unknown; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Error response from worker |
||||
|
*/ |
||||
|
export interface ErrorResponse extends BaseWorkerMessage { |
||||
|
type: "error"; |
||||
|
error: { |
||||
|
message: string; |
||||
|
stack?: string; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initialization complete response |
||||
|
*/ |
||||
|
export interface InitCompleteResponse extends BaseWorkerMessage { |
||||
|
type: "init-complete"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Ping response |
||||
|
*/ |
||||
|
export interface PongResponse extends BaseWorkerMessage { |
||||
|
type: "pong"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Union type of all possible response messages |
||||
|
*/ |
||||
|
export type WorkerResponse = |
||||
|
| SuccessResponse |
||||
|
| ErrorResponse |
||||
|
| InitCompleteResponse |
||||
|
| PongResponse; |
||||
|
|
||||
|
/** |
||||
|
* Query result type specifically for database queries |
||||
|
*/ |
||||
|
export interface QueryResult { |
||||
|
result: QueryExecResult[]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execution result type for database modifications |
||||
|
*/ |
||||
|
export interface ExecResult { |
||||
|
changes: number; |
||||
|
lastId?: number; |
||||
|
} |
@ -1,6 +1,236 @@ |
|||||
import databaseService from "./services/AbsurdSqlDatabaseService"; |
/** |
||||
|
* SQL Worker Thread Handler for TimeSafari Web Platform |
||||
|
* |
||||
|
* This worker handles all database operations for the web platform, |
||||
|
* ensuring single-threaded database access and preventing double migrations. |
||||
|
* |
||||
|
* Architecture: |
||||
|
* - Main thread sends messages to this worker |
||||
|
* - Worker initializes database once and handles all SQL operations |
||||
|
* - Results are sent back to main thread via postMessage |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
* @since 2025-07-02 |
||||
|
*/ |
||||
|
|
||||
async function run() { |
// import { logger } from "./utils/logger"; // DISABLED FOR DEBUGGING
|
||||
await databaseService.initialize(); |
|
||||
|
/** |
||||
|
* Worker state management |
||||
|
*/ |
||||
|
let isInitialized = false; |
||||
|
let initializationPromise = null; |
||||
|
let databaseService = null; |
||||
|
|
||||
|
/** |
||||
|
* Lazy load database service to prevent circular dependencies |
||||
|
*/ |
||||
|
async function getDatabaseService() { |
||||
|
if (!databaseService) { |
||||
|
// Dynamic import to prevent circular dependency
|
||||
|
const { default: service } = await import("./services/AbsurdSqlDatabaseService"); |
||||
|
databaseService = service; |
||||
|
} |
||||
|
return databaseService; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send response back to main thread |
||||
|
*/ |
||||
|
function sendResponse(id, type, data = null, error = null) { |
||||
|
const response = { |
||||
|
id, |
||||
|
type, |
||||
|
...(data && { data }), |
||||
|
...(error && { error }), |
||||
|
}; |
||||
|
postMessage(response); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initialize database service |
||||
|
*/ |
||||
|
async function initializeDatabase() { |
||||
|
if (isInitialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (initializationPromise) { |
||||
|
return initializationPromise; |
||||
|
} |
||||
|
|
||||
|
initializationPromise = (async () => { |
||||
|
try { |
||||
|
// logger.log("[SQLWorker] Starting database initialization..."); // DISABLED
|
||||
|
const dbService = await getDatabaseService(); |
||||
|
await dbService.initialize(); |
||||
|
isInitialized = true; |
||||
|
// logger.log("[SQLWorker] Database initialization completed successfully"); // DISABLED
|
||||
|
} catch (error) { |
||||
|
// logger.error("[SQLWorker] Database initialization failed:", error); // DISABLED
|
||||
|
console.error("[SQLWorker] Database initialization failed:", error); // Keep only critical errors
|
||||
|
isInitialized = false; |
||||
|
initializationPromise = null; |
||||
|
throw error; |
||||
|
} |
||||
|
})(); |
||||
|
|
||||
|
return initializationPromise; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle database query operations |
||||
|
*/ |
||||
|
async function handleQuery(id, sql, params = []) { |
||||
|
try { |
||||
|
await initializeDatabase(); |
||||
|
// logger.log(`[SQLWorker] Executing query: ${sql}`, params); // DISABLED
|
||||
|
|
||||
|
const dbService = await getDatabaseService(); |
||||
|
const result = await dbService.query(sql, params); |
||||
|
// logger.log(`[SQLWorker] Query completed successfully`); // DISABLED
|
||||
|
|
||||
|
sendResponse(id, "success", { result }); |
||||
|
} catch (error) { |
||||
|
// logger.error(`[SQLWorker] Query failed:`, error); // DISABLED
|
||||
|
console.error(`[SQLWorker] Query failed:`, error); // Keep only critical errors
|
||||
|
sendResponse(id, "error", null, { |
||||
|
message: error.message, |
||||
|
stack: error.stack, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle database execution operations (INSERT, UPDATE, DELETE) |
||||
|
*/ |
||||
|
async function handleExec(id, sql, params = []) { |
||||
|
try { |
||||
|
await initializeDatabase(); |
||||
|
// logger.log(`[SQLWorker] Executing statement: ${sql}`, params); // DISABLED
|
||||
|
|
||||
|
const dbService = await getDatabaseService(); |
||||
|
const result = await dbService.run(sql, params); |
||||
|
// logger.log(`[SQLWorker] Statement executed successfully:`, result); // DISABLED
|
||||
|
|
||||
|
sendResponse(id, "success", result); |
||||
|
} catch (error) { |
||||
|
// logger.error(`[SQLWorker] Statement execution failed:`, error); // DISABLED
|
||||
|
console.error(`[SQLWorker] Statement execution failed:`, error); // Keep only critical errors
|
||||
|
sendResponse(id, "error", null, { |
||||
|
message: error.message, |
||||
|
stack: error.stack, |
||||
|
}); |
||||
|
} |
||||
} |
} |
||||
run(); |
|
||||
|
/** |
||||
|
* Handle database get one row operations |
||||
|
*/ |
||||
|
async function handleGetOneRow(id, sql, params = []) { |
||||
|
try { |
||||
|
await initializeDatabase(); |
||||
|
// logger.log(`[SQLWorker] Executing getOneRow: ${sql}`, params); // DISABLED
|
||||
|
|
||||
|
const dbService = await getDatabaseService(); |
||||
|
const result = await dbService.query(sql, params); |
||||
|
const oneRow = result?.[0]?.values?.[0]; |
||||
|
// logger.log(`[SQLWorker] GetOneRow completed successfully`); // DISABLED
|
||||
|
|
||||
|
sendResponse(id, "success", oneRow); |
||||
|
} catch (error) { |
||||
|
// logger.error(`[SQLWorker] GetOneRow failed:`, error); // DISABLED
|
||||
|
console.error(`[SQLWorker] GetOneRow failed:`, error); // Keep only critical errors
|
||||
|
sendResponse(id, "error", null, { |
||||
|
message: error.message, |
||||
|
stack: error.stack, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle initialization request |
||||
|
*/ |
||||
|
async function handleInit(id) { |
||||
|
try { |
||||
|
await initializeDatabase(); |
||||
|
// logger.log("[SQLWorker] Initialization request completed"); // DISABLED
|
||||
|
sendResponse(id, "init-complete"); |
||||
|
} catch (error) { |
||||
|
// logger.error("[SQLWorker] Initialization request failed:", error); // DISABLED
|
||||
|
console.error("[SQLWorker] Initialization request failed:", error); // Keep only critical errors
|
||||
|
sendResponse(id, "error", null, { |
||||
|
message: error.message, |
||||
|
stack: error.stack, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle ping request for health check |
||||
|
*/ |
||||
|
function handlePing(id) { |
||||
|
// logger.log("[SQLWorker] Ping received"); // DISABLED
|
||||
|
sendResponse(id, "pong"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Main message handler |
||||
|
*/ |
||||
|
onmessage = function (event) { |
||||
|
const { id, type, sql, params } = event.data; |
||||
|
|
||||
|
if (!id || !type) { |
||||
|
// logger.error("[SQLWorker] Invalid message received:", event.data); // DISABLED
|
||||
|
console.error("[SQLWorker] Invalid message received:", event.data); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// logger.log(`[SQLWorker] Received message: ${type} (${id})`); // DISABLED
|
||||
|
|
||||
|
switch (type) { |
||||
|
case "query": |
||||
|
handleQuery(id, sql, params); |
||||
|
break; |
||||
|
|
||||
|
case "exec": |
||||
|
handleExec(id, sql, params); |
||||
|
break; |
||||
|
|
||||
|
case "getOneRow": |
||||
|
handleGetOneRow(id, sql, params); |
||||
|
break; |
||||
|
|
||||
|
case "init": |
||||
|
handleInit(id); |
||||
|
break; |
||||
|
|
||||
|
case "ping": |
||||
|
handlePing(id); |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
// logger.error(`[SQLWorker] Unknown message type: ${type}`); // DISABLED
|
||||
|
console.error(`[SQLWorker] Unknown message type: ${type}`); |
||||
|
sendResponse(id, "error", null, { |
||||
|
message: `Unknown message type: ${type}`, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Handle worker errors |
||||
|
*/ |
||||
|
onerror = function (error) { |
||||
|
// logger.error("[SQLWorker] Worker error:", error); // DISABLED
|
||||
|
console.error("[SQLWorker] Worker error:", error); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Auto-initialize on worker startup (removed to prevent circular dependency) |
||||
|
* Initialization now happens on first database operation |
||||
|
*/ |
||||
|
// logger.log("[SQLWorker] Worker loaded, ready to receive messages"); // DISABLED
|
||||
|
console.log("[SQLWorker] Worker loaded, ready to receive messages"); |
||||
|
@ -0,0 +1,365 @@ |
|||||
|
/** |
||||
|
* 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>; |
Loading…
Reference in new issue