npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stream-io/ws

v0.4.4

Published

WebSocket client for browsers and Node.js

Readme

WebSocket client for browsers and Node.js

This is a WebSocket wrapper that aims to fix some common annoyances we delt with when building our frontend SDKs:

Native WebSocket: API is events-based. You wait for the open event to ensure a connection is open, and for error event to handle errors.

This library: API is Promise-based: you await ws.open(), and wrap it with try/catch to handle errors.


Native WebSocket: instances are single-use. Connection starts as soon as the instance is created. After connection closes, a new instance must be created to reconnect.

This library: instances can be reused: you can ws.open() and ws.close() multiple times, without reinstalling event handlers. Great for using WebSockets from UI components.


Native WebSocket: with an events-based API, business logic is expressed with callbacks and state variables.

This library: business logic can be written imperatively with procedures.

Installation

npm install @stream-io/ws
yarn add @stream-io/ws
pnpm add @stream-io/ws

Basics

Opening a connection

Create a client instance and open a connection:

import { WebSocketClient } from "@stream-io/ws";
const ws = new WebSocketClient({ url: "wss://example.com" });
await ws.open();

After ws.open() resolves, the connection is ready.

This method can reject if the URL is malformed, the server is unreachable, refuses to upgrade the connection, goes away or closes the connection before it's ready.

Unlike the native WebSocket, WebSocketClient doesn't dispatch the open event when the connection is ready, or error event in case of problems. Await ws.open() instead, and wrap it with try/catch to handle errors.

Sending and receiving messages

Send a message with ws.send():

const ws = new WebSocketClient({ url: "wss://example.com" });
await ws.open();
ws.send({ type: "hello", name: "world" });

Make sure you've awaited ws.open() before sending a message.

By default, message payload is serialized as JSON. If you want to just send UTF-8 strings, provide a text format when creating the client:

import { WebSocketClient, textFormat } from "@stream-io/ws";
const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
await ws.open();
ws.send("Hello, world!");

You might already have types for the JSON message payloads. To have your messages strongly typed, use jsonFormat() with a type argument:

import { WebSocketClient, jsonFormat } from "@stream-io/ws";

type MessagePayload = { type: "hello"; name: string } | { type: "goodbye" };

const ws = new WebSocketClient({
  url: "wss://example.com",
  format: jsonFormat<MessagePayload>(),
});
await ws.open();
ws.send({ type: "hello", name: "world" });

Strong typing is not the same as validation. Passing a type argument doesn't have any runtime effect. To validate payloads, implement a custom message format.

To receive a message, listen for the message event:

const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
await ws.open();
ws.on("message", (message) => {
  console.log(message);
});

Install message event handlers before or synchroniously after ws.open(), otherwise some messages can be missed.

The message event is only dispatched on the current connection. Although it's technically possible to receive a message after ws.close() was called, the client will not dispatch these events.

