Replace the HTML preview with a worker-rendered PDF preview
Rendering moves into a module Web Worker behind a latest-wins RenderClient with a watchdog and a handshake-based main-thread fallback. The preview shows the real PDF through pdf.js at 2x with zoom, page indicator, audit issues popover and a focus modal. A4/Letter and margin controls are frozen into each invoice at issue and can be saved as the default (migration M3). react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB to 0.5 MB. The HTML twin and its styles are removed.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// Browser-only module worker: the one place react-pdf runs in the app. Not importable from Node tests.
|
||||
import { installStreamAsyncIterator } from "../../lib/streamPolyfill";
|
||||
import { initBrowserRendering } from "./browserInit";
|
||||
import { renderInvoicePdf } from "./invoice";
|
||||
import type { WorkerMessage, WorkerRequest } from "./jobs";
|
||||
|
||||
interface WorkerScope {
|
||||
onmessage: ((event: MessageEvent<WorkerRequest>) => void) | null;
|
||||
postMessage(message: WorkerMessage, options?: { transfer: Transferable[] }): void;
|
||||
}
|
||||
const scope = self as unknown as WorkerScope;
|
||||
|
||||
installStreamAsyncIterator();
|
||||
|
||||
const post = (message: WorkerMessage, transfer: Transferable[] = []) => scope.postMessage(message, { transfer });
|
||||
|
||||
const failure = (id: number | null, err: unknown): WorkerMessage => ({
|
||||
type: "error",
|
||||
id,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined,
|
||||
});
|
||||
|
||||
const started = initBrowserRendering();
|
||||
|
||||
scope.onmessage = async (event) => {
|
||||
const request = event.data;
|
||||
if (request?.type !== "render") return;
|
||||
const { job } = request;
|
||||
try {
|
||||
await started.catch(() => initBrowserRendering());
|
||||
const t0 = performance.now();
|
||||
const out = await renderInvoicePdf({ model: job.model, prefs: job.prefs });
|
||||
const ms = Math.round(performance.now() - t0);
|
||||
// Hand the buffer over instead of copying it; a view onto part of a larger buffer is copied first.
|
||||
const whole = out.bytes.byteOffset === 0 && out.bytes.byteLength === out.bytes.buffer.byteLength;
|
||||
const bytes = whole ? out.bytes : out.bytes.slice();
|
||||
post(
|
||||
{ type: "result", result: { id: job.id, bytes, pages: out.pages, issues: out.issues, fingerprint: out.fingerprint, ms } },
|
||||
[bytes.buffer],
|
||||
);
|
||||
} catch (err) {
|
||||
post(failure(job.id, err));
|
||||
}
|
||||
};
|
||||
|
||||
started.then(
|
||||
() => post({ type: "ready" }),
|
||||
(err) => post(failure(null, err)),
|
||||
);
|
||||
Reference in New Issue
Block a user