Updaters take a storage object instead of touching the filesystem: a
publicStorage (what the site serves: data/, assets/) and a privateStorage
(updater state). Two implementations share one small interface
(exists / readJson / writeJson / readBytes / writeText / writeBytes):
- FilesystemStorage over a directory, used by npm run splatnet, so dist/
and storage/ end up exactly as they always have and the S3 sync is
untouched;
- BucketStorage over an R2 bucket binding (MemoryBucket in tests), with a
thin per-run cache: exists() lists a directory once and answers from
memory, readJson() parses a document once, and writeJson() skips the put
when the content is unchanged.
The updater base class and localization processor keep their original
structure with awaits added; the processor reads its document lazily and
writes once per update, as splatoon3.ink's does. Objects written to a
bucket carry the content type and cache-control the S3 sync used to apply.
Tests cover the storage contract (run against both implementations), the
BucketStorage cache behaviour, MemoryBucket's R2-style listing, the
localization processor, the updater base, and each concrete updater.
scripts/compare-data.mjs compares two data directories ignoring SplatNet's
random ordering and ICS timestamps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>