session restore added

This commit is contained in:
Pagwin 2026-07-05 02:59:30 -04:00
parent 71a428ee48
commit 4f01b3f1af

View file

@ -1755,6 +1755,9 @@
}
// --- seed with the example problem ---
// Only used for a fresh tab; a tab reload/restore refills the form from
// sessionStorage instead (see restoreSession below).
function seedDefault() {
addCity({name: "0", type: "hub", renown: 2});
addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
addCity({name: "2", type: "hub", renown: 2});
@ -1767,7 +1770,8 @@
"steel": 2,
"brass": 1,
"electrum": 2
})
});
}
// --- deep links ---
// /?solve=<id> loads one solve; /?solves=<id1>,<id2>,… loads an arbitrary
@ -1785,13 +1789,103 @@
document.getElementById("output").prepend(card);
pending.set(token, {token, card, n, confirmed: true});
}
// --- session persistence -------------------------------------------
// The browser natively restores plain form controls (the Game inputs,
// start-resource numbers, tradeable checkboxes) on a reload / "reopen
// closed tab", but not the SPA-built cards (cities, agents, terms, …) nor
// the queue of solve cards under #output — those are recreated by script
// each load. We mirror both into sessionStorage (per-tab, and preserved
// across tab restore) so the whole page comes back as it was left.
const SESSION_KEY = "dws_session";
// Tokens of every solve card currently on the page, top-to-bottom.
function currentTokens() {
return [...document.querySelectorAll("#output [data-token]")]
.map(e => e.dataset.token);
}
function saveSession() {
// buildProblem() samples log terms and can throw on a half-typed
// expression; if so, keep whatever problem we last stored so a
// transiently-invalid form doesn't wipe the saved inputs.
let problem = null, maxTime;
try {
problem = buildProblem();
maxTime = +document.getElementById("time").value;
} catch (e) {/* keep previously-saved problem */}
let prev = {};
try {prev = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "{}");}
catch (e) {/* ignore corrupt/absent state */}
const data = {
problem: problem || prev.problem || null,
maxTime: problem ? maxTime : prev.maxTime,
tokens: currentTokens(),
solutionCount,
};
try {sessionStorage.setItem(SESSION_KEY, JSON.stringify(data));}
catch (e) {/* storage may be full/unavailable; state just won't persist */}
}
// Debounced: coalesce bursts of edits (and card add/remove mutations)
// into a single write.
let saveTimer = null;
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(saveSession, 300);
}
// Refill the form and re-open the solve queue from a prior session.
// Returns true if there was a session to restore.
function restoreSession() {
let data;
try {data = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "null");}
catch (e) {return false;}
if (!data) return false;
if (data.problem) applyProblem(data.problem, data.maxTime);
const tokens = data.tokens || [];
// Reserve a card per token and let the shared /job_status stream
// resolve each (rendering a finished solve fetched from the server,
// or tracking one still queued/running). Load in reverse so the
// first token ends up on top, mirroring how they were prepended.
for (const token of tokens.slice().reverse()) loadSharedSolve(token);
if (tokens.length) syncStream();
// Keep future solve numbering above anything from the old session.
if (typeof data.solutionCount === "number")
solutionCount = Math.max(solutionCount, data.solutionCount);
return true;
}
// --- startup ---
const restored = restoreSession();
if (!restored) seedDefault();
// Deep links still work on top of a restored session; skip any token the
// restored queue already holds so it isn't duplicated.
const params = new URLSearchParams(location.search);
const sharedTokens = (params.get("solves") || params.get("solve") || "")
.split(",").map(s => s.trim()).filter(Boolean);
const present = new Set(currentTokens());
const newShared = sharedTokens.filter(t => !present.has(t));
// Load in reverse so the first id ends up on top (each load prepends).
for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token);
if (sharedTokens.length) syncStream();
for (const token of newShared.slice().reverse()) loadSharedSolve(token);
if (newShared.length) syncStream();
updateNotifyUi();
// Wire autosave only after the initial load has settled, so the seed /
// restore / deep-link work above doesn't churn through it. Edits to any
// control save; card add/remove and queue changes save via observers.
document.addEventListener("input", scheduleSave);
document.addEventListener("change", scheduleSave);
const cardObserver = new MutationObserver(scheduleSave);
for (const id of ["cities", "agents", "terms", "constraints",
"conversions", "optional_conversions"])
cardObserver.observe(document.getElementById(id), {childList: true});
// The solve queue: card add/remove plus the data-token that appears when a
// pending card is replaced by its rendered solution.
new MutationObserver(scheduleSave).observe(document.getElementById("output"),
{childList: true, subtree: true, attributes: true, attributeFilter: ["data-token"]});
saveSession();
</script>
</body>