107 lines
3.2 KiB
JavaScript
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 {};
|
|
}
|
|
}
|