@place/persistence
Storage adapters for @place/reactivity state. One primitive — persistedState() — wraps a state so it loads from a backing store on creation and saves on every change. The adapter is a plain object (load + save, optionally observe / refresh), so adapters are easy to test, swap, and compose.
persistedState wraps a plain state() with an auto-save watch. The underlying State<T> stays exposed — compose it with collection(), history(), etc. unchanged.persistedState(adapter, options?)
Returns { state, dispose }. The state is initialized from adapter.load(); every change triggers adapter.save(value). When the adapter exposes observe (cross-tab, server sync), external changes re-load the state. Pass { equals } for a structural comparator on object-shaped state.
// persistedState(adapter) — a State<T> that loads from the adapter
// on creation and saves on every change. Persistence sits ON TOP of
// reactivity: the state primitive knows nothing about storage; the
// adapter is a plain object with load() + save().
import { persistedState, localStorageAdapter } from '@place/persistence'
const { state: theme, dispose } = persistedState(
localStorageAdapter('app:theme', 'dark'),
)
theme() // 'dark' on first run, or the persisted value
theme.set('light') // writes to localStorage immediately
dispose() // stops the auto-save watch (rarely needed —
// most state lives for the app's lifetime)
localStorageAdapter(key, default, options?)
Sync adapter backed by localStorage (or any compatible Storage). JSON-serializable values by default; pass custom serialize / deserialize for richer types. Save errors are swallowed — corrupt data falls back to the default.
// localStorageAdapter — sync, JSON-serializable values.
import { localStorageAdapter } from '@place/persistence'
const adapter = localStorageAdapter('notes:draft', '', {
// Optional — defaults are JSON.stringify / JSON.parse.
serialize: (v) => JSON.stringify(v),
deserialize: (raw) => JSON.parse(raw),
})
// Corrupt JSON or a save error (quota exceeded, storage disabled)
// falls back to the default value rather than throwing.
memoryAdapter(initial)
In-memory adapter — useful for tests and as a no-op fallback.
// memoryAdapter — in-memory, for tests and no-op fallbacks.
import { memoryAdapter } from '@place/persistence'
const adapter = memoryAdapter({ count: 0 })
// Nothing touches disk / localStorage — useful in unit tests and
// as a graceful fallback where no real storage exists.
crossTabAdapter(inner, channelName)
Wraps any adapter so writes propagate to other tabs of the same origin via BroadcastChannel. Conflict policy is last-write-wins. Composes cleanly over localStorageAdapter, indexedDBAdapter, or serverAdapter.
// crossTabAdapter(inner, channelName) — wraps any adapter so
// writes propagate to other tabs of the same origin in near-
// realtime, via BroadcastChannel. Conflict policy: last-write-wins.
import {
persistedState,
localStorageAdapter,
crossTabAdapter,
} from '@place/persistence'
const { state: prefs } = persistedState(
crossTabAdapter(
localStorageAdapter('app:prefs', {}),
'app-prefs-sync',
),
)
// Edit prefs in tab A → tab B's persistedState re-loads and updates.
// The save → broadcast → reload loop is broken internally so writes
// don't echo forever.
indexedDBAdapter(key, default, options?)
Async storage for large values. The PersistenceAdapter contract is sync at load(), so the adapter keeps a cached value and fires observe callbacks when the async load resolves — consumer code never deals with promises just to read state.
// indexedDBAdapter — async storage for large values. The adapter
// contract is sync at load() — it keeps a cached value, kicks off
// the async IDB load on construction, and fires observe() when the
// load resolves so persistedState picks up the stored value.
import { persistedState, indexedDBAdapter } from '@place/persistence'
const { state: doc } = persistedState(
indexedDBAdapter('editor:doc', { blocks: [] }, {
dbName: 'place', // default
storeName: 'kv', // default
}),
)
serverAdapter(options)
HTTP + WebSocket backed persistence — syncs through a tiny key-value sync server. Caches over async storage like indexedDBAdapter; the WebSocket pushes a change signal per write, so other clients re-fetch via the same load path.
// serverAdapter — HTTP + WebSocket backed persistence. Speaks to a
// tiny key-value sync server: GET/PUT /kv/:key, plus a WebSocket
// that pushes a change signal per write. Like IndexedDB, it caches
// over async storage and fires observe() on remote changes.
import { persistedState, serverAdapter } from '@place/persistence'
const { state: shared } = persistedState(
serverAdapter({
baseUrl: 'http://localhost:5180',
key: 'doc:42',
defaultValue: { title: '' },
// wsUrl defaults to baseUrl with http(s) → ws(s).
}),
)
See also
- state · watch · derived — the reactivity primitives
- @place/data —
collection()composes with persisted state