From 8a87fe38c55230053de7c04785e830298e53307e Mon Sep 17 00:00:00 2001 From: NicolasCantu Date: Fri, 28 Nov 2025 00:13:42 +0100 Subject: [PATCH] refactor(data.worker): optimize database access in service worker by implementing direct IndexedDB functions and enhancing message handling --- public/data.worker.js | 235 +++++++++++++++----------- src/services/database.service.ts | 276 ++++++++++++++++--------------- 2 files changed, 288 insertions(+), 223 deletions(-) diff --git a/public/data.worker.js b/public/data.worker.js index 7cf5a02..38619d1 100644 --- a/public/data.worker.js +++ b/public/data.worker.js @@ -1,82 +1,75 @@ -const EMPTY32BYTES = String('').padStart(64, '0'); +// public/data.worker.js + +const DB_NAME = "4nk"; +const DB_VERSION = 1; +const EMPTY32BYTES = String("").padStart(64, "0"); // ============================================ // SERVICE WORKER LIFECYCLE // ============================================ -self.addEventListener('install', (event) => { +self.addEventListener("install", (event) => { event.waitUntil(self.skipWaiting()); }); -self.addEventListener('activate', (event) => { +self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); // ============================================ -// MESSAGE HANDLER +// INDEXEDDB DIRECT ACCESS (READ-ONLY) // ============================================ -self.addEventListener('message', async (event) => { - const data = event.data; - console.log('[Service Worker] Message received:', data.type); - - if (data.type === 'SCAN') { - try { - const myProcessesId = data.payload; - if (myProcessesId && myProcessesId.length != 0) { - const scanResult = await scanMissingData(myProcessesId, event.source); - - if (scanResult.toDownload.length != 0) { - console.log('[Service Worker] Sending TO_DOWNLOAD message'); - event.source.postMessage({ type: 'TO_DOWNLOAD', data: scanResult.toDownload }); - } - - if (scanResult.diffsToCreate.length > 0) { - console.log('[Service Worker] Sending DIFFS_TO_CREATE message'); - event.source.postMessage({ type: 'DIFFS_TO_CREATE', data: scanResult.diffsToCreate }); - } - } else { - event.source.postMessage({ status: 'error', message: 'Empty lists' }); - } - } catch (error) { - console.error('[Service Worker] Scan error:', error); - event.source.postMessage({ status: 'error', message: error.message }); - } - } -}); - -// ============================================ -// DATABASE COMMUNICATION -// ============================================ - -async function requestFromMainThread(client, action, payload) { +/** + * Ouvre une connexion à la BDD directement depuis le Service Worker + */ +function openDB() { return new Promise((resolve, reject) => { - const messageId = `sw_${Date.now()}_${Math.random()}`; - - const messageHandler = (event) => { - if (event.data.id === messageId) { - self.removeEventListener('message', messageHandler); - if (event.data.type === 'DB_RESPONSE') { - resolve(event.data.result); - } else if (event.data.type === 'DB_ERROR') { - reject(new Error(event.data.error)); - } - } - }; - - self.addEventListener('message', messageHandler); - - client.postMessage({ - type: 'DB_REQUEST', - id: messageId, - action, - payload + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); +} + +/** + * Récupère un objet spécifique (équivalent à GET_OBJECT) + */ +function getObject(db, storeName, key) { + return new Promise((resolve, reject) => { + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + const request = store.get(key); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); +} + +/** + * Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS) + * Optimisé pour utiliser une seule transaction. + */ +function getMultipleObjects(db, storeName, keys) { + return new Promise((resolve, reject) => { + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + const results = []; + + let completed = 0; + if (keys.length === 0) resolve([]); + + keys.forEach((key) => { + const request = store.get(key); + request.onsuccess = () => { + if (request.result) results.push(request.result); + completed++; + if (completed === keys.length) resolve(results); + }; + request.onerror = () => { + console.warn(`[SW] Erreur lecture clé ${key}`); + completed++; + if (completed === keys.length) resolve(results); + }; }); - - setTimeout(() => { - self.removeEventListener('message', messageHandler); - reject(new Error('Database request timeout')); - }, 10000); }); } @@ -84,40 +77,57 @@ async function requestFromMainThread(client, action, payload) { // SCAN LOGIC // ============================================ -async function scanMissingData(processesToScan, client) { - console.log('[Service Worker] Scanning for missing data...'); - - const myProcesses = await requestFromMainThread(client, 'GET_MULTIPLE_OBJECTS', { - storeName: 'processes', - keys: processesToScan - }); +async function scanMissingData(processesToScan) { + console.log("[Service Worker] 🚀 Scanning with DIRECT DB ACCESS..."); + + let db; + try { + db = await openDB(); + } catch (e) { + console.error("[SW] Impossible d'ouvrir la BDD:", e); + return { toDownload: [], diffsToCreate: [] }; + } + + // 1. Récupération directe des processus + const myProcesses = await getMultipleObjects( + db, + "processes", + processesToScan + ); let toDownload = new Set(); let diffsToCreate = []; - - if (myProcesses && myProcesses.length != 0) { + + if (myProcesses && myProcesses.length !== 0) { for (const process of myProcesses) { + if (!process || !process.states) continue; + const firstState = process.states[0]; + // Sécurisation : on vérifie que firstState existe + if (!firstState) continue; + const processId = firstState.commited_in; + for (const state of process.states) { if (state.state_id === EMPTY32BYTES) continue; - + for (const [field, hash] of Object.entries(state.pcd_commitment)) { - if (state.public_data[field] !== undefined || field === 'roles') continue; - - const existingData = await requestFromMainThread(client, 'GET_OBJECT', { - storeName: 'data', - key: hash - }); - + // On ignore les données publiques ou les rôles + if ( + (state.public_data && state.public_data[field] !== undefined) || + field === "roles" + ) + continue; + + // 2. Vérification directe dans 'data' + const existingData = await getObject(db, "data", hash); + if (!existingData) { toDownload.add(hash); - - const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', { - storeName: 'diffs', - key: hash - }); - + + // 3. Vérification directe dans 'diffs' + const existingDiff = await getObject(db, "diffs", hash); + if (!existingDiff) { diffsToCreate.push({ process_id: processId, @@ -130,23 +140,64 @@ async function scanMissingData(processesToScan, client) { new_value: null, notify_user: false, need_validation: false, - validation_status: 'None' + validation_status: "None", }); } } else { - if (toDownload.delete(hash)) { - console.log(`[Service Worker] Removing ${hash} from the set`); + // Si on a trouvé la donnée, on est sûr de ne pas avoir besoin de la télécharger + if (toDownload.has(hash)) { + toDownload.delete(hash); } } } } } - } + } - console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length }); + // On ferme la connexion BDD pour libérer les ressources + db.close(); + + console.log("[Service Worker] Scan complete:", { + toDownload: toDownload.size, + diffsToCreate: diffsToCreate.length, + }); return { toDownload: Array.from(toDownload), - diffsToCreate: diffsToCreate + diffsToCreate: diffsToCreate, }; } +// ============================================ +// MESSAGE HANDLER +// ============================================ + +self.addEventListener("message", async (event) => { + const data = event.data; + + if (data.type === "SCAN") { + try { + const myProcessesId = data.payload; + if (myProcessesId && myProcessesId.length !== 0) { + // Appel direct de la nouvelle fonction optimisée + const scanResult = await scanMissingData(myProcessesId); + + if (scanResult.toDownload.length !== 0) { + event.source.postMessage({ + type: "TO_DOWNLOAD", + data: scanResult.toDownload, + }); + } + + if (scanResult.diffsToCreate.length > 0) { + event.source.postMessage({ + type: "DIFFS_TO_CREATE", + data: scanResult.diffsToCreate, + }); + } + } + } catch (error) { + console.error("[Service Worker] Scan error:", error); + // On évite de spammer l'UI avec des erreurs internes du worker + } + } +}); diff --git a/src/services/database.service.ts b/src/services/database.service.ts index cc97b78..15ebc97 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -1,4 +1,4 @@ -import Services from './service'; +import Services from "./service"; /** * Database service managing IndexedDB operations via Web Worker and Service Worker @@ -7,13 +7,16 @@ 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 void; reject: (error: any) => void }> = new Map(); + private pendingMessages: Map< + number, + { resolve: (value: any) => void; reject: (error: any) => void } + > = new Map(); // ============================================ // INITIALIZATION & SINGLETON @@ -37,36 +40,39 @@ export class Database { // ============================================ private initIndexedDBWorker(): void { - this.indexedDBWorker = new Worker(new URL('../workers/database.worker.ts', import.meta.url), { type: 'module' }); - + 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') { + + if (type === "SUCCESS") { pending.resolve(result); - } else if (type === 'ERROR') { + } else if (type === "ERROR") { pending.reject(new Error(error)); } } }; this.indexedDBWorker.onerror = (error) => { - console.error('[Database] IndexedDB Worker error:', error); + console.error("[Database] IndexedDB Worker error:", error); }; } private async waitForWorkerReady(): Promise { - return this.sendMessageToWorker('INIT', {}); + 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')); + reject(new Error("IndexedDB Worker not initialized")); return; } @@ -90,74 +96,93 @@ export class Database { // ============================================ private initServiceWorker(): void { - this.registerServiceWorker('/data.worker.js'); + this.registerServiceWorker("/data.worker.js"); } private async registerServiceWorker(path: string): Promise { - if (!('serviceWorker' in navigator)) return; - console.log('[Database] Initializing Service Worker:', path); + 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 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/'))) { + 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(/^\//,'')); + 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: '/' }); + 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'); + console.log("[Database] Service Worker already active"); this.serviceWorkerRegistration = existingValidWorker; await this.serviceWorkerRegistration.update(); } - navigator.serviceWorker.addEventListener('message', async (event) => { - if (event.data.type === 'DB_REQUEST') { - await this.handleDatabaseRequest(event.data); - return; - } + navigator.serviceWorker.addEventListener("message", async (event) => { + // ✅ SIMPLIFICATION : Plus besoin de gérer les "DB_REQUEST" await this.handleServiceWorkerMessage(event.data); }); - if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); + if (this.serviceWorkerCheckIntervalId) + clearInterval(this.serviceWorkerCheckIntervalId); this.serviceWorkerCheckIntervalId = window.setInterval(async () => { - const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); + 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 }); + activeWorker?.postMessage({ type: "SCAN", payload }); } }, 5000); } catch (error) { - console.error('[Database] Service Worker error:', error); + console.error("[Database] Service Worker error:", error); } } - private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise { + 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); + navigator.serviceWorker.removeEventListener( + "controllerchange", + listener + ); resolve(registration.active); } }; - navigator.serviceWorker.addEventListener('controllerchange', listener); + navigator.serviceWorker.addEventListener("controllerchange", listener); } }); } @@ -168,10 +193,12 @@ export class Database { await this.serviceWorkerRegistration.update(); if (this.serviceWorkerRegistration.waiting) { - this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); + this.serviceWorkerRegistration.waiting.postMessage({ + type: "SKIP_WAITING", + }); } } catch (error) { - console.error('Error checking for service worker updates:', error); + console.error("Error checking for service worker updates:", error); } } } @@ -179,73 +206,34 @@ export class Database { // ============================================ // SERVICE WORKER MESSAGE HANDLERS // ============================================ - private async handleDatabaseRequest(request: any): Promise { - const { id, action, payload } = request; - - try { - let result; - - switch (action) { - case 'GET_OBJECT': - result = await this.getObject(payload.storeName, payload.key); - break; - - case 'GET_MULTIPLE_OBJECTS': - result = await this.sendMessageToWorker('GET_MULTIPLE_OBJECTS', payload); - break; - - case 'GET_ALL_OBJECTS': - result = await this.sendMessageToWorker('GET_ALL_OBJECTS', payload); - break; - - case 'GET_ALL_OBJECTS_WITH_FILTER': - result = await this.sendMessageToWorker('GET_ALL_OBJECTS_WITH_FILTER', payload); - break; - - default: - throw new Error(`Unknown database action: ${action}`); - } - - if (this.serviceWorkerRegistration?.active) { - this.serviceWorkerRegistration.active.postMessage({ - type: 'DB_RESPONSE', - id, - result - }); - } - } catch (error: any) { - console.error('[Database] Error handling database request:', error); - - if (this.serviceWorkerRegistration?.active) { - this.serviceWorkerRegistration.active.postMessage({ - type: 'DB_ERROR', - id, - error: error.message || String(error) - }); - } - } - } + + // ✅ NETTOYAGE : handleDatabaseRequest() a été supprimé private async handleServiceWorkerMessage(message: any) { switch (message.type) { - case 'TO_DOWNLOAD': + case "TO_DOWNLOAD": await this.handleDownloadList(message.data); break; - case 'DIFFS_TO_CREATE': + case "DIFFS_TO_CREATE": await this.handleDiffsToCreate(message.data); break; default: - console.warn('Unknown message type received from service worker:', message); + 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`); + console.log( + `[Database] Creating ${diffs.length} diffs from Service Worker scan` + ); try { await this.saveDiffs(diffs); - console.log('[Database] Diffs created successfully'); + console.log("[Database] Diffs created successfully"); } catch (error) { - console.error('[Database] Error creating diffs:', error); + console.error("[Database] Error creating diffs:", error); } } @@ -264,15 +252,17 @@ export class Database { try { const valueBytes = await service.fetchValueFromStorage(hash); if (valueBytes) { - const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); + const blob = new Blob([valueBytes], { + type: "application/octet-stream", + }); await service.saveBlobToDb(hash, blob); document.dispatchEvent( - new CustomEvent('newDataReceived', { + new CustomEvent("newDataReceived", { detail: { processId, stateId, hash }, - }), + }) ); } else { - console.log('Request data from managers of the process'); + console.log("Request data from managers of the process"); if (!requestedStateId.includes(stateId)) { await service.requestDataFromPeers(processId, [stateId], [roles]); requestedStateId.push(stateId); @@ -289,35 +279,50 @@ export class Database { // ============================================ public async getStoreList(): Promise<{ [key: string]: string }> { - return this.sendMessageToWorker('GET_STORE_LIST', {}); + 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 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 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 }); + return this.sendMessageToWorker("GET_OBJECT", { storeName, key }); } public async dumpStore(storeName: string): Promise> { - return this.sendMessageToWorker('DUMP_STORE', { storeName }); + return this.sendMessageToWorker("DUMP_STORE", { storeName }); } public async deleteObject(storeName: string, key: string): Promise { - await this.sendMessageToWorker('DELETE_OBJECT', { storeName, key }); + await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key }); } public async clearStore(storeName: string): Promise { - await this.sendMessageToWorker('CLEAR_STORE', { storeName }); + 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 requestStoreByIndex( + storeName: string, + indexName: string, + request: string + ): Promise { + return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", { + storeName, + indexName, + request, + }); } public async clearMultipleStores(storeNames: string[]): Promise { @@ -332,24 +337,22 @@ export class Database { public async saveDevice(device: any): Promise { try { - const existing = await this.getObject('wallet', '1'); + const existing = await this.getObject("wallet", "1"); if (existing) { - await this.deleteObject('wallet', '1'); + await this.deleteObject("wallet", "1"); } } catch (e) {} await this.addObject({ - storeName: 'wallet', - object: { pre_id: '1', device }, + storeName: "wallet", + object: { pre_id: "1", device }, key: null, }); } public async getDevice(): Promise { - const result = await this.getObject('wallet', '1'); - console.log(result); - - return result ? result['device'] : null; + const result = await this.getObject("wallet", "1"); + return result ? result["device"] : null; } // ============================================ @@ -358,27 +361,32 @@ export class Database { public async saveProcess(processId: string, process: any): Promise { await this.addObject({ - storeName: 'processes', + storeName: "processes", object: process, key: processId, }); } - public async saveProcessesBatch(processes: Record): Promise { + 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 })), + storeName: "processes", + objects: Object.entries(processes).map(([key, value]) => ({ + key, + object: value, + })), }); } public async getProcess(processId: string): Promise { - return this.getObject('processes', processId); + return this.getObject("processes", processId); } public async getAllProcesses(): Promise> { - return this.dumpStore('processes'); + return this.dumpStore("processes"); } // ============================================ @@ -387,14 +395,14 @@ export class Database { public async saveBlob(hash: string, data: Blob): Promise { await this.addObject({ - storeName: 'data', + storeName: "data", object: data, key: hash, }); } public async getBlob(hash: string): Promise { - return this.getObject('data', hash); + return this.getObject("data", hash); } // ============================================ @@ -406,7 +414,7 @@ export class Database { for (const diff of diffs) { await this.addObject({ - storeName: 'diffs', + storeName: "diffs", object: diff, key: null, }); @@ -414,11 +422,11 @@ export class Database { } public async getDiff(hash: string): Promise { - return this.getObject('diffs', hash); + return this.getObject("diffs", hash); } public async getAllDiffs(): Promise> { - return this.dumpStore('diffs'); + return this.dumpStore("diffs"); } // ============================================ @@ -426,14 +434,17 @@ export class Database { // ============================================ public async getSharedSecret(address: string): Promise { - return this.getObject('shared_secrets', address); + return this.getObject("shared_secrets", address); } - public async saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise { + 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', + storeName: "unconfirmed_secrets", object: secret, key: null, }); @@ -443,7 +454,7 @@ export class Database { if (sharedSecrets && sharedSecrets.length > 0) { for (const { key, value } of sharedSecrets) { await this.addObject({ - storeName: 'shared_secrets', + storeName: "shared_secrets", object: value, key: key, }); @@ -451,10 +462,13 @@ export class Database { } } - public async getAllSecrets(): Promise<{ shared_secrets: Record; unconfirmed_secrets: any[] }> { - const sharedSecrets = await this.dumpStore('shared_secrets'); - const unconfirmedSecrets = await this.dumpStore('unconfirmed_secrets'); - + 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),