session restore added
This commit is contained in:
parent
71a428ee48
commit
4f01b3f1af
1 changed files with 109 additions and 15 deletions
124
index.html
124
index.html
|
|
@ -1755,19 +1755,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- seed with the example problem ---
|
// --- seed with the example problem ---
|
||||||
addCity({name: "0", type: "hub", renown: 2});
|
// Only used for a fresh tab; a tab reload/restore refills the form from
|
||||||
addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
|
// sessionStorage instead (see restoreSession below).
|
||||||
addCity({name: "2", type: "hub", renown: 2});
|
function seedDefault() {
|
||||||
addCity({name: "3", type: "foundry", renown: 2});
|
addCity({name: "0", type: "hub", renown: 2});
|
||||||
addCity({name: "4", type: "monument", renown: 2});
|
addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
|
||||||
addAgent({kind: "Planner"});
|
addCity({name: "2", type: "hub", renown: 2});
|
||||||
addTerms({
|
addCity({name: "3", type: "foundry", renown: 2});
|
||||||
"renown": 0,
|
addCity({name: "4", type: "monument", renown: 2});
|
||||||
"luxuries": 1,
|
addAgent({kind: "Planner"});
|
||||||
"steel": 2,
|
addTerms({
|
||||||
"brass": 1,
|
"renown": 0,
|
||||||
"electrum": 2
|
"luxuries": 1,
|
||||||
})
|
"steel": 2,
|
||||||
|
"brass": 1,
|
||||||
|
"electrum": 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- deep links ---
|
// --- deep links ---
|
||||||
// /?solve=<id> loads one solve; /?solves=<id1>,<id2>,… loads an arbitrary
|
// /?solve=<id> loads one solve; /?solves=<id1>,<id2>,… loads an arbitrary
|
||||||
|
|
@ -1785,13 +1789,103 @@
|
||||||
document.getElementById("output").prepend(card);
|
document.getElementById("output").prepend(card);
|
||||||
pending.set(token, {token, card, n, confirmed: true});
|
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 params = new URLSearchParams(location.search);
|
||||||
const sharedTokens = (params.get("solves") || params.get("solve") || "")
|
const sharedTokens = (params.get("solves") || params.get("solve") || "")
|
||||||
.split(",").map(s => s.trim()).filter(Boolean);
|
.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).
|
// Load in reverse so the first id ends up on top (each load prepends).
|
||||||
for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token);
|
for (const token of newShared.slice().reverse()) loadSharedSolve(token);
|
||||||
if (sharedTokens.length) syncStream();
|
if (newShared.length) syncStream();
|
||||||
updateNotifyUi();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue