import { type KeyValueStorage, type KeyValueStorageErrorPolicy } from "./keyValueStore.js"; /** * The one async storage seam a save store persists through. Every backend * satisfies this same three-method shape — the browser's `localStorage` * (offline), an in-memory map (tests/SSR), or a database/Convex/HTTP endpoint * (cloud) — so a game switches offline saves for cloud saves by swapping the * backend and changing nothing else. Keys are opaque namespaced strings; values * are already-serialized strings, so a backend never needs to know the save * shape. */ export interface SaveBackend { read(key: string): Promise; write(key: string, value: string): Promise; remove(key: string): Promise; } /** * A {@link SaveBackend} over a synchronous {@link KeyValueStorage} — the * browser's `localStorage` by default (offline, on-device saves), a test stub, * or `null` for a no-op backend. Default fallback ignores failures; `errorMode: "throw"` * rejects failed operations and unavailable storage, so SaveStore status/onError can report them. * Use {@link memorySaveBackend} for deliberate ephemeral persistence. * * @capability local-save persist a game to on-device localStorage (offline) */ export declare function localSaveBackend(storage?: KeyValueStorage | null, policy?: KeyValueStorageErrorPolicy): SaveBackend; /** A memory-only {@link SaveBackend} — saves survive a reload only within the same session. For tests, SSR, or a "no persistence" mode that still exercises the same save code path. */ export declare function memorySaveBackend(): SaveBackend; /** Adopt any async `read`/`write`/`remove` trio as a {@link SaveBackend} — the seam for cloud saves backed by a database, an HTTP endpoint, or Convex (see `@jgengine/convex/convexSaveBackend`). Reads/writes may reject; the save store surfaces the failure as `"error"` status instead of throwing. */ export declare function remoteSaveBackend(backend: SaveBackend): SaveBackend; /** Lifecycle of the last save/load — drive a "Saving…"/"Saved" indicator or a loading gate off it. `"error"` includes rejected reads, writes, removals and slot metadata updates; metadata failure can follow a successful payload write. */ export type SaveStatus = "idle" | "loading" | "saving" | "saved" | "error"; /** Autosave cadence: debounce writes `debounceMs` after the last edit, but never wait longer than `maxWaitMs` while edits keep coming (`0` removes the ceiling). `autosave: true` uses `{ debounceMs: 1000, maxWaitMs: 15000 }`. */ export interface SaveAutoConfig { debounceMs?: number; maxWaitMs?: number; } /** The timer seam autosave schedules through — defaults to the ambient `setTimeout`/`clearTimeout`; inject fakes in tests. */ export interface SaveTimers { set(handler: () => void, ms: number): unknown; clear(handle: unknown): void; } /** The versioned envelope written to the backend — `version` lets a later game build detect and {@link SaveStoreConfig.migrate | migrate} an older payload. */ export interface SaveEnvelope { version: number; savedAt: number; value: T; } /** How a {@link createSaveStore} is wired — the backend it persists through, the initial value, and optional slot/versioning/autosave/serialization knobs. */ export interface SaveStoreConfig { /** Where saves live — {@link localSaveBackend}, {@link memorySaveBackend}, or a {@link remoteSaveBackend}. */ backend: SaveBackend; /** The value a fresh save starts from (nothing stored yet, or after `clear`). */ initial: T; /** Storage namespace, joined with the slot into the backend key. Default `"save"`. */ key?: string; /** Active save slot — pass distinct slots for multiple parallel saves. Default `"default"`. */ slot?: string; /** Current save-format version. Default `1`. Bump it on a breaking shape change and pass {@link migrate}. */ version?: number; /** Upgrade an older payload to the current shape. Receives the stored value and the version it was written at (`0` for a pre-versioned/raw payload). */ migrate?: (data: unknown, fromVersion: number) => T; /** Autosave on every `set`/`patch`. `false` (default) saves only on an explicit `save()`. */ autosave?: boolean | SaveAutoConfig; serialize?: (envelope: SaveEnvelope) => string; deserialize?: (raw: string) => unknown; /** Injectable clock (ms). Default `Date.now`. */ now?: () => number; /** Injectable timers for autosave. Default ambient `setTimeout`/`clearTimeout`. */ timers?: SaveTimers; /** Notified on any backend read/write failure — status is already `"error"` when it runs. */ onError?: (error: unknown) => void; } /** * A pluggable-backend game save with autosave, named slots, and versioned * migration. `value()`/`patch()` hold the live state; `load()` hydrates it from * the backend; `save()` (or autosave) writes it back. Backend failures surface * as `"error"` status and through `onError` — a save never throws into a tick. */ export interface SaveStore { value(): T; status(): SaveStatus; slot(): string; /** Monotonic change counter — bumps on any value or status change; drive `useSyncExternalStore` off it. */ revision(): number; /** Hydrate the active slot from the backend into `value()`; corrupt payloads fall back to `initial`. */ load(): Promise; /** Replace the value and mark it dirty (schedules autosave). */ set(value: T): void; /** Read-modify-write the value and mark it dirty; returns the next value. */ patch(mutate: (previous: T) => T): T; /** Flush the current value to the backend now (serialized against in-flight saves). */ save(): Promise; /** Delete the active slot and reset `value()` to `initial`. */ clear(): Promise; /** * Delete any slot: remove its stored payload and drop it from the slot index, so `slots()` and * `peek()` stop seeing it. Resolves `true` when a payload was removed and `false` when the slot * held nothing (an unknown slot is a no-op). Deleting the active slot resets `value()` to * `initial`; other slots leave the live value alone. */ deleteSlot(slot: string): Promise; /** * Read a stored slot without touching the live value, active slot or status — what a save-select * menu shows. Returns `null` when the slot holds nothing. */ peek(slot?: string): Promise<{ value: T; savedAt: number; } | null>; /** Flush any dirty state, switch to `slot`, and load it. */ switchSlot(slot: string): Promise; /** The slots this store has written (for a load/save menu). */ slots(): Promise; subscribe(listener: () => void): () => void; /** Stop autosave timers and drop listeners. */ dispose(): void; } /** * Create a {@link SaveStore}. Same call for offline and cloud — only the * `backend` differs (localStorage, memory, or an async DB/Convex endpoint). Turn * on `autosave` and every `set`/`patch` persists on a debounce; leave it off and * call `save()` at checkpoints. Bump `version` + pass `migrate` when the save * shape changes so old players keep their progress. * * @capability game-save save/load game state with a pluggable backend, autosave, slots, and migration */ export declare function createSaveStore(config: SaveStoreConfig): SaveStore;