Pure: turns user-supplied dataset URLs into the absolute, slash-
terminated form the scene loader's INTERNAL consumers expect.
Centralized here so the various edge cases (relative paths, missing
protocol, missing trailing slash) get a single canonical form.
The original implementation used window.location.origin directly;
this version takes the origin as a parameter so it's testable
without a DOM.
Contract: trailing slash is INTENTIONAL (HIGH-6 audit)
normalizeURL ALWAYS returns a URL ending in /. This is the inverse
of config/url-params.ts::normalizeDataSourceUrl, which STRIPS the
trailing slash from user input per the CLAUDE.md gotcha ("Data Source URLs
Normalize Trailing Slashes"). The two layers run in sequence:
user ?src=... → normalizeDataSourceUrl (strip /)
→ scene loader stores raw
→ normalizeURL (add / back, absolute-ify)
→ internal consumers
The trailing slash here is required because at least one downstream
consumer — ui/overlay-manager.ts::renderImageOverlay — builds child
URLs by raw string concatenation:
Without the trailing slash, that concatenates to …/dataset.zarroverlays/...
and 404s. Other downstream consumers tolerate the trailing slash:
zarrita's FetchStore.resolve() explicitly appends / if missing
(see @zarrita/storagedist/src/fetch.js).
cache/multi-level-caching-store/fetch-retry.ts::buildUrl strips
/+$ off the base before joining (replace(//+$/, '')).
Result: producers that need a child-buildable base (overlay-manager)
get one; producers that prefer slash-stripped (the underlying network
layer) get one via their own normalization. Do NOT remove the trailing
slash without first auditing every consumer of
rootGroup.userData.zarrBaseUrl and every ctx.normalizeURL call site.
EXCEPTION — zipped stores. A .zarr.zip is a single file, so it gets NO
trailing slash: the rationale above is about making directory children
concatenable, and an archive has no URL-addressable children (its members are
read through the store).
Archive members, including image overlays, are read through the store via
rootGroup.userData.readOverlayFile rather than built as child URLs.
URL normalization helpers for the scene loader.
Pure: turns user-supplied dataset URLs into the absolute, slash- terminated form the scene loader's INTERNAL consumers expect. Centralized here so the various edge cases (relative paths, missing protocol, missing trailing slash) get a single canonical form.
The original implementation used
window.location.origindirectly; this version takes the origin as a parameter so it's testable without a DOM.Contract: trailing slash is INTENTIONAL (HIGH-6 audit)
normalizeURLALWAYS returns a URL ending in/. This is the inverse ofconfig/url-params.ts::normalizeDataSourceUrl, which STRIPS the trailing slash from user input per the CLAUDE.md gotcha ("Data Source URLs Normalize Trailing Slashes"). The two layers run in sequence:user
?src=...→ normalizeDataSourceUrl (strip/) → scene loader stores raw → normalizeURL (add/back, absolute-ify) → internal consumersThe trailing slash here is required because at least one downstream consumer —
ui/overlay-manager.ts::renderImageOverlay— builds child URLs by raw string concatenation:Without the trailing slash, that concatenates to
…/dataset.zarroverlays/...and 404s. Other downstream consumers tolerate the trailing slash:FetchStore.resolve()explicitly appends/if missing (see@zarrita/storagedist/src/fetch.js).cache/multi-level-caching-store/fetch-retry.ts::buildUrlstrips/+$off the base before joining (replace(//+$/, '')).Result: producers that need a child-buildable base (overlay-manager) get one; producers that prefer slash-stripped (the underlying network layer) get one via their own normalization. Do NOT remove the trailing slash without first auditing every consumer of
rootGroup.userData.zarrBaseUrland everyctx.normalizeURLcall site.EXCEPTION — zipped stores. A
.zarr.zipis a single file, so it gets NO trailing slash: the rationale above is about making directory children concatenable, and an archive has no URL-addressable children (its members are read through the store).Archive members, including image overlays, are read through the store via
rootGroup.userData.readOverlayFilerather than built as child URLs.