[]previous and next page

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.

For agentsUpdated

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:

socket-io-server.tsts
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#

socket-io-client.tsts
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.