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

@culpeo/async-ws

v1.2.1

Published

Promise-first WebSocket client for Node.js and browsers

Readme

@culpeo/async-ws

Promise-first WebSocket client for Node.js and browsers.

npm version license bundle size

@culpeo/async-ws is a cross-platform WebSocket client that turns the event-driven WebSocket API into a small, imperative, promise-based interface.

Features

  • Works in both Node.js and browsers from one package
  • Promise-based connect(), send(), receive(), and close() APIs
  • Async iteration support with for await...of
  • Message buffering for messages that arrive before receive() is called
  • Configurable maxBufferSize with oldest-message eviction when full
  • Connect timeout and AbortSignal support
  • Keep-alive with automatic ping/pong (Node.js)
  • Server-side socket adoption with fromSocket() (Node.js)
  • Exposed WebSocket properties (protocol, url, bufferedAmount, extensions)
  • Clean close information via lastCloseInfo
  • TypeScript-first with bundled type definitions
  • Binary and text message support
  • Browser build uses the native WebSocket; Node build uses ws

Install

npm install @culpeo/async-ws
yarn add @culpeo/async-ws
pnpm add @culpeo/async-ws

Quick Start

import { WebSocketClient } from "@culpeo/async-ws";

const client = new WebSocketClient();

await client.connect("wss://echo.websocket.events");
await client.send("hello");

const message = await client.receive();
console.log(message.data); // string | ArrayBuffer
console.log(message.binary); // boolean

await client.close();

API Reference

WebSocketClient

Constructor

new WebSocketClient(options?: ClientOptions)

Creates a new client instance.

Constructor options

  • maxBufferSize?: number
    • Maximum number of incoming messages to keep buffered before they are consumed
    • Default: 0 (unlimited)
    • When the limit is reached, the oldest buffered message is dropped
  • keepAlive?: KeepAliveOptions
    • Enables automatic ping/pong keep-alive (Node.js only)
    • Throws if used in a browser environment

Properties

client.readyState

readonly readyState: WebSocketState

Returns the current client state:

  • "idle"
  • "connecting"
  • "open"
  • "closing"
  • "closed"
  • "errored"

client.protocol

readonly protocol: string

Returns the negotiated subprotocol, or "" when not connected.

client.url

readonly url: string

Returns the URL of the WebSocket connection, or "" when not connected.

client.bufferedAmount

readonly bufferedAmount: number

Returns the number of bytes queued for transmission, or 0 when not connected.

client.extensions

readonly extensions: string

Returns the negotiated extensions, or "" when not connected.

client.lastCloseInfo

readonly lastCloseInfo: WebSocketCloseInfo | null

Returns close metadata from the most recent close event, or null if the socket has not closed yet.

Static Methods

fromSocket()

static fromSocket(rawSocket: unknown, options?: ClientOptions): WebSocketClient

Wraps an already-open WebSocket into a WebSocketClient in the "open" state, ready to send and receive. Intended for server scenarios where a WebSocketServer hands you an established connection.

Node.js only. Throws in browser builds.

import { WebSocketServer } from "ws";
import { WebSocketClient } from "@culpeo/async-ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", async (socket) => {
  const client = WebSocketClient.fromSocket(socket);

  for await (const msg of client) {
    console.log("received:", msg.data);
    await client.send("echo: " + msg.data);
  }
});

The client takes ownership of the socket lifecycle — calling close() will close the underlying socket. Call fromSocket() immediately in the connection handler to avoid missing messages.

Accepts any WebSocket-compatible object (validated structurally, not via instanceof), so it works even when multiple copies of the ws package are installed.

Instance Methods

connect()

connect(url: string | URL, options?: ConnectOptions): Promise<void>

Opens a WebSocket connection and resolves when the connection is established.

Rejects when:

  • the client is already connecting, open, or closing
  • the socket constructor throws
  • the connection errors before opening
  • the socket closes before opening
ConnectOptions
  • protocols?: string | string[] — WebSocket subprotocols to request
  • headers?: Record<string, string> — custom handshake headers in Node.js
  • timeout?: number — connection timeout in milliseconds; rejects if the connection is not established within this time
  • signal?: AbortSignal — an abort signal to cancel the connection attempt

In browsers, passing headers throws because the native WebSocket API does not support custom headers.

send()

send(data: string | ArrayBuffer | ArrayBufferView): Promise<void>

Sends text or binary data.

Resolves when the underlying socket accepts the payload. Rejects if the client is not open or if the underlying adapter reports an error.

receive()

receive(): Promise<WebSocketMessage>

Resolves with the next incoming message.

Behavior:

  • If buffered messages exist, returns the oldest buffered message immediately
  • If no buffered message exists, waits for the next incoming message
  • If the socket closes after buffering messages, buffered messages are still drained first
  • Rejects when the client is not open and no buffered messages remain

close()

close(code?: number, reason?: string): Promise<void>

Starts the close handshake and resolves when the socket closes.

Behavior:

  • Resolves immediately if the client is idle, already closed, or errored
  • If a close is already in progress, waits for the close event
  • Validates custom close codes before calling the underlying socket
  • Accepts 1000 or values in the range 3000-4999

Async iterator

