Files
idle_clicker/public/js/services/storage.js
T

107 lines
3.2 KiB
JavaScript

// Persistence in browser cookies.
//
// A cookie holds ~4 KB, so a save is base64url-encoded and split in chunks:
// ic_save_<theme>_n -> number of chunks
// ic_save_<theme>_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 {};
}
}