Skip to content

Off the main thread

@lamina/pdf is synchronous after init(). On the main thread, a full rewrite of a 50 MB file blocks the page for about 200 ms. @lamina/pdf/worker runs the same API in a Web Worker. You call the same method names, and each returns a Promise.

packages/pdf/examples/worker-client.ts
// `@lamina/pdf/worker`: the sync API's async twin in a Web Worker, so a large file never blocks
// the page. Open, count the pages, save with progress, and abort a save the user cancels. Runs
// in browsers (test/browser/worker.browser.test.ts).
import { isLaminaError } from "@lamina/pdf";
import { connect } from "@lamina/pdf/worker";
/** What the example saw: the page count, the saved size, and whether the abort was reported. */
export interface WorkerClientResult {
readonly pages: number;
readonly savedBytes: number;
readonly progressEvents: number;
readonly abortedAsCancelled: boolean;
}
/**
* Opens `bytes` in a worker, saves them, then starts a second save and aborts it.
*
* @param bytes - any PDF; it is copied to the worker (`transfer: true` would move it).
*/
export async function workerClient(bytes: Uint8Array): Promise<WorkerClientResult> {
const client = await connect();
try {
await using doc = await client.open(bytes);
const pages = await doc.pageCount();
let progressEvents = 0;
const saved = await doc.save({ mode: "fullRewrite" }, { onProgress: () => progressEvents++ });
// A cancel button: abort the signal and the save stops at its next step.
const controller = new AbortController();
const saving = doc.save({ mode: "fullRewrite" }, { signal: controller.signal });
controller.abort(new Error("the user cancelled"));
let abortedAsCancelled = false;
try {
await saving;
} catch (e) {
abortedAsCancelled = isLaminaError(e, "cancelled");
}
return { pages, savedBytes: saved.byteLength, progressEvents, abortedAsCancelled };
} finally {
client.terminate();
}
}
  • Same names, async. Getters become methods: await doc.pageCount(). doc.page(i) returns a remote page, and page.text() returns a PageText (call client.loadExtract() first).
  • Not served:
    • transact: its callback can’t cross to the worker, so use apply(ops).
    • saveJob: the remote save is already stepped.
    • Builder: build on the main thread or in your own worker.
  • Cancellation. Pass signal to any call. An aborted call rejects with cancelled, and error.cause is the signal’s reason.
    • A call that hasn’t started never runs.
    • save stops at its next step, and the document stays usable.
    • Any other call runs to the end in the worker, because it is one synchronous call. client.terminate() is the hard stop.
  • Buffers. Saved bytes and page text come back transferred, not copied. Your input is copied unless you pass transfer: true, which moves it and leaves your buffer empty.
  • Crashes. If the worker dies, pending calls reject with worker-crashed and client.on("crashed", …) fires. client.restart() starts a fresh worker, and documents must be opened again.
  • Version. connect() checks that the worker comes from the same @lamina/pdf build, and rejects with worker-version-mismatch otherwise.
  • Bundlers and CSP. Vite and webpack bundle the worker with no setup. In Vite, exclude @lamina/pdf from optimizeDeps, as for the main entry. To host the worker file yourself, pass connect({ worker: () => new Worker(url, { type: "module" }) }).