If a message payload cannot be parsed (e.g. you're using jsonFormat() and the message is not a valid JSON stirng), the recverror event is dispatched. Listening to this event can be useful for logging, and it's also an opportunity to close the faulty connection:

const ws = new WebSocketClient({
  url: "wss://example.com",
  format: jsonFormat(),
});
await ws.open();
ws.on("recverror", (err) => {
  console.error(err);
  ws.close(4000, "unparseable payload");
});

Closing connection

Close a connection with ws.close():

const ws = new WebSocketClient({ url: "wss://example.com" });
await ws.open();
await ws.close();

You can also specify a reason for closure. Be sure to only use status codes in the 3000-4999 range, and only use the 4000-4999 range for application-specific reasons:

await ws.close(4018, "I'm a teapot");

No close event is dispatched when you close the connection this way. Instead, the connection is fully closed after ws.close() resolves.

This method never rejects, and it's usually not necessary to await it. New connection can be opened immediately after calling ws.close():

const ws = new WebSocketClient({ url: "wss://example.com" });
await ws.open();
ws.close();
await ws.open();

This is useful when using WebSockets from something like a React component. This code doesn't have problems running in <StrictMode />:

const ws = useMemo(() => new WebSocketClient({ url }), [url]);
const [isReady, setReady] = useState(false);
useEffect(() => {
  ws.open().then(() => setReady(true));
  return () => {
    setReady(false);
    ws.close();
  };
}, [ws]);

Calling ws.close() interrupts any current async operations. For example, if it's called before the connection is ready, the ws.open() method rejects with an AbortedError:

import { WebSocketClient, AbortedError } from "@stream-io/ws";
const ws = new WebSocketClient({ url: "wss://example.com" });
ws.open().catch((err) => {
  if (err instanceof AbortedError) {
    console.log("Connection cancelled");
  }
});
ws.close();

It's not possible to end up in a race condition between ws.open() and ws.close() calls. The latest call determines the state of the connection.

Handling connection closure

Connection be closed without an explicit ws.close() call:

  1. connection closure can be initiated server-side;
  2. server can go offline abruptly;
  3. network distruption can cause the connection to be dropped; etc.

We say that in these cases the connection is closed externally, and the client dispatches the close event:

const ws = new WebSocketClient({ url: "wss://example.com" });
await ws.open();
ws.on("close", (code, reason) => {
  console.log("Connection closed with code", code, "and reason", reason);
});

The close event is only dispatched if the connection was ready. If the connection closes before it's ready, ws.open() rejects with a WebSocketClosedError, which may contain a status code and reason for closure (although these are rarely useful in case of abrupt closure):

import { WebSocketClient, WebSocketClosedError } from "@stream-io/ws";
const ws = new WebSocketClient({ url: "wss://example.com" });

try {
  await ws.open();
} catch (err) {
  if (err instanceof WebSocketClosedError) {
    console.log("Connection closed abruptly");
    if (err.code && err.reason) {
      console.log("Code:", err.code);
      console.log("Reason:", err.reason);
    }
  }
}

Executing procedures

Procedures are a way of writing your WebSocket-based business logic without dealing with event handlers and state variables.

Say, for example, you expect your server to acknowledge message delivery. Here's a vanilla WebSocket implementation:

function sendAndAck(ws: WebSocket, message: string): Promise<void> {
  const { promise, resolve } = Promise.withResolvers<void>();
  const handleMessage = (event: MessageEvent) => {
    if (message === `ack:${message}`) {
      ws.removeEventListener("message", handleMessage);
      resolve();
    }
  };
  ws.addEventListeners("message", handleMessage);
  ws.send(message);
  return promise;
}

The logic is split between the function and the event handler. It's also easy to have a memory leak if you're not careful about cleaning up event listeners.

Here's the same logic implemented as a procedure:

function sendAndAck(ws: WebSocketClient<string>, message: string): Promise<void> {
  return ws.exec(function* ({ send, recv }) {
    yield send(message);
    while (yield recv() !== `ack:${message}`) {}
  });
}

Procedure is a function that gives the client commands to perform actions or check certain conditions. Procedures are generators that are executed with ws.exec().

Make sure you've awaited ws.open() before executing a procedure.

Supported commands are:

  1. send(message) instructs the client to send a message;
  2. recv() instructs the client to wait for an incoming message, and returns the message payload;
  3. expect(predicate) checks the next incoming message against the predicate and throws UnexpectedMessageError if the check fails.
  4. settle(promise) awaits a promise. This comes with some caveats, see below.

These commands are passed to your generator as the only argument. The generator must yield the command for it to be executed.

The ws.exec() promise resolves with the return value of the generator.

Procedures are great for turning event-based logic into imperative logic. They allow you to write code in a straightforward, linear manner, and avoid storing state.

Here's a sample client that asks the server for a password and checks the response:

import { WebSocketClient, UnexpectedMessageError, textFormat } from "@stream-io/ws";

const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
await ws.open();
const superSecretPassword = "p@$$w0rd";

try {
  await ws.exec(function* ({ send, expect }) {
    yield send("password?");
    yield expect((res) => res === superSecretPassword);
  });
} catch (err) {
  if (err instanceof UnexpectedMessageError) {
    ws.close(4001, "wrong password");
  }
}

Another sample client echoes everything the server sends, until it receives the message saying "stop":

await ws.exec(function* ({ send, recv }) {
  while (true) {
    const message = yield recv();
    if (message === "stop") {
      return;
    }
    yield send(message);
  }
});

Procedures are interrupted by explicit ws.close() calls. In that case, ws.exec() rejects with an AbortedError.

Procedures are also interrupted by external closures, in which case ws.exec() rejects with a WebSocketClosedError containing status code and reason for closure.

Async actions in procedures

Procedures can wait for async actions to complete by yielding the settle(promise) command. Procedure resumes once the promise resolves. Rejections can be caught with try/catch.

This should be used with caution:

  1. Any message that arrives while waiting is missed by subsequent recv or expect comamnds. If the procedure is running with suppressMessageEvents option enabled, it's missed completely.
  2. If the connection closes while waiting, the procedure is interrupted immediately, but the async action itself will keep executing.
  3. The resolved value of the promise is ignored.

Behaviors

Resilient

In practice, a client needs to keep a healthy WebSocket connection, and to try reconnecting if the connection is closed externally (for example, after a network disruption).

To make the client resilient to external closures, wrap it with makeResilient:

import { WebSocketClient, makeResilient, exponentialBackoff } from "@stream-io/ws";
const ws = new WebSocketClient({ url: "wss://example.com" });
const resilient = makeResilient(ws, {
  retry: exponentialBackoff(),
  maxTimeoutMs: 30_000,
});
await resilient.open();

The resilient.open() method will keep trying to connect until it succeeds, or until it can no longer retry, in which case it rejects.

External closures don't dispatch the close event on the resilient client. Instead, it tries to reconnect until it succeeds, or until it can no longer retry, in which case the gaveup event is dispatched.

The resilient.healthy() promise can be awaited to make sure the connection is healthy. It resolves when the current reconnection attempt suceeds (or immediately if the connection is healthy). It rejects if the client gives up trying to reconnect.

Retrying is affected by the retry policy and the maximum timeout.

The retry policy controls how often and how many reconnection attempts can be made. We recommend the exponentialBackoff policy: each subsequent attempt is delayed by an increasing amount of time, and the total number of attempts is configurable:

makeResilient(ws, {
  retry: exponentialBackoff({
    minDelayMs: 1000, // how long to delay the second attempt
    maxDelayMs: 10_000, // maximum delay after an attempt
    factor: 2, // multiply delay by this factor after every new attempt,
    maxAttempts: Number.POSITIVE_INFINITY, // make no more than this number of attempts
    jitter: 0.1, // add a little randomness to the delays (percentage of `minDelayMs`)
  }),
  maxTimeoutMs: 30_000,
});

The maximum timeout controls how long the connection can stay unhealthy before the client gives up trying to reconnect.

When the maximum timeout is reached, or the retry policy prohibits further attempts, the gaveup event is dispatched, and the connection remains closed.

Wrapping the client with makeResilient doesn't modify the behavior of ws.send() and ws.exec(). These methods still throw or reject if the connection is not opened at the moment they are called. You should either:

  1. handle these errors, or
  2. await the resilient.healthy() promise before sending a message or executing a procedure:
await resilient.healthy();
resilient.send("ping");

Note that this can delay sending a message or executing a procedure by up to the maxTimeoutMs. Procedures will still be cancelled with a WebSocketClosedError if the connection drops while the procedure is executing.

For logging purposes, the reconnect event is dispatched before the first reconnection attempt is made:

resilient.on('reconnect', await (code, reason) => {
  console.log('Connection dropped with code', code, 'and reason', reason);
  await resilient.healthy();
  console.log('Connection restored');
})

Authenticated

If you're implementing a handshake procedure between the client and the server, wrap the WebSocketClient instance with makeAuthenticated:

import { WebSocketClient, textFormat, makeAuthenticated } from "@stream-io/ws";
const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
const superSecretToken = "token";
const authenticated = makeAuthenticated(ws)(function* ({ send, expect }) {
  yield send(`token:${superSecretToken}`);
  yield expect((m) => m === "ok");
});
await authenticated.open();

(Note that the double parenthesis in the example above: makeAuthenticated(ws)(handshake). This is required for better type inference.)

The connection wrapped with makeAuthenticated is not considered ready before the handshake is completed. If it throws an error, authenticated.open() rejects with the same error.

No message events are dispatched by the authenticated client before the handshake completes.

The value returned by the handshake is passed on to the authenticated.open() promise:

const authenticated = makeAuthenticated(ws)(function* ({ send, recv }) {
  yield send(`token:${superSecretToken}`);
  const res = yield recv();
  return res;
});
const res = await authenticated.open();
console.log("Authenticated:", res);

Health checked

It's common to check if both the client and the server are still there by sending a periodic health check message. Failing a health check indicates that the connection should be closed.

This behvior can be added by wrapping the client with makeHealthChecked:

import { WebSocketClient, textFormat, makeHealthChecked } from "@stream-io/ws";
const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
const hchecked = makeHealthChecked(ws, {
  checkEveryMs: 25_000,
  timeoutMs: 10_000,
})(function* ({ send, expect }) {
  yield send("ping");
  yield expect((m) => m === "pong");
});
await hchecked.open();

(Note that the double parenthesis in the example above: makeHealthChecked(ws, options)(check). This is required for better type inference.)

Every checkEveryMs, the health check procedure is executed. If it throws, or fails to complete within the timeoutMs, the close event is dispatched and the connection is closed.

Combining behaviors

Resilient, authenticated, and health checked behaviors can be composed together.

In this example, the client performs the handshake on every reconnect. If the handshake fails, a new connection attempt is made according to the retry policy. While connected, it also performs health checks, and if a health check fails, the client reconnects:

import {
  WebSocketClient,
  textFormat,
  makeAuthenticated,
  makeHealthChecked,
  makeResilient,
  exponentialBackoff,
} from "@stream-io/ws";
const ws = new WebSocketClient({
  url: "wss://example.com",
  format: textFormat(),
});
const superSecretToken = "token";
const authenticated = makeAuthenticated(ws)(function* ({ send, expect }) {
  yield send(`token:${superSecretToken}`);
  yield expect((m) => m === "ok");
});
const hchecked = makeHealthChecked(authenticated, {
  checkEveryMs: 25_000,
  timeoutMs: 10_000,
})(function* ({ send, expect }) {
  yield send("ping");
  const res = yield recv();
  return res;
});
const resilient = makeResilient(hchecked, {
  retry: exponentialBackoff(),
  maxTimeoutMs: 30_000,
});
const res = await resilient.open();
console.log("Connected. Authentication result:", res);

Since the resilient.healthy() promise resolves with the return value of resilient.open(), the latest handshake result can be always accessed like this:

const res = await resilient.healthy();
resilient.send(`hello from ${res}`);

This is useful if the handshake result affects how you send your messages (e.g. messages need to be signed with a key shared during handshake).

Advanced

Custom message formats

Message formats provide type safety, validation and (de-)serialization functionality when sending and receiving messages.

To support a custom message format, implement the MessageFormat<T> interface. It has a formatter to serialize and a parser to deserialize message payloads. The type argument is the payload type:

const integerFormat: MessageFormat<number> = {
  parser(data) {
    const int = Number.parseInt(data, 10);
    if (!Number.isSafeInteger(int)) {
      throw new Error("Expected message to be a valid integer string");
    }
    return int;
  },

  formatter(int) {
    return int.toString(10);
  },
};

Throwing an error from the parser dispatches the recverror event. Another option is to return a special recvIgnore() value which prevents the message event from dispatching. It's useful for "forgiving" parsers that ignore invalid payloads.

The formatter must return one of the types that can be sent via WebSocket: string, ArrayBuffer (or ArrayBufferView), Blob.

In this example messages are validated using a Zod schema:

import type { MessageFormat } from "@stream-io/ws";
import * as z from "zod/v4";
const MessageSchema = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("error"), error: z.string() }),
]);
const format: MessageFormat<z.infer<typeof MessageSchema>> = {
  parser: (data) => MessageSchema.parse(JSON.parse(obj)),
  formatter: (obj) => JSON.stringify(obj),
};

Custom retry policies

Retry policy is an async function that returns true if another attempt should be made, or false if not. Before returning, it can implement a delay or other logic based on the attempt number and the latest error that caused a retry.

This sample policy implements a 1 second delay after every attempt, and allows no more than 3 attempts:

import type { Retry } from "@stream-io/ws";
const thrice: Retry = ({ attempt }) =>
  attempt < 3
    ? new Promise((resolve) => setTimeout(() => resolve(true), 1000))
    : Promise.resolve(false);

Note that the first time the retry policy is called, the attempt number is 1, since the first (0th) attempt is always made unconditionally.

Custom behaviors

Behaviors change or add functionality to the client while implementing a compatible interface.

We recommend defining behaviors as a wrapper function that accepts and returns a WebSocketLike<T, R> object, where T is the message payload type, and R is the return type of ws.open(). The return type may also extend WebSocketLike.

For example, this is the signature of the makeResilient behavior:

import type { WebSocketLike } from "@stream-io/ws";
interface ResilientWebSocket<T, R = void> extends WebSocketLike<T, R> {
  healthy(): Promise<R>;
  // ...
}
function makeResilient<T, R>(
  ws: WebSocketLike<T, R>,
  options: ResilientWebSocketOptions,
): ResilientWebSocket<T, R> {
  // ...
}

Reference

WebSocketClient

This is the main class you use to create a WebSocket connection.

constructor(options)

Creates a client instance. Unlike the native WebSocket, connection is not started once the client is created. Instead, you open the connection with ws.open().

options.url URL of the WebSocket endpoint. Usually starts with ws:// or wss://.

options.format Message format to parse (deserialize) and format (serialize) message payloads. Use textFormat() to send a receive raw UTF-8 strings, and jsonFormat<T>() for JSON strings.

This option is used to infer the message payload type for the client. It's usually a mistake to pass the WebSocketClient<T> type argument explicitly. Instead, use an appropriate message format.

open()

Opens a connection. You should always await ws.open() before sending messages or executing procedures.

Returns: promise that resolves once the connection is ready.

If the connection closes before it's ready, this method rejects with a WebSocketClosedError which may contain a status code and reason for closure (although these are rarely useful in case of an abrupt closure).

Opening a connection can be interrupted by calling ws.close(), in which case this method rejects with an AbortedError.

close(code, reason)

Closes a connection with optionally specified status code and reason. Be sure to only use status codes in the 3000-4999 range, and only use the 4000-4999 range for application-specific reasons.

Returns: promise that resolves once the connection is closed.

Unlike the native WebSocket, no close event is dispatched as a result of calling this method. Instead, the connection is fully closed once the returned promise resolved.

