Files
the-beacon/js/state-export.js
Alexander Whitestone 6d4b8d86f3
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Successful in 3s
Smoke Test / smoke (pull_request) Failing after 6s
feat: export beacon state snapshots for #166
2026-04-17 02:47:20 -04:00

119 lines
3.7 KiB
JavaScript

(function (global) {
const STORE_KEY = 'compounding-intelligence:beacon-state';
const MAX_SNAPSHOTS = 300;
function _safeNumber(value, fallback = 0) {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
function _tickKey(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) return '0';
return value.toFixed(3);
}
function _resolveSink(explicitSink) {
if (explicitSink) return explicitSink;
return global.CompoundingIntelligence || null;
}
function _resolveStorage(explicitStorage) {
if (explicitStorage) return explicitStorage;
return typeof global.localStorage !== 'undefined' ? global.localStorage : null;
}
function buildSnapshot(gameState = {}) {
return {
source: 'the-beacon',
kind: 'idle_game_state',
timestamp: new Date().toISOString(),
tick: _safeNumber(gameState.tick, 0),
phase: _safeNumber(gameState.phase, 1),
trust: _safeNumber(gameState.trust, 0),
resources: {
code: _safeNumber(gameState.code, 0),
compute: _safeNumber(gameState.compute, 0),
knowledge: _safeNumber(gameState.knowledge, 0),
users: _safeNumber(gameState.users, 0),
impact: _safeNumber(gameState.impact, 0),
ops: _safeNumber(gameState.ops, 0),
},
project_progress: {
active: Array.isArray(gameState.activeProjects) ? [...gameState.activeProjects] : [],
completed: Array.isArray(gameState.completedProjects) ? [...gameState.completedProjects] : [],
active_count: Array.isArray(gameState.activeProjects) ? gameState.activeProjects.length : 0,
completed_count: Array.isArray(gameState.completedProjects) ? gameState.completedProjects.length : 0,
},
};
}
function readStore({ storage, storeKey = STORE_KEY } = {}) {
const resolved = _resolveStorage(storage);
if (!resolved) return [];
try {
const raw = resolved.getItem(storeKey);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (_) {
return [];
}
}
function writeStore(entries, { storage, storeKey = STORE_KEY } = {}) {
const resolved = _resolveStorage(storage);
if (!resolved) return false;
resolved.setItem(storeKey, JSON.stringify(entries));
return true;
}
function writeSnapshot(snapshot, { storage, storeKey = STORE_KEY, sink } = {}) {
const entries = readStore({ storage, storeKey });
entries.push(snapshot);
while (entries.length > MAX_SNAPSHOTS) entries.shift();
writeStore(entries, { storage, storeKey });
const resolvedSink = _resolveSink(sink);
if (resolvedSink && typeof resolvedSink.ingestSnapshot === 'function') {
resolvedSink.ingestSnapshot(snapshot);
}
if (typeof global.dispatchEvent === 'function' && typeof global.CustomEvent === 'function') {
global.dispatchEvent(new global.CustomEvent('compounding-intelligence:state-export', { detail: snapshot }));
}
return snapshot;
}
function onTickBoundary(gameState, options = {}) {
const snapshot = buildSnapshot(gameState);
const key = _tickKey(snapshot.tick);
if (onTickBoundary._lastTickKey === key) return null;
onTickBoundary._lastTickKey = key;
return writeSnapshot(snapshot, options);
}
function resetTickBoundary() {
onTickBoundary._lastTickKey = null;
}
resetTickBoundary();
const api = {
STORE_KEY,
MAX_SNAPSHOTS,
buildSnapshot,
readStore,
writeStore,
writeSnapshot,
onTickBoundary,
resetTickBoundary,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
global.StateExport = api;
})(typeof window !== 'undefined' ? window : globalThis);