npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@verse8/local-cache

v0.1.0

Published

Verse8 device-local, best-effort cache for games — never synced across devices or platforms, may be evicted at any time

Downloads

124

Readme

@verse8/local-cache

This is a device-local, best-effort cache. It is never synced across devices, browsers, or platforms (web / app / OneStore) and any entry can be evicted at any time. Do not store player progress here — use the game server. Use it for data you can regenerate: seeded terrain, decoded assets, precomputed tables.

Verse8 games run as a cross-site iframe ({id}.verse8.games inside verse8.io). WebKit (Safari, every iOS browser) makes third-party IndexedDB and localStorage ephemeral — writes succeed, but nothing reaches disk and it all disappears when the page is discarded. This package moves the bytes to the hosting shell's first-party origin over a MessageChannel, where they persist, and falls back to in-frame storage everywhere else.

Two surfaces ship in one package:

| Entry | Runs in | Global (CDN) | |---|---|---| | @verse8/local-cache | the game | window.Verse8LocalCache | | @verse8/local-cache/parent | the shell (verse8.io, onestore.verse8.io) | window.Verse8LocalCacheParent |

Install (game)

pnpm add @verse8/local-cache
# npm i @verse8/local-cache / yarn add @verse8/local-cache
<!-- or CDN -->
<script src="https://unpkg.com/@verse8/local-cache@latest/dist/index.global.js"></script>

Quick start (game)

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 (always possible)
  const r = await Verse8LocalCache.set(key, fresh);
  if (!r.ok) console.debug("terrain not cached:", r.reason); // fine — still playable
  return fresh;
}

No init() call is required — the first call bootstraps. Call Verse8LocalCache.init({ ... }) only to pass options (see below).

init() timing. The backend decision runs once, on the first init() or the first data call. Options passed after that (silent, handshakeTimeoutMs, limits, …) cannot affect a handshake that already happened — only debug applies immediately. If you need options, call init() before your first get/set.

API

All methods are async. Failures are values, never exceptions (except programming errors such as a non-string key).

init(opts?: {
  parentOrigin?: string;        // auto-resolved; see "How the host is found"
  handshakeTimeoutMs?: number;  // default 1500
  limits?: Partial<Limits>;     // in-frame fallback only
  dbName?: string;              // in-frame fallback only
  debug?: boolean;
  silent?: boolean;             // suppress the one-time "host unsupported" warning
}): void

ready(): Promise<'host' | 'local' | 'memory'>   // resolves once the backend is decided
getBackend(): 'host' | 'local' | 'memory' | null

get(key): Promise<ArrayBuffer | null>          // null = miss (normal path)
set(key, value: ArrayBuffer | ArrayBufferView, opts?: { transfer?: boolean }): Promise<
  { ok: true } | { ok: false; reason: 'QUOTA_EXCEEDED' | 'TOO_LARGE' | 'UNAVAILABLE' }>
  // default: value is copied (your buffer stays usable)
  // { transfer: true }: raw ArrayBuffer is moved zero-copy and DETACHED (byteLength 0) afterwards
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>

Backends — what ready() tells you

| Backend | When | Durability | |---|---|---| | host | Game is framed by a Verse8 shell that runs @verse8/local-cache/parent | Durable (shell's first-party IndexedDB) | | local | Game is the top-level document (Verse8 mobile app WebView, standalone) — or the host is silent/declines | Durable when top-level. Ephemeral on Safari/iOS when cross-site framed | | memory | IndexedDB unusable (private mode, storage blocked) | Lost on reload |

The SDK prints one console line describing its decision:

  • top-level → console.info(...first-party context...)
  • host silent / declined / untrusted parent → console.warn(...host does not support the local cache; falling back to in-frame IndexedDB. On Safari/iOS this storage is EPHEMERAL...)
  • no IndexedDB → console.warn(...in-memory...)

Pass init({ silent: true }) to suppress it. Either way, getBackend() lets you branch (e.g. skip caching multi-MB blobs on memory).

If the shell tears the bridge down mid-session (route change, stop()), the SDK receives a BYE, fails in-flight calls fast, warns once, and continues on the in-frame fallback for the rest of the session — writes from then on are not on the shell origin, which is fine for a best-effort cache.

How the host is found

init({ parentOrigin }) wins. Otherwise, in order: window.verse8.parentOrigin (set by the shell-served v8-inject.js) → location.ancestorOrigins[0] if it is *.verse8.io?parentOrigin= query if *.verse8.io / local dev. Nothing trusted → no handshake → fallback. Top-level documents never handshake.

Namespacing

You get one namespace per game; the shell derives it from the browser-stamped origin of your CONNECT message, so games cannot read each other's cache. Inside your namespace, keys are yours — prefix them yourself if you want per-account separation (\${account}:terrain:${seed}``).

Policy (v1)

| | Default | |---|---| | Per game (namespace) | 64 MB soft cap → QUOTA_EXCEEDED | | Records per game | 10 000 → QUOTA_EXCEEDED | | Single value | 32 MB → TOO_LARGE | | Key length | 512 UTF-16 units → TOO_LARGE | | Shell total (all games) | 512 MB → least-recently-used games evicted on next connect |

usedBytes counts value bytes plus key bytes and a flat 128-byte per-record allowance, so it tracks real footprint rather than payload only.

Guarantee level: best-effort. Entries disappear when: the shell total cap is exceeded (LRU), the user clears site data, the browser purges the shell origin (Safari ITP purges script-writable storage of sites without user interaction for 7 days of Safari use), or the app is uninstalled. The shell calls navigator.storage.persist() once to reduce browser-initiated eviction; it is advisory.

Shell integration (/parent)

import { startLocalCacheParent } from "@verse8/local-cache/parent";

const handle = startLocalCacheParent({
  isAllowedGameOrigin: (origin) => /^https:\/\/[a-z0-9-]+\.verse8\.games$/.test(origin),
  // limits, dbName, resolveNamespace, requestPersistentStorage, onTelemetry — optional
});
// on unmount / navigation:
handle.stop();

One listener serves every game iframe on the page. Each CONNECT brings its own MessagePort; the port is bound to the namespace derived from event.origin and served from the shell's IndexedDB (verse8-local-cache). Data never leaves the shell origin. A newer CONNECT from the same browsing context (event.source) supersedes the older connection, so SDK retries and iframe reloads never accumulate live ports (two SDK copies in one game document would fight — don't do that). If the shell has no IndexedDB, the parent declines every CONNECT (READY ok:false) rather than pretending to be durable; the game then falls back and warns.

Mount exactly one parent per shell page (a module-level singleton / ref count if several components can render games) — a second parent never sees ports the first one adopted, and its stop() would BYE connections it doesn't own.

Telemetry events: connect, reject, evict, request, disconnect, error.

Wire protocol

See PROTOCOL.md.

Development

pnpm install
pnpm typecheck && pnpm build && pnpm test

License

MIT