LLearnCode
Academy/games/Production game systems

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.

Loading progressEvidence grows with practice
Useful preparation · 1 prerequisite

Use this as a map, not a lock. If these ideas are familiar, continue.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
Read the codetypescript · educational example
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.
  1. Shared starting point

    Two devices both read revision 4. Each can produce a valid local proposal.

  2. First write

    One request matches revision 4 and produces revision 5.

  3. 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 →