It's not necessary to await this method. It never rejects, and ws.open() can be called again immediately after ws.close().

Calling this method interrupts any running async operations, such as opening a connection, executing a procedure, waiting for a handshake or a health check. These async operations will reject with an AbortedError.

send(message)

Sends a message. Before sending, the message is serialized using the provided message format. Make sure you await ws.open before sending a message, otherwise this method throws an error.

This method is synchronous, and as with the native WebSockets, the message is queued, and its delivery is not guaranteed.

exec(procedure, options)

Executes a procedure. Make sure you await ws.open before sending a message, otherwise this method rejects.

Returns: promise that resolves with the return value of the procedure.

If the connection closes before the procedure completes, this method rejects with a WebSocketClosedError which contains a status code and reason for closure.

Executing the procedure can be interrupted by calling ws.close(), in which case this method rejects with an AbortedError.

options.suppressMessageEvents When true, no message events are dispatched while the procedure is executing. Useful when the procedure exclusively handles all incoming messages. Default: false.

on(event, cb)

Installs an event listener.

Returns: function that can be called to uninstall the listener.

const unlisten = ws.on("message", (message) => {});
unlisten();

All listeners are guaranteed to run once when an event is dispatched, even if one of the event listeners throws an error. In browsers, the error event will be dispatched on the window in case on of the listeners throws. In Node.js, thrown errors are ignored.

event One of supported events.

cb Listener to be executed when the event is dispatched.

Events

message Dispatched on incoming messages. Before dispatching this event, the message is deserialized using the provided message format.

Callback signature is (message: T) => void, T is message payload type as defined by the provided message format.

This event can be suppressed in two cases:

  1. the parser for the provided message format returned recvIgnore(), a special value indicating that the message must be ignored;
  2. a procedure is running with the suppressMessageEvents option.

recverror Dispatched when the parser for the provided message format throws an error.

Callback signature is (error: any) => void.

close Dispatched when the connection is closed externally: closure was initiated server-side, server went offline abruptly, network distruption caused the connection to be dropped, etc.

Callback signature is (code: number, reason: string) => void.

Unlike the native WebSocket, closing the connection with ws.close() does not dispatch this event.

Formats

textFormat()

Pass this format to the WebSocketClient constructor to send and receive messages as UTF-8 strings.

jsonFormat()

Pass this format to the WebSocketClient constructor to send and receive as JSON strings. This format is most useful when message payload type is known. Then it can be passed as a type parameter:

