import Services from "./service"; /** * Database service managing IndexedDB operations via Web Worker and Service Worker */ export class Database { // ============================================ // PRIVATE PROPERTIES // ============================================ private static instance: Database; private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; private serviceWorkerCheckIntervalId: number | null = null; private indexedDBWorker: Worker | null = null; private messageIdCounter: number = 0; private pendingMessages: Map< number, { resolve: (value: any) => void; reject: (error: any) => void } > = new Map(); // ============================================ // INITIALIZATION & SINGLETON // ============================================ private constructor() { this.initIndexedDBWorker(); this.initServiceWorker(); } public static async getInstance(): Promise { if (!Database.instance) { Database.instance = new Database(); await Database.instance.waitForWorkerReady(); } return Database.instance; } // ============================================ // INDEXEDDB WEB WORKER // ============================================ private initIndexedDBWorker(): void { this.indexedDBWorker = new Worker( new URL("../workers/database.worker.ts", import.meta.url), { type: "module" } ); this.indexedDBWorker.onmessage = (event) => { const { id, type, result, error } = event.data; const pending = this.pendingMessages.get(id); if (pending) { this.pendingMessages.delete(id); if (type === "SUCCESS") { pending.resolve(result); } else if (type === "ERROR") { pending.reject(new Error(error)); } } }; this.indexedDBWorker.onerror = (error) => { console.error("[Database] IndexedDB Worker error:", error); }; } private async waitForWorkerReady(): Promise { return this.sendMessageToWorker("INIT", {}); } private sendMessageToWorker(type: string, payload: any): Promise { return new Promise((resolve, reject) => { if (!this.indexedDBWorker) { reject(new Error("IndexedDB Worker not initialized")); return; } const id = this.messageIdCounter++; this.pendingMessages.set(id, { resolve, reject }); this.indexedDBWorker.postMessage({ type, payload, id }); // Timeout de sécurité (30 secondes) setTimeout(() => { if (this.pendingMessages.has(id)) { this.pendingMessages.delete(id); reject(new Error(`Worker message timeout for type: ${type}`)); } }, 30000); }); } // ============================================ // SERVICE WORKER // ============================================ private initServiceWorker(): void { this.registerServiceWorker("/data.worker.js"); } private async registerServiceWorker(path: string): Promise { if (!("serviceWorker" in navigator)) return; console.log("[Database] Initializing Service Worker:", path); try { const registrations = await navigator.serviceWorker.getRegistrations(); for (const registration of registrations) { const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL; const scope = registration.scope; if ( scope.includes("/src/service-workers/") || (scriptURL && scriptURL.includes("/src/service-workers/")) ) { console.warn(`[Database] Removing old Service Worker (${scope})`); await registration.unregister(); } } const existingValidWorker = registrations.find((r) => { const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; return url && url.endsWith(path.replace(/^\//, "")); }); if (!existingValidWorker) { console.log("[Database] Registering new Service Worker"); this.serviceWorkerRegistration = await navigator.serviceWorker.register( path, { type: "module", scope: "/" } ); } else { console.log("[Database] Service Worker already active"); this.serviceWorkerRegistration = existingValidWorker; await this.serviceWorkerRegistration.update(); } navigator.serviceWorker.addEventListener("message", async (event) => { await this.handleServiceWorkerMessage(event.data); }); if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); this.serviceWorkerCheckIntervalId = window.setInterval(async () => { const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation( this.serviceWorkerRegistration! )); const service = await Services.getInstance(); const payload = await service.getMyProcesses(); if (payload && payload.length != 0) { activeWorker?.postMessage({ type: "SCAN", payload }); } }, 5000); } catch (error) { console.error("[Database] Service Worker error:", error); } } private async waitForServiceWorkerActivation( registration: ServiceWorkerRegistration ): Promise { return new Promise((resolve) => { if (registration.active) { resolve(registration.active); } else { const listener = () => { if (registration.active) { navigator.serviceWorker.removeEventListener( "controllerchange", listener ); resolve(registration.active); } }; navigator.serviceWorker.addEventListener("controllerchange", listener); } }); } private async checkForUpdates(): Promise { if (this.serviceWorkerRegistration) { try { await this.serviceWorkerRegistration.update(); if (this.serviceWorkerRegistration.waiting) { this.serviceWorkerRegistration.waiting.postMessage({ type: "SKIP_WAITING", }); } } catch (error) { console.error("Error checking for service worker updates:", error); } } } // ============================================ // SERVICE WORKER MESSAGE HANDLERS // ============================================ private async handleServiceWorkerMessage(message: any) { switch (message.type) { case "TO_DOWNLOAD": await this.handleDownloadList(message.data); break; case "DIFFS_TO_CREATE": await this.handleDiffsToCreate(message.data); break; default: console.warn( "Unknown message type received from service worker:", message ); } } private async handleDiffsToCreate(diffs: any[]): Promise { console.log( `[Database] Creating ${diffs.length} diffs from Service Worker scan` ); try { await this.saveDiffs(diffs); console.log("[Database] Diffs created successfully"); } catch (error) { console.error("[Database] Error creating diffs:", error); } } private async handleDownloadList(downloadList: string[]): Promise { let requestedStateId: string[] = []; const service = await Services.getInstance(); for (const hash of downloadList) { const diff = await service.getDiffByValue(hash); if (!diff) { console.warn(`Missing a diff for hash ${hash}`); continue; } const processId = diff.process_id; const stateId = diff.state_id; const roles = diff.roles; try { const valueBytes = await service.fetchValueFromStorage(hash); if (valueBytes) { const blob = new Blob([valueBytes], { type: "application/octet-stream", }); await service.saveBlobToDb(hash, blob); document.dispatchEvent( new CustomEvent("newDataReceived", { detail: { processId, stateId, hash }, }) ); } else { console.log("Request data from managers of the process"); if (!requestedStateId.includes(stateId)) { await service.requestDataFromPeers(processId, [stateId], [roles]); requestedStateId.push(stateId); } } } catch (e) { console.error(e); } } } // ============================================ // GENERIC INDEXEDDB OPERATIONS // ============================================ public async getStoreList(): Promise<{ [key: string]: string }> { return this.sendMessageToWorker("GET_STORE_LIST", {}); } public async addObject(payload: { storeName: string; object: any; key: any; }): Promise { await this.sendMessageToWorker("ADD_OBJECT", payload); } public async batchWriting(payload: { storeName: string; objects: { key: any; object: any }[]; }): Promise { await this.sendMessageToWorker("BATCH_WRITING", payload); } public async getObject(storeName: string, key: string): Promise { return this.sendMessageToWorker("GET_OBJECT", { storeName, key }); } public async dumpStore(storeName: string): Promise> { return this.sendMessageToWorker("DUMP_STORE", { storeName }); } public async deleteObject(storeName: string, key: string): Promise { await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key }); } public async clearStore(storeName: string): Promise { await this.sendMessageToWorker("CLEAR_STORE", { storeName }); } public async requestStoreByIndex( storeName: string, indexName: string, request: string ): Promise { return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", { storeName, indexName, request, }); } public async clearMultipleStores(storeNames: string[]): Promise { for (const storeName of storeNames) { await this.clearStore(storeName); } } // ============================================ // BUSINESS METHODS - DEVICE // ============================================ public async saveDevice(device: any): Promise { try { const existing = await this.getObject("wallet", "1"); if (existing) { await this.deleteObject("wallet", "1"); } } catch (e) {} await this.addObject({ storeName: "wallet", object: { pre_id: "1", device }, key: null, }); } public async getDevice(): Promise { const result = await this.getObject("wallet", "1"); return result ? result["device"] : null; } // ============================================ // BUSINESS METHODS - PROCESS // ============================================ public async saveProcess(processId: string, process: any): Promise { await this.addObject({ storeName: "processes", object: process, key: processId, }); } public async saveProcessesBatch( processes: Record ): Promise { if (Object.keys(processes).length === 0) return; await this.batchWriting({ storeName: "processes", objects: Object.entries(processes).map(([key, value]) => ({ key, object: value, })), }); } public async getProcess(processId: string): Promise { return this.getObject("processes", processId); } public async getAllProcesses(): Promise> { return this.dumpStore("processes"); } // ============================================ // BUSINESS METHODS - BLOBS // ============================================ public async saveBlob(hash: string, data: Blob): Promise { await this.addObject({ storeName: "data", object: data, key: hash, }); } public async getBlob(hash: string): Promise { return this.getObject("data", hash); } // ============================================ // BUSINESS METHODS - DIFFS // ============================================ public async saveDiffs(diffs: any[]): Promise { if (diffs.length === 0) return; for (const diff of diffs) { await this.addObject({ storeName: "diffs", object: diff, key: null, }); } } public async getDiff(hash: string): Promise { return this.getObject("diffs", hash); } public async getAllDiffs(): Promise> { return this.dumpStore("diffs"); } // ============================================ // BUSINESS METHODS - SECRETS // ============================================ public async getSharedSecret(address: string): Promise { return this.getObject("shared_secrets", address); } public async saveSecretsBatch( unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[] ): Promise { if (unconfirmedSecrets && unconfirmedSecrets.length > 0) { for (const secret of unconfirmedSecrets) { await this.addObject({ storeName: "unconfirmed_secrets", object: secret, key: null, }); } } if (sharedSecrets && sharedSecrets.length > 0) { for (const { key, value } of sharedSecrets) { await this.addObject({ storeName: "shared_secrets", object: value, key: key, }); } } } public async getAllSecrets(): Promise<{ shared_secrets: Record; unconfirmed_secrets: any[]; }> { const sharedSecrets = await this.dumpStore("shared_secrets"); const unconfirmedSecrets = await this.dumpStore("unconfirmed_secrets"); return { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; } } export default Database;