clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

The tap that took the update is handed back

Dami committed a2c47c7
Applying a new version at the start of a board was right about when and wrong
about what it costs. I wrote that the reload costs a player the wait and nothing
else; a screenshot of it says otherwise. They tap a puzzle, the app reloads, and
they are looking at the title window. The tap is gone, and what they actually
got for pressing the button was a place at the back of the queue.

So the reload carries their intent. The family they were opening goes into
session storage on the way out and is read back once on the way in, and the app
opens it rather than starting at the title. Session storage and not local, so it
cannot outlive the tab and drop somebody onto a board they never picked; read
once and cleared, so any launch that is not that reload begins where it always
did.

This is the half of the pattern PROXX has that I had left out, and the reason it
has it.
commit
a2c47c76a2f0f82a41ea2d60c0689d6b9aa7d1b3
parent
65a78924d32ed61869df110711f0e2e9373cd0bf
author
Dami <dami.etoile@gmail.com>
3 files changed, +57 -9
Msrc/klotski.gleam26+++++++++++++++++++++-----
Msrc/klotski/browser.gleam15++++++++++++---
Msrc/klotski/browser_ffi.mjs25++++++++++++++++++++++++-
M src/klotski.gleam +21 -5
@@ -291,8 +291,7 @@ fn init(raw: String) -> #(Model, Effect(Message)) {
291291 probing: [],292292 walking: False,293293 )294- #(295- model,294+ let boot =296295 effect.batch([297296 browser.watch_frame(Ticked),298297 browser.watch_viewport(ViewportChanged),
@@ -316,8 +315,21 @@ fn init(raw: String) -> #(Model, Effect(Message)) {
316315 browser.load_fonts(FontsReady),317316 browser.register_service_worker(),318317 browser.request_persistence(),319- ]),320- )318+ ])319+ // A reload that was taken to install a new version hands back what the player320+ // had just asked for, so the update costs them the wait and genuinely nothing321+ // else. Any other launch starts at the title window, which is the only thing322+ // an empty resume can mean.323+ let resume =324+ families.all()325+ |> list.find(fn(family) { family.id == browser.take_resume() })326+ case resume {327+ Ok(family) -> {328+ let #(model, opened) = open_family(model, family)329+ #(model, effect.batch([boot, opened]))330+ }331+ Error(_) -> #(model, boot)332+ }321333 }322334 323335 // ------------------------------------------------------------------ update
@@ -879,7 +891,11 @@ fn begin_play(model: Model) -> #(Model, Effect(Message)) {
879891 // last board is finished with, and a reload costs a player the wait and880892 // nothing else. Almost every time there is nothing waiting and this does881893 // nothing at all.882- #(model, effect.batch([browser.apply_update(), page(model), paint(model)]))894+ #(model, effect.batch([895+ browser.apply_update(model.family.id),896+ page(model),897+ paint(model),898+ ]))883899 }884900 }885901 }
M src/klotski/browser.gleam +12 -3
M src/klotski/browser_ffi.mjs +24 -1
@@ -281,13 +281,24 @@ export function registerServiceWorker() {
281281 /// until the response arrives, so the old worker is still controlling and the282282 /// waiting one stays waiting. It has to be told to skip, and the reload has to283283 /// wait until it has actually taken over.284-export function applyUpdate() {284+const RESUME = "klotski.resume";285+286+export function applyUpdate(resume) {285287 if (!("serviceWorker" in navigator)) return;286288 navigator.serviceWorker287289 .getRegistration()288290 .then((registration) => {289291 const worker = registration && registration.waiting;290292 if (!worker) return;293+ // What the player was in the middle of asking for, kept across the reload294+ // so it can be handed straight back to them. Without this the reload lands295+ // on the title window and the tap that triggered it is simply gone —296+ // which is not "the wait and nothing else", it is being sent to the back297+ // of the queue for pressing the button. Session storage, so it cannot298+ // outlive the tab and strand somebody on a board they did not pick.299+ try {300+ sessionStorage.setItem(RESUME, resume);301+ } catch (_) {}291302 navigator.serviceWorker.addEventListener(292303 "controllerchange",293304 () => window.location.reload(),
@@ -298,6 +309,18 @@ export function applyUpdate() {
298309 .catch(() => {});299310 }300311 312+/// What the app was opening when it reloaded to take an update, once. Reading it313+/// clears it, so a later reload starts from the title window like any other.314+export function takeResume() {315+ try {316+ const value = sessionStorage.getItem(RESUME);317+ sessionStorage.removeItem(RESUME);318+ return value ?? "";319+ } catch (_) {320+ return "";321+ }322+}323+301324 // ------------------------------------------------------------- the surveyor302325 303326 // The build substitutes the worker's fingerprinted URL for `__SOLVER_WORKER__`