robosocket
v0.1.1
Published
Reliable, backend-agnostic WebSocket client for React and React Native.
Readme
RoboSocket
A reliable, backend-agnostic WebSocket client for React and React Native. Handles reconnects, heartbeats, guaranteed delivery, and offline queuing so you don't have to hand-roll it — works against any RFC6455 WebSocket server, no server-side library required.
See PROGRESS.md for the full v1 scope and what's intentionally not built yet.
Install
npm install robosocketOptional peer dependencies, only needed if you use the corresponding feature:
react— for therobosocket/reactandrobosocket/react-nativehooks@msgpack/msgpack— for the built-in msgpack serializer
Quick start
import { RoboSocket } from "robosocket";
type Events = {
message: { text: string };
typing: { userId: string };
};
const socket = new RoboSocket<Events>({
url: "wss://api.example.com/ws",
auth: async () => getAccessToken(),
});
await socket.connect();
socket.on("message", (payload) => console.log(payload.text));
await socket.emit("message", { text: "hello" });
const user = await socket.request("get-user", { id: 1 });React
import {
useConnectionStatus,
useSocket,
useSubscription,
} from "robosocket/react";
function Chat() {
const socket = useSocket<Events>({ url: "wss://api.example.com/ws" });
const status = useConnectionStatus(socket);
useSubscription(socket, "chat:123");
return <div>{status}</div>;
}useSocket connects on mount and calls socket.destroy() on unmount. Other
hooks: useNetworkQuality, useStats, useSocketRequest.
React Native
Same hooks, plus app-lifecycle integration. RN's own modules aren't imported directly by RoboSocket (they only resolve inside a Metro/RN project) — you inject them yourself:
import { AppState } from "react-native";
import NetInfo from "@react-native-community/netinfo";
import { createAsyncStorageStore, useSocket } from "robosocket/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
const socket = useSocket(
{
url: "wss://api.example.com/ws",
queueStore: createAsyncStorageStore(AsyncStorage, "robosocket:queue"),
},
{ appState: AppState, netInfo: NetInfo },
);With both injected: backgrounding the app disconnects the socket (most OSes
kill it anyway), foregrounding reconnects immediately, and a NetInfo
connectivity-restored event triggers an immediate reconnect instead of
waiting on the backoff timer. Set pauseOnBackground: false to keep the
socket alive in the background.
DevTools
Web only (RN needs a native-styled equivalent, not built yet):
import { RoboSocketDevTools } from "robosocket/devtools";
<RoboSocketDevTools socket={socket} />;A small floating overlay showing connection state, latency, reconnects, queue size, subscribed channels, and a live event log.
Presence
socket.on("presence", ({ userId, status }) => console.log(userId, status));
await socket.updatePresence("away");
socket.getPresence("user-123"); // => "online" | "offline" | "away" | "busy" | undefined
socket.getAllPresence(); // => { [userId]: status }No special wire protocol — updatePresence just emits a presence:update
event, and any incoming event named presence with { userId, status }
data both updates the local presence map and fires normally to .on("presence", ...)
listeners. Your backend defines what triggers a presence broadcast.
Serializers
JSON by default. Swap in msgpack for smaller/faster binary payloads:
import { createMsgpackSerializer } from "robosocket";
const socket = new RoboSocket({
url: "wss://api.example.com/ws",
serializer: createMsgpackSerializer(), // requires @msgpack/msgpack installed
});Need protobuf? Implement the Serializer interface yourself — protobuf
requires per-message-type schemas ahead of time, which doesn't fit a library
where emit(anyEventName, anyShapeData) is the whole point, so it's not
built in:
import type { Serializer } from "robosocket";
const protobufSerializer: Serializer = {
binary: true,
encode: (packet) => myProtoEncode(packet),
decode: (raw) => myProtoDecode(raw),
};Offline queue storage
import { createLocalStorageStore, createMemoryStore } from "robosocket";
new RoboSocket({
url: "...",
queueStore: createLocalStorageStore("robosocket:queue"), // web
// or createMemoryStore() (default), or createAsyncStorageStore(...) on RN
});Implement QueueStore (load()/save()) for any other backing store.
Plugins & middleware
The extension point for anything not built in — encryption included. RoboSocket never touches crypto code; bring your own and drop it in as middleware:
socket.useOutgoing(async (packet, next) => next(await myEncrypt(packet)));
socket.useIncoming(async (packet, next) => next(await myDecrypt(packet)));
socket.usePlugin({
name: "analytics",
onConnect: (client) => track("socket_connected"),
onDisconnect: (client) => track("socket_disconnected"),
});Reserved event names
connect, disconnect, reconnect, reconnecting, reconnect_failed,
error, message, raw, binary, latency, heartbeat_timeout,
queue_updated, queue_overflow, queue_flushed, auth_error,
auth_refreshed, and presence are used internally. In particular, message
is also emitted as a catch-all for every incoming packet — if your backend
sends an application event literally named message, your handler fires
twice (once as the named event, once as the catch-all). Pick a different
event name for your own "generic message" concept (e.g. chat).
Backend-friendly client behavior
RoboSocket is client-only and has no control over your backend's scaling or ingress — but a few client-side defaults exist specifically to avoid making a backend's job harder during an outage:
- Reconnect backoff is jittered (
reconnectJitter, defaulttrue) — a server restart or shared network blip doesn't cause every client to reconnect in the same instant. - Heartbeats are jittered (
heartbeatJitter, defaulttrue) — clients that connect around the same moment don't end up pinging in lockstep, which would otherwise create periodic load spikes. - Auth-refresh is jittered (
authRefreshJitter, defaulttrue) — a fleet whose tokens share a fixed-TTL expiry won't all hit your refresh endpoint at the same instant. - Offline queue flush can be paced (
queueFlushPacing, default0/ off) — after a shared outage, many clients reconnecting at once would otherwise each dump their whole queue simultaneously. Set this above0(milliseconds between packets) if you expect large simultaneous reconnect events; left at0for low-latency single-client catch-up.
If you're tuning these for your own infrastructure: turning jitter off is fine for a handful of clients, but at fleet scale it re-introduces exactly the thundering-herd risk these defaults exist to avoid.
Development
npm install
npm run typecheck
npm test
npm run build
node examples/node-e2e-smoke.mjs # real end-to-end check against an actual ws serverThe unit suite mocks the WebSocket transport; examples/node-e2e-smoke.mjs
runs the actual built package against a real ws server and is what caught
a real bug (a hasOwnProperty check that silently dropped every message
from real transports whose message-event data is inherited via the
prototype chain, e.g. the ws package) that the mocked suite couldn't see.
