WebSocket
One socket from the browser to your host, carrying every call and every live subscription on the page.
Installation
pnpm add @sigx/actors-wsClient side, the transport is the whole integration:
import { actorsPlugin } from '@sigx/actors/app';
import { socketTransport } from '@sigx/actors-ws/client';
app.use(actorsPlugin({ transport: socketTransport({ url: 'wss://example.com/_sigx/socket' }) }));
Server side on Node, attach the upgrade listener:
import { createServer } from 'node:http';
import { createAppHandler } from '@sigx/actors/node';
import { attachActorSocket } from '@sigx/actors-ws/node';
const server = createServer(createAppHandler(app));
await app.start(); // the session needs a running host
attachActorSocket(server, { host: app.host! });
await new Promise<void>((r) => server.listen(7311, r));
useActorState and useActorAction need no changes — the plugin routes them over the socket
for you.
What it buys
Three things, and they are all about the shape of the connection rather than raw speed:
- No cross-origin preflight. A WebSocket upgrade is not a CORS-preflighted request, so a
browser talking to an actor host on another origin stops paying an
OPTIONSround trip per call. - No held-open POST. Live reads over HTTP ride a long-lived response body, which proxies, mobile NATs and corporate middleboxes all treat with suspicion. A WebSocket is the thing that infrastructure expects to stay open.
- Subscriptions change incrementally. Adding one is a ~40-byte
{i,sub}message and removing one is{i,uns}— nothing reopens, so a page whose live set changes as components mount does not reconnect and re-seed every time.
Per-call latency and throughput are not the win. Against a warm
same-origin HTTP/2 connection, a socket call measures about the same — and reliability is
worse, because sockets drop and in-flight calls fail with them.
fetchTransport() remains the default, and
it is the right default.
What happens when it drops
In-flight calls fail, and are never retried. Re-sending a call whose response was lost would mean re-running an actor method that may not be idempotent — a correctness bug wearing a reliability costume. They reject, and the caller decides.
Subscriptions re-establish on their own. They are declarative, so replaying them is safe.
Each one re-seeds on reconnect, and fingerprint() suppresses the value if it has not changed
— so a reconnect does not repaint the page.
Reconnection backs off from retryMs (default 300 ms) to maxRetryMs (default 10 s).
It is host-affine
Every call re-dispatches through placement anyway, so a per-call endpoint makes no sense
over a socket and is ignored, with a __DEV__ warning. If you need to reach a specific
host, that is locality routing's job.
Not only WebSocket
The primary seam is a link, not a URL:
socketTransport({
connect: (handlers) => ({
send: (message) => channel.emit('sigx', message),
close: () => channel.disconnect(),
}),
});
url is sugar that dials the global WebSocket. Supply connect instead and socket.io, a
MessageChannel, a React Native bridge or anything else message-oriented can carry the same
protocol — see Installation for worked
recipes.
Next steps
- Installation — options, adapters and interop.
- API — the exported surface.
- Socket sessions — the server half, and its posture.
- Host transports — the other kind of transport, between hosts.
