SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { deadline } from './async.js';
const KEYS = ['profile', 'loadout', 'map', 'best', 'campaign', 'difficulty', 'character', 'armorySync', 'customizationSync'];
export type SaveBucket = { owner: string; values: Map<string, string>; unreadable: Set<string>; saveFailed: boolean };
export type SyncPreferences = { enabled: boolean; imported: boolean };
/** Account-owned cache with serialized writes; old device saves remain the guest save. */
export function createSaveStore(onFailure: () => void = () => {}) {
const buckets = new Map<string, Promise<SaveBucket>>();
const queues = new Map<string, Promise<void>>();
const available = runtimeHasDomain('storage');
let active: SaveBucket = { owner: '', values: new Map(), unreadable: new Set(), saveFailed: false };
const storageKey = (owner: string, key: string) => owner ? `blacksite.account.${owner}.${key}` : `blacksite.${key}`;
function persist(bucket: SaveBucket, suffix: string, raw: string) {
if (!available || bucket.unreadable.has(suffix)) { bucket.saveFailed = true; onFailure(); return; }
const key = storageKey(bucket.owner, suffix);
const write = (queues.get(key) ?? Promise.resolve()).then(async () => {
const actual = Promise.resolve().then(() => storage.setItem(key, raw));
try { await deadline(() => actual, 3000); }
catch { bucket.saveFailed = true; onFailure(); }
// A slow old write must settle before any newer value is sent for this key.
await actual.catch(() => undefined);
});
queues.set(key, write);
void write.finally(() => { if (queues.get(key) === write) queues.delete(key); });
}
async function load(owner: string): Promise<SaveBucket> {
if (owner && !/^[a-f0-9]{64}$/.test(owner)) throw new Error('invalid-account');
const existing = buckets.get(owner);
if (existing) return existing;
const pending = (async () => {
const bucket: SaveBucket = { owner, values: new Map(), unreadable: new Set(), saveFailed: !available };
if (available) await Promise.all(KEYS.map(async suffix => {
try {
const raw = await deadline(() => storage.getItem(storageKey(owner, suffix)), 3000);
if (raw !== null && typeof raw !== 'string') throw new Error('invalid-save');
if (typeof raw === 'string') {
JSON.parse(raw); // A corrupt existing value must not be overwritten with defaults.
bucket.values.set(suffix, raw);
}
} catch { bucket.unreadable.add(suffix); bucket.saveFailed = true; }
}));
return bucket;
})();
buckets.set(owner, pending);
return pending;
}
const suffix = (key: string) => key.replace(/^blacksite\./, '');
const get = <T>(key: string, fallback: T, bucket = active): T => {
try { return JSON.parse(bucket.values.get(suffix(key)) ?? 'null') ?? fallback; }
catch { return fallback; }
};
const set = (key: string, value: unknown, bucket = active) => {
const id = suffix(key);
const raw = JSON.stringify(value);
bucket.values.set(id, raw);
persist(bucket, id, raw);
};
return {
load,
activate(bucket: SaveBucket) { active = bucket; },
owner: () => active.owner,
bucket: () => active,
get, set,
preferences: (): SyncPreferences => {
const value = get<Partial<SyncPreferences>>('blacksite.armorySync', {});
return { enabled: value?.enabled === true, imported: value?.imported === true };
},
async guestChoices() {
const bucket = await load('');
return { character: get('blacksite.character', 'assaulter', bucket),
loadout: get('blacksite.loadout', { primary: 'm4a1', secondary: 'm1911', throwable: 'frag' }, bucket),
equippedArmor: get<Record<string, unknown>>('blacksite.profile', {}, bucket).armour ?? null,
difficulty: get('blacksite.difficulty', 'normal', bucket), map: get('blacksite.map', 'town', bucket) };
},
async guestProfile(): Promise<Record<string, unknown>> {
return get('blacksite.profile', {}, await load(''));
},
flush: () => Promise.all([...queues.values()]),
};
}
export type SaveStore = ReturnType<typeof createSaveStore>;
