Back to Black Site
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.

src/save-store.ts
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>;