skill prompt
# Verse8 Local Cache — agent implementation guide
`@verse8/local-cache` is a **device-local, best-effort** byte cache. Use it only for data the game can regenerate. It is never the source of truth.
## What it is / is not
- Device-local. Never synced across devices, browsers, or platforms (web / app / OneStore). Another device starts with an empty cache.
- Best-effort. Any entry can disappear at any time (quota pressure, LRU eviction, user clearing site data, browser purge). `get()` returning `null` is a normal miss, not an error.
- For regenerable derived data only: seeded terrain, decoded/transcoded assets, precomputed tables, baked meshes.
- **Never store player progress, inventory, currency, unlocks, or settings that must follow the account.** Those go to the game server via `@agent8/gameserver`.
## SDK (`@verse8/local-cache`)
Install:
```bash
pnpm add @verse8/local-cache
```
Or via CDN — the bundle exposes a single `Verse8LocalCache` global:
```html
<script src="https://unpkg.com/@verse8/local-cache@latest/dist/index.global.js"></script>
```
Import and use. Every call site MUST implement regenerate-on-miss:
```ts
import { Verse8LocalCache } from "@verse8/local-cache";
async function loadTerrain(seed: number): Promise<ArrayBuffer> {
const key = `terrain:${seed}`;
const cached = await Verse8LocalCache.get(key);
if (cached) return cached; // hit
const fresh = generateTerrain(seed); // miss → regenerate (must always be possible)
const r = await Verse8LocalCache.set(key, fresh); // failure is non-fatal
if (!r.ok) console.debug("terrain not cached:", r.reason); // QUOTA_EXCEEDED | TOO_LARGE | UNAVAILABLE
return fresh;
}
```
API (all `async`; failures are values, never exceptions):
```ts
get(key): Promise<ArrayBuffer | null> // null = miss
set(key, value: ArrayBuffer | ArrayBufferView, opts?: { transfer?: boolean }): Promise<
{ ok: true } | { ok: false; reason: "QUOTA_EXCEEDED" | "TOO_LARGE" | "UNAVAILABLE" }>
delete(key): Promise<void>
keys(prefix?): Promise<string[]>
clear(): Promise<void>
quota(): Promise<{ usedBytes: number; limitBytes: number }>
getJSON<T>(key): Promise<T | null> // UTF-8 JSON convenience
setJSON(key, value): Promise<SetResult>
ready(): Promise<"host" | "local" | "memory"> // resolves once the backend is decided
getBackend(): "host" | "local" | "memory" | null
init(opts?): void // OPTIONAL — only to pass options
```
Optional backend branching (e.g. skip multi-MB blobs when nothing will persist):
```ts
const backend = await Verse8LocalCache.ready();
if (backend !== "memory") {
await Verse8LocalCache.set(key, bigBlob);
}
```
Limits (v1): 64 MB per game, 32 MB per value, 512-char key. Exceeding them returns `QUOTA_EXCEEDED` / `TOO_LARGE` — never throws.
## Rules
- Use ONLY for regenerable derived data. If losing the entry would lose anything the player earned or chose, it does not belong here — use `@agent8/gameserver`.
- ALWAYS regenerate on miss. Every `get()` must have a fallback path that produces the data without the cache.
- Treat every `set()` failure (`QUOTA_EXCEEDED`, `TOO_LARGE`, `UNAVAILABLE`) as non-fatal: log at debug level and continue with the freshly generated value. Never surface it to the player, never retry in a loop.
- Do NOT call `init()` unless you need options (`silent`, `parentOrigin`, `handshakeTimeoutMs`, `debug`, ...). The first `get`/`set` bootstraps automatically.
- Do NOT rely on the cache being present later, on the same device or any other. Do not use it for cross-session or cross-device state.
- Prefix keys per account (`` `${account}:terrain:${seed}` ``) if per-user separation matters. The namespace is per game, not per user.
- Values are `ArrayBuffer`. `set` copies by default; pass `{ transfer: true }` only for a raw `ArrayBuffer` you will not touch again (it becomes detached).
- Do not build a sync/blocking wrapper around it. There is no synchronous API by design.
- Do not add extra warning suppression or console noise; the SDK prints exactly one diagnostic line about which backend it chose.
Last updated on