Pular para o conteúdo
Português (BR)

WebsocketClientActor

Este conteúdo não está disponível em sua língua ainda.

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.

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.

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.

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:

HookWhen
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.

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.

An unexpected disconnect is handled by the BrokerActor lifecycle:

  • The actor transitions to disconnected and fires BrokerDisconnected on the event stream.
  • The reconnect cycle starts, with exponential backoff per the reconnect settings (initialDelayMs, maxDelayMs, factor, maxAttempts).
  • After a successful re-connect, onConnected() runs and BrokerConnected fires.

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.

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.

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.

Three primary fits:

  1. Subscribing to real-time data feeds — stock tickers, crypto exchanges, social-media streams.
  2. Outbound WebSocket clients to third-party services (Slack RTM, streaming LLM APIs, IoT vendor APIs).
  3. 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.

  • 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.