type MessagePayload = { type: "hello"; name: string } | { type: "goodbye" };
const ws = new WebSocketClient({
  url: "wss://example.com",
  format: jsonFormat<MessagePayload>(),
});

Strong typing is not the same as validation. Passing a type argument doesn't have any runtime effect. To validate payloads, implement a custom message format.

recvIgnore()

This is a special value that can be returned by a custom format that indicates that an incoming message should be ignored. Returning recvIgnore() prevents the message event from dispatching

Custom formats

Custom formats must implement the MessageFormat<T> interface and provide a formatter to serialize and a parser to deserialize message payloads.

Formatter receives a message of type T and must return one of the types that can be sent via WebSocket: string, ArrayBuffer (or ArrayBufferView), Blob.

Parser receives an incoming message (MessageEvent's event.data) and must return parsed payload of type T, or a special recvIgnore() value.

Resilient

makeResilient(ws, options)

Wraps the provided client and makes it resilient to abrupt closures. See Resilient behavior.

ws Client to be wrapped. It can be the WebSocketClient instance, or any other WebSocketLike object (e.g. the client wrapped with some other behavior).

options.retry Retry policy to be used when reconnecting. Can be one of the built-in policies, like exponentialBackoff, or a custom one.

options.maxTimeoutMs The longest the connection can stay unhealthy before the client gives up trying to reconnect.

options.allowedCloseCodes Optional list of codes that are considered normal reasons for closure. When closed externally with one of these codes, the client does not try to reconnect, and the close event is dispatched as usual.

resilient.open()

Same as ws.open, but if an attempt to open a connection fails, the client tries again according to the retry policy and the maxTimeoutMs option. When the client can no longer retry, it rejects with the latest error thrown by the wrapped client.

Returns: the return value of the latest successful open() call on the wrapped client. For a regular WebSocketClient it's undefined, but other behaviors may return a value.

resilient.healthy()

If a reconnection attempt is currently in progress, returns a promise that resolves when this attempt succeeds. If the attempt is unsuccessful and the client gives up trying to reconnect, it rejects with the latest error thrown by the wrapped client.

Resolves immediately if the connection is ready.

This promise resolves with the return value of the latest successful open() call on the wrapped client. For a regular WebSocketClient it's undefined, but other behaviors may return a value.

resilient.close(code, reason)

Same as ws.close. If a reconnection attempt is currently in progress, it is interrupted (as well as any other async operation). The interrupted resilient.healthy() promise rejects with an AbortedError.

resilient.send(message)

Same as ws.send. If the connection is not ready, this method throws an error. You might want to await the resilient.healthy() promise before sending a message, although it still doesn't guarantee delivery:

await resilient.healthy();
resilient.send("ping");

resilient.exec(procedure)

Same as ws.exec. If the connection is not ready, this method rejects. You might want to await the resilient.healthy() promise before executing a procedure:

await resilient.healthy();
resilient.exec(function* ({ send, recv, expect }) {
  // ...
});

resilient.on(event, cb)

Same as ws.on, but this method supports events that are specific to the resilient behavior.

Events

message and recverror events are same as on the wrapped client.

close Dispatched when the connection is closed externally with one of the allowed close codes.

Callback signature is (code: number, reason: string) => void.

gaveup Dispatched when the connection is closed externally, and no further reconnection attempts can be made. For example, this event is dispatched when the connection isn't restored within maxTimeoutMs.

Callback signature is (error: any, code: number, reason: string) => void, where error is the latest error thrown by the wrapped client.

reconnect Dispatched as soon as reconnection attempts begin. At this point the resilient.healthy() promise represents the current reconnection attempt.

Callback signature is (code: number, reason: string) => void, with the status code and reason of the external closure that caused reconnection attempts to begin.

Authenticated

makeAuthenticated(ws, options)(handshake)

Wraps the provided client and adds a handshake procedure that runs as soon as the connection is open. See Authenticated behavior.

ws Client to be wrapped. It can be the WebSocketClient instance, or any other WebSocketLike object (e.g. the client wrapped with some other behavior).

options.timeoutMs Maximum amount of time the handshake procedure can run before it's considered failed.

options.closeArgs When connection is closed because the handshake procedure has failed, these arguments are used as status code and reason for closure. E.g. [3000, "handshake failed"].

handshake Procedure that runs as soon as the connection is open. The return value of this procedure is used to resolve the authenticated.open() promise. Throwing from this procedure rejects the authenticated.open() promise.

No message events are dispatched before the handshake completes.

The handshake procedure can be interrupted by timing out or closing the connection.

All methods and events are the same as on the wrapped client, except for:

authenticated.open()

Same as ws.open, with an added handshake procedure that runs as soon at the connection open. This method does not resolve or reject until the handshake procedure completes, fails, or times out.

Returns: the return value of the handshake procedure.

authenticated.close(code, reason)

Same as ws.close. If a handshake procedure is currently in running, it is interrupted (as well as any other async operation).

Health checked

makeHealthChecked(ws, options)(check)

Wraps the provided client and adds a periodically running health check. Failing the health check closes the connection. See Health checked behavior.

ws Client to be wrapped. It can be the WebSocketClient instance, or any other WebSocketLike object (e.g. the client wrapped with some other behavior).

options.checkEveryMs Amount of time between health checks. The countdown starts as soon as one health check completes, so actual time between two health checks starting can be longer than that.

options.timeoutMs Maximum amount of time the health check procedure can run before it's considered failed.

options.closeArgs When connection is closed because the health checks procedure has failed, these arguments are used as status code and reason for closure. E.g. [3008, "health check failed"].

options.timers Optional timers implementation. Since health checks are expected to run in background and may be throttled by browsers, passing an alternative timer implementation (e.g. running timers from a Web Worker) can reduce throttling.

check Procedure that is executed periodically to perform a health check. Throwing from this procedure closes the connection and dispatches the close event.

All methods and events are the same as on the wrapped client.

Retry

exponentialBackoff(options)

Retry policy that allows a specified maximum number of attempts, each with an exponentially growing delay after an attempt.

options.minDelayMs Delay after the first (0) attempt. Subsequent attempts will be delayed for longer. Default: 1 second.

options.maxDelayMs Maximum possible delay after an attempt. Default: no restriction.

options.maxAttempts Maximum total amount of retry attempts to be made. Default: 3.

options.factor Each subsequent delay is multiplied by this factor, but is restricted by the maximum delay. Default: 2.

options.jitter Adds randomness to delays, in percetage of minDelayMs. Default: 0.1 (±0.1 second)

With default settings, a total of 4 attempts are made by this policy:

Attempt 0: executed immediately.
Attempt 1: after 1s ±0.1s.
Attempt 2: after 2s ±0.1s.
Attempt 3: after 4s ±0.1s.

Custom retry policies

Retry is an interface that can be implemented by custom retry policies. It is an async function with the following signature:

(context: RetryContext): Promise<boolean>

When called, it is provided the following context:

attempt Current attempt index. It starts with 1, because the first (0) attempt is always performed immediately and unconditionally.

context.latestError Latest error that caused a retry attempt.

Retry policy can resolve with a delay, thus implementing a delay between retry attempts.

It must resolve with a boolean indicating if further retry attempts are allowed. If it resolves with false, retries attempts stop.

WebSocketClosedError

Async operations interrupted by abrupt external closures reject with this error. It may contain information about the reason for closure:

error.code Optional status code sent when the connection closed.

error.reason Optional reason for closure.

AbortedError

Async operations interrupted by closing the connection with ws.close() reject with this error.

Design notes

This library does its best to follow these principles:

Don't dispatch events triggered by explicit actions in user code.

Opening a connection with ws.open() doesn't dispatch open. Closing the connection with ws.close() does not dispatch close.

Instead, events are triggered by external actions: if the server decides to close the connection, the close event is dispatched by the client.

(By the same principle input elements don't dispatch change when their value is updated with input.value = "new value". It's useful, since it's usually the external that require handling.)

Gracefully handle abrupt connection closures in async operations.

Any operation like ws.open() or ws.exec() can be interrupted at by an abrupt closure, caused externally (network drop, server went away, etc.) or by an explicit ws.close() call.

In case of an abrupt external closure, async operations reject with a WebSocketClosedError.

When interrupted by an explicit ws.close() call, async operations reject with an AbortedError.

This is particularly useful when using WebSocket from UI components. Since UI can be unmounted and cleaned up at any moment, it should be a requirement that ws.close() is always safe to call.