// Persistence in browser cookies. // // A cookie holds ~4 KB, so a save is base64url-encoded and split in chunks: // ic_save__n -> number of chunks // ic_save__0.. -> the chunks // Each theme has its own save slot. Preferences (theme, language) are stored // in a single small cookie. import { readCookie, removeCookie, writeCookie } from './cookies.js'; const CHUNK_SIZE = 3000; const MAX_CHUNKS = 6; const PREFS_KEY = 'ic_prefs'; const saveKey = (themeId) => `ic_save_${themeId}`; // ------------------------------------------------------------------ encoding export function encode(text) { const bytes = new TextEncoder().encode(text); let binary = ''; for (const b of bytes) binary += String.fromCharCode(b); return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); } export function decode(encoded) { const base64 = encoded.replaceAll('-', '+').replaceAll('_', '/'); const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)); return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0))); } // ------------------------------------------------------------------- chunked function writeChunked(key, text) { const encoded = encode(text); const chunks = []; for (let i = 0; i < encoded.length; i += CHUNK_SIZE) chunks.push(encoded.slice(i, i + CHUNK_SIZE)); if (chunks.length > MAX_CHUNKS) throw new Error('save_too_large'); const previous = Number(readCookie(`${key}_n`)) || 0; chunks.forEach((chunk, i) => writeCookie(`${key}_${i}`, chunk)); writeCookie(`${key}_n`, String(chunks.length)); for (let i = chunks.length; i < previous; i++) removeCookie(`${key}_${i}`); } function readChunked(key) { const count = Number(readCookie(`${key}_n`)); if (!Number.isInteger(count) || count < 1 || count > MAX_CHUNKS) return null; let encoded = ''; for (let i = 0; i < count; i++) { const chunk = readCookie(`${key}_${i}`); if (chunk == null) return null; encoded += chunk; } try { return decode(encoded); } catch { return null; } } function removeChunked(key) { const count = Number(readCookie(`${key}_n`)) || 0; for (let i = 0; i < count; i++) removeCookie(`${key}_${i}`); removeCookie(`${key}_n`); } // -------------------------------------------------------------------- public /** False when the browser refuses cookies (private mode, blocked, ...). */ export function cookiesAvailable() { try { writeCookie('ic_probe', '1'); const ok = readCookie('ic_probe') === '1'; removeCookie('ic_probe'); return ok; } catch { return false; } } /** Throws if the save does not fit in cookies. */ export function saveGameText(themeId, text) { writeChunked(saveKey(themeId), text); } export function loadGameText(themeId) { return readChunked(saveKey(themeId)); } export function deleteGame(themeId) { removeChunked(saveKey(themeId)); } export function savePrefs(prefs) { writeCookie(PREFS_KEY, encode(JSON.stringify(prefs))); } export function loadPrefs() { try { const raw = readCookie(PREFS_KEY); const prefs = raw && JSON.parse(decode(raw)); return prefs && typeof prefs === 'object' ? prefs : {}; } catch { return {}; } }