client[Symbol.asyncIterator](): AsyncGenerator<WebSocketMessage>

Allows consumption with for await...of.

Behavior:

  • Yields incoming messages as they arrive
  • Ends iteration on a clean close
  • Throws on unexpected or error-driven termination
  • Does not automatically close the socket if you break out of the loop

Types

ConnectOptions

interface ConnectOptions {
  protocols?: string | string[];
  headers?: Record<string, string>;
  timeout?: number;
  signal?: AbortSignal;
}

Connection-time options.

ClientOptions

interface ClientOptions {
  maxBufferSize?: number;
  keepAlive?: KeepAliveOptions;
}

Client-level configuration.

KeepAliveOptions

interface KeepAliveOptions {
  interval: number;
  timeout?: number;
}
  • interval — milliseconds between pings
  • timeout — milliseconds to wait for a pong before terminating the connection (default: interval)

WebSocketMessage

interface WebSocketMessage {
  data: string | ArrayBuffer;
  binary: boolean;
}

Represents a received message payload.

WebSocketCloseInfo

interface WebSocketCloseInfo {
  code: number;
  reason: string;
  wasClean: boolean;
}

Represents close metadata captured from the underlying socket.

WebSocketState

type WebSocketState =
  "idle" | "connecting" | "open" | "closing" | "closed" | "errored";

Represents the client lifecycle state.

Browser vs Node

@culpeo/async-ws ships one API for both environments:

  • Node.js build uses the ws package internally
  • Browser build uses the native WebSocket implementation

This is handled at build time with Rollup. The browser bundle aliases the Node adapter module to a browser-specific adapter, so application code does not need environment checks or separate imports.

In practice, that means you write this once:

import { WebSocketClient } from "@culpeo/async-ws";

…and the appropriate adapter is selected by the published package exports and browser build.

Async Iterator

import { WebSocketClient } from "@culpeo/async-ws";

const client = new WebSocketClient();
await client.connect("wss://example.com/ws");

try {
  for await (const message of client) {
    if (!message.binary) {
      console.log("text:", message.data);
    }
  }
} finally {
  await client.close();
}

This is useful when you want a stream-like consumer loop without manually calling receive() each time.

Error Handling

All core operations are async and communicate failure by rejecting:

  • connect() rejects on invalid state, connection failure, early close, timeout, or abort
  • send() rejects when called before the socket is open or when the adapter fails to send
  • receive() rejects when the client is not in a receivable state and no buffered messages remain
  • close() rejects for invalid close codes

Additional notes:

  • Connection errors are treated as terminal for pending receivers
  • A socket error is typically followed by a close event; close metadata is exposed through lastCloseInfo
  • If buffered messages exist when a close happens, those messages are still delivered before receive() starts rejecting

A simple pattern:

try {
  await client.connect("wss://example.com/ws");
  await client.send("ping");
  const reply = await client.receive();
  console.log(reply);
} catch (error) {
  console.error("WebSocket operation failed", error);
  console.error("Last close info:", client.lastCloseInfo);
}

Message Buffering

Incoming messages are buffered when they arrive before a consumer calls receive().

By default, buffering is unlimited:

const client = new WebSocketClient();

To cap memory usage, set maxBufferSize:

const client = new WebSocketClient({ maxBufferSize: 100 });

When the buffer is full:

  • the oldest message is removed
  • the newest message is stored

This makes buffering predictable for bursty message streams while keeping the public API simple.

Connection Timeout and Abort

Use timeout to reject if the connection isn't established within a deadline:

await client.connect("wss://example.com/ws", { timeout: 5000 });

Use signal to cancel a connection attempt at any time:

const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);

await client.connect("wss://example.com/ws", { signal: controller.signal });

Both can be combined:

await client.connect("wss://example.com/ws", {
  timeout: 10000,
  signal: controller.signal,
});

Server-Side Socket Adoption (Node.js)

Use fromSocket() to wrap connections from a WebSocketServer:

import { WebSocketServer } from "ws";
import { WebSocketClient } from "@culpeo/async-ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", async (socket) => {
  const client = WebSocketClient.fromSocket(socket);

  const msg = await client.receive();
  await client.send("got: " + msg.data);
  await client.close();
});

The same ClientOptions are supported:

const client = WebSocketClient.fromSocket(socket, {
  maxBufferSize: 100,
  keepAlive: { interval: 30000 },
});

Keep-Alive (Node.js)

Enable automatic ping/pong to detect dead connections:

const client = new WebSocketClient({
  keepAlive: { interval: 30000, timeout: 5000 },
});

await client.connect("wss://example.com/ws");
  • Sends a ping every interval milliseconds
  • If no pong is received within timeout milliseconds, the connection is terminated
  • timeout defaults to interval if omitted
  • Not available in browsers — the constructor throws if keepAlive is configured in a browser environment

Building from Source

git clone <your-fork-or-repo-url>
cd <repo-directory>
npm install

Run tests:

npm test
npm run test:browser

Build the package:

npm run build

Current build outputs include:

  • CommonJS for Node.js
  • ESM for Node.js
  • Browser ESM
  • Browser IIFE bundle
  • Bundled TypeScript declarations

License

MIT