Add a web worker for db operations

This commit is contained in:
omaroughriss 2025-11-27 16:51:43 +01:00
parent 9c8c5fc24c
commit 66a27cb03c

View File

@ -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<string, StoreDefinition> = {
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<IDBDatabase> {
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<any> {
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<Record<string, any>> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const result: Record<string, any> = {};
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).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<any[]> {
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<any[]> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const requests = keys.map((key) => {
return new Promise<any>((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<any[]> {
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<any[]> {
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<string, string> {
const storeList: Record<string, string> = {};
Object.keys(STORE_DEFINITIONS).forEach((key) => {
storeList[key] = STORE_DEFINITIONS[key].name;
});
return storeList;
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener('message', async (event: MessageEvent<WorkerMessagePayload>) => {
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);
});