// Main-thread wrapper around the decrypt Web Worker. // // Spawns a single worker lazily on first use. Uses request-id correlation so // concurrent batches (e.g. a backfill arriving while a realtime insert // dispatches) can't mix up their results. Falls back to inline decrypt if // the Worker constructor isn't available (non-browser environments, some // strict CSPs). import sodium from 'libsodium-wrappers-sumo'; export interface DecryptItem { id: string; ciphertext: Uint8Array; nonce: Uint8Array; key: Uint8Array; } interface Pending { resolve: (results: DecryptResult[]) => void; reject: (err: unknown) => void; } interface DecryptResult { id: string; plaintext: string | null; } let worker: Worker | null = null; let workerBroken = false; const pending = new Map(); function spawn(): Worker | null { if (workerBroken) return null; if (worker) return worker; try { worker = new Worker(new URL('../workers/decrypt.worker.ts', import.meta.url), { type: 'module', }); worker.addEventListener('message', (ev: MessageEvent) => { const data = ev.data as { id?: string; results?: DecryptResult[] }; if (!data.id) return; const entry = pending.get(data.id); if (!entry) return; pending.delete(data.id); entry.resolve(data.results ?? []); }); worker.addEventListener('error', (ev) => { console.warn('decrypt worker error, falling back to inline', ev.message); workerBroken = true; worker?.terminate(); worker = null; for (const entry of pending.values()) { entry.reject(new Error('decrypt worker crashed')); } pending.clear(); }); return worker; } catch (err: unknown) { console.warn('decrypt worker spawn failed, falling back to inline', err); workerBroken = true; return null; } } export async function decryptBatch(items: DecryptItem[]): Promise { if (items.length === 0) return []; const w = spawn(); if (!w) return inlineDecrypt(items); return new Promise((resolve, reject) => { const id = crypto.randomUUID(); pending.set(id, { resolve, reject }); try { w.postMessage({ id, items }); } catch (err: unknown) { pending.delete(id); // postMessage can fail if the Uint8Array view is detached — retry inline. console.warn('decrypt worker postMessage failed, using inline', err); inlineDecrypt(items).then(resolve, reject); } }); } // Fallback path — same semantics as the worker but on the main thread. Used // when the worker failed to spawn or crashed mid-session. async function inlineDecrypt(items: DecryptItem[]): Promise { await sodium.ready; return items.map((item) => { try { const plain = sodium.crypto_secretbox_open_easy(item.ciphertext, item.nonce, item.key); return { id: item.id, plaintext: new TextDecoder('utf-8', { fatal: false }).decode(plain), }; } catch { return { id: item.id, plaintext: null }; } }); }