WebsocketClientActor
WebsocketClientActor opens a client-side WebSocket
connection to a remote endpoint. Inbound frames are decoded
into typed messages you handle in onMessage; outbound messages
are encoded and sent (buffered while disconnected, resent after
reconnect). For accepting incoming WS connections (server
side), see server WebSocket.
import { match } from 'ts-pattern';import { WebsocketClientActor, WebsocketClientOptions, websocketSend } from 'actor-ts';
type ClientMessage = { kind: 'ping'; n: number };type ServerMessage = { kind: 'pong'; n: number } | { kind: 'event'; data: unknown };
class Feed extends WebsocketClientActor<ClientMessage, ServerMessage> { constructor() { const webSocketClientOptions = WebsocketClientOptions.create<ClientMessage, ServerMessage>().withUrl('ws://localhost:8080/ws'); super(webSocketClientOptions); }
override onConnected(): void { this.send({ kind: 'ping', n: 1 }); }
onMessage(message: ServerMessage): void { // decoded server message match(message) .with({ kind: 'pong' }, (m) => this.onPong(m)) .exhaustive(); }
private onPong(message: PongMessage): void { this.log.info(`pong ${message.n}`); }}
const feed = system.spawn(Feed, 'feed');The two type parameters read from the client’s point of view:
TOut = the messages this client sends, TIn = the
decoded server messages it receives.
WebsocketClientActor extends the shared
BrokerActor base, so it inherits
reconnect-with-backoff, the outbound buffer across reconnects,
and the circuit breaker for free.
Settings
Section titled “Settings”The constructor takes Partial<WebsocketClientOptionsType<TOut, TIn>>.
Only url is required; everything else has a default.
interface WebsocketClientOptionsType<TOut, TIn> extends BrokerCommonOptionsType { url: string; // required — ws:// or wss:// protocols?: string | string[]; // subprotocols codec?: WebsocketCodec<TOut, TIn>; // default jsonCodec() maxFrameBytes?: number; // default 1 MiB; oversize inbound dropped with a warning onInvalidMessage?: 'drop' | 'hook' | 'disconnect'; // default 'drop' pingIntervalMs?: number; // application-level ping; default off}Because the settings extend BrokerCommonOptionsType, the
reconnect ({ initialDelayMs, maxDelayMs, factor, maxAttempts }),
outboundBuffer, and circuitBreaker blocks all apply — see the
BrokerActor base for their semantics.
Sending and receiving
Section titled “Sending and receiving”Inside the actor, this.send(message) encodes the message via the
codec and enqueues it; while disconnected the message is buffered
and resent after reconnect. Inbound frames are decoded and
delivered to onMessage.
class Chat extends WebsocketClientActor<ClientMessage, ServerMessage> { constructor() { const webSocketClientOptions = WebsocketClientOptions.create<ClientMessage, ServerMessage>().withUrl('wss://chat.example.com/ws'); super(webSocketClientOptions); }
override onConnected(): void { this.send({ kind: 'setName', name: 'alice' }); // encode + enqueue → true }
onMessage(message: ServerMessage): void { // one call per decoded server frame, in frame order }}send(message: TOut): boolean returns whether the message was
accepted (it can be rejected if the outbound buffer is full).
For a raw frame — bypassing the codec — use
sendRaw(frame: WebsocketFrame): boolean.
Sending from another actor
Section titled “Sending from another actor”Other actors don’t call send directly; they push a typed send
through the ref with the websocketSend helper:
import { websocketSend } from 'actor-ts';
feed.tell(websocketSend({ kind: 'ping', n: 42 }));onMessage(message: TIn) is abstract — you must implement it. The
rest are optional overrides:
| Hook | When |
|---|---|
onConnected() | The connection is (re)established. Good place for a handshake / re-subscribe. |
onDisconnected(cause?: Error) | The connection dropped; cause is set on error-driven drops. |
onInvalidMessage(error: WebsocketDecodeError) | A frame failed to decode (only when onInvalidMessage: 'hook'). |
onSelfMessage(message: TSelf) | Handle the optional third type parameter — messages the actor sends to itself. |
Codecs
Section titled “Codecs”The codec turns TOut messages into wire frames and wire frames
back into TIn messages. The default is jsonCodec():
import { jsonCodec } from 'actor-ts';
new Feed(); // uses jsonCodec() — text frames <-> JSON
// With runtime validation (zod etc.):class Validated extends WebsocketClientActor<ClientMessage, ServerMessage> { constructor() { const webSocketClientOptions = WebsocketClientOptions.create<ClientMessage, ServerMessage>() .withUrl('wss://...') .withCodec(jsonCodec<ClientMessage, ServerMessage>({ validate: (value: unknown): ServerMessage => ServerMessageSchema.parse(value), })); super( webSocketClientOptions, ); } onMessage(message: ServerMessage): void { /* already validated */ }}For binary protocols, rawCodec() is the escape hatch —
TOut = TIn = WebsocketFrame, so you handle raw text/binary frames
yourself:
import { match } from 'ts-pattern';import { rawCodec, type WebsocketFrame } from 'actor-ts';
class Binary extends WebsocketClientActor<WebsocketFrame, WebsocketFrame> { constructor() { const webSocketClientOptions = WebsocketClientOptions.create<WebsocketFrame, WebsocketFrame>() .withUrl('wss://...') .withCodec(rawCodec()); super(webSocketClientOptions); } onMessage(frame: WebsocketFrame): void { match(frame) .with({ kind: 'binary' }, (f) => this.onBinary(f)) .otherwise(() => {}); }
private onBinary(frame: BinaryFrame): void { this.handleBytes(frame.data); // Uint8Array }}A WebsocketFrame is { kind: 'text'; data: string } or
{ kind: 'binary'; data: Uint8Array }. Decode failures throw a
WebsocketDecodeError; the inbound frame-size cap (maxFrameBytes) is
enforced on the raw frame before decode.
Reconnect and buffering
Section titled “Reconnect and buffering”An unexpected disconnect is handled by the BrokerActor lifecycle:
- The actor transitions to
disconnectedand firesBrokerDisconnectedon the event stream. - The reconnect cycle starts, with exponential backoff per the
reconnectsettings (initialDelayMs,maxDelayMs,factor,maxAttempts). - After a successful re-connect,
onConnected()runs andBrokerConnectedfires.
Outbound messages sent while disconnected are buffered per the outbound-buffer policy and resent after reconnect. The inherited circuit breaker trips after repeated connection failures so a dead endpoint doesn’t spin the reconnect loop forever.
Ping / keep-alive
Section titled “Ping / keep-alive”class KeepAlive extends WebsocketClientActor<ClientMessage, ServerMessage> { constructor() { const webSocketClientOptions = WebsocketClientOptions.create<ClientMessage, ServerMessage>() .withUrl('wss://...') .withPingIntervalMs(30_000); super(webSocketClientOptions); }
onMessage(message: ServerMessage): void {}}An application-level ping every pingIntervalMs keeps
intermediate proxies / load balancers from closing idle
connections — most production setups want it. The protocol-level
WebSocket ping (control frame) is separate and handled by the
runtime; the application-level ping is for proxies that inspect
payload, not frames.
HOCON config
Section titled “HOCON config”Client defaults can be set under
actor-ts.io.broker.websocket:
actor-ts.io.broker.websocket { url = "wss://realtime.example.com/feed" protocols = ["my-proto-v1"] pingIntervalMs = 30000 maxFrameBytes = 1048576 reconnect { initialDelayMs = 500, maxDelayMs = 30000, factor = 2.0 } // maxAttempts omitted = unlimited (the default) circuitBreaker { /* ... */ } outboundBuffer { /* ... */ }}Precedence follows the broker convention: constructor argument > HOCON > built-in defaults.
When to use it
Section titled “When to use it”Three primary fits:
- Subscribing to real-time data feeds — stock tickers, crypto exchanges, social-media streams.
- Outbound WebSocket clients to third-party services (Slack RTM, streaming LLM APIs, IoT vendor APIs).
- Custom WS-based protocols layered on top — use
rawCodec()for binary wire formats.
For server-side WebSocket (accepting client connections), use the server WebSocket page.
Where to next
Section titled “Where to next”- I/O overview — the bigger picture.
- Server WebSocket — the
websocket()directive +WebsocketServerActor. - SSE — server-sent events as a one-way streaming alternative.
- BrokerActor base — the shared reconnect / buffer / circuit-breaker lifecycle.
