Appearance
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
getStatemust 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
keyper game per origin; namespace it ("studio:my-game") if you host several games on one domain.