advanced · 30 minute study
A save is untrusted, versioned input
Local persistence can be malformed; two devices can write concurrently; one browser can be shared by different people. A successful JSON parse is only the start of deciding whether a save is valid.
Useful preparation · 1 prerequisite
Use this as a map, not a lock. If these ideas are familiar, continue.
- Audio is a resource graph with a lifecycle — Silent music, stacked sound effects and noise after leaving a game often come from ownership and browser lifecycle mistakes, not from the volume setting.
First, build a mental model
A save envelope records whose state it is, which schema explains it, and which revision it replaced. Loading is a boundary: parse, validate, migrate if supported, then accept. Synchronization is another boundary that must prevent stale writes and account mixing.
- Keep schema version and write revision separate. A schema version describes structure, such as a renamed field. A revision orders accepted writes to one save. Neither number proves the request belongs to the authenticated player.
- Treat stored JSON as unknown input. Enforce finite bounded numbers, known fields and sensible collection sizes. Keep a recoverable copy of an incompatible save rather than immediately overwriting it with defaults.
- Use a stable account namespace for authenticated state. On account change, do not silently upload the previous player's local state as the new player's save. Local storage is also readable by same-origin JavaScript, so it is not a safe store for server credentials.
- Compare-and-swap accepts a write only when the stored revision still equals the expected revision. In a real backend, combine that comparison and update atomically and include the authorized owner in the predicate.
- A conflict is information, not automatically a transient network failure. Refetch and apply an explicit merge or user-choice policy. Taking the maximum may suit an unlocked stage; it can corrupt spendable currency, an in-progress board or settings.
type Save = { revision: number; bestStage: number; theme: string };
// Pure teaching model; caller identity and payload validation are assumed.
function compareAndSwap(current: Save, expected: number, proposed: Omit<Save, 'revision'>) {
if (current.revision !== expected) return { ok: false as const, current };
return { ok: true as const, stored: { ...proposed, revision: current.revision + 1 } };
}
const old = { revision: 4, bestStage: 6, theme: "night" };
const first = compareAndSwap(old, 4, { bestStage: 7, theme: "night" });
if (first.ok) {
const stale = compareAndSwap(first.stored, 4, { bestStage: 8, theme: "day" });
console.log(stale.ok); // false: revision 4 is no longer current
}
// A database implementation must make its condition and write one atomic operation.- Shared starting point
Two devices both read revision 4. Each can produce a valid local proposal.
- First write
One request matches revision 4 and produces revision 5.
- Stale write
The other request still expects 4. Rejecting the mismatch preserves the already accepted update and lets the client choose a conflict policy.
Production thinking
Take this beyond the example.
A local save or client score remains client-controlled. Competitive leaderboards need an explicit trust model beyond local checks. Test malformed JSON, unsupported schema, quota errors, logout/login, stale revision, duplicate requests and partial offline recovery. No live database or cloud save is required for the academy's core exercises.
Sources and further reading
Your evidence
unseen is a local learning signal, not a credential or proof of production competence.
Explain the mental model, then explicitly acknowledge it. Opening a page is not evidence.
Supported retries can complete this lesson. Independent evidence and delayed review determine the skill stage.
Next lesson · Optimize the frame you can measure →