Worker bundling pattern
Use this pattern when adding browser workers to SpatialData.js packages. The goal is that application code imports a normal package API and does not need to know where the worker script lives.
Public API shape
Expose a small function from the package entry that owns the worker. For
low-level zarrextra consumers, that is:
import { enableWorkerChunkDecode } from 'zarrextra/workers';
enableWorkerChunkDecode();
For higher-level packages, prefer an idempotent ensure*() wrapper and call it
from the normal runtime path so application authors do not have to remember an
extra registration step. @spatialdata/vis does this with:
import { ensureCodecWorkers } from '@spatialdata/vis';
ensureCodecWorkers();
SpatialCanvas and the shared renderer hook call this automatically in browser
contexts. The export exists for hosts that want to activate the worker before
mounting UI, but normal vis consumers should not need to call either
ensureCodecWorkers() or enableWorkerChunkDecode().
Keep workerUrl as an optional escape hatch for unusual deployments, but do not
require consumers, demos, or downstream packages to import worker files directly.
In particular, avoid examples like:
new URL('../../../some-package/src/workers/my-worker.ts', import.meta.url);
That path only works for a particular source layout and can break in deployed builds, especially when the worker needs its own codec registration or other browser-only setup.
The first version of a worker-backed API should optimize for the easy path: users get the codecs and worker behavior provided by the library with one call. That should not make the API monolithic forever. Leave room for later options such as choosing codec families, using a lighter worker bundle, passing hooks around decode, or providing custom codec handlers. Keep those as progressive configuration points rather than requirements for the common case.
Source and package entries
Workers need two URL factories:
- a source-mode factory for monorepo Vite dev/builds;
- a package-mode factory for the published
distfiles.
The source factory should let Vite create a worker asset from the TypeScript worker entry:
import workerUrl from './my-worker?worker&url';
export function defaultWorkerUrl(): string {
return workerUrl;
}
The published package factory should point to the already-built worker file next to the package entry:
export function defaultWorkerUrl(): URL {
const workerFile = './my-worker.js';
return new URL(workerFile, import.meta.url);
}
Keep the worker file name in a variable. A literal
new URL('./my-worker.js', import.meta.url) can be interpreted by bundlers
during library build and inlined into the entry, instead of remaining a runtime
URL to the sibling worker asset.
Share the rest of the implementation between the source and package entries:
const controls = createWorkerControls(defaultWorkerUrl);
export const enableMyWorker = controls.enableMyWorker;
export const disableMyWorker = controls.disableMyWorker;
For zarrextra/workers, src/workers/index.ts is the source entry and
src/workers/index.package.ts is the package entry used by the Vite library
build.
Worker implementation
The worker file should be self-sufficient. If it needs codecs, WASM modules, or global registration, initialize those inside the worker before handling work. Do not assume registration that happened on the main thread is visible in the worker.
For fizarrita chunk decode, codec-worker.ts imports codec-worker-init.ts,
which registers zarrextra codecs into the worker-side zarrita.registry
before fizarrita decodes chunks.
Treat side-effect-only worker imports as part of the worker's public behavior.
@fideus-labs/fizarrita/codec-worker installs the message handler by import
side effect. Because that package declares "sideEffects": false, production
worker builds can tree-shake the handler away unless the worker build preserves
it. zarrextra currently disables Rollup tree-shaking for the codec worker
bundle and has a regression test that checks the generated worker contains the
fizarrita handshake strings.
Build configuration
When publishing a worker-backed package:
- Build the public package entries as normal.
- Use the package entry for worker-facing exports.
- Build each worker script as its own browser bundle in
dist. - Include the worker bundle in the package
files/exportsas appropriate.
For zarrextra, the package build emits:
dist/workers.jsforzarrextra/workers;dist/codec-worker.jsas the worker asset used by that entry.
Testing checklist
Add tests at the level where the failure would hurt users:
- Unit test the public helper so it has a no-argument path and still accepts
workerUrlas an override. - Unit test any higher-level
ensure*()wrapper so repeated calls do not create repeated worker pools. - Unit test the package entry, not only the source entry, so the default URL is
a sibling
.jsworker file and not a source.tspath. - Build the package and inspect the generated entry for
new URL("./my-worker.js", import.meta.url). - Inspect or smoke-test the generated worker bundle for any required registration strings, such as codec ids and worker message-handler handshakes.
- Build a demo or small consumer app that calls the public helper without
passing
workerUrl.
The important invariant is: consumers should call the exported API, and the package should make the correct worker bundle reachable in both Vite source builds and published package builds.