DEV Community

Cover image for Every realtime feature I shipped turned into socket plumbing, so I built Liveflux
BPDM 🧩⚡
BPDM 🧩⚡

Posted on • Originally published at liveflux.bpdm.dev

Every realtime feature I shipped turned into socket plumbing, so I built Liveflux

At work, every realtime feature starts the same way: open a WebSocket, push messages into state, render a list. Ten minutes, done. Then it meets production.

The socket drops on flaky train wifi and never comes back. Two components mount the same feed and open two sockets. A burst of messages janks the main thread. A fast re-render tears the list mid-update. The ten-minute feature is now a week of socket plumbing: reconnect backoff, dedup, backpressure, tear-free reads. None of which was the feature.

I'd written that plumbing enough times to want it to live in one place. So I built Liveflux.

You describe the fold; it owns the socket

The shift is to stop thinking in "messages" and start thinking in "a reducer over a stream." You say which channel to subscribe to and how each event folds into the state your component renders. Liveflux owns the connection.

import { useStream } from '@liveflux/react';

type Trade = { id: number; symbol: string; price: number };

export function Trades() {
  // upsert -> Trade[]: a matching id updates in place, a new id is appended.
  const trades = useStream<Trade>({
    channel: 'trades',
    into: { strategy: 'upsert', key: 'id', cap: 50 },
  });

  return trades.map((t) => <Row key={t.id} symbol={t.symbol} price={t.price} />);
}
Enter fullscreen mode Exit fullscreen mode

That's the whole component. The fold strategies cover most UIs: append (a log), upsert (a keyed list), replace (the latest value), or your own reducer.

Because the library owns the connection, the hard parts are handled once, for every subscription:

  • On an unexpected close it backs off with jitter and replays every active subscription, so streams resume on their own.
  • Identical subscriptions share one socket, ref-counted; it closes when the last subscriber leaves.
  • Oversized inbound frames are dropped before decoding, so a burst can't lock the UI.
  • Reads go through useSyncExternalStore, so state stays tear-free under concurrent rendering.

One engine, five transports

The engine is transport-agnostic. Point it at whatever your backend speaks by swapping the adapter, and the component doesn't change:

  • @liveflux/ws for any plain WebSocket
  • @liveflux/sse for Server-Sent Events
  • @liveflux/socketio for Socket.IO
  • @liveflux/graphql-ws for GraphQL subscriptions over WebSocket
  • @liveflux/phoenix for Phoenix Channels

The core is framework-agnostic too; React is the first binding.

Honest status

It's pre-alpha and it's a solo project, so the API is still settling. But it's typed end-to-end and tree-shakeable, and every adapter is covered by a shared conformance suite plus real-server end-to-end tests (a real SSE server, a real Socket.IO server, a real graphql-ws server). The reconnect, dedup, and backpressure behaviour is actually exercised, not just claimed.

If you build realtime UIs, I'd really like to know where this holds up and where it breaks in a real app.

Have a look

Top comments (0)