Appearance
Writing a custom adapter
Need IndexedDB, native storage, a sync server, or compression? Implement the three-method SaveAdapter contract and hand it to the SaveManager — no other line of your game changes.
The contract
ts
import type { SaveAdapter } from "@idlekitjs/core";
interface SaveAdapter {
read(key: string): string | null | Promise<string | null>;
write(key: string, value: string): void | Promise<void>;
remove(key: string): void | Promise<void>;
}Rules of the road:
- Values are opaque strings — the
SaveManagerowns the JSON envelope (version, timestamp); your adapter just stores bytes. - Sync or async both work — the manager awaits everything.
readmay throw or reject — the manager catches it and treats the save as absent. Prefer returningnullfor "no save"; reserve errors for real failures.
Example: sessionStorage
The smallest real adapter — persists for the tab session only:
ts
import type { SaveAdapter } from "@idlekitjs/core";
export class SessionStorageAdapter implements SaveAdapter {
read(key: string): string | null {
return sessionStorage.getItem(key);
}
write(key: string, value: string): void {
sessionStorage.setItem(key, value);
}
remove(key: string): void {
sessionStorage.removeItem(key);
}
}Example: REST backend
Async adapters are the same shape with promises:
ts
import type { SaveAdapter } from "@idlekitjs/core";
export class HttpAdapter implements SaveAdapter {
constructor(private readonly baseUrl: string) {}
async read(key: string): Promise<string | null> {
const response = await fetch(`${this.baseUrl}/saves/${key}`);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`save read failed: ${response.status}`);
}
return response.text();
}
async write(key: string, value: string): Promise<void> {
await fetch(`${this.baseUrl}/saves/${key}`, { method: "PUT", body: value });
}
async remove(key: string): Promise<void> {
await fetch(`${this.baseUrl}/saves/${key}`, { method: "DELETE" });
}
}Async adapters and page close
The autosave plugin's close-time save relies on the write finishing before the page unloads — which async network writes can't guarantee. Pair a remote adapter with a local one (write locally always, sync remotely opportunistically), or accept that the last seconds may be lost.
Wiring it up
ts
const save = new SaveManager<State>({
key: "my-game",
version: 1,
adapter: new SessionStorageAdapter(),
});Everything downstream — autosave, migrations, offline progress — works unchanged, because they only ever see the SaveManager.
Testing your adapter
Round-trip it through a real SaveManager (see the MemoryAdapter tests pattern): save a state, load it back, assert equality; then corrupt the stored string and assert load() returns null instead of throwing.
Related
- Storage overview — the contract in context.
- Saving your game — versioning and migrations.