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 { armoryDigest, emptyArmory, mergeArmory, profileArmory, type Armory, type GameProfile } from './armory.js';
import { openArmoryCollection, type ArmoryCollection } from './armory-host.js';
import { deadline } from './async.js';
import type { SaveStore } from './save-store.js';
export type SyncState = { supported: boolean; unsupported: boolean; message: string; busy: boolean; enabled: boolean; canEnable: boolean; canSave: boolean; canImport: boolean; canLoad: boolean };
type GameAccess = { isSafe(): boolean; profile(): GameProfile; applyArmory(value: Armory): void };
type Prepared = Awaited<ReturnType<ArmoryCollection['prepare']>>;
export function createArmorySync(store: SaveStore, game: () => GameAccess | undefined,
render: (state: SyncState) => void, open = openArmoryCollection) {
let owner = '';
let generation = 0;
let collection: ArmoryCollection | undefined;
let supported = false, unsupported = false;
let busy = false;
let remote: Armory | undefined;
let remoteDigest: string | undefined;
let importAvailable = false;
let message = 'Sign in to sync your armory';
let uncertain: { change: Prepared; digest: string } | undefined;
const current = () => profileArmory(game()?.profile() ?? store.get('blacksite.profile', {}));
const preferences = () => store.preferences();
const valid = (version: number) => version === generation && owner !== '' && store.owner() === owner;
const present = () => {
const ready = owner !== '' && store.owner() === owner && !!game()?.isSafe();
let dirty = false;
try { dirty = armoryDigest(current()) !== remoteDigest; } catch { /* Invalid local data cannot publish. */ }
render({ supported, unsupported, message: message + (store.bucket().saveFailed ? ' · Device save unavailable' : ''), busy,
enabled: preferences().enabled, canEnable: ready && !!collection && !busy && !preferences().enabled,
canSave: ready && !!collection && !busy && preferences().enabled && (dirty || !!uncertain),
canImport: ready && supported && importAvailable && !preferences().imported && !busy, canLoad: ready && supported && !busy });
};
function reconcile() {
if (owner !== store.owner() || !game()?.isSafe() || !remote) return;
const merged = mergeArmory(current(), remote);
remote = undefined;
if (armoryDigest(merged) !== armoryDigest(current())) game()!.applyArmory(merged);
}
const errorMessage = (error: unknown) => {
const reason = error instanceof Error ? error.message : '';
if (/app-data-(unavailable|no-relays)/.test(reason)) return 'This runtime does not support the features needed for sync.';
if (/incompatible|invalid|deleted/.test(reason)) return 'Account armory cannot be loaded · Device progress kept';
if (/denied|reject|consent|permission/i.test(reason)) return 'Account save not approved · Device progress kept';
if (/stale|conflict/i.test(reason)) return 'Account changed on another device · Try saving again';
return 'Armory sync failed · Device progress kept. Try again';
};
async function read(version: number) {
if (!collection) {
const opened = await deadline(() => open());
if (!valid(version)) return;
collection = opened; supported = true; unsupported = false;
}
if (!valid(version)) return;
const result = await deadline(() => collection!.get(owner, 'progress'));
if (!valid(version)) return;
if (result.incomplete) throw new Error('armory-incomplete');
if (result.record?.deleted) throw new Error('armory-deleted');
supported = true; unsupported = false;
remote = result.record?.data ?? emptyArmory();
remoteDigest = armoryDigest(remote);
reconcile();
return result.record;
}
async function restore() {
if (busy || !owner || store.owner() !== owner) return;
const version = generation;
busy = true;
message = 'Loading account armory…'; present();
try {
await read(version);
if (valid(version)) message = game()?.isSafe()
? (armoryDigest(current()) === remoteDigest ? 'Armory is up to date' : 'Changes saved on this device · Ready to save to account')
: 'Account armory ready · Applies when you return to the menu';
} catch (error) { if (valid(version)) { unsupported = /app-data-(unavailable|no-relays)/.test(error instanceof Error ? error.message : ''); message = errorMessage(error); } }
finally { if (version === generation) { busy = false; present(); } }
}
return {
async select(pubkey: string) {
owner = pubkey; const version = ++generation;
collection = undefined; supported = false; unsupported = false; remote = undefined; remoteDigest = undefined; uncertain = undefined; busy = false; importAvailable = false;
message = pubkey ? 'Loading account armory…' : 'Sign in to sync your armory'; present();
if (!pubkey) {
busy = true; message = 'Checking sync support…'; present();
try {
const opened = await deadline(open);
if (version !== generation) return;
collection = opened; supported = true; message = 'Sign in to sync your armory';
} catch {
if (version === generation) { unsupported = true; message = 'This runtime does not support the features needed for sync.'; }
} finally { if (version === generation) { busy = false; present(); } }
return;
}
if (store.owner() !== pubkey) return;
let available = false;
try { available = armoryDigest(profileArmory(await store.guestProfile())) !== armoryDigest(emptyArmory()); } catch { /* Keep corrupt legacy data local. */ }
if (!valid(version)) return;
importAvailable = available;
await restore();
},
suspend() {
++generation; owner = ''; supported = false; unsupported = false; remote = undefined; uncertain = undefined; collection = undefined; busy = false;
message = 'Account changed · Player save switches after this mission'; present();
},
changed() {
if (owner && owner === store.owner()) message = 'Changes saved on this device · Ready to save to account';
present();
},
menu() { reconcile(); present(); },
restore,
enable() {
if (!owner || store.owner() !== owner || !game()?.isSafe() || !collection || busy) return;
store.set('blacksite.armorySync', { ...preferences(), enabled: true });
message = 'Armory sync enabled · Ready to save to account'; present();
},
async importGuest() {
if (!owner || busy || !game()?.isSafe() || preferences().imported) return;
const version = generation;
const source = profileArmory(await store.guestProfile());
if (!valid(version) || !game()?.isSafe()) return;
game()!.applyArmory(mergeArmory(current(), source));
store.set('blacksite.armorySync', { ...preferences(), imported: true });
message = 'Device armory imported · Ready to save to account'; present();
},
async save() {
if (busy || !owner || store.owner() !== owner || !game()?.isSafe() || !preferences().enabled) return;
const version = generation;
const player = owner;
busy = true; message = 'Saving armory to account…'; present();
try {
const base = await read(version);
if (!valid(version) || !game()?.isSafe()) return;
const snapshot = current();
const digest = armoryDigest(snapshot);
if (digest === remoteDigest) { uncertain = undefined; message = 'Armory is up to date'; return; }
let change: Prepared;
if (uncertain && uncertain.digest === digest && uncertain.change.template.content &&
JSON.parse(uncertain.change.template.content).previous === (base?.revision ?? null)) {
change = uncertain.change;
} else {
change = await deadline(() => collection!.prepare({ id: 'progress', title: 'Black Site armory', data: snapshot, base: base ?? null }));
}
if (!valid(version) || !game()?.isSafe()) return;
uncertain = { change, digest };
const saved = await deadline(() => change.publish(), 60000);
if (!valid(version) || saved.author !== player) return;
remoteDigest = digest;
uncertain = undefined;
message = armoryDigest(current()) === digest ? 'Saved to account' : 'Saved to account · Newer changes still on this device';
} catch (error) {
if (valid(version)) {
if (/stale|conflict|denied|consent|permission/i.test(error instanceof Error ? error.message : '')) uncertain = undefined;
message = errorMessage(error);
}
} finally { if (version === generation) { busy = false; present(); } }
},
close() { ++generation; owner = ''; collection = undefined; remote = undefined; uncertain = undefined; },
};
}
export type ArmorySync = ReturnType<typeof createArmorySync>;
