Bundling into an application
Everything these packages need at runtime resolves through their exports maps,
so a bundler finds it without configuration. There is exactly one thing an
application has to wire up by hand: the URL of the parquet worker.
If you use @spatialdata/vis, that is one call:
import { ensureWorkers } from '@spatialdata/vis';
// Vite. See "Other bundlers" below.
import workerUrl from '@spatialdata/core/parquet-worker?worker&url';
ensureWorkers({ parquet: { workerUrl } });
Core on its own has no ensureWorkers; call
enableParquetWorker({ workerUrl }) from @spatialdata/core instead.
The two workers
| Worker | Does | Enabled by | Needs a URL? |
|---|---|---|---|
| Codec | Zarr chunk decode — imagery, labels | ensureCodecWorkers() (vis) | No |
| Parquet | Parquet decode and scans — points, shapes, tables | enableParquetWorker() (core) | Yes, from a bundled app |
ensureWorkers() starts both. Every option is optional, so a bare
ensureWorkers() is valid; pass false for either worker to leave it off, and
per-worker objects to override defaults:
ensureWorkers({
codec: { chunkCacheMaxBytes: 512 * 1024 * 1024 },
parquet: { workerUrl, requestTimeoutMs: 120_000 },
});
It returns what is actually running — { codec, parquet } — which is not always
what you asked for. Neither worker starts outside a browser, and the parquet
worker reports false if it fails to load.
Both are idempotent, and the renderer paths call ensureCodecWorkers() for you,
so the reason to call ensureWorkers() is the parquet worker — or wanting both
up before any UI mounts.
Why only one of them needs a URL
The codec worker ships as a single self-contained file, so your bundler can copy it as a static asset and rewrite the URL core hands it. That is why it needs no configuration.
The parquet worker cannot be built that way: it imports shared chunks and
apache-arrow, and it loads a 6.6 MB parquet-wasm of its own. Copied as an asset
it is 11 kB whose every import 404s; made self-contained it would carry an inlined
copy of that wasm. So your bundler has to build it, and the URL import is what
asks it to:
import workerUrl from '@spatialdata/core/parquet-worker?worker&url';
That import is doing the work — the string it yields is almost incidental.
Calling enableParquetWorker() with no URL is right only where core is loaded
as published ESM without being re-bundled: a dev server serving node_modules, an
import map, a CDN. There dist/parquet-worker.js really does sit next to the chunk
asking for it. Once a bundler inlines core's chunk into its own output, it does not.
If you get it wrong
A worker that fails to load is detected, switched off, and logged — core falls back to main-thread decoding rather than waiting for a request timeout. So a wrong URL costs performance, not correctness, with one exception:
loadPointsMatchingFeatureCodes — the feature-index scan that fetches a selected
feature's points from beyond the resident memory window — has no main-thread
fallback and throws without a working worker. Gate any UI that depends on it:
const { parquet } = ensureWorkers({ parquet: { workerUrl } });
if (parquet) {
// offer on-demand feature loading
}
isParquetWorkerEnabled() from core answers the same question later, and reports
false after a failed startup rather than staying optimistic.
parquet-wasm: nothing to configure
Core reads Parquet through a vendored
parquet-wasm build, loaded from the
@spatialdata/core/parquet-wasm export. Your bundler resolves that subpath,
follows the glue's own new URL('parquet_wasm_bg.wasm', import.meta.url), and
emits the wasm alongside your other assets — in both the main bundle and the
worker's.
You should see something like assets/parquet_wasm_bg-B1vu2ek6.wasm in your build
output. With default asset naming both graphs produce the same hashed filename, so
that is one file, not two. If you have a custom assetFileNames (or webpack
assetModuleFilename) routing .wasm somewhere unhashed, check you have not
turned it into two copies of 6.6 MB.
Other bundlers
| Bundler | How to wire it |
|---|---|
| Vite / Rollup | import workerUrl from '@spatialdata/core/parquet-worker?worker&url', then ensureWorkers({ parquet: { workerUrl } }) — verified in CI against a real production build |
| webpack | createWorker with a local entry file, below. This docs site does it, so every docs build exercises it. |
| Others | Whatever your bundler provides for building a worker from a module, then pass either a URL (workerUrl) or a factory (createWorker). Check the worker and a .wasm both appear in your output. |
| No bundler | Nothing. ensureWorkers() with no options already resolves. |
webpack, and createWorker
webpack cannot hand you a URL for a bundled worker ahead of time: it only builds one
when it can see the new Worker(new URL(...)) form literally. So give it a factory
instead of a URL:
ensureWorkers({
parquet: {
createWorker: () =>
new Worker(new URL('./parquetWorkerEntry.ts', import.meta.url), { type: 'module' }),
},
});
That URL must point at a local file in your own source, which is one line:
// parquetWorkerEntry.ts
import '@spatialdata/core/parquet-worker';
Pointing it straight at the bare specifier — new URL('@spatialdata/core/parquet-worker', import.meta.url) — does not work. webpack resolves it to core's published
dist/parquet-worker.js and emits that file as a static asset, still carrying its
relative imports to sibling chunks and a bare apache-arrow: 9 kB whose every import
404s, the same shape of failure as copying the worker. Going through a local file makes
webpack treat it as a worker entry and bundle the graph behind it.
createWorker takes precedence over workerUrl. It is a factory rather than a Worker
because enabling tears down and rebuilds — an instance could only be used once.
Checking your build
Two things worth asserting in CI, both cheap:
- No emitted chunk references a file the build did not emit. A missing wasm or worker asset is invisible in dev and fatal in production.
- The app actually starts the worker and decodes something. Our own
tests/production/browser/parquet-worker.spec.tsdoes this against a real consumer build; it is the only place the published worker entry, its module format, and its private parquet-wasm resolution are all exercised at once.