From 66a27cb03c78e83723d51ba51a8a307d43a391b9 Mon Sep 17 00:00:00 2001 From: omaroughriss Date: Thu, 27 Nov 2025 16:51:43 +0100 Subject: [PATCH] Add a web worker for db operations --- src/workers/indexeddb.worker.ts | 381 ++++++++++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 src/workers/indexeddb.worker.ts diff --git a/src/workers/indexeddb.worker.ts b/src/workers/indexeddb.worker.ts new file mode 100644 index 0000000..1dc40a0 --- /dev/null +++ b/src/workers/indexeddb.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); +});