import { convertToEpub, type ConvertInput } from './convertToEpub'; import type { ConvertedBook } from './types'; import type { ConversionWorkerRequest, ConversionWorkerResponse, } from './conversion-worker-protocol'; const DEFAULT_TIMEOUT_MS = 120_000; async function convertInWorker(input: ConvertInput, timeoutMs: number): Promise { return await new Promise((resolve, reject) => { const worker = new Worker( new URL('../../../workers/send-conversion.worker.ts', import.meta.url), { type: 'module', }, ); const cleanup = () => { worker.onmessage = null; worker.onerror = null; worker.onmessageerror = null; worker.terminate(); }; const timer = setTimeout(() => { cleanup(); reject(new Error(`Conversion worker timed out after ${timeoutMs}ms`)); }, timeoutMs); worker.onmessage = (event: MessageEvent) => { clearTimeout(timer); if (event.data.type === 'error') { cleanup(); reject(new Error(event.data.payload.message)); return; } const { epubBuffer, name, title, author } = event.data.payload; cleanup(); resolve({ file: new File([epubBuffer], name, { type: 'application/epub+zip' }), title, author, }); }; worker.onerror = () => { clearTimeout(timer); cleanup(); reject(new Error('Conversion worker failed')); }; worker.onmessageerror = () => { clearTimeout(timer); cleanup(); reject(new Error('Conversion worker message deserialization failed')); }; const request: ConversionWorkerRequest = { type: 'convert', payload: input }; worker.postMessage(request); }); } /** * Convert a document to EPUB off the main thread, falling back to in-thread * conversion when Web Workers are unavailable or the worker fails. * * `kind: 'page'` is the exception — its asset bundler uses the * `@tauri-apps/plugin-http` fetch to bypass CORS, and that binding lives on * `window.__TAURI_INTERNALS__` which a Web Worker can't reach. So page * clips always run on the main thread. */ export async function convertToEpubWithWorker( input: ConvertInput, timeoutMs: number = DEFAULT_TIMEOUT_MS, ): Promise { if (input.kind === 'page' || typeof Worker === 'undefined') { return convertToEpub(input); } try { return await convertInWorker(input, timeoutMs); } catch (error) { console.warn('Conversion worker failed, falling back to main thread:', error); return convertToEpub(input); } } const CONVERTIBLE_EXT: Record = { docx: 'docx', rtf: 'rtf', html: 'html', htm: 'html', txt: 'txt', }; /** * If a file is a document Readest can't read natively, convert it to EPUB; * otherwise return it unchanged. Shared by the `/send` page and the inbox * drainer so both channels convert identically. */ export async function convertFileIfNeeded(file: File): Promise { const ext = file.name.includes('.') ? file.name.split('.').pop()!.toLowerCase() : ''; const kind = CONVERTIBLE_EXT[ext]; if (!kind) return file; if (kind === 'txt') { return (await convertToEpubWithWorker({ kind: 'txt', file })).file; } const bytes = await file.arrayBuffer(); return (await convertToEpubWithWorker({ kind, bytes, fileName: file.name })).file; }