import Services from './service'; export class Database { 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 constructor to prevent direct instantiation from outside private constructor() {} // Method to access the singleton instance of Database public static async getInstance(): Promise { if (!Database.instance) { Database.instance = new Database(); await Database.instance.init(); } return Database.instance; } // Initialize the database private async init(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onupgradeneeded = () => { const db = request.result; Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => { if (!db.objectStoreNames.contains(name)) { let store = db.createObjectStore(name, options as IDBObjectStoreParameters); 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); }; }); } public async getDb(): Promise { if (!this.db) { await this.init(); } return this.db!; } 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) { if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported console.log('registering worker at', path); try { // Get existing service worker registrations const registrations = await navigator.serviceWorker.getRegistrations(); if (registrations.length === 0) { // No existing workers: register a new one. this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' }); console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope); } else if (registrations.length === 1) { // One existing worker: update it (restart it) without unregistering. this.serviceWorkerRegistration = registrations[0]; await this.serviceWorkerRegistration.update(); console.log('Service Worker updated'); } else { // More than one existing worker: unregister them all and register a new one. console.log('Multiple Service Worker(s) detected. Unregistering all...'); await Promise.all(registrations.map(reg => reg.unregister())); console.log('All previous Service Workers unregistered.'); this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' }); console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope); } await this.checkForUpdates(); // Set up a global message listener for responses from the service worker. navigator.serviceWorker.addEventListener('message', async (event) => { console.log('Received message from service worker:', event.data); await this.handleServiceWorkerMessage(event.data); }); // Set up a periodic check to ensure the service worker is active and to send a SCAN message. 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('Service Worker registration failed:', error); } } // Helper function to wait for service worker activation 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() { 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' }); } } catch (error) { console.error('Error checking for service worker updates:', error); } } } private async handleServiceWorkerMessage(message: any) { switch (message.type) { case 'TO_DOWNLOAD': await this.handleDownloadList(message.data); break; default: console.warn('Unknown message type received from service worker:', message); } } 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; } const processId = diff.process_id; const stateId = diff.state_id; const roles = diff.roles; 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, } })); } 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); } } } catch (e) { console.error(e); } } } 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); } } } }; 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 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; } 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 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; } 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; } } 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; } } 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; } } // 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); 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); }); } catch (e) { throw e; } } } export default Database;