place

@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.

Persistence sits on top of reactivity
The reactivity primitive doesn't know about storage. 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.

ts
// 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.

ts
// 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.

ts
// 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.

ts
// 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.

ts
// 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.

ts
// 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