Getting Started (Browser)
Install recached-edge@^0.3.1 — every earlier version is unusable
Two separate defects, both fixed as of 0.3.1:
Packaging (0.1.3 – 0.3.0). The published tarball omitted wasm-pack's snippets/ directory, which the generated glue imports on its first line, and published wasm-pack's pkg/ output instead of the SDK — so npm install recached-edge failed at module resolution before any application code ran, and createCache was never on npm at all. Fixed by publishing the SDK package (with snippets/) and gating every release on a tarball that is packed and imported in CI.
Clock panic (0.1.1 – 0.2.0). core-engine read the clock via std::time::SystemTime::now(), which panics on wasm32-unknown-unknown. The clock is read on nearly every operation, so no store write completes on those versions — and a client whose write-ahead log passed the compaction threshold erased its own persisted cache before the replacement snapshot was written. Fixed in 0.2.1.
The Recached server is unaffected by both: it runs on a native target where the clock works normally, and it ships as a binary rather than through npm.
The recached-edge package is the TypeScript SDK for the browser WASM client. It gives you a Cache class backed by the same core-engine as the server, with optional WebSocket sync to a Recached server instance.
Install
npm install recached-edge
# or
pnpm add recached-edge
# or
yarn add recached-edgeInitialize
Use createCache() — it initializes the WASM module and returns a ready Cache instance.
import { createCache } from 'recached-edge'
// Local-only, in-memory cache (no server connection)
const cache = await createCache()For bundlers that support top-level await (Vite, Next.js, modern webpack):
// lib/cache.ts — shared singleton
import { createCache } from 'recached-edge'
export const cache = await createCache()Connect to a server
Pass connect to createCache() to enable WebSocket sync. Omit it for local-only mode.
import { createCache } from 'recached-edge'
// Connect to the Recached server WebSocket port
const cache = await createCache({
connect: { url: 'ws://localhost:6380' },
})
// With TLS (production)
const cache = await createCache({
connect: { url: 'wss://cache.yourdomain.com:6380' },
})
// With auth (if RECACHED_PASSWORD is set on the server)
const cache = await createCache({
connect: { url: 'ws://localhost:6380', password: 'your-secret' },
})Once connected, any mutation from the server (SET, DEL, etc.) is automatically pushed to the local WASM store. Any local write is forwarded to the server and fanned out to other connected clients.
Basic usage
// Strings
cache.set('theme', 'dark')
console.log(cache.get('theme')) // 'dark'
// With expiry (seconds)
cache.setEx('session:token', 'abc123', 3600)
// Check existence and TTL
cache.exists('theme') // true
cache.ttl('session:token') // remaining seconds
// Delete
cache.del('theme')
cache.get('theme') // null
// React to any mutation (from any source — local, server, or other tabs)
const unsubscribe = cache.onMutation(() => {
const count = cache.get('cart:count')
console.log('Cart count is now:', count)
})
// Stop listening
unsubscribe()createCache options
import { createCache } from 'recached-edge'
const cache = await createCache({
// Enable IndexedDB persistence (survives page refresh)
persistence: true,
// BroadcastChannel name for cross-tab mutation sharing
broadcastChannel: 'my-app-cache',
// Connect to the Recached server WebSocket port
connect: {
url: 'ws://localhost:6380',
// Server password (if RECACHED_PASSWORD is set)
password: 'your-secret',
},
})All three options are independent — you can use persistence and cross-tab sync without a server connection.
React
If you are using React, install the official hooks package instead:
npm install @recached/reactimport { RecachedProvider, useKey } from '@recached/react'
function App() {
return (
<RecachedProvider options={{ connect: { url: 'ws://localhost:6380' } }}>
<CartBadge userId={42} />
</RecachedProvider>
)
}
function CartBadge({ userId }: { userId: number }) {
const count = useKey(`cart:${userId}:count`)
return <span className="badge">{count ?? '0'}</span>
}See the React hooks docs for the full guide.
Vue
If you are using Vue 3, install the official composables package instead:
npm install @recached/vue// main.ts
import { createApp } from 'vue'
import { RecachedPlugin } from '@recached/vue'
import App from './App.vue'
const app = createApp(App)
app.use(RecachedPlugin, { connect: { url: 'ws://localhost:6380' } })
app.mount('#app')<!-- CartBadge.vue -->
<script setup lang="ts">
import { useKey } from '@recached/vue'
const props = defineProps<{ userId: number }>()
const count = useKey(`cart:${props.userId}:count`)
</script>
<template>
<span class="badge">{{ count ?? '0' }}</span>
</template>See the Vue composables docs for the full guide.
Without a server (local-only cache)
Do not pass connect to createCache(). The WASM module runs as a pure in-memory cache with TTL — no server, no WebSocket, no backend changes required.
This is a supported mode, not a degraded one: the same core-engine that runs on the server runs in the tab, so the local command surface is identical either way.
const cache = await createCache({
persistence: true, // IndexedDB WAL — survives refresh, no server needed
broadcastChannel: 'my-app', // cross-tab fan-out — no server needed
}) // no `connect` — nothing is networkedWorks with no server: get/set/del, getJSON/setJSON, getBytes/setBytes, setEx and TTL expiry, exists/ttl, incr/decr, jset/jget/jmerge, getMatching, onMutation, persistence, broadcastChannel.
Silently does nothing with no server — these do not throw, they have nowhere to send to: publish, subscribe/unsubscribe/onMessage (pub/sub is server-brokered and does not fall back to BroadcastChannel), liveQuery, syncToken/syncScopes, and pendingWrites/onOutboxFull.
persistence: true with no connect
Every write still records an outbox row in IndexedDB for a replay that can never happen, and past 10,000 writes the console shows offline write queue full. Wasted I/O and a misleading warning — your data and the WAL are fine. Use persistence: false if the noise matters to you.
import { createCache } from 'recached-edge'
const cache = await createCache() // no connect option — local-only
async function getUser(id: number): Promise<User> {
const key = `user:${id}`
const cached = cache.getJSON<User>(key)
if (cached !== null) return cached
const user: User = await fetch(`/api/users/${id}`).then(r => r.json())
cache.setJSON(key, user, 60) // cache for 60s
return user
}
async function getProducts(): Promise<Product[]> {
const cached = cache.getJSON<Product[]>('products')
if (cached !== null) return cached
const products: Product[] = await fetch('/api/products').then(r => r.json())
cache.setJSON('products', products, 300) // cache for 5 minutes
return products
}
// Invalidate on mutation
async function updateUserName(id: number, name: string): Promise<void> {
await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
cache.del(`user:${id}`) // next call to getUser() will refetch
}This pattern replaces the manual fetchedAt timestamp approach you might use with Zustand or Redux. TTL is declared once at write time; get() returns null automatically when the entry has expired.
Manual reactivity (non-React frameworks)
onMutation fires whenever the local store changes — from a local write, a server push, or a cross-tab BroadcastChannel message. It is the low-level hook used by useKey and useKeyJSON internally.
// Svelte
import { onMount, onDestroy } from 'svelte'
import { writable } from 'svelte/store'
import { cache } from '../lib/cache'
export let productId: string
const key = `stock:${productId}`
const stock = writable<string | null>(cache.get(key))
let unsubscribe: () => void
onMount(() => {
stock.set(cache.get(key))
unsubscribe = cache.onMutation(() => stock.set(cache.get(key)))
})
onDestroy(() => unsubscribe?.())The callback receives no arguments — it signals that something changed. Read the specific key you care about inside the callback.
Bundler configuration
Vite
Vite handles WASM imports natively. No extra config needed for most setups.
If you see issues with the WASM file not being served, add to vite.config.ts:
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
exclude: ['recached-edge'],
},
})Next.js (App Router)
The cache is browser-only: createCache() is async, fetches a .wasm file, and touches indexedDB and BroadcastChannel. So it must be created in a client component after hydration, never at module scope in anything the server renders.
@recached/react does this for you — its provider builds the cache in an effect:
// app/providers.tsx
'use client'
import { RecachedProvider } from '@recached/react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
// Local-only: drop `connect` and no socket is ever opened.
<RecachedProvider options={{ persistence: true, broadcastChannel: 'my-app' }}>
{children}
</RecachedProvider>
)
}// app/layout.tsx — a server component; only Providers is client-side
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body><Providers>{children}</Providers></body>
</html>
)
}The provider renders null until the cache is ready
createCache() resolves in an effect, which does not run during SSR — so anything inside <RecachedProvider> is absent from the server-rendered HTML and appears on hydration. Wrapping your entire app therefore opts the whole page out of SSR. Mount it around the subtree that actually reads the cache, and keep content you need server-rendered (or indexed) outside it.
Without the React SDK, do the same thing by hand — build the cache in useEffect, or reach for next/dynamic with ssr: false on the component that uses it:
'use client'
import { useEffect, useState } from 'react'
import { createCache, type Cache } from 'recached-edge'
export function useLocalCache(): Cache | null {
const [cache, setCache] = useState<Cache | null>(null)
useEffect(() => {
let cancelled = false
createCache({ persistence: true }).then((c) => !cancelled && setCache(c))
return () => { cancelled = true }
}, [])
return cache
}webpack
Add to your webpack config:
module.exports = {
experiments: {
asyncWebAssembly: true,
},
}