Add a web worker for db operations
This commit is contained in:
parent
9c8c5fc24c
commit
66a27cb03c
381
src/workers/indexeddb.worker.ts
Normal file
381
src/workers/indexeddb.worker.ts
Normal 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);
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user