'use strict'; importScripts('/sha256.js'); let dir, handle, access, hash, pos = 0, claim, name; let chain = Promise.resolve(); self.onmessage = e => { chain = chain.then(() => run(e.data)); }; async function close() { if (access) { access.flush(); access.close(); access = null; } } async function run(m) { try { if (m.op === 'open') { claim = m.file; if (!/^[A-Za-z0-9_-]{1,128}$/.test(claim.id) || !/^[a-f0-9]{64}$/.test(claim.cid)) throw Error('Invalid file identity'); dir = await (await navigator.storage.getDirectory()).getDirectoryHandle('videos', { create: true }); name = `${claim.id}.${claim.cid}.direct.part`; handle = await dir.getFileHandle(name, { create: true }); access = await handle.createSyncAccessHandle(); pos = access.getSize(); if (pos > claim.size) { access.truncate(0); pos = 0; } hash = self.Sha256.create(); for (let at = 0; at < pos; at += 1024 * 1024) { const b = new Uint8Array(Math.min(1024 * 1024, pos - at)); access.read(b, { at }); hash.update(b); } self.postMessage({ op: 'opened', offset: pos }); } else if (m.op === 'chunk') { const b = new Uint8Array(m.buf); if (!access || b.length > 65536 || pos + b.length > claim.size) throw Error('Invalid file chunk'); if (access.write(b, { at: pos }) !== b.length) throw Error('Incomplete OPFS write'); hash.update(b); pos += b.length; access.flush(); self.postMessage({ op: 'progress', received: pos }); } else if (m.op === 'abort') { await close(); self.postMessage({ op: 'closed' }); } else if (m.op === 'finish') { await close(); if (pos !== claim.size || hash.hex() !== claim.cid) { await dir.removeEntry(name); throw Error('File verification failed; retry starts from zero'); } const extension = /^(mp4|m4a|webm|ogg|opus|mkv|mp3|wav|aac)$/.test(claim.extension) ? claim.extension : 'mp4'; const final = `${claim.id}.${extension}`; // Copy only after verification; existing saved playback remains intact until then. const out = await (await dir.getFileHandle(final, { create: true })).createSyncAccessHandle(); try { const f = await handle.getFile(); for (let at = 0; at < f.size; at += 1024 * 1024) out.write(new Uint8Array(await f.slice(at, at + 1024 * 1024).arrayBuffer()), { at }); out.truncate(f.size); out.flush(); } finally { out.close(); } await dir.removeEntry(name); for await (const [old, entry] of dir.entries()) { if (entry.kind === 'file' && old !== final && old.startsWith(claim.id + '.') && /\.(mp4|m4a|webm|ogg|opus|mkv|mp3|wav|aac)$/.test(old)) await dir.removeEntry(old); } self.postMessage({ op: 'done', file: claim }); } } catch (e) { await close(); self.postMessage({ op: 'error', error: e.message }); } }