Actors/Packages/WebSocket/Installation
@sigx/actors-ws · Preview

Installation#

One package, two halves: a transport in the browser and an upgrade adapter on the server. The adapter is Node-only; every other runtime already has what it needs.

Terminal
pnpm add @sigx/actors-ws

ws is an optional peer (>= 8), imported lazily and only by the Node adapter — and only once a server actually sees an upgrade. Browsers, Bun, Deno and Workers never pull it in.

The client#

TypeScript
import { actorsPlugin } from '@sigx/actors/app';
import { socketTransport } from '@sigx/actors-ws/client';

app.use(actorsPlugin({ transport: socketTransport({ url: 'wss://example.com/_sigx/socket' }) }));
OptionDefaultMeaning
connect(handlers)build one connection attempt; the primary seam
urlsugar over connect, dialling the global WebSocket
retryMs300backoff floor between failed attempts
maxRetryMs10_000backoff ceiling

Nothing is dialled until the first call, so a transport configured at module scope costs nothing on pages that never touch an actor.

The Node adapter#

TypeScript
import { attachActorSocket } from '@sigx/actors-ws/node';

const detach = attachActorSocket(server, { host, origin: 'same-origin' });

It registers an upgrade listener and returns a detach function; sockets an already detached listener accepted keep running until they close on their own.

OptionDefaultMeaning
path/_sigx/socketupgrade path, matched exactly — never a prefix
wssbring your own new WebSocketServer({ noServer: true }); without one, ws is imported dynamically

Everything else is forwarded to the session — see Socket sessions for origin, maxMessageBytes, maxConcurrent, maxSubscriptions, pingMs, revalidateMs, maxConnectionMs, throttlePolicy and stats. The adapter supplies the session's bufferedBytes() seam itself, from the socket's bufferedAmount, so socketStats() reports the host's own send-buffer depth with nothing to configure.

Two details worth knowing:

  • An unmatched upgrade is left alone unless nothing else is listening for one, in which case the socket is destroyed rather than hanging unanswered forever.
  • Messages arriving before the session finishes constructing are buffered, not dropped. ws begins reading the moment the upgrade completes, which is earlier than the session's own async setup can finish.

Interop#

The adapter is a convenience, not a requirement. createActorSocketSession takes a Request and a pair of callbacks, so any server that can hand you those can drive it.

An existing ws server#

Construct the session in your own upgrade handler, and use toRequest() to build the WinterCG Request a Node upgrade does not come with:

TypeScript
import { createActorSocketSession } from '@sigx/actors/server';
import { toRequest } from '@sigx/actors-ws/node';

wss.on('connection', async (socket, req) => {
    const session = await createActorSocketSession({
        host,
        request: toRequest(req),
        send: (message) => socket.send(message),
        close: (code, reason) => socket.close(code, reason),
    });
    socket.on('message', (data) => session.handle(String(data)));
    socket.on('close', () => session.close());
});

Cookies and Origin carry over verbatim, which is exactly what the session's upgrade-time authentication and origin check need.

socket.io#

About ten lines per side. The protocol is text, so carry it as an event payload:

TypeScript
// server
io.on('connection', async (socket) => {
    const session = await createActorSocketSession({
        host,
        request: toRequest(socket.request),
        send: (message) => socket.emit('sigx', message),
        close: () => socket.disconnect(true),
    });
    socket.on('sigx', (message: string) => session.handle(message));
    socket.on('disconnect', () => session.close());
});

// client
socketTransport({
    connect: (handlers) => {
        io.on('connect', handlers.onOpen);
        io.on('sigx', handlers.onMessage);
        io.on('disconnect', handlers.onClose);
        return { send: (m) => io.emit('sigx', m), close: () => io.disconnect() };
    },
});

This is a recipe rather than a package — there is nothing to abstract.

uWebSockets.js#

Build the Request synchronously inside upgrade(). uWS reuses its req object and it is dead the moment the handler returns, so reading headers from it later yields garbage. Read what you need, construct the Request, then go async.

Bun and Deno#

No adapter needed. Both hand you a real Request at upgrade time and a real WebSocket afterwards, so createActorSocketSession wires up directly.

Security#

The origin check happens at the upgrade, and it is the one check that cannot be deferred to per-message: a browser will open a cross-origin WebSocket with cookies attached and no preflight to stop it. The default is posture.origin ?? 'same-origin'.

Identity is pinned once, at the upgrade, from the cookies the browser sent. For an app with sign-out or short-lived sessions, set revalidateMs or maxConnectionMs — see session lifetime.

TLS is your terminator's job; use wss:// in production.

Next steps#