Socket.io to @shadow-js/realtime
Port a Socket.io server and client to channels, rules, acknowledgments, presence and resumable delivery in @shadow-js/realtime, with a map from rooms, namespaces, middleware, acks and the Redis adapter, and what Socket.io does that this package does not.
Socket.io gives you events on a socket, rooms to group sockets, acknowledgment callbacks and a reconnecting client.
@shadow-js/realtime keeps those ideas and changes where the structure lives: channels replace rooms and
namespaces, rules replace middleware and per-event checks, a number on every message replaces hand-written
recovery after a reconnect, and acknowledgments are promises. This page maps one to the other. The full model is in
Real time with @shadow-js/realtime.
The map#
| Socket.io | @shadow-js/realtime |
|---|---|
io.use((socket, next) => ...) authentication |
hub.websocket({ authenticate }) reads the session once; the identity does not change for the connection |
socket.join(room) |
client.channel("chat:" + room) and a listener; the server's rule decides whether the user may join |
io.to(room).emit(event, data) |
hub.publish("chat:" + room, event, data) from the server; channel.publish(event, data) from a client the rule allows |
Namespace /admin |
The first segment of the channel name (admin:*) with its own rule |
socket.emit(event, data, callback) |
await channel.publish(event, data) (resolves with the message number), or channel.request(event, data) for a reply from a server handler |
socket.on(event, (data, callback) => ...) on the server |
A handlers entry of the channel rule: ({ data, identity }) => reply |
socket.broadcast.emit |
Publish with echo: false on the sender's subscription |
io.in(room).fetchSockets() |
Presence: channel.presence.members() |
socket.recovered and connection state recovery |
Built in: the client resumes from the last message number and is told about a gap |
@socket.io/redis-adapter |
redisAdapter (Redis 6.2 or later, or Valkey), and a Durable Object adapter for Cloudflare |
| HTTP long-polling fallback | Server-sent events plus an HTTP POST, chosen automatically when WebSocket is not available |
Server#
Socket.io, in one file, does connection handling, rooms and the ack:
import { Server } from "socket.io";
const io = new Server(3000);
io.use((socket, next) => (socket.handshake.auth["token"] ? next() : next(new Error("unauthorized"))));
io.on("connection", (socket) => {
socket.on("join", (room: string) => socket.join(room));
socket.on("message", (room: string, text: string, ack: (id: number) => void) => {
io.to(room).emit("message", { text, from: socket.id });
ack(Date.now());
});
});
The same server with rules. Joining and publishing are checked in one place, the message has an id that the server assigned, and the history is kept for resume:
In a Dusk app, export const WS = hub.websocket({ authenticate }) in routes/rt.ts replaces new Server(3000): the same server
process serves pages and sockets, with Dusk's origin check, connection caps and graceful shutdown.
Client#
import { io } from "socket.io-client";
const socket = io({ auth: { token } });
socket.emit("join", "lobby");
socket.on("message", (message) => render(message));
socket.emit("message", "lobby", "hello", (id: number) => console.log("sent", id));
Differences to plan for#
- Channels are closed until a rule opens them. In Socket.io any client may join any room unless your code checks it.
- Payloads are JSON. Socket.io can carry binary values; here binary frames are refused. Send a URL or an ID and fetch the bytes over HTTP.
- No Socket.io wire protocol. A Socket.io client cannot connect to this server, so clients and servers move together. Migrate one channel family at a time behind a feature flag, keeping both servers running during the move.
- No long-polling. WebSocket first, then server-sent events with HTTP posts, which cover every runtime Dusk supports.
- Delivery is per channel. Messages of one channel are ordered and never duplicated; there is no ordering between channels, and no exactly-once guarantee across a gap (the client is told, so it can reload).
- Rooms with huge membership use presence deliberately: a presence set is capped (1,000 members by default). For "who is online" at larger scale, keep a counter in your own store and publish it.