- commit
- 308450f03f151b1502baa15e95c6760d74aab45c
- parent
- b772422c8c1aabda6e5054b1178a0577eb26a7ad
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 87 | ++---------------------------- |
| A | src/ | 79 | +++++++++++++++++++++++++++ |
@@ -5,90 +5,17 @@
55 * catalog's stable pack/level records into serpentine sheets and provides the66 * small amount of progress and cursor state the scene needs.77 */8-9-export const INDEX_COLUMNS = 5;10-export const INDEX_ROWS = 3;11-export const INDEX_NODES_PER_SHEET = INDEX_COLUMNS * INDEX_ROWS;12-export const PROGRESS_PREFIX = 'miau.index.progress/';13-14-const clone = value => typeof structuredClone === 'function'15- ? structuredClone(value)16- : JSON.parse(JSON.stringify(value));8+import { cloneData } from '../data/clone.js';9+export { PROGRESS_PREFIX, getProgressKey, createProgressStore } from './progress.js';1710 1811 function nonEmpty(value, label) {1912 if (typeof value !== 'string' || value.length === 0) throw new TypeError(`${label} must be a non-empty string`);2013 return value;2114 }2215 23-function progressKey(packId, levelId) {24- return `${nonEmpty(packId, 'packId')}/${nonEmpty(levelId, 'levelId')}`;25-}26-27-/** The storage key used for one proof. It is intentionally human-readable. */28-export function getProgressKey(packId, levelId) {29- return `${PROGRESS_PREFIX}${progressKey(packId, levelId)}`;30-}31-32-function storageWorks(storage) {33- return storage && typeof storage.getItem === 'function'34- && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function';35-}36-37-/**38- * A localStorage backed solved set. A memory fallback keeps private browsing39- * and server side rendering usable without changing the scene API.40- */41-export function createProgressStore(storage) {42- let candidate = storage;43- if (candidate === undefined) {44- try { candidate = globalThis.localStorage; } catch { candidate = null; }45- }46- const backing = storageWorks(candidate) ? candidate : null;47- const memory = new Set();48- const read = key => {49- if (memory.has(key)) return true;50- if (!backing) return false;51- try {52- const solved = backing.getItem(key) === '1';53- if (solved) memory.add(key);54- return solved;55- } catch { return false; }56- };57- const write = (key, solved) => {58- if (solved) memory.add(key); else memory.delete(key);59- if (!backing) return;60- try {61- if (solved) backing.setItem(key, '1');62- else backing.removeItem(key);63- } catch { /* A full or blocked store still has the memory fallback. */ }64- };65- return Object.freeze({66- key: getProgressKey,67- isSolved(packId, levelId) { return read(getProgressKey(packId, levelId)); },68- get(packId, levelId) { return read(getProgressKey(packId, levelId)); },69- setSolved(packId, levelId, solved = true) {70- const key = getProgressKey(packId, levelId);71- write(key, Boolean(solved));72- return Boolean(solved);73- },74- markSolved(packId, levelId) { write(getProgressKey(packId, levelId), true); return true; },75- clear(packId, levelId) { write(getProgressKey(packId, levelId), false); },76- clearAll() {77- for (const key of [...memory]) write(key, false);78- if (!backing) return;79- try {80- const keys = [];81- for (let index = 0; index < backing.length; index++) {82- const key = backing.key(index);83- if (key?.startsWith(PROGRESS_PREFIX)) keys.push(key);84- }85- keys.forEach(key => backing.removeItem(key));86- } catch { /* Clearing memory remains safe when storage is unavailable. */ }87- },88- snapshot() { return [...memory].map(key => key.slice(PROGRESS_PREFIX.length)); },89- });90-}91-16+export const INDEX_COLUMNS = 5;17+export const INDEX_ROWS = 3;18+export const INDEX_NODES_PER_SHEET = INDEX_COLUMNS * INDEX_ROWS;9219 function levelEntries(source) {9320 if (Array.isArray(source)) return source;9421 if (source && typeof source.getLevels === 'function') return source.getLevels();@@ -164,7 +91,7 @@ function makeSheet(group, sheetEntries, sheetIndex, globalSheetIndex, allSheetId
16491 column: coordinate.column,16592 x: coordinate.column,16693 y: coordinate.row,167- level: clone(entry.level),94+ level: cloneData(entry.level),16895 };16996 });17097 for (const node of nodes) {@@ -318,7 +245,7 @@ export function createIndexNavigator(layout, options = {}) {
318245 const node = current();319246 if (!node) return { ok: false, reason: 'empty' };320247 if (node.locked) return { ok: false, reason: 'locked', node };321- return { ok: true, packId: node.packId, levelId: node.levelId, level: clone(node.level), node };248+ return { ok: true, packId: node.packId, levelId: node.levelId, level: cloneData(node.level), node };322249 }323250 return {324251 get current() { return state(); },@@ -0,0 +1,79 @@
1+/** Progress persistence for the INDEX scene. */2+3+export const PROGRESS_PREFIX = 'miau.index.progress/';4+5+function 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+}11+12+function progressKey(packId, levelId) {13+ return `${nonEmpty(packId, 'packId')}/${nonEmpty(levelId, 'levelId')}`;14+}15+16+/** The storage key used for one proof. It is intentionally human-readable. */17+export function getProgressKey(packId, levelId) {18+ return `${PROGRESS_PREFIX}${progressKey(packId, levelId)}`;19+}20+21+function storageWorks(storage) {22+ return storage && typeof storage.getItem === 'function'23+ && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function';24+}25+26+/**27+ * A localStorage backed solved set. A memory fallback keeps private browsing28+ * and server side rendering usable without changing the scene API.29+ */30+export 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+}