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

@laioutr/valtio-liveblocks

v0.2.1

Published

Bind a valtio proxy to Liveblocks Storage (LiveObject/LiveList/LiveMap)

Readme

@laioutr/valtio-liveblocks

Bind a valtio proxy to a Liveblocks Storage root (LiveObject / LiveList / LiveMap) for two-way, conflict-free sync. Adapted from valtio-yjs.

Overview

  • Local proxy mutations are mirrored into Storage; remote Storage updates are applied back onto the proxy. Both directions are granular — work is proportional to what changed, not to document size.
  • A single extension point, classify, decides whether a plain object becomes a LiveObject or a LiveMap.
  • bind is adopt-only: it never writes to Storage during init. Storage is seeded server-side.

Install

Peers are valtio and @liveblocks/client.

pnpm add @laioutr/valtio-liveblocks

Usage

import { bind } from '@laioutr/valtio-liveblocks';
import { proxy } from 'valtio';

const { root } = await room.getStorage();
const state = proxy<MyShape>({} as MyShape);

const unbind = bind(state, root, room);
// mutate `state` → Storage; remote Storage changes → `state`
unbind(); // stop syncing

bind(proxy, root, room, classify?) returns an unbind function. It adopts whatever Storage already holds onto the proxy — it does not populate an empty room (see below).

Server-side: mutateRoot

mutateRoot is the server-side, one-shot complement to bind. It materialises the Storage root as a short-lived valtio proxy, runs a mutation function against it, captures the emitted ops, and replays them onto the Live tree — all inside a mutateStorage callback. The proxy is created and discarded in a single call; mutateRoot never subscribes to remote updates.

import { mutateRoot } from '@laioutr/valtio-liveblocks';
import { Liveblocks } from '@liveblocks/node';

const liveblocks = new Liveblocks({ secret });

await liveblocks.mutateStorage(roomId, async ({ root }) => {
  await mutateRoot(root, async (state) => {
    state.title = 'updated';
    state.count = (state.count ?? 0) + 1;
    state.tags.push('new-tag');
  });
});

mutateRoot(root, mutate, classify?) — the caller owns the mutateStorage call; mutateRoot owns the proxy lifecycle and the apply. If mutate throws, the error propagates and no ops are applied — nothing partial lands in Storage. Pass the same classify you used to seed the room so newly inserted sub-trees match the existing Storage shape.

Seeding & the adopt-only contract

bind reads Storage into the proxy and never writes during init. This keeps concurrent cold-start safe: a client that binds before its Storage view has finished syncing cannot write a fresh empty container that last-write-wins-clobbers another client's data.

Storage must therefore be populated server-side, before clients bind. Build the wire payload with jsonToPlainLsonObject and push it once via @liveblocks/node:

import { jsonToPlainLsonObject } from '@laioutr/valtio-liveblocks';
import { Liveblocks } from '@liveblocks/node';

const liveblocks = new Liveblocks({ secret });
await liveblocks.initializeStorageDocument(roomId, jsonToPlainLsonObject(data, classify));

By the time a client calls bind, the room is populated and bind simply adopts it.

Classification (LiveObject vs LiveMap)

Arrays always become LiveList and scalars pass through. For plain objects, classify chooses the container:

type ClassifyFn = (path: Path, node: object, parent: unknown) => 'object' | 'map';
  • 'object'LiveObject — fixed, known keys. This is the default (defaultClassify).
  • 'map'LiveMap — an open-ended, id-keyed dictionary that adds/removes entries often.

Use the same classify for bind, toLson, and jsonToPlainLson* so client-bound and server-initialized rooms produce an identical Storage shape.

API

  • bind(proxy, root, room, classify?) => () => void — two-way sync; returns an unbind.
  • mutateRoot(root, mutate, classify?) => Promise<void> — server-side, one-shot apply; no room or unbind required.
  • toLson(value, classify, path?, parent?) => Lson — build a detached Live tree from plain JS.
  • fromLson(root) => object — deep-read a Live root into a plain JS tree.
  • applyValtioOpsToLive(root, plain, ops, classify?) => void — replay a captured op buffer onto a Live tree (lower-level primitive; mutateRoot composes this).
  • jsonToPlainLson(value, classify, path?, parent?) => PlainLson — plain JS → PlainLson wire format (constructs no Live* instances).
  • jsonToPlainLsonObject(root, classify) => PlainLsonObject — root payload for initializeStorageDocument (the Storage root is always a LiveObject).
  • defaultClassify — classifies every object as LiveObject.
  • Types: ClassifyFn, Path, PlainLson, ValtioOp.

Performance vs valtio-yjs

Measured on a large real-world JSON document (≈59.9k nodes, 3.6 MB) against a local Liveblocks dev server. Reproduce with the bundled benchmark: LB_DEV=1 BENCH_SNAPSHOT=<doc.json> pnpm exec vitest run __tests__/bench.integration.test.ts.

| Metric | Yjs (valtio-yjs) | Storage (this package) | | --- | --- | --- | | Build from JSON | 274.8 ms | 43.0 ms | | Read back to plain JS | 13.0 ms | 4.3 ms | | Retained heap | 59.8 MB | 41.5 MB | | Encoded size | 4.1 MB | 5.37 MB | | Per-edit round-trip (A→B) | 100.4 ms | 100.6 ms | | Repeated edits to one field | op-log grows | tracks state, not edit count |

Storage builds ~6× faster, reads ~3× faster, and uses ~30% less heap; realtime latency is the same (~100 ms, network-throttle-bound). Yjs is slightly smaller on the wire but stores the whole tree as one monolithic document, whereas Storage shards per node and grows with state rather than edit history. (Storage must be seeded server-side — see above.)

Caveats

  • One mutation path per bound root. Write through the proxy or through room.batch directly — not both for the same root. Same-tick writes to different keys co-exist; a same-key collision resolves last-write-wins.
  • undefined vs null. Storage cannot hold undefined: writes lower undefinednull and reads collapse nullundefined. Model optional fields as field?: T, never field: T | null.
  • Array element identity is not preserved across a structural remote change (insert / delete / move): values converge, but a proxy array element may be re-created rather than mutated in place.
  • Unsupported values (Date, Map, Set, class instances) are not serializable — they warn and store null. Use plain, JSON-compatible values.