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:
349
WORKER_ONLY_DATABASE_IMPLEMENTATION.md
Normal file
349
WORKER_ONLY_DATABASE_IMPLEMENTATION.md
Normal file
@@ -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"
|
||||||
|
```
|
||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
classpath 'com.android.tools.build:gradle:8.10.1'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ http {
|
|||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self';" always;
|
||||||
|
|
||||||
|
# SharedArrayBuffer support headers for absurd-sql
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
|
|
||||||
# Rate limiting
|
# Rate limiting
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SplashScreen": {
|
||||||
|
"launchShowDuration": 3000,
|
||||||
|
"launchAutoHide": true,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"androidSplashResourceName": "splash",
|
||||||
|
"androidScaleType": "CENTER_CROP",
|
||||||
|
"showSpinner": false,
|
||||||
|
"androidSpinnerStyle": "large",
|
||||||
|
"iosSpinnerStyle": "small",
|
||||||
|
"spinnerColor": "#999999",
|
||||||
|
"splashFullScreen": true,
|
||||||
|
"splashImmersive": true
|
||||||
|
},
|
||||||
"CapacitorSQLite": {
|
"CapacitorSQLite": {
|
||||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||||
"iosIsEncryption": false,
|
"iosIsEncryption": false,
|
||||||
|
|||||||
32
index.html
32
index.html
@@ -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">
|
|
||||||
<title>TimeSafari</title>
|
<!-- 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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
|
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||||
|
import { usePlatformService } from "../utils/usePlatformService";
|
||||||
|
import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TopMessage extends Vue {
|
export default class TopMessage extends Vue {
|
||||||
@@ -28,7 +31,7 @@ export default class TopMessage extends Vue {
|
|||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await this.getActiveAccountSettings();
|
||||||
if (
|
if (
|
||||||
settings.warnIfTestServer &&
|
settings.warnIfTestServer &&
|
||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
@@ -54,5 +57,95 @@ export default class TopMessage extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings for the active account using the platform service composable.
|
||||||
|
* This replaces the direct call to databaseUtil.retrieveSettingsForActiveAccount()
|
||||||
|
* and demonstrates the new composable pattern.
|
||||||
|
*/
|
||||||
|
private async getActiveAccountSettings() {
|
||||||
|
const { dbQuery } = usePlatformService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get default settings first
|
||||||
|
const defaultSettings = await this.getDefaultSettings();
|
||||||
|
|
||||||
|
// If no active DID, return defaults
|
||||||
|
if (!defaultSettings.activeDid) {
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get account-specific settings using the composable
|
||||||
|
const result = await dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE accountDid = ?",
|
||||||
|
[defaultSettings.activeDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result?.values?.length) {
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map and filter settings
|
||||||
|
const overrideSettings = mapColumnsToValues(
|
||||||
|
result.columns,
|
||||||
|
result.values,
|
||||||
|
)[0] as any;
|
||||||
|
|
||||||
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge settings
|
||||||
|
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||||
|
|
||||||
|
// Handle searchBoxes parsing
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to retrieve account settings for ${defaultSettings.activeDid}:`, error);
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default settings using the platform service composable
|
||||||
|
*/
|
||||||
|
private async getDefaultSettings() {
|
||||||
|
const { dbQuery } = usePlatformService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE id = ?",
|
||||||
|
[MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result?.values?.length) {
|
||||||
|
return {
|
||||||
|
id: MASTER_SETTINGS_KEY,
|
||||||
|
activeDid: undefined,
|
||||||
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = mapColumnsToValues(result.columns, result.values)[0] as any;
|
||||||
|
|
||||||
|
// Handle searchBoxes parsing
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to retrieve default settings:", error);
|
||||||
|
return {
|
||||||
|
id: MASTER_SETTINGS_KEY,
|
||||||
|
activeDid: undefined,
|
||||||
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
let lastCleanupDate: string | null = null;
|
let lastCleanupDate: string | null = null;
|
||||||
export let memoryLogs: string[] = [];
|
export let memoryLogs: string[] = [];
|
||||||
|
|
||||||
|
// Flag to prevent infinite logging loops during database operations
|
||||||
|
let isLoggingToDatabase = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message to the database with proper handling of concurrent writes
|
* Logs a message to the database with proper handling of concurrent writes
|
||||||
* @param message - The message to log
|
* @param message - The message to log
|
||||||
@@ -179,36 +182,44 @@ export async function logToDb(
|
|||||||
message: string,
|
message: string,
|
||||||
level: string = "info",
|
level: string = "info",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
// Prevent infinite logging loops - if we're already trying to log to database,
|
||||||
const todayKey = new Date().toDateString();
|
// just log to console instead to break circular dependency
|
||||||
|
if (isLoggingToDatabase) {
|
||||||
|
console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flag to prevent circular logging
|
||||||
|
isLoggingToDatabase = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
memoryLogs.push(`${new Date().toISOString()} ${message}`);
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
// Insert using actual schema: date, message (no level column)
|
const todayKey = new Date().toDateString();
|
||||||
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
|
||||||
todayKey, // Use date string to match schema
|
|
||||||
`[${level.toUpperCase()}] ${message}`, // Include level in message
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Clean up old logs (keep only last 7 days) - do this less frequently
|
try {
|
||||||
// Only clean up if the date is different from the last cleanup
|
memoryLogs.push(`${new Date().toISOString()} ${message}`);
|
||||||
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
// Insert using actual schema: date, message (no level column)
|
||||||
const sevenDaysAgo = new Date(
|
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
||||||
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
todayKey, // Use date string to match schema
|
||||||
).toDateString(); // Use date string to match schema
|
`[${level.toUpperCase()}] ${message}`, // Include level in message
|
||||||
memoryLogs = memoryLogs.filter((log) => log.split(" ")[0] > sevenDaysAgo);
|
]);
|
||||||
await platform.dbExec("DELETE FROM logs WHERE date < ?", [sevenDaysAgo]);
|
|
||||||
lastCleanupDate = todayKey;
|
// Clean up old logs (keep only last 7 days) - do this less frequently
|
||||||
|
// Only clean up if the date is different from the last cleanup
|
||||||
|
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
||||||
|
const sevenDaysAgo = new Date(
|
||||||
|
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
||||||
|
).toDateString(); // Use date string to match schema
|
||||||
|
memoryLogs = memoryLogs.filter((log) => log.split(" ")[0] > sevenDaysAgo);
|
||||||
|
await platform.dbExec("DELETE FROM logs WHERE date < ?", [sevenDaysAgo]);
|
||||||
|
lastCleanupDate = todayKey;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error logging to database:", error, " ... for original message:", message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} finally {
|
||||||
// Log to console as fallback
|
// Always reset the flag to prevent permanent blocking of database logging
|
||||||
// eslint-disable-next-line no-console
|
isLoggingToDatabase = false;
|
||||||
console.error(
|
|
||||||
"Error logging to database:",
|
|
||||||
error,
|
|
||||||
" ... for original message:",
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,13 +228,13 @@ export async function logConsoleAndDb(
|
|||||||
message: string,
|
message: string,
|
||||||
isError = false,
|
isError = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const level = isError ? "error" : "info";
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
logger.error(`${new Date().toISOString()}`, message);
|
console.error(message);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${new Date().toISOString()}`, message);
|
console.log(message);
|
||||||
}
|
}
|
||||||
await logToDb(message, level);
|
|
||||||
|
await logToDb(message, isError ? "error" : "info");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
128
src/interfaces/worker-messages.ts
Normal file
128
src/interfaces/worker-messages.ts
Normal file
@@ -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,10 +1,14 @@
|
|||||||
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
|
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { logger } from "./utils/logger";
|
// import { logger } from "./utils/logger"; // DISABLED FOR DEBUGGING
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM;
|
const platform = process.env.VITE_PLATFORM;
|
||||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
|
// Debug: Check SharedArrayBuffer availability
|
||||||
|
console.log(`[SharedArrayBuffer] Available: ${typeof SharedArrayBuffer !== 'undefined'}`);
|
||||||
|
console.log(`[Browser] User Agent: ${navigator.userAgent}`);
|
||||||
|
console.log(`[Headers] Check COOP/COEP in Network tab if SharedArrayBuffer is false`);
|
||||||
|
|
||||||
// Only import service worker for web builds
|
// Only import service worker for web builds
|
||||||
if (pwa_enabled) {
|
if (pwa_enabled) {
|
||||||
import("./registerServiceWorker"); // Web PWA support
|
import("./registerServiceWorker"); // Web PWA support
|
||||||
@@ -12,23 +16,18 @@ if (pwa_enabled) {
|
|||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp();
|
||||||
|
|
||||||
function sqlInit() {
|
// Note: Worker initialization is now handled by WebPlatformService
|
||||||
// see https://github.com/jlongster/absurd-sql
|
// This ensures single-point database access and prevents double migrations
|
||||||
const worker = new Worker(
|
|
||||||
new URL("./registerSQLWorker.js", import.meta.url),
|
|
||||||
{
|
|
||||||
type: "module",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// This is only required because Safari doesn't support nested
|
|
||||||
// workers. This installs a handler that will proxy creating web
|
|
||||||
// workers through the main thread
|
|
||||||
initBackend(worker);
|
|
||||||
}
|
|
||||||
if (platform === "web" || platform === "development") {
|
if (platform === "web" || platform === "development") {
|
||||||
sqlInit();
|
// logger.log( // DISABLED
|
||||||
|
// "[Web] Database initialization will be handled by WebPlatformService",
|
||||||
|
// );
|
||||||
|
console.log(
|
||||||
|
"[Web] Database initialization will be handled by WebPlatformService",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("[Web] SQL not initialized for platform", { platform });
|
// logger.warn("[Web] SQL not initialized for platform", { platform }); // DISABLED
|
||||||
|
console.warn("[Web] SQL not initialized for platform", { platform });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
run();
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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");
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
|||||||
try {
|
try {
|
||||||
await this.initializationPromise;
|
await this.initializationPromise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
|
// logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); // DISABLED
|
||||||
|
console.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
|
||||||
this.initializationPromise = null; // Reset on failure
|
this.initializationPromise = null; // Reset on failure
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,15 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
|||||||
}
|
}
|
||||||
operation.resolve(result);
|
operation.resolve(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
// logger.error( // DISABLED
|
||||||
|
// "Error while processing SQL queue:",
|
||||||
|
// error,
|
||||||
|
// " ... for sql:",
|
||||||
|
// operation.sql,
|
||||||
|
// " ... with params:",
|
||||||
|
// operation.params,
|
||||||
|
// );
|
||||||
|
console.error(
|
||||||
"Error while processing SQL queue:",
|
"Error while processing SQL queue:",
|
||||||
error,
|
error,
|
||||||
" ... for sql:",
|
" ... for sql:",
|
||||||
@@ -196,7 +205,10 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
|||||||
|
|
||||||
// If initialized but no db, something went wrong
|
// If initialized but no db, something went wrong
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
logger.error(
|
// logger.error( // DISABLED
|
||||||
|
// `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||||
|
// );
|
||||||
|
console.error(
|
||||||
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||||
);
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -130,4 +130,15 @@ export interface PlatformService {
|
|||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<{ changes: number; lastId?: number }>;
|
): Promise<{ changes: number; lastId?: number }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a SQL query and returns the first row as an array.
|
||||||
|
* @param sql - The SQL query to execute
|
||||||
|
* @param params - The parameters to pass to the query
|
||||||
|
* @returns Promise resolving to the first row as an array, or undefined if no results
|
||||||
|
*/
|
||||||
|
dbGetOneRow(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<unknown[] | undefined>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
|||||||
*/
|
*/
|
||||||
export class PlatformServiceFactory {
|
export class PlatformServiceFactory {
|
||||||
private static instance: PlatformService | null = null;
|
private static instance: PlatformService | null = null;
|
||||||
|
private static callCount = 0; // Debug counter
|
||||||
|
private static creationLogged = false; // Only log creation once
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets or creates the singleton instance of PlatformService.
|
* Gets or creates the singleton instance of PlatformService.
|
||||||
@@ -27,11 +29,20 @@ export class PlatformServiceFactory {
|
|||||||
* @returns {PlatformService} The singleton instance of PlatformService
|
* @returns {PlatformService} The singleton instance of PlatformService
|
||||||
*/
|
*/
|
||||||
public static getInstance(): PlatformService {
|
public static getInstance(): PlatformService {
|
||||||
|
PlatformServiceFactory.callCount++;
|
||||||
|
|
||||||
if (PlatformServiceFactory.instance) {
|
if (PlatformServiceFactory.instance) {
|
||||||
|
// Normal case - return existing instance silently
|
||||||
return PlatformServiceFactory.instance;
|
return PlatformServiceFactory.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only log when actually creating the instance
|
||||||
const platform = process.env.VITE_PLATFORM || "web";
|
const platform = process.env.VITE_PLATFORM || "web";
|
||||||
|
|
||||||
|
if (!PlatformServiceFactory.creationLogged) {
|
||||||
|
console.log(`[PlatformServiceFactory] Creating singleton instance for platform: ${platform}`);
|
||||||
|
PlatformServiceFactory.creationLogged = true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case "capacitor":
|
case "capacitor":
|
||||||
@@ -45,4 +56,14 @@ export class PlatformServiceFactory {
|
|||||||
|
|
||||||
return PlatformServiceFactory.instance;
|
return PlatformServiceFactory.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check singleton usage stats
|
||||||
|
*/
|
||||||
|
public static getStats(): { callCount: number; instanceExists: boolean } {
|
||||||
|
return {
|
||||||
|
callCount: PlatformServiceFactory.callCount,
|
||||||
|
instanceExists: PlatformServiceFactory.instance !== null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,13 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
"[CapacitorPlatformService] Error while processing SQL queue:",
|
"[CapacitorPlatformService] Error while processing SQL queue:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
logger.error(
|
||||||
|
`[CapacitorPlatformService] Failed operation - Type: ${operation.type}, SQL: ${operation.sql}`,
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
`[CapacitorPlatformService] Failed operation - Params:`,
|
||||||
|
operation.params,
|
||||||
|
);
|
||||||
operation.reject(error);
|
operation.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,31 +186,140 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
sql: string,
|
sql: string,
|
||||||
params: unknown[] = [],
|
params: unknown[] = [],
|
||||||
): Promise<R> {
|
): Promise<R> {
|
||||||
// Convert parameters to SQLite-compatible types
|
// Log incoming parameters for debugging (HIGH PRIORITY)
|
||||||
const convertedParams = params.map((param) => {
|
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) {
|
if (param === null || param === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof param === "object" && param !== null) {
|
if (typeof param === "object" && param !== null) {
|
||||||
// Convert objects and arrays to JSON strings
|
// Enhanced debug logging for all objects (HIGH PRIORITY)
|
||||||
return JSON.stringify(param);
|
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") {
|
if (typeof param === "boolean") {
|
||||||
// Convert boolean to integer (0 or 1)
|
// Convert boolean to integer (0 or 1)
|
||||||
return param ? 1 : 0;
|
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;
|
return param;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log converted parameters for debugging (HIGH PRIORITY)
|
||||||
|
logger.warn(`[CapacitorPlatformService] Converted params:`, convertedParams);
|
||||||
|
|
||||||
return new Promise<R>((resolve, reject) => {
|
return new Promise<R>((resolve, reject) => {
|
||||||
const operation: QueuedOperation = {
|
// Create completely plain objects that Vue cannot make reactive
|
||||||
type,
|
// Step 1: Deep clone the converted params to ensure they're plain objects
|
||||||
sql,
|
const plainParams = JSON.parse(JSON.stringify(convertedParams));
|
||||||
params: convertedParams,
|
|
||||||
resolve: (value: unknown) => resolve(value as R),
|
// Step 2: Create operation object using Object.create(null) for no prototype
|
||||||
reject,
|
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);
|
this.operationQueue.push(operation);
|
||||||
|
|
||||||
// If we're already initialized, start processing the queue
|
// 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
|
* Execute database migrations for the Capacitor platform
|
||||||
*
|
*
|
||||||
@@ -1074,4 +1259,21 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
params || [],
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import {
|
|||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { QueryExecResult } from "@/interfaces/database";
|
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.
|
* Platform service implementation for web browser platform.
|
||||||
@@ -16,11 +24,215 @@ import databaseService from "../AbsurdSqlDatabaseService";
|
|||||||
* - Image capture using the browser's file input
|
* - Image capture using the browser's file input
|
||||||
* - Image selection from local filesystem
|
* - Image selection from local filesystem
|
||||||
* - Image processing and conversion
|
* - Image processing and conversion
|
||||||
|
* - Database operations via worker thread messaging
|
||||||
*
|
*
|
||||||
* Note: File system operations are not available in the web platform
|
* Note: File system operations are not available in the web platform
|
||||||
* due to browser security restrictions. These methods throw appropriate errors.
|
* due to browser security restrictions. These methods throw appropriate errors.
|
||||||
*/
|
*/
|
||||||
export class WebPlatformService implements PlatformService {
|
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
|
* Gets the capabilities of the web platform
|
||||||
* @returns Platform capabilities object
|
* @returns Platform capabilities object
|
||||||
@@ -358,30 +570,43 @@ export class WebPlatformService implements PlatformService {
|
|||||||
/**
|
/**
|
||||||
* @see PlatformService.dbQuery
|
* @see PlatformService.dbQuery
|
||||||
*/
|
*/
|
||||||
dbQuery(
|
async dbQuery(
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<QueryExecResult | undefined> {
|
): 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
|
* @see PlatformService.dbExec
|
||||||
*/
|
*/
|
||||||
dbExec(
|
async dbExec(
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<{ changes: number; lastId?: number }> {
|
): 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(
|
async dbGetOneRow(
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<unknown[] | undefined> {
|
): Promise<unknown[] | undefined> {
|
||||||
return databaseService
|
await this.ensureWorkerReady();
|
||||||
.query(sql, params)
|
return this.sendWorkerMessage<unknown[] | undefined>({
|
||||||
.then((result: QueryExecResult[]) => result[0]?.values[0]);
|
type: "getOneRow",
|
||||||
|
sql,
|
||||||
|
params,
|
||||||
|
} as GetOneRowRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
365
src/utils/usePlatformService.ts
Normal file
365
src/utils/usePlatformService.ts
Normal file
@@ -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>;
|
||||||
@@ -1214,31 +1214,35 @@ export default class AccountViewView extends Vue {
|
|||||||
this.loadingProfile = false;
|
this.loadingProfile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Only check service worker on web platform - Capacitor/Electron don't support it
|
||||||
/**
|
if (!Capacitor.isNativePlatform()) {
|
||||||
* Beware! I've seen where this "ready" never resolves.
|
try {
|
||||||
*/
|
/**
|
||||||
const registration = await navigator.serviceWorker?.ready;
|
* Service workers only exist on web platforms
|
||||||
this.subscription = await registration.pushManager.getSubscription();
|
*/
|
||||||
if (!this.subscription) {
|
const registration = await navigator.serviceWorker?.ready;
|
||||||
if (this.notifyingNewActivity || this.notifyingReminder) {
|
this.subscription = await registration.pushManager.getSubscription();
|
||||||
// the app thought there was a subscription but there isn't, so fix the settings
|
if (!this.subscription) {
|
||||||
this.turnOffNotifyingFlags();
|
if (this.notifyingNewActivity || this.notifyingReminder) {
|
||||||
|
// the app thought there was a subscription but there isn't, so fix the settings
|
||||||
|
this.turnOffNotifyingFlags();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Cannot Set Notifications",
|
||||||
|
text: "This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
|
||||||
|
},
|
||||||
|
7000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/**
|
} else {
|
||||||
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
// On native platforms (Capacitor/Electron), skip service worker checks
|
||||||
*/
|
// Native notifications are handled differently
|
||||||
} catch (error) {
|
this.subscription = null;
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Cannot Set Notifications",
|
|
||||||
text: "This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
|
|
||||||
},
|
|
||||||
7000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export async function createBuildConfig(mode: string): Promise<UserConfig> {
|
|||||||
server: {
|
server: {
|
||||||
port: parseInt(process.env.VITE_PORT || "8080"),
|
port: parseInt(process.env.VITE_PORT || "8080"),
|
||||||
fs: { strict: false },
|
fs: { strict: false },
|
||||||
|
headers: {
|
||||||
|
// Enable SharedArrayBuffer for absurd-sql
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ export default defineConfig(async () => {
|
|||||||
const appConfig = await loadAppConfig();
|
const appConfig = await loadAppConfig();
|
||||||
|
|
||||||
return mergeConfig(baseConfig, {
|
return mergeConfig(baseConfig, {
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
// Enable SharedArrayBuffer for absurd-sql
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
|
|||||||
Reference in New Issue
Block a user