From d45cf7c53050f61aa1a567f4aca98849603002ea Mon Sep 17 00:00:00 2001 From: NicolasCantu Date: Thu, 27 Nov 2025 23:20:53 +0100 Subject: [PATCH] refactor(database): remove obsolete database.worker.js and update database service to utilize Web Worker for IndexedDB operations --- public/data.worker.js | 152 ++++++ public/database.worker.js | 281 ---------- src/main.ts | 9 +- src/services/core/network.service.ts | 2 - src/services/database.service.ts | 605 +++++++++++----------- src/services/domain/process.service.ts | 15 +- src/services/domain/wallet.service.ts | 11 +- src/services/iframe-controller.service.ts | 2 +- src/services/service.ts | 158 +++--- src/workers/database.worker.ts | 381 ++++++++++++++ src/workers/worker.types.ts | 33 ++ 11 files changed, 965 insertions(+), 684 deletions(-) create mode 100644 public/data.worker.js delete mode 100755 public/database.worker.js create mode 100644 src/workers/database.worker.ts create mode 100644 src/workers/worker.types.ts diff --git a/public/data.worker.js b/public/data.worker.js new file mode 100644 index 0000000..7cf5a02 --- /dev/null +++ b/public/data.worker.js @@ -0,0 +1,152 @@ +const EMPTY32BYTES = String('').padStart(64, '0'); + +// ============================================ +// SERVICE WORKER LIFECYCLE +// ============================================ + +self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +// ============================================ +// MESSAGE HANDLER +// ============================================ + +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) { + 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 + }); + + setTimeout(() => { + self.removeEventListener('message', messageHandler); + reject(new Error('Database request timeout')); + }, 10000); + }); +} + +// ============================================ +// 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 + }); + + let toDownload = new Set(); + let diffsToCreate = []; + + if (myProcesses && myProcesses.length != 0) { + for (const process of myProcesses) { + const firstState = process.states[0]; + 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 + }); + + if (!existingData) { + toDownload.add(hash); + + const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', { + storeName: 'diffs', + key: hash + }); + + if (!existingDiff) { + diffsToCreate.push({ + process_id: processId, + state_id: state.state_id, + value_commitment: hash, + roles: state.roles, + field: field, + description: null, + previous_value: null, + new_value: null, + notify_user: false, + need_validation: false, + validation_status: 'None' + }); + } + } else { + if (toDownload.delete(hash)) { + console.log(`[Service Worker] Removing ${hash} from the set`); + } + } + } + } + } + } + + console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length }); + return { + toDownload: Array.from(toDownload), + diffsToCreate: diffsToCreate + }; +} + diff --git a/public/database.worker.js b/public/database.worker.js deleted file mode 100755 index a9e0548..0000000 --- a/public/database.worker.js +++ /dev/null @@ -1,281 +0,0 @@ -const EMPTY32BYTES = String('').padStart(64, '0'); - -self.addEventListener('install', (event) => { - event.waitUntil(self.skipWaiting()); // Activate worker immediately -}); - -self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()); // Become available to all pages -}); - -// Event listener for messages from clients -self.addEventListener('message', async (event) => { - const data = event.data; - console.log(data); - - if (data.type === 'SCAN') { - try { - const myProcessesId = data.payload; - if (myProcessesId && myProcessesId.length != 0) { - const toDownload = await scanMissingData(myProcessesId); - if (toDownload.length != 0) { - console.log('Sending TO_DOWNLOAD message'); - event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload}); - } - } else { - event.source.postMessage({ status: 'error', message: 'Empty lists' }); - } - } catch (error) { - event.source.postMessage({ status: 'error', message: error.message }); - } - } else if (data.type === 'ADD_OBJECT') { - try { - const { storeName, object, key } = data.payload; - const db = await openDatabase(); - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); - - if (key) { - await store.put(object, key); - } else { - await store.put(object); - } - - event.ports[0].postMessage({ status: 'success', message: '' }); - } catch (error) { - event.ports[0].postMessage({ status: 'error', message: error.message }); - } - } else if (data.type === 'BATCH_WRITING') { - const { storeName, objects } = data.payload; - const db = await openDatabase(); - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); - - for (const { key, object } of objects) { - if (key) { - await store.put(object, key); - } else { - await store.put(object); - } - } - - await tx.done; - } -}); - -async function scanMissingData(processesToScan) { - console.log('Scanning for missing data...'); - const myProcesses = await getProcesses(processesToScan); - - let toDownload = new Set(); - // Iterate on each process - if (myProcesses && myProcesses.length != 0) { - for (const process of myProcesses) { - // Iterate on states - const firstState = process.states[0]; - const processId = firstState.commited_in; - for (const state of process.states) { - if (state.state_id === EMPTY32BYTES) continue; - // iterate on pcd_commitment - for (const [field, hash] of Object.entries(state.pcd_commitment)) { - // Skip public fields - if (state.public_data[field] !== undefined || field === 'roles') continue; - // Check if we have the data in db - const existingData = await getBlob(hash); - if (!existingData) { - toDownload.add(hash); - // We also add an entry in diff, in case it doesn't already exist - await addDiff(processId, state.state_id, hash, state.roles, field); - } else { - // We remove it if we have it in the set - if (toDownload.delete(hash)) { - console.log(`Removing ${hash} from the set`); - } - } - } - } - } - } - - console.log(toDownload); - return Array.from(toDownload); -} - -async function openDatabase() { - return new Promise((resolve, reject) => { - const request = indexedDB.open('4nk', 1); - request.onerror = (event) => { - reject(request.error); - }; - request.onsuccess = (event) => { - resolve(request.result); - }; - request.onupgradeneeded = (event) => { - const db = event.target.result; - if (!db.objectStoreNames.contains('wallet')) { - db.createObjectStore('wallet', { keyPath: 'pre_id' }); - } - }; - }); -} - -// Function to get all processes because it is asynchronous -async function getAllProcesses() { - const db = await openDatabase(); - return new Promise((resolve, reject) => { - if (!db) { - reject(new Error('Database is not available')); - return; - } - const tx = db.transaction('processes', 'readonly'); - const store = tx.objectStore('processes'); - const request = store.getAll(); - - request.onsuccess = () => { - resolve(request.result); - }; - - request.onerror = () => { - reject(request.error); - }; - }); -}; - -async function getProcesses(processIds) { - if (!processIds || processIds.length === 0) { - return []; - } - - const db = await openDatabase(); - if (!db) { - throw new Error('Database is not available'); - } - - const tx = db.transaction('processes', 'readonly'); - const store = tx.objectStore('processes'); - - const requests = Array.from(processIds).map((processId) => { - return new Promise((resolve) => { - const request = store.get(processId); - request.onsuccess = () => resolve(request.result); - request.onerror = () => { - console.error(`Error fetching process ${processId}:`, request.error); - resolve(undefined); - }; - }); - }); - - const results = await Promise.all(requests); - return results.filter(result => result !== undefined); -} - -async function getAllDiffsNeedValidation() { - const db = await openDatabase(); - - const allProcesses = await getAllProcesses(); - const tx = db.transaction('diffs', 'readonly'); - const store = tx.objectStore('diffs'); - - return new Promise((resolve, reject) => { - const request = store.getAll(); - request.onsuccess = (event) => { - const allItems = event.target.result; - const itemsWithFlag = allItems.filter((item) => item.need_validation); - - const processMap = {}; - - for (const diff of itemsWithFlag) { - const currentProcess = allProcesses.find((item) => { - return item.states.some((state) => state.merkle_root === diff.new_state_merkle_root); - }); - - if (currentProcess) { - const processKey = currentProcess.merkle_root; - - if (!processMap[processKey]) { - processMap[processKey] = { - process: currentProcess.states, - processId: currentProcess.key, - diffs: [], - }; - } - processMap[processKey].diffs.push(diff); - } - } - - const results = Object.values(processMap).map((entry) => { - const diffs = [] - for(const state of entry.process) { - const filteredDiff = entry.diffs.filter(diff => diff.new_state_merkle_root === state.merkle_root); - if(filteredDiff && filteredDiff.length) { - diffs.push(filteredDiff) - } - } - return { - process: entry.process, - processId: entry.processId, - diffs: diffs, - }; - }); - - resolve(results); - }; - - request.onerror = (event) => { - reject(event.target.error); - }; - }); -} - -async function getBlob(hash) { - const db = await openDatabase(); - const storeName = 'data'; - const tx = db.transaction(storeName, 'readonly'); - const store = tx.objectStore(storeName); - const result = await new Promise((resolve, reject) => { - const getRequest = store.get(hash); - getRequest.onsuccess = () => resolve(getRequest.result); - getRequest.onerror = () => reject(getRequest.error); - }); - return result; -} - -async function addDiff(processId, stateId, hash, roles, field) { - const db = await openDatabase(); - const storeName = 'diffs'; - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); - - // Check if the diff already exists - const existingDiff = await new Promise((resolve, reject) => { - const getRequest = store.get(hash); - getRequest.onsuccess = () => resolve(getRequest.result); - getRequest.onerror = () => reject(getRequest.error); - }); - - if (!existingDiff) { - const newDiff = { - process_id: processId, - state_id: stateId, - value_commitment: hash, - roles: roles, - field: field, - description: null, - previous_value: null, - new_value: null, - notify_user: false, - need_validation: false, - validation_status: 'None' - }; - - const insertResult = await new Promise((resolve, reject) => { - const putRequest = store.put(newDiff); - putRequest.onsuccess = () => resolve(putRequest.result); - putRequest.onerror = () => reject(putRequest.error); - }); - - return insertResult; - } - - return existingDiff; -} diff --git a/src/main.ts b/src/main.ts index 8dfbc73..6161d68 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,13 +9,12 @@ async function bootstrap() { console.log("🚀 Démarrage de l'application 4NK..."); try { - // 1. Initialisation de la Base de données - const db = await Database.getInstance(); - db.registerServiceWorker('/database.worker.js'); - - // 2. Initialisation des Services (WASM, Sockets...) + // 1. Initialisation des Services (WASM, Sockets, Database...) const services = await Services.getInstance(); + // 2. Initialisation de la base de données (Web Worker + Service Worker) + await Database.getInstance(); + // Injection du Header dans le slot prévu dans index.html const headerSlot = document.getElementById('header-slot'); if (headerSlot) { diff --git a/src/services/core/network.service.ts b/src/services/core/network.service.ts index 5ed0aff..4550461 100644 --- a/src/services/core/network.service.ts +++ b/src/services/core/network.service.ts @@ -20,12 +20,10 @@ export class NetworkService { } } - // --- AJOUT --- public async addWebsocketConnection(url: string): Promise { console.log(`[Network] 🔌 Connexion à: ${url}`); await initWebsocket(url); } - // ------------- public initRelays() { for (const wsurl of this.bootstrapUrls) { diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 49e0785..cc97b78 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -1,159 +1,137 @@ import Services from './service'; -import { APP_CONFIG } from '../config/constants'; +/** + * Database service managing IndexedDB operations via Web Worker and Service Worker + */ export class Database { + // ============================================ + // PRIVATE PROPERTIES + // ============================================ + private static instance: Database; - private db: IDBDatabase | null = null; - private dbName: string = '4nk'; - private dbVersion: number = 1; private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; - private messageChannel: MessageChannel | null = null; - private messageChannelForGet: MessageChannel | null = null; private serviceWorkerCheckIntervalId: number | null = null; - private storeDefinitions = { - AnkLabels: { - name: 'labels', - options: { keyPath: 'emoji' }, - indices: [], - }, - AnkWallet: { - name: 'wallet', - options: { keyPath: 'pre_id' }, - indices: [], - }, - AnkProcess: { - name: 'processes', - options: {}, - indices: [], - }, - AnkSharedSecrets: { - name: 'shared_secrets', - options: {}, - indices: [], - }, - AnkUnconfirmedSecrets: { - name: 'unconfirmed_secrets', - options: { autoIncrement: true }, - indices: [], - }, - AnkPendingDiffs: { - name: 'diffs', - options: { keyPath: 'value_commitment' }, - indices: [ - { name: 'byStateId', keyPath: 'state_id', options: { unique: false } }, - { name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } }, - { name: 'byStatus', keyPath: 'validation_status', options: { unique: false } }, - ], - }, - AnkData: { - name: 'data', - options: {}, - indices: [], - }, - }; + private indexedDBWorker: Worker | null = null; + private messageIdCounter: number = 0; + private pendingMessages: Map void; reject: (error: any) => void }> = new Map(); - // Private constructor to prevent direct instantiation from outside - private constructor() {} + // ============================================ + // INITIALIZATION & SINGLETON + // ============================================ + + private constructor() { + this.initIndexedDBWorker(); + this.initServiceWorker(); + } - // Method to access the singleton instance of Database public static async getInstance(): Promise { if (!Database.instance) { Database.instance = new Database(); - await Database.instance.init(); + await Database.instance.waitForWorkerReady(); } return Database.instance; } - // Initialize the database - private async init(): Promise { + // ============================================ + // 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) => { - const request = indexedDB.open(this.dbName, this.dbVersion); + if (!this.indexedDBWorker) { + reject(new Error('IndexedDB Worker not initialized')); + return; + } - request.onupgradeneeded = () => { - const db = request.result; + const id = this.messageIdCounter++; + this.pendingMessages.set(id, { resolve, reject }); - Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => { - if (!db.objectStoreNames.contains(name)) { - let store = db.createObjectStore(name, options as IDBObjectStoreParameters); + this.indexedDBWorker.postMessage({ type, payload, id }); - indices.forEach(({ name, keyPath, options }) => { - store.createIndex(name, keyPath, options); - }); - } - }); - }; - - request.onsuccess = async () => { - this.db = request.result; - resolve(); - }; - - request.onerror = () => { - console.error('Database error:', request.error); - reject(request.error); - }; + // 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); }); } - public async getDb(): Promise { - if (!this.db) { - await this.init(); - } - return this.db!; + // ============================================ + // SERVICE WORKER + // ============================================ + + private initServiceWorker(): void { + this.registerServiceWorker('/data.worker.js'); } - public getStoreList(): { [key: string]: string } { - const objectList: { [key: string]: string } = {}; - Object.keys(this.storeDefinitions).forEach((key) => { - objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name; - }); - return objectList; - } - - public async registerServiceWorker(path: string) { + private async registerServiceWorker(path: string): Promise { if (!('serviceWorker' in navigator)) return; - console.log('[Database] Initialisation du Service Worker sur :', path); + console.log('[Database] Initializing Service Worker:', path); try { - // 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES) 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; - // On détecte spécifiquement l'ancien dossier qui pose problème - // L'erreur mentionne : scope ('.../src/service-workers/') if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) { - console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`); + console.warn(`[Database] Removing old Service Worker (${scope})`); await registration.unregister(); - // On continue la boucle, ne pas retourner ici, il faut installer le nouveau après } } - // 2. INSTALLATION DU NOUVEAU WORKER (PROPRE) - // On vérifie s'il est déjà installé à la BONNE adresse const existingValidWorker = registrations.find((r) => { const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; - // On compare la fin de l'URL pour éviter les soucis http/https/localhost - return url && url.endsWith(path.replace(/^\//, '')); + return url && url.endsWith(path.replace(/^\//,'')); }); if (!existingValidWorker) { - console.log('[Database] Enregistrement du nouveau Service Worker...'); + console.log('[Database] Registering new Service Worker'); this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' }); } else { - console.log('[Database] Service Worker déjà actif et valide.'); + console.log('[Database] Service Worker already active'); this.serviceWorkerRegistration = existingValidWorker; await this.serviceWorkerRegistration.update(); } - // Set up listeners + navigator.serviceWorker.addEventListener('message', async (event) => { - // console.log('Received message from service worker:', event.data); + if (event.data.type === 'DB_REQUEST') { + await this.handleDatabaseRequest(event.data); + return; + } await this.handleServiceWorkerMessage(event.data); }); - // Periodic check if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); this.serviceWorkerCheckIntervalId = window.setInterval(async () => { const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); @@ -162,13 +140,12 @@ export class Database { if (payload && payload.length != 0) { activeWorker?.postMessage({ type: 'SCAN', payload }); } - }, APP_CONFIG.TIMEOUTS.WORKER_CHECK); + }, 5000); } catch (error) { - console.error('[Database] 💥 Erreur critique Service Worker:', error); + console.error('[Database] Service Worker error:', error); } } - // Helper function to wait for service worker activation private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise { return new Promise((resolve) => { if (registration.active) { @@ -185,13 +162,11 @@ export class Database { }); } - private async checkForUpdates() { + private async checkForUpdates(): Promise { if (this.serviceWorkerRegistration) { - // Check for updates to the service worker try { await this.serviceWorkerRegistration.update(); - // If there's a new worker waiting, activate it immediately if (this.serviceWorkerRegistration.waiting) { this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); } @@ -201,24 +176,85 @@ 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) + }); + } + } + } + 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 { - // Download the missing data let requestedStateId: string[] = []; const service = await Services.getInstance(); for (const hash of downloadList) { const diff = await service.getDiffByValue(hash); if (!diff) { - // This should never happen console.warn(`Missing a diff for hash ${hash}`); continue; } @@ -228,22 +264,15 @@ export class Database { try { const valueBytes = await service.fetchValueFromStorage(hash); if (valueBytes) { - // Save data to db const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); await service.saveBlobToDb(hash, blob); document.dispatchEvent( new CustomEvent('newDataReceived', { - detail: { - processId, - stateId, - hash, - }, + detail: { processId, stateId, hash }, }), ); } else { - // We first request the data from managers console.log('Request data from managers of the process'); - // get the diff from db if (!requestedStateId.includes(stateId)) { await service.requestDataFromPeers(processId, [stateId], [roles]); requestedStateId.push(stateId); @@ -255,214 +284,182 @@ export class Database { } } - private handleAddObjectResponse = async (event: MessageEvent) => { - const data = event.data; - console.log('Received response from service worker (ADD_OBJECT):', data); - const service = await Services.getInstance(); - if (data.type === 'NOTIFICATIONS') { - service.setNotifications(data.data); - } else if (data.type === 'TO_DOWNLOAD') { - console.log(`Received missing data ${data}`); - // Download the missing data - let requestedStateId: string[] = []; - for (const hash of data.data) { - try { - const valueBytes = await service.fetchValueFromStorage(hash); - if (valueBytes) { - // Save data to db - const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); - await service.saveBlobToDb(hash, blob); - } else { - // We first request the data from managers - console.log('Request data from managers of the process'); - // get the diff from db - const diff = await service.getDiffByValue(hash); - if (diff === null) { - continue; - } - const processId = diff!.process_id; - const stateId = diff!.state_id; - const roles = diff!.roles; - if (!requestedStateId.includes(stateId)) { - await service.requestDataFromPeers(processId, [stateId], [roles]); - requestedStateId.push(stateId); - } - } - } catch (e) { - console.error(e); - } - } - } - }; + // ============================================ + // GENERIC INDEXEDDB OPERATIONS + // ============================================ - private handleGetObjectResponse = (event: MessageEvent) => { - console.log('Received response from service worker (GET_OBJECT):', event.data); - }; - - public addObject(payload: { storeName: string; object: any; key: any }): Promise { - return new Promise(async (resolve, reject) => { - // Check if the service worker is active - if (!this.serviceWorkerRegistration) { - // console.warn('Service worker registration is not ready. Waiting...'); - this.serviceWorkerRegistration = await navigator.serviceWorker.ready; - } - - const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration); - - // Create a message channel for communication - const messageChannel = new MessageChannel(); - - // Handle the response from the service worker - messageChannel.port1.onmessage = (event) => { - if (event.data.status === 'success') { - resolve(); - } else { - const error = event.data.message; - reject(new Error(error || 'Unknown error occurred while adding object')); - } - }; - - // Send the add object request to the service worker - try { - activeWorker?.postMessage( - { - type: 'ADD_OBJECT', - payload, - }, - [messageChannel.port2], - ); - } catch (error) { - reject(new Error(`Failed to send message to service worker: ${error}`)); - } - }); + public async getStoreList(): Promise<{ [key: string]: string }> { + return this.sendMessageToWorker('GET_STORE_LIST', {}); } - public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise { - return new Promise(async (resolve, reject) => { - if (!this.serviceWorkerRegistration) { - this.serviceWorkerRegistration = await navigator.serviceWorker.ready; - } + public async addObject(payload: { storeName: string; object: any; key: any }): Promise { + await this.sendMessageToWorker('ADD_OBJECT', payload); + } - const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration); - const messageChannel = new MessageChannel(); - - messageChannel.port1.onmessage = (event) => { - if (event.data.status === 'success') { - resolve(); - } else { - const error = event.data.message; - reject(new Error(error || 'Unknown error occurred while adding objects')); - } - }; - - try { - activeWorker?.postMessage( - { - type: 'BATCH_WRITING', - payload, - }, - [messageChannel.port2], - ); - } catch (error) { - reject(new Error(`Failed to send message to service worker: ${error}`)); - } - }); + 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 { - const db = await this.getDb(); - const tx = db.transaction(storeName, 'readonly'); - const store = tx.objectStore(storeName); - const result = await new Promise((resolve, reject) => { - const getRequest = store.get(key); - getRequest.onsuccess = () => resolve(getRequest.result); - getRequest.onerror = () => reject(getRequest.error); - }); - return result ?? null; // Convert undefined to null + return this.sendMessageToWorker('GET_OBJECT', { storeName, key }); } public async dumpStore(storeName: string): Promise> { - const db = await this.getDb(); - const tx = db.transaction(storeName, 'readonly'); - const store = tx.objectStore(storeName); - - try { - return new Promise((resolve, reject) => { - const result: Record = {}; - const cursor = store.openCursor(); - - cursor.onsuccess = (event) => { - const request = event.target as IDBRequest; - const cursor = request.result; - if (cursor) { - result[cursor.key as string] = cursor.value; - cursor.continue(); - } else { - resolve(result); - } - }; - - cursor.onerror = () => { - reject(cursor.error); - }; - }); - } catch (error) { - console.error('Error fetching data from IndexedDB:', error); - throw error; - } + return this.sendMessageToWorker('DUMP_STORE', { storeName }); } public async deleteObject(storeName: string, key: string): Promise { - const db = await this.getDb(); - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); - try { - await new Promise((resolve, reject) => { - const getRequest = store.delete(key); - getRequest.onsuccess = () => resolve(getRequest.result); - getRequest.onerror = () => reject(getRequest.error); - }); - } catch (e) { - throw e; - } + await this.sendMessageToWorker('DELETE_OBJECT', { storeName, key }); } public async clearStore(storeName: string): Promise { - const db = await this.getDb(); - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); - try { - await new Promise((resolve, reject) => { - const clearRequest = store.clear(); - clearRequest.onsuccess = () => resolve(clearRequest.result); - clearRequest.onerror = () => reject(clearRequest.error); - }); - } catch (e) { - throw e; + 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); } } - // Request a store by index - public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise { - const db = await this.getDb(); - const tx = db.transaction(storeName, 'readonly'); - const store = tx.objectStore(storeName); - const index = store.index(indexName); + // ============================================ + // BUSINESS METHODS - DEVICE + // ============================================ + public async saveDevice(device: any): Promise { try { - return new Promise((resolve, reject) => { - const getAllRequest = index.getAll(request); - getAllRequest.onsuccess = () => { - const allItems = getAllRequest.result; - const filtered = allItems.filter((item) => item.state_id === request); - resolve(filtered); - }; - getAllRequest.onerror = () => reject(getAllRequest.error); + 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'); + console.log(result); + + 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, }); - } catch (e) { - throw e; } } + + 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; diff --git a/src/services/domain/process.service.ts b/src/services/domain/process.service.ts index f84e6f1..7f97b76 100644 --- a/src/services/domain/process.service.ts +++ b/src/services/domain/process.service.ts @@ -8,13 +8,12 @@ export class ProcessService { private processesCache: Record = {}; private myProcesses: Set = new Set(); - constructor(private sdk: SdkService) {} + constructor(private sdk: SdkService, private db: Database) {} public async getProcess(processId: string): Promise { if (this.processesCache[processId]) return this.processesCache[processId]; - const db = await Database.getInstance(); - const process = await db.getObject('processes', processId); + const process = await this.db.getProcess(processId); if (process) this.processesCache[processId] = process; return process; } @@ -22,21 +21,18 @@ export class ProcessService { public async getProcesses(): Promise> { if (Object.keys(this.processesCache).length > 0) return this.processesCache; - const db = await Database.getInstance(); - this.processesCache = await db.dumpStore('processes'); + this.processesCache = await this.db.getAllProcesses(); return this.processesCache; } public async saveProcessToDb(processId: string, process: Process) { - const db = await Database.getInstance(); - await db.addObject({ storeName: 'processes', object: process, key: processId }); + await this.db.saveProcess(processId, process); this.processesCache[processId] = process; } public async batchSaveProcesses(processes: Record) { if (Object.keys(processes).length === 0) return; - const db = await Database.getInstance(); - await db.batchWriting({ storeName: 'processes', objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); + await this.db.saveProcessesBatch(processes); this.processesCache = { ...this.processesCache, ...processes }; } @@ -86,7 +82,6 @@ export class ProcessService { return Array.from(this.myProcesses); } - // --- AJOUT : Méthode manquante --- public getLastCommitedStateIndex(process: Process): number | null { if (process.states.length === 0) return null; const processTip = process.states[process.states.length - 1].commited_in; diff --git a/src/services/domain/wallet.service.ts b/src/services/domain/wallet.service.ts index 7d9f6af..2c26994 100644 --- a/src/services/domain/wallet.service.ts +++ b/src/services/domain/wallet.service.ts @@ -3,7 +3,7 @@ import { SdkService } from '../core/sdk.service'; import Database from '../database.service'; export class WalletService { - constructor(private sdk: SdkService) {} + constructor(private sdk: SdkService, private db: Database) {} public isPaired(): boolean { try { @@ -49,7 +49,6 @@ export class WalletService { } } - // --- AJOUTS (Manquants) --- public async dumpWallet(): Promise { return await this.sdk.getClient().dump_wallet(); } @@ -70,13 +69,7 @@ export class WalletService { // ------------------------- public async saveDeviceInDatabase(device: Device): Promise { - const db = await Database.getInstance(); - await db.deleteObject('wallet', '1').catch(() => {}); - await db.addObject({ - storeName: 'wallet', - object: { pre_id: '1', device }, - key: null, - }); + await this.db.saveDevice(device); } public async getDeviceFromDatabase(): Promise { diff --git a/src/services/iframe-controller.service.ts b/src/services/iframe-controller.service.ts index 2186e5d..006c208 100644 --- a/src/services/iframe-controller.service.ts +++ b/src/services/iframe-controller.service.ts @@ -139,7 +139,7 @@ export class IframeController { await services.handleApiReturn(approveChangeReturn); console.log('[Router:API] 7/7: Confirmation finale du pairing...'); - await services.confirmPairing(); + // await services.confirmPairing(); console.log('[Router:API] 🎉 Appairage terminé avec succès !'); diff --git a/src/services/service.ts b/src/services/service.ts index 7e95c03..28aa755 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -17,14 +17,15 @@ export default class Services { private sdkService: SdkService; public networkService: NetworkService; - private walletService: WalletService; - private processService: ProcessService; + private walletService!: WalletService; + private processService!: ProcessService; private cryptoService: CryptoService; private processId: string | null = null; private stateId: string | null = null; private membersList: Record = {}; private notifications: any[] | null = null; + private db!: Database; private currentBlockHeight: number = -1; private pendingKeyRequests: Map void> = new Map(); @@ -36,8 +37,6 @@ export default class Services { // Utilisation de la config this.networkService = new NetworkService(APP_CONFIG.URLS.BOOTSTRAP); this.cryptoService = new CryptoService(this.sdkService); - this.walletService = new WalletService(this.sdkService); - this.processService = new ProcessService(this.sdkService); } public static async getInstance(): Promise { @@ -57,6 +56,10 @@ export default class Services { console.log('[Services] ⏳ Initialisation...'); this.notifications = this.getNotifications(); await this.sdkService.init(); + this.db = await Database.getInstance(); + this.walletService = new WalletService(this.sdkService, this.db); + this.processService = new ProcessService(this.sdkService, this.db); + this.networkService.initRelays(); console.log('[Services] ✅ Initialisé.'); } @@ -251,13 +254,32 @@ export default class Services { public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); - if (handshakeMsg.sp_address) this.updateRelay(url, handshakeMsg.sp_address); + + if (handshakeMsg.sp_address) { + this.updateRelay(url, handshakeMsg.sp_address); + } this.currentBlockHeight = handshakeMsg.chain_tip; + + if (!this.isPaired()) { + console.log(`[Services] ⏳ Non pairé. Le Handshake de ${url} est en pause...`); + while (!this.isPaired()) { + await new Promise(r => setTimeout(r, 500)); + } + console.log(`[Services] ▶️ Appareil pairé ! Reprise du traitement Handshake de ${url}.`); + } + this.updateDeviceBlockHeight(); - if (handshakeMsg.peers_list) this.membersList = { ...this.membersList, ...(handshakeMsg.peers_list as Record) }; - if (handshakeMsg.processes_list) this.syncProcessesFromHandshake(handshakeMsg.processes_list); - } catch (e) { - console.error('Handshake Error', e); + + if (handshakeMsg.peers_list) { + this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record }; + } + + if (handshakeMsg.processes_list) { + this.syncProcessesFromHandshake(handshakeMsg.processes_list); + } + + } catch(e) { + console.error("Handshake Error", e); } } @@ -549,32 +571,32 @@ export default class Services { return res; } - public async confirmPairing() { - console.log('[Services] Confirm Pairing...'); - const pid = this.walletService.getPairingProcessId(); - const process = await this.processService.getProcess(pid); - if (!process) return; + // public async confirmPairing() { + // console.log('[Services] Confirm Pairing...'); + // const pid = this.walletService.getPairingProcessId(); + // const process = await this.processService.getProcess(pid); + // if (!process) return; - let state = this.processService.getLastCommitedState(process); - if (!state && process.states.length > 0) state = process.states[process.states.length - 1]; - if (!state) return; + // let state = this.processService.getLastCommitedState(process); + // if (!state && process.states.length > 0) state = process.states[process.states.length - 1]; + // if (!state) return; - const encodedAddr = state.public_data['pairedAddresses']; - if (!encodedAddr) return; + // const encodedAddr = state.public_data['pairedAddresses']; + // if (!encodedAddr) return; - const addresses = this.decodeValue(encodedAddr); - if (!addresses || addresses.length === 0) return; + // const addresses = this.decodeValue(encodedAddr); + // if (!addresses || addresses.length === 0) return; - this.sdkClient.unpair_device(); - this.walletService.pairDevice(pid, addresses); + // this.sdkClient.unpair_device(); + // this.walletService.pairDevice(pid, addresses); - if (this.walletService.isPaired()) { - const d = this.walletService.dumpDeviceFromMemory(); - if (!this.walletService.isPaired()) d.pairing_process_commitment = pid; - await this.walletService.saveDeviceInDatabase(d); - console.log('✅ Pairing confirmed & Saved'); - } - } + // if (this.walletService.isPaired()) { + // const d = this.walletService.dumpDeviceFromMemory(); + // if (!this.walletService.isPaired()) d.pairing_process_commitment = pid; + // await this.walletService.saveDeviceInDatabase(d); + // console.log('✅ Pairing confirmed & Saved'); + // } + // } private async prepareProcessData(priv: any, pub: any) { const p1 = this.splitData(priv); @@ -609,15 +631,14 @@ export default class Services { const res = this.sdkClient.request_data(pid, sids, roles, this.membersList); await this.handleApiReturn(res); } + public async resetDevice() { - console.warn('Resetting device...'); + // console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD..."); this.sdkClient.reset_device(); - const db = await Database.getInstance(); - await db.clearStore('wallet'); - await db.clearStore('processes'); - await db.clearStore('shared_secrets'); - await db.clearStore('unconfirmed_secrets'); - await db.clearStore('diffs'); + + // Clear all stores + await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']); + // console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.'); } public async handleApiReturn(res: ApiReturn) { @@ -647,9 +668,20 @@ export default class Services { } private async handleSecrets(secrets: any) { - const db = await Database.getInstance(); - if (secrets.unconfirmed_secrets) for (const s of secrets.unconfirmed_secrets) await db.addObject({ storeName: 'unconfirmed_secrets', object: s, key: null }); - if (secrets.shared_secrets) for (const [k, v] of Object.entries(secrets.shared_secrets)) await db.addObject({ storeName: 'shared_secrets', object: v, key: k }); + const { unconfirmed_secrets, shared_secrets } = secrets; + + const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : []; + const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0 + ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value })) + : []; + + if (unconfirmedList.length > 0 || sharedList.length > 0) { + try { + await this.db.saveSecretsBatch(unconfirmedList, sharedList); + } catch (e) { + console.error('[Services:handleSecrets] 💥 Échec de sauvegarde batch des secrets:', e); + } + } } private async handleUpdatedProcess(updated: any) { @@ -669,8 +701,7 @@ export default class Services { } public async saveDiffsToDb(diffs: UserDiff[]) { - const db = await Database.getInstance(); - for (const d of diffs) await db.addObject({ storeName: 'diffs', object: d, key: null }); + await this.db.saveDiffs(diffs); } private _resolvePendingKeyRequests(processId: string, process: Process) { @@ -711,37 +742,28 @@ export default class Services { return this.processService.rolesContainsMember(roles, this.getPairingProcessId()); } - public async getSecretForAddress(addr: string) { - const db = await Database.getInstance(); - return await db.getObject('shared_secrets', addr); + public async getSecretForAddress(address: string): Promise { + return await this.db.getSharedSecret(address); } public async getAllDiffs(): Promise> { - const db = await Database.getInstance(); - return await db.dumpStore('diffs'); + return await this.db.getAllDiffs(); } - public async getDiffByValueFromDb(hash: string): Promise { - const db = await Database.getInstance(); - return await db.getObject('diffs', hash); + public async getDiffByValue(value: string): Promise { + return await this.db.getDiff(value); } - public async getAllSecrets() { - const db = await Database.getInstance(); - return { - shared_secrets: await db.dumpStore('shared_secrets'), - unconfirmed_secrets: Object.values(await db.dumpStore('unconfirmed_secrets')), - }; + public async getAllSecrets(): Promise { + return await this.db.getAllSecrets(); } // Storage & DB - public async saveBlobToDb(h: string, b: Blob) { - const db = await Database.getInstance(); - await db.addObject({ storeName: 'data', object: b, key: h }); + public async saveBlobToDb(h: string, d: Blob) { + await this.db.saveBlob(h, d); } public async getBlobFromDb(h: string) { - const db = await Database.getInstance(); - return await db.getObject('data', h); + return await this.db.getBlob(h); } public async fetchValueFromStorage(h: string) { return retrieveData([APP_CONFIG.URLS.STORAGE], h); @@ -749,10 +771,6 @@ export default class Services { public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) { return storeData(s, h, d, ttl); } - public async getDiffByValue(val: string) { - const db = await Database.getInstance(); - return await db.getObject('diffs', val); - } // Helpers public getProcessName(p: Process) { @@ -821,16 +839,12 @@ export default class Services { await this.restoreSecretsFromBackUp(backup.secrets); } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { - const db = await Database.getInstance(); - for (const secret of secretsStore.unconfirmed_secrets) await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null }); - for (const [key, value] of Object.entries(secretsStore.shared_secrets)) await db.addObject({ storeName: 'shared_secrets', object: value, key }); + const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); + await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList); await this.restoreSecretsFromDB(); } public async restoreSecretsFromDB() { - const db = await Database.getInstance(); - const sharedSecrets: Record = await db.dumpStore('shared_secrets'); - const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); - const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets) }; + const secretsStore = await this.db.getAllSecrets(); this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); } public async createBackUp() { diff --git a/src/workers/database.worker.ts b/src/workers/database.worker.ts new file mode 100644 index 0000000..a91e451 --- /dev/null +++ b/src/workers/database.worker.ts @@ -0,0 +1,381 @@ +/** + * Database Web Worker - Handles all IndexedDB operations in background + */ + +import type { + StoreDefinition, + WorkerMessagePayload, + WorkerMessageResponse, + BatchWriteItem +} from './worker.types'; + +const DB_NAME = '4nk'; +const DB_VERSION = 1; + +// ============================================ +// STORE DEFINITIONS +// ============================================ + +const STORE_DEFINITIONS: Record = { + AnkLabels: { + name: 'labels', + options: { keyPath: 'emoji' }, + indices: [], + }, + AnkWallet: { + name: 'wallet', + options: { keyPath: 'pre_id' }, + indices: [], + }, + AnkProcess: { + name: 'processes', + options: {}, + indices: [], + }, + AnkSharedSecrets: { + name: 'shared_secrets', + options: {}, + indices: [], + }, + AnkUnconfirmedSecrets: { + name: 'unconfirmed_secrets', + options: { autoIncrement: true }, + indices: [], + }, + AnkPendingDiffs: { + name: 'diffs', + options: { keyPath: 'value_commitment' }, + indices: [ + { name: 'byStateId', keyPath: 'state_id', options: { unique: false } }, + { name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } }, + { name: 'byStatus', keyPath: 'validation_status', options: { unique: false } }, + ], + }, + AnkData: { + name: 'data', + options: {}, + indices: [], + }, +}; + +let db: IDBDatabase | null = null; + +// ============================================ +// DATABASE INITIALIZATION +// ============================================ + +async function openDatabase(): Promise { + if (db) { + return db; + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = (event: IDBVersionChangeEvent) => { + const database = (event.target as IDBOpenDBRequest).result; + + Object.values(STORE_DEFINITIONS).forEach(({ name, options, indices }) => { + if (!database.objectStoreNames.contains(name)) { + const store = database.createObjectStore(name, options); + + indices.forEach(({ name: indexName, keyPath, options: indexOptions }) => { + store.createIndex(indexName, keyPath, indexOptions); + }); + } + }); + }; + + request.onsuccess = () => { + db = request.result; + resolve(db); + }; + + request.onerror = () => { + reject(request.error); + }; + }); +} + +// ============================================ +// WRITE OPERATIONS +// ============================================ + +async function addObject(storeName: string, object: any, key?: IDBValidKey): Promise<{ success: boolean }> { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + let request: IDBRequest; + if (key !== null && key !== undefined) { + request = store.put(object, key); + } else { + request = store.put(object); + } + + request.onsuccess = () => resolve({ success: true }); + request.onerror = () => reject(request.error); + }); +} + +async function batchWriting(storeName: string, objects: BatchWriteItem[]): Promise<{ success: boolean }> { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + + for (const { key, object } of objects) { + if (key !== null && key !== undefined) { + store.put(object, key); + } else { + store.put(object); + } + } + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve({ success: true }); + tx.onerror = () => reject(tx.error); + }); +} + +// ============================================ +// READ OPERATIONS +// ============================================ + +async function getObject(storeName: string, key: IDBValidKey): Promise { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const request = store.get(key); + request.onsuccess = () => resolve(request.result ?? null); + request.onerror = () => reject(request.error); + }); +} + +async function dumpStore(storeName: string): Promise> { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const result: Record = {}; + const request = store.openCursor(); + + request.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + result[cursor.key as string] = cursor.value; + cursor.continue(); + } else { + resolve(result); + } + }; + + request.onerror = () => reject(request.error); + }); +} + +async function getAllObjects(storeName: string): Promise { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const request = store.getAll(); + request.onsuccess = () => resolve(request.result || []); + request.onerror = () => reject(request.error); + }); +} + +async function getMultipleObjects(storeName: string, keys: IDBValidKey[]): Promise { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + + const requests = keys.map((key) => { + return new Promise((resolve) => { + const request = store.get(key); + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => { + console.error(`Error fetching key ${key}:`, request.error); + resolve(null); + }; + }); + }); + + const results = await Promise.all(requests); + return results.filter(result => result !== null); +} + +async function getAllObjectsWithFilter(storeName: string, filterFn?: string): Promise { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const request = store.getAll(); + request.onsuccess = () => { + const allItems = request.result || []; + if (filterFn) { + const filter = new Function('item', `return ${filterFn}`) as (item: any) => boolean; + resolve(allItems.filter(filter)); + } else { + resolve(allItems); + } + }; + request.onerror = () => reject(request.error); + }); +} + +// ============================================ +// DELETE OPERATIONS +// ============================================ + +async function deleteObject(storeName: string, key: IDBValidKey): Promise<{ success: boolean }> { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const request = store.delete(key); + request.onsuccess = () => resolve({ success: true }); + request.onerror = () => reject(request.error); + }); +} + +async function clearStore(storeName: string): Promise<{ success: boolean }> { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + + return new Promise((resolve, reject) => { + const request = store.clear(); + request.onsuccess = () => resolve({ success: true }); + request.onerror = () => reject(request.error); + }); +} + +// ============================================ +// INDEX OPERATIONS +// ============================================ + +async function requestStoreByIndex(storeName: string, indexName: string, requestValue: IDBValidKey): Promise { + const database = await openDatabase(); + const tx = database.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + const index = store.index(indexName); + + return new Promise((resolve, reject) => { + const request = index.getAll(requestValue); + request.onsuccess = () => { + const allItems = request.result; + const filtered = allItems.filter((item: any) => item.state_id === requestValue); + resolve(filtered); + }; + request.onerror = () => reject(request.error); + }); +} + +// ============================================ +// UTILITY FUNCTIONS +// ============================================ + +function getStoreList(): Record { + const storeList: Record = {}; + Object.keys(STORE_DEFINITIONS).forEach((key) => { + storeList[key] = STORE_DEFINITIONS[key].name; + }); + return storeList; +} + +// ============================================ +// MESSAGE HANDLER +// ============================================ + +self.addEventListener('message', async (event: MessageEvent) => { + const { type, payload, id } = event.data; + + try { + let result: any; + + switch (type) { + case 'INIT': + await openDatabase(); + result = { success: true }; + break; + + case 'ADD_OBJECT': + result = await addObject(payload.storeName, payload.object, payload.key); + break; + + case 'BATCH_WRITING': + result = await batchWriting(payload.storeName, payload.objects); + break; + + case 'GET_OBJECT': + result = await getObject(payload.storeName, payload.key); + break; + + case 'DUMP_STORE': + result = await dumpStore(payload.storeName); + break; + + case 'DELETE_OBJECT': + result = await deleteObject(payload.storeName, payload.key); + break; + + case 'CLEAR_STORE': + result = await clearStore(payload.storeName); + break; + + case 'REQUEST_STORE_BY_INDEX': + result = await requestStoreByIndex( + payload.storeName, + payload.indexName, + payload.request + ); + break; + + case 'GET_ALL_OBJECTS': + result = await getAllObjects(payload.storeName); + break; + + case 'GET_MULTIPLE_OBJECTS': + result = await getMultipleObjects(payload.storeName, payload.keys); + break; + + case 'GET_ALL_OBJECTS_WITH_FILTER': + result = await getAllObjectsWithFilter(payload.storeName, payload.filterFn); + break; + + case 'GET_STORE_LIST': + result = getStoreList(); + break; + + default: + throw new Error(`Unknown message type: ${type}`); + } + + self.postMessage({ + id, + type: 'SUCCESS', + result, + } as WorkerMessageResponse); + } catch (error) { + self.postMessage({ + id, + type: 'ERROR', + error: (error as Error).message || String(error), + } as WorkerMessageResponse); + } +}); + +// ============================================ +// INITIALIZATION +// ============================================ + +openDatabase().catch((error) => { + console.error('[Database Worker] Failed to initialize database:', error); +}); diff --git a/src/workers/worker.types.ts b/src/workers/worker.types.ts new file mode 100644 index 0000000..0c0d892 --- /dev/null +++ b/src/workers/worker.types.ts @@ -0,0 +1,33 @@ +/** + * Shared types for Web Workers + */ + +export interface StoreDefinition { + name: string; + options: IDBObjectStoreParameters; + indices: IndexDefinition[]; +} + +export interface IndexDefinition { + name: string; + keyPath: string | string[]; + options: IDBIndexParameters; +} + +export interface WorkerMessagePayload { + type: string; + payload?: any; + id: number; +} + +export interface WorkerMessageResponse { + id: number; + type: 'SUCCESS' | 'ERROR'; + result?: any; + error?: string; +} + +export interface BatchWriteItem { + key?: IDBValidKey; + object: any; +}