The
@awacloud/oconvWorker entry — one conversion per message, three message kinds.
Module worker (exported as the ./src/worker.js sub-path — spawned by URL/path, DOM-free) | Source packages/front/office/oconv/src/worker.js | Deps — | Worker-safe yes
src/worker.js builds its own ModuleRuntime from ./main.js's manifest and runs unmodified in a browser Worker and a Bun Worker — same file, same call. It is exported as the ./src/worker.js sub-path (see the boundary note) and is never reached through runtime.resolve(): a host spawns it by URL/path, or by the @awacloud/oconv/src/worker.js specifier resolved to a URL.
Resolve
// By specifier — the same file under an `exports`-aware resolver and under a
// prefix-only import map (`@awacloud/oconv/` → the package root). Never
// through ModuleRuntime.
const worker = new Worker(
import.meta.resolve('@awacloud/oconv/src/worker.js'),
{ type: 'module' }
);
A host can equally pass the file's real URL or path, e.g.
new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
from a module that sits next to it: a new URL() is resolved directly and
never goes through an import map.
API
Three message kinds, discriminated on the posted data shape — a toMd message never carries markdown/target, so the order is unambiguous: typeof data.markdown === 'string' routes to fromMd first, then typeof data.target === 'string' routes to convert, everything else routes to toMd.
| Kind | In | Out |
|---|---|---|
toMd |
{id, name, bytes: ArrayBuffer, at?, sha256?} (bytes transferred) |
{id, ms, error, chars, warnings, losses, markdown} |
fromMd |
{id, name?, markdown, target?, opts?, assets?, defaultFaces?} |
{id, ms, error, bytes, warnings, losses} |
convert |
{id, name?, bytes: ArrayBuffer, format?, target, includeNotes?, opts?, defaultFaces?} |
{id, ms, error, bytes, warnings, losses} |
error is a string, or null on success — every failure comes back as DATA, never a thrown exception across the worker boundary. opts, assets and defaultFaces are threaded onto the outgoing facade call only when the caller's message carried the key, so an older message without them produces exactly the call it made before any of the three existed.
The losses key (all three replies) is the facade's { code, detail } ledger itself, verbatim, structured-cloned — [] when error is set. Reader codes carry a string detail; the pdf writer's layout/*, text/unencodable and inline/* codes carry an object detail (and an index / kind), and both cross the worker boundary unchanged.
Examples
toMd — post a document, await the reply
const reply = new Promise((resolve) => { worker.onmessage = (ev) => resolve(ev.data); });
const buffer = docxBytes.slice().buffer; // a fresh, transferable ArrayBuffer
worker.postMessage(
{ id: 1, name: 'report.docx', bytes: buffer, at: new Date().toISOString() },
[buffer]
);
const data = await reply;
data.markdown; // string, or null when data.error is set
fromMd — post markdown, get bytes back
const reply = new Promise((resolve) => { worker.onmessage = (ev) => resolve(ev.data); });
worker.postMessage({ id: 2, name: 'out.docx', markdown: '# Title\n\nBody.' });
const data = await reply;
data.bytes; // Uint8Array (structured-cloned), or null on error
Reading the loss ledger
const reply = new Promise((resolve) => { worker.onmessage = (ev) => resolve(ev.data); });
worker.postMessage({ id: 3, name: 'out.pdf', markdown: '# T

' });
const data = await reply;
data.warnings; // 1 — the count, kept for existing callers
data.losses[0].code; // 'layout/image-dropped'
data.losses[0].detail; // { index: '1.i0', name: 'missing.png', alt: 'alt', reason: 'no-bytes', bytes: 0 }
Notes
-
warningsis a count kept for existing callers;lossesis the ledger itself (warnings === losses.lengthon every reply). Addinglossesis additive: the three request envelopes are unchanged and no reply key was removed. -
sha256is part of the frozentoMdenvelope but unused: the facade'stoMdalways computes its ownsourceSha256from the posted bytes and takes no override. -
The
toMdenvelope carries no reader option at all — neitherincludeNotesnorformOpBudgetcrosses the worker boundary; a caller needing either calls the facade'stoMddirectly on the main thread. -
The
toMdenvelope stays byte-unchanged across thefromMdandconvertadditions — both are additive message kinds; a regression test pins the exact reply key set of all three (lossesincluded). -
A registered
oconvDefaultFacesdescriptor is not serialized into the worker — its factory closes over its bytes, soModuleRuntime#serializecannot ship it. The host resolvesoconvDefaultFaceson its own thread and posts the resulting map as thedefaultFacesfield instead (seeoconv's Notes). -
byteson thefromMd/convertreply is structured-cloned, not transferred on the response — a transfer-list optimisation on the reply path is a documented future concern, not today's contract. -
DOM-free by construction: the file touches no
document/windowAPI, onlyself.onmessage/self.postMessageand the module graph it builds.
See also
- docs/api/README.md — full module index +
exportsboundary note oconv— the facade this worker wraps- pdf-writer.md —
opts.pdfreference, including the worker-forwarding contract - loss matrix