mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-29 20:56:35 -05:00
Move updaters onto public and private storage
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>
This commit is contained in:
35
test/storage/MemoryBucket.test.js
Normal file
35
test/storage/MemoryBucket.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MemoryBucket } from '../../src/common/storage/index.js';
|
||||
|
||||
// MemoryBucket stands in for the R2 binding in tests, so its list() must paginate like R2's.
|
||||
test('list filters by prefix, sorts, and paginates with cursors', async () => {
|
||||
const bucket = new MemoryBucket;
|
||||
for (const key of ['assets/images/stage/b.png', 'assets/images/stage/a.png', 'assets/images/stage/deep/c.png', 'assets/images/weapon/w.png', 'data/x.json'])
|
||||
await bucket.put(key, 'x');
|
||||
|
||||
const all = await bucket.list({ prefix: 'assets/images/stage/' });
|
||||
assert.deepEqual(all.objects.map(o => o.key), ['assets/images/stage/a.png', 'assets/images/stage/b.png', 'assets/images/stage/deep/c.png']);
|
||||
assert.equal(all.truncated, false);
|
||||
|
||||
const first = await bucket.list({ prefix: 'assets/', limit: 2 });
|
||||
assert.equal(first.truncated, true);
|
||||
const second = await bucket.list({ prefix: 'assets/', limit: 2, cursor: first.cursor });
|
||||
assert.deepEqual([...first.objects, ...second.objects].map(o => o.key).length, 4);
|
||||
assert.equal(second.truncated, false);
|
||||
});
|
||||
|
||||
test('put and get round-trip strings, bytes, and streams with metadata', async () => {
|
||||
const bucket = new MemoryBucket;
|
||||
await bucket.put('a.json', '{"a":1}', { httpMetadata: { contentType: 'application/json' } });
|
||||
await bucket.put('b.png', new Uint8Array([1, 2]));
|
||||
await bucket.put('c.png', new Response(new Uint8Array([3])).body);
|
||||
|
||||
assert.deepEqual(await (await bucket.get('a.json')).json(), { a: 1 });
|
||||
assert.equal((await bucket.head('a.json')).httpMetadata.contentType, 'application/json');
|
||||
assert.deepEqual(new Uint8Array(await (await bucket.get('b.png')).arrayBuffer()), new Uint8Array([1, 2]));
|
||||
assert.deepEqual(new Uint8Array(await (await bucket.get('c.png')).arrayBuffer()), new Uint8Array([3]));
|
||||
assert.equal(await bucket.get('missing'), null);
|
||||
await bucket.delete('a.json');
|
||||
assert.equal(await bucket.head('a.json'), null);
|
||||
});
|
||||
Reference in New Issue
Block a user