clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / src / scenes / progress.js
progress.js 2.8K
1/** Progress persistence for the INDEX scene. */23export const PROGRESS_PREFIX = 'miau.index.progress/';45function nonEmpty(value, label) {6 if (typeof value !== 'string' || value.length === 0) {7 throw new TypeError(`${label} must be a non-empty string`);8 }9 return value;10}1112function progressKey(packId, levelId) {13 return `${nonEmpty(packId, 'packId')}/${nonEmpty(levelId, 'levelId')}`;14}1516/** The storage key used for one proof. It is intentionally human-readable. */17export function getProgressKey(packId, levelId) {18 return `${PROGRESS_PREFIX}${progressKey(packId, levelId)}`;19}2021function storageWorks(storage) {22 return storage && typeof storage.getItem === 'function'23 && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function';24}2526/**27 * A localStorage backed solved set. A memory fallback keeps private browsing28 * and server side rendering usable without changing the scene API.29 */30export function createProgressStore(storage) {31 let candidate = storage;32 if (candidate === undefined) {33 try { candidate = globalThis.localStorage; } catch { candidate = null; }34 }35 const backing = storageWorks(candidate) ? candidate : null;36 const memory = new Set();37 const read = key => {38 if (memory.has(key)) return true;39 if (!backing) return false;40 try {41 const solved = backing.getItem(key) === '1';42 if (solved) memory.add(key);43 return solved;44 } catch { return false; }45 };46 const write = (key, solved) => {47 if (solved) memory.add(key); else memory.delete(key);48 if (!backing) return;49 try {50 if (solved) backing.setItem(key, '1');51 else backing.removeItem(key);52 } catch { /* A full or blocked store still has the memory fallback. */ }53 };54 return Object.freeze({55 key: getProgressKey,56 isSolved(packId, levelId) { return read(getProgressKey(packId, levelId)); },57 get(packId, levelId) { return read(getProgressKey(packId, levelId)); },58 setSolved(packId, levelId, solved = true) {59 const key = getProgressKey(packId, levelId);60 write(key, Boolean(solved));61 return Boolean(solved);62 },63 markSolved(packId, levelId) { write(getProgressKey(packId, levelId), true); return true; },64 clear(packId, levelId) { write(getProgressKey(packId, levelId), false); },65 clearAll() {66 for (const key of [...memory]) write(key, false);67 if (!backing) return;68 try {69 const keys = [];70 for (let index = 0; index < backing.length; index++) {71 const key = backing.key(index);72 if (key?.startsWith(PROGRESS_PREFIX)) keys.push(key);73 }74 keys.forEach(key => backing.removeItem(key));75 } catch { /* Clearing memory remains safe when storage is unavailable. */ }76 },77 snapshot() { return [...memory].map(key => key.slice(PROGRESS_PREFIX.length)); },78 });79}