zarrextra Overview
zarrextra is the layer between zarrita and
@spatialdata/core. It holds the things a SpatialData reader needs that are not
specific to SpatialData: opening a store and reading its whole hierarchy up front,
navigating that hierarchy, decoding image codecs zarrita does not ship, and moving
chunk decode off the main thread.
It is published outside the @spatialdata/* namespace because nothing in it knows
what an image or a table is. If you are reading SpatialData stores, you want
@spatialdata/core, which re-exports the parts of this package you are likely to
need. If you are reading some other zarr hierarchy and want the same tree and codec
machinery, you can depend on zarrextra alone.
:::info alpha prerelease
Versioned alongside the @spatialdata/* packages for now, but able to version
independently in future releases. The API described here is not yet stable.
:::
npm install zarrextra
# or
pnpm add zarrextra
Opening a store
openExtraConsolidated takes a URL or any zarrita Readable, resolves consolidated
metadata, and reads the entire hierarchy into a tree — every group, every array, and
every node's attributes — before returning.
import { openExtraConsolidated, isErr } from 'zarrextra';
const result = await openExtraConsolidated('https://example.com/store.zarr');
if (isErr(result)) {
console.error(result.error);
} else {
const { zarritaStore, tree } = result.value;
}
It returns a Result, not a thrown error: failing to open a store is an
ordinary outcome for a URL a user typed, and the message is worth handling rather
than catching.
The ConsolidatedStore it resolves to has two halves, and which one you want depends
on whether you need data or only structure:
| Field | What it is | Use it for |
|---|---|---|
zarritaStore | zarr.Listable<Readable> — the store, with metadata served from memory | Opening arrays, reading chunks |
tree | ZarrTree — the hierarchy as a plain object | Enumerating, navigating, reading metadata without I/O |
The tree is the reason this package exists. Because every node's attributes and array metadata are already in memory, a question like "what columns does this table have, and what type is each one?" is answerable synchronously, before deciding whether any of it is worth loading. Tree nodes covers the shape of a node, how to tell a group from an array, and how to read a data type out of one.
Store extras
Two smaller helpers that come up when a store is not being consumed whole:
createPrefixedStore(store, prefix)— aReadableview rooted at a subpath. Used to hand a table's own subtree toanndata.jswithout it knowing about SpatialData's layout above.loadOmeZarrMultiscalesFromStore(store, path)— reads an OME-Zarr multiscales group into Viv-compatible pixel sources, reusing an already-open store rather than making the viewer open its own.
Result
Result<T, E> is a small Rust-style success-or-error union, used wherever a failure
is expected rather than exceptional:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
with Ok, Err, isOk, isErr, unwrap and unwrapOr. It is re-exported from
@spatialdata/core, so importing it from either package gives the same type. See
error handling for how core uses it.
Codecs and workers
SpatialData stores in the wild use image codecs zarrita does not register by
default, and decoding them on the main thread will stall a render. Those two concerns
are documented where they are used rather than repeated here:
| Topic | Where |
|---|---|
| Registering JP2K and experimental HTJ2K decode, encoding HTJ2K | package README |
| Which setup each context needs (Node, browser with vis, browser without) | codec fixtures |
| Shipping a worker entry point consumers can use without private URLs | worker bundling pattern |
The short version: in Node, call registerJpeg2kCodec() / registerExperimentalHtj2kCodec()
on the main thread. In a browser using @spatialdata/vis, do nothing — the renderer
path enables the bundled codec worker for you. In a browser without vis, call
enableWorkerChunkDecode() from zarrextra/workers before loading codec-backed data.
Weight
zarrita is deliberately minimal. zarrextra is not, and it is worth being explicit
about where that lands before depending on it.
Measured from the current build:
| Entry | Raw | Gzipped |
|---|---|---|
zarrextra | 15.4 kB | 5.2 kB |
zarrextra/codec-worker | 3.2 MB | 899 kB |
The main entry is small, and the WASM codec packages are optionalDependencies whose
decoders are injected by the caller — so importing zarrextra does not drag
OpenJPEG or OpenJPH in. What is heavy is the codec worker, and it is one bundle
carrying every codec: enabling worker decode for JP2K also ships OpenJPH, and vice
versa. An application reading uncompressed or blosc-compressed stores — which is most
of them — gets no benefit from either.
The install footprint is also larger than the import graph suggests. zod is a hard
dependency reached by a single internal schema module that nothing currently calls,
so every consumer installs it whether or not anything uses it. Which way that should
resolve is open: array metadata is read from a store with a bare JSON.parse today
and typed as an unvalidated record, so there is
at least as good a case for validating more at that boundary — and earning the
dependency — as for dropping it.
:::caution subject to redesign
None of this is settled. Splitting the worker so applications can opt into lighter bundles when they do not need JP2K or HTJ2K is the obvious first move, and is already noted as future work in the package README. Treat the packaging — not the APIs on this page — as the part most likely to change.
:::