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 = new MessageChannel(); private messageChannelForGet: MessageChannel = new MessageChannel(); private storeDefinitions = { 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: [], }, }; // 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); indices.forEach(({ name, keyPath, options }) => { store.createIndex(name, keyPath, options); }); } }); }; request.onsuccess = () => { this.db = request.result; this.initServiceWorker(); 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; } private createMessageChannel(responseHandler: (event: MessageEvent) => void): MessageChannel { const messageChannel = new MessageChannel(); messageChannel.port1.onmessage = responseHandler; return messageChannel; } private async initServiceWorker() { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' }); console.log('Service Worker registered with scope:', registration.scope); this.serviceWorkerRegistration = registration; this.checkForUpdates(); // Set up the message channels this.messageChannel.port1.onmessage = this.handleAddObjectResponse; this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse; // Optionally, initialize service worker with some data } catch (error) { console.error('Service Worker registration failed:', error); } } } 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 handleAddObjectResponse = (event: MessageEvent) => { console.log('Received response from service worker (ADD_OBJECT):', event.data); }; 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((resolve, reject) => { // Check if the service worker is active if (!this.serviceWorkerRegistration?.active) { reject(new Error('Service worker is not active')); return; } // 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 { this.serviceWorkerRegistration.active.postMessage( { type: 'ADD_OBJECT', 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 { // Wait for both getAllKeys() and getAll() to resolve const [keys, values] = await Promise.all([ new Promise((resolve, reject) => { const request = store.getAllKeys(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }), new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }), ]); // Combine keys and values into an object const result: Record = Object.fromEntries(keys.map((key, index) => [key, values[index]])); return result; } catch (error) { console.error("Error fetching data from IndexedDB:", error); throw error; } } } export default Database;