Skip to content

Save & load in the browser

Versioned localStorage saves with autosave and a reset button — the persistence setup every shipped web game needs.

Setup

ts
import { createEngine, SaveManager } from "@idlekitjs/core";
import { LocalStorageAdapter } from "@idlekitjs/storage/local-storage";
import { autosave } from "@idlekitjs/plugins/autosave";

interface State {
  coins: number;
  generators: number;
}

const engine = createEngine<State>({
  initialState: { coins: 0, generators: 0 },
  // renderer + scheduler as usual
});

const save = new SaveManager<State>({
  key: "my-game",
  version: 1,
  adapter: new LocalStorageAdapter(),
});

engine.use(autosave({ manager: save, getState: () => engine.state, intervalMs: 15_000 }));

Autosave now writes every 15 s, when the tab hides, and on page close.

Load before start

ts
const hadSave = await engine.load(save); // false on first run — fine
engine.start();

engine.load restores the state and emits loaded, the event every extension restores from. Always load through the game, and before engine.start() (no initial-state flash).

Save on meaningful moments

The interval is a safety net; important progress deserves an immediate write. Mechanics expose callbacks exactly for this:

ts
const projectsExt = projects<State>({
  // ...
  onComplete: (_id, state) => void save.save(state),
});

Or manually after any big action: void save.save(engine.state);

Reset button

ts
const resetEl = document.querySelector<HTMLButtonElement>("#reset")!;
resetEl.addEventListener("click", () => {
  if (confirm("Wipe your save?")) {
    void save.clear().then(() => location.reload());
  }
});

Reloading after clear() is the honest reset: every extension restarts from initialState with no half-cleared runtime.

Evolving the schema

When State changes shape, bump the version and add a migration — old saves upgrade transparently on load:

ts
const save = new SaveManager<State>({
  key: "my-game",
  version: 2,
  adapter: new LocalStorageAdapter(),
  migrations: {
    2: (data) => ({ ...(data as object), generators: 0 }), // new field, default
  },
});

Unversioned/corrupted saves come back as null from load() — the game starts fresh instead of crashing. See Saving your game for multi-step chains.

Gotchas

  • getState must return the live state (() => engine.state), not a captured snapshot.
  • Keep the state JSON-safe (State) — Maps and class instances don't survive the round-trip.
  • One key per game per origin; namespace it ("studio:my-game") if you host several games on one domain.