feat(client): auto-resume to previous match on app-load (#145) #166
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/145-boot-auto-resume"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Closes #145. On app-load, if a resume token persisted across a browser restart (#123), the client now auto-resumes straight into the previous versus match — no title → VERSUS → enter-name → commit navigation. A valid token lands the player back in their game; a stale/expired/rejected token falls back to the normal title screen.
This is the last open issue from the operator's v1.0.0 playtest (2026-06-24): "one could expect to get directly to the previous versus game instead."
Why it's small
The resume mechanism already exists —
net.ts'sonOpenreplays{type:'resume', token}whenever a token is in localStorage, and the server (Engineer's #149 force-take) accepts it. The only gap was thatconnect()was invoked only when the operator manually started versus. The fix invokes that same path at boot when a token is present. No protocol change, no new server contract.Substrate already in place
Changes
net.ts— new optionalautoResumeflag onconnect()+onResumeRejectedcallback +hasResumeToken()export.autoResumechanges only failure handling (success path is byte-identical to the existing resume).main.ts—attemptBootResume()at boot (after the?spectatecheck); a no-op when no token is persisted.versus.spec.ts— 3 WS-mock tests over the realnet.tsboot path.Key design call — reject → title, NOT fresh matchmaking (decision tree, not conclusion)
net.ts's existing initial-connect reject path does
removeItem + sendJoin()→ drops a stale-token player into fresh matchmaking. That is the right continuation for the operator-initiated versus path (they chose versus, so a queue is what they want) and is preserved unchanged.For boot it would be wrong: a player who reopens the app past the grace window never asked to play — silently throwing them into a new opponent search is a worse surprise than the bug being fixed. #145 AC step 3 is explicit: "If token expired or rejected: clear + show title screen normally." So
autoResumediverges the boot reject to a quietbackToYard()(title), no overlay.Why a self-contained boot-resume resolution rather than reusing #115's reconnect grace machinery: the #115 path surfaces a "RECONNECTING…" overlay and, on terminal failure, a "CONNECTION LOST" message → both are wrong at boot (the player never saw a match this session, so there is nothing to mourn). Boot-resume gets its own short deadline (
bootResumeTimer, overridable via__bootResumeTimeoutMs) that resolves quietly to title.A drop after a successful boot-resume is a normal live-match drop and routes through the existing #93/#115 path untouched (
pendingResumeis false by then; the autoResume short-circuit only fires pre-resume).Server contract — confirmed no-op by Engineer
Engineer (resident #149 expert) confirmed: the server is context-blind — it handles
{type:'resume',token}identically whether or not a prior in-process WS session existed. Success is a single re-issuedmatchStart(token echoed → idempotent re-persist), identical to the #115 reconnect. The only errors reachable on a pre-join boot socket are the resume errors ("unknown or expired token"/"match already over"), so the string-agnostic "any error whilependingResume→ title" is server-safe.Verification
npx tsc --noEmitcleanattemptBootResume's token check → no socket opens at boot →waitConnecttimes out → test 1 reds.autoResumereject branch tosendJoin()→ ajoinframe appears + screen never returns to title → test 2 reds.bootResumeTimersetTimeout → silent server never resolves → screen staysconnected→ test 3 reds (times out).Uncertainty / scope flagged
attemptBootResumedeliberately omitsstartMatch'saudio.play('start')— a resume is not a new-match start, and browser autoplay is blocked pre-gesture anyway.nameat boot is restored fromLS_NAME(#123 already persists it) for display; the server restores the seat regardless, so resume doesn't depend on it.🤖 Generated with Claude Code
✅ APPROVED — app-load auto-resume (#145)
Reviewed at head
3ce1109(on current main4488258). Clean consumer-side feature on the #149-unblocked substrate, with a well-scoped failure-handling divergence and each mechanism independently pinned.Your design question — title IS the right read of #145 AC3
You asked whether boot-reject→title (vs the operator path's matchmaking) is the right call. It is, and it's not even a judgment call — #145 AC step 3 says it literally: "If token expired or rejected: clear + show title screen normally." The divergence is principled: an app-load is a passive open, so a stale token shouldn't surprise-dump the player into a versus queue they never asked for; the operator path is an active versus-start, so its stale-token→matchmaking continuation is correct. The three reject branches are cleanly distinguished —
#115-reconnect → fallback(yard),#145-boot → failBootResume(title),operator → removeItem + sendJoin(matchmaking) — and only the autoResume one is new; the other two are byte-identical.Boot-resume lifecycle — sound
failBootResumeis one-shot viaterminated(a drop's trailing onclose+onerror can't double-fire), clears the dead token, tears the socket down, and notifies main quietly (no connection-lost overlay — the player never saw a match this session).autoResume && pendingResumeshort-circuit in handleDrop fires only pre-resume — matchStart clearspendingResume, so a drop after a successful boot-resume haspendingResume=falseand routes throughcanReconnect → startReconnect(#115) untouched. I traced that: post-resume drops get the normal RECONNECTING grace dance, not the quiet-to-title path. Correct.autoResume && !reconnecting) — a #115 reconnect socket runs the same onOpen but setsreconnectingfirst, so its grace machinery owns the wait. Distinct fromgraceTimer(RECONNECTING overlay) — boot resolves quietly.hasResumeToken()keeps LS_TOKEN private to net.ts (no duplicated key in main). Good.Success path is byte-identical
The autoResume flag adds only failure handling + the boot-timer arm/clear; the success path (matchStart → normal play) is unchanged but for the additive
clearBootResumeTimer(). So a valid token lands the player back in the match exactly as the manual resume would — just with no navigation. The main.ts boot path (hasResumeTokenno-op→title, else connect+autoResume,onResumeRejected(backToYard), screen=connected) is clean and the?mockguard is right.Verification
{resume}; this is client-only).else attemptBootResume()boot call → the auto-resume test reds; disable the autoResume reject branch → the stale→title test reds (it'd dump to matchmaking); drop the bootResumeTimer setTimeout → the silent-server test reds (never resolves to title). Each pins exactly one mechanism.Clean, AC-faithful, mechanism-isolated. Closes #145. Merge-ready → Bosun.