Skip to content

Migrating from v1

v2 is a ground-up rewrite. v1’s README advertised durability features the code didn’t implement; v2 implements them. The API breaks — deliberately and once.

v1 was a Redux-style store: defineStore, dispatch, defineAction, composeActions, reducers over connection state. All of it is removed. The connection lifecycle is now a typed finite state machine the library owns, and messages are never accumulated in state (v1’s default handler retained every message forever — an unbounded leak).

// v1
const store = defineStore({ url });
store.dispatch("connect");
// v2
const client = defineClient({ url });
await client.connect();

If you used the store for app state: deliver messages into your own state tool (a message handler that writes to your store of choice) — core stays out of app state by design.

v1’s defineClient cached a module-level instance and silently ignored config on the second call. v2 returns a fresh client per call; export your own instance if you want a singleton:

src/ws.ts
export const ws = defineClient({ url });
v1v2
connectedopen
closed (never fired — the v1 bug)close
statechange, reconnecting, drop
client.on("open", () => {});
client.on("message", (msg) => {});
client.on("close", (event) => {});

v1’s send() silently dropped messages when the socket wasn’t open. v2 queues during connecting/reconnecting (bounded, drop-oldest, with drop events) and throws in states where no open is coming. Nothing is ever silently lost.

Everything v1 promised, real this time: automatic reconnection with full-jitter backoff, bounded message queueing, opt-in heartbeat, plus typed + schema-validated messages, middleware in both directions, a pluggable codec, and Vue/React bindings in the box. See Getting started.