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

purrcat

v0.1.8

Published

Lightweight WebSocket client with auto-reconnect, backoff strategies, bounded message buffering, and async iterables

Readme

purrcat

Lightweight WebSocket client with auto-reconnect, backoff strategies, bounded message buffering, and async iterables

A lightweight, event-driven WebSocket client library. Perfect for real-time applications that need reliable connections with automatic reconnection and message buffering.

Highlights

Microscopic: weighs 8.7KB minified (2.4KB gzipped)

Reliable: automatic reconnection with exponential/linear backoff + jitter

Modern: async iterables for generator-based message streams

Type-Safe: full TypeScript support with generic message types

Zero Dependencies: uses native WebSocket API only

Flexible: callback-based or generator-based APIs, your choice

🎮 Try It Live

Interactive Demo →

Try purrcat in your browser with our interactive demo. Test WebSocket connections, send messages, and see reconnection in action.

📚 Documentation

Architecture Documentation →

Learn about purrcat's internal architecture, design decisions, and how to choose between callback and generator APIs.

Features

  • Automatic reconnection with exponential/linear backoff + jitter
  • Bounded message buffer with configurable overflow policies
  • Generator-based streams for async iteration
  • TypeScript support
  • Browser & Node.js compatible (Node.js 20+ required for native WebSocket)
  • Zero dependencies (uses native WebSocket API)
  • Tiny bundle size
  • AbortSignal support for stream cancellation

Table of Contents

Installation

npm:

npm install purrcat

UMD build (via unpkg):

<script src="https://unpkg.com/purrcat/dist/index.global.js"></script>

Requirements

Runtime (when using the library):

  • Browser: Modern browsers with WebSocket support
  • Node.js: 20.0.0 or higher (uses native WebSocket API)

Development:

  • Node.js: 24.13.0 (tested with this version)

Usage

Browser (UMD)

When using the UMD build via <script> tag, the library is available as a global purrcat:

<script src="https://unpkg.com/purrcat/dist/index.global.js"></script>
<script>
  const socket = purrcat.createSocket({
    url: 'wss://echo.websocket.org',
  });

  // Listen for messages
  socket.onMessage(message => {
    console.log('Received:', message);
  });

  // Listen for events
  socket.onEvent(event => {
    console.log('Event:', event.type);
  });

  // Send messages
  socket.send({ type: 'hello', message: 'world' });
</script>

Type-Safe Messages (Generic Types)

You can define your own message types for type safety:

import createSocket from 'purrcat';

// Define your message types
type Incoming =
  | { type: 'message'; from: string; text: string; id: string }
  | { type: 'user_joined'; userId: string; name: string };

type Outgoing = { type: 'send_message'; text: string } | { type: 'join_room'; roomId: string };

// Create socket with type parameters
const socket = createSocket<Incoming, Outgoing>({
  url: 'wss://example.com/ws',
});

// Type-safe message receiving
for await (const msg of socket.messages()) {
  if (msg.type === 'message') {
    console.log(msg.from); // ✅ TypeScript knows this exists
    console.log(msg.text); // ✅ Type-safe
  }
}

// Type-safe message sending
socket.send({ type: 'send_message', text: 'hello' }); // ✅
socket.send({ type: 'send_message', from: 'user' }); // ❌ Type error!

Generator-based Streams

import createSocket from 'purrcat';

const socket = createSocket({
  url: 'wss://echo.websocket.org',
});

// Consume messages as async iterable
(async () => {
  for await (const message of socket.messages()) {
    console.log('Received:', message);
  }
})();

// Consume events as async iterable
(async () => {
  for await (const event of socket.events()) {
    console.log('Event:', event.type, event.ts);
  }
})();

socket.send({ type: 'hello', message: 'world' });

// Send multiple messages from a stream
async function* messageGenerator() {
  yield { type: 'ping' };
  yield { type: 'pong' };
  yield { type: 'ping' };
}

await socket.sendMessages(messageGenerator());

With AbortSignal

const controller = new AbortController();

(async () => {
  for await (const message of socket.messages({ signal: controller.signal })) {
    console.log(message);
    if (shouldStop) {
      controller.abort();
    }
  }
})();

Callback-based API

const socket = createSocket({
  url: 'wss://example.com/ws',
});

// Subscribe to messages
const unsubscribeMessage = socket.onMessage(data => {
  console.log('Message:', data);
});

// Subscribe to events
const unsubscribeEvent = socket.onEvent(event => {
  console.log('Event:', event.type, event.meta);
});

// Unsubscribe when done
unsubscribeMessage();
unsubscribeEvent();

Reconnection Options

const socket = createSocket({
  url: 'wss://example.com/ws',
  reconnect: {
    enabled: true,
    attempts: 10,
    interval: 1000,
    backoff: 'exponential', // or 'linear'
    maxInterval: 30000,
  },
});

// Or simply use boolean
const socket2 = createSocket({
  url: 'wss://example.com/ws',
  reconnect: true, // Uses default reconnect config
});

Bounded Buffer with Overflow Policy

const socket = createSocket({
  url: 'wss://example.com/ws',
  buffer: {
    receive: {
      size: 100, // Maximum receive buffer size
      overflow: 'oldest', // or 'newest' or 'error'
    },
    send: {
      size: 50, // Maximum send queue size
      overflow: 'newest', // or 'oldest' or 'error'
    },
  },
});

// Listen for dropped messages
socket.onEvent(event => {
  if (event.type === 'dropped') {
    console.warn('Message dropped:', event.meta?.reason, event.meta?.bufferType);
  }
});

Manual Connection Control

const socket = createSocket({
  url: 'wss://example.com/ws',
  reconnect: false, // Disable auto-connect
});

// Connect manually
socket.connect();

// Close connection
socket.close(1000, 'Normal closure');

API

createSocket(options)

Creates a new WebSocket client instance.

Options

| Option | Type | Default | Description | | ----------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | url | string | required | WebSocket server URL | | protocols | string \| string[] | - | WebSocket subprotocols | | reconnect | boolean \| ReconnectConfig | true | Reconnection configuration. ReconnectConfig is { enabled?: boolean, attempts?: number, interval?: number, backoff?: ReconnectBackoff, maxInterval?: number } | | buffer | { receive?: BufferConfig, send?: BufferConfig } | { receive: { size: 100, overflow: 'oldest' }, send: { size: 100, overflow: 'oldest' } } | Message buffer configuration (receive buffer and send queue). BufferConfig is { size?: number, overflow?: BufferOverflowPolicy } |

Socket Methods

messages({ signal? })

Returns an async iterable of messages. Messages are buffered and yielded in order.

for await (const message of socket.messages({ signal: abortSignal })) {
  console.log(message);
}

events({ signal? })

Returns an async iterable of socket events.

for await (const event of socket.events({ signal: abortSignal })) {
  console.log(event.type, event.ts, event.meta);
}

onMessage(callback)

Registers a message callback. Returns an unsubscribe function.

const unsubscribe = socket.onMessage(data => {
  console.log(data);
});
// Later...
unsubscribe();

onEvent(callback)

Registers an event callback. Returns an unsubscribe function.

const unsubscribe = socket.onEvent(event => {
  console.log(event.type);
});
// Later...
unsubscribe();

send(data)

Sends a message to the server. Automatically stringifies objects.

socket.send('Hello');
socket.send({ type: 'message', text: 'Hello' });
socket.send(new ArrayBuffer(8));

sendMessages(messages, options?)

Sends multiple messages from an async iterable stream. Returns a Promise that resolves when all messages are sent.

const messages = [{ type: 'ping' }, { type: 'pong' }, { type: 'ping' }];

// Using async generator
async function* messageStream() {
  for (const msg of messages) {
    yield msg;
  }
}

await socket.sendMessages(messageStream());

// With AbortSignal
const controller = new AbortController();
await socket.sendMessages(messageStream(), { signal: controller.signal });

connect()

Manually connect to the WebSocket server.

socket.connect();

close(code?, reason?)

Closes the WebSocket connection. Prevents automatic reconnection.

socket.close(1000, 'Normal closure');

SocketEvent Types

  • open - Connection opened
  • close - Connection closed (meta: { code, reason, wasClean }). wasClean indicates whether the connection closed cleanly (true) or abnormally (false, e.g., network failure)
  • error - Error occurred
  • reconnect - Reconnection scheduled or attempt started (meta: { attempt, interval? }). If interval is present, it's scheduled; otherwise, it's an attempt in progress
  • received - Message received from server (meta: { message })
  • sent - Message sent to server (meta: { message })
  • dropped - Message dropped due to buffer overflow (meta: { reason })

SocketEvent Structure

interface SocketEvent {
  type: SocketEventType;
  ts: number; // Timestamp in milliseconds
  meta?: Record<string, any>; // Additional metadata
}

Examples

Complete Example

import createSocket from 'purrcat';

const socket = createSocket({
  url: 'wss://api.example.com/ws',
  reconnect: {
    enabled: true,
    attempts: 10,
  },
  buffer: {
    receive: {
      size: 50,
      overflow: 'oldest',
    },
    send: {
      size: 50,
      overflow: 'oldest',
    },
  },
});

// Handle events
socket.onEvent(event => {
  switch (event.type) {
    case 'open':
      console.log('Connected');
      break;
    case 'close':
      const { code, reason, wasClean } = event.meta || {};
      if (wasClean) {
        console.log('Disconnected normally', { code, reason });
      } else {
        console.log('Disconnected abnormally (network issue?)', { code, reason });
      }
      break;
    case 'error':
      console.error('Error:', event.meta?.error);
      break;
    case 'reconnect':
      if (event.meta?.interval) {
        console.log(`Reconnecting in ${event.meta.interval}ms (attempt ${event.meta.attempt})`);
      } else {
        console.log(`Reconnecting (attempt ${event.meta?.attempt})`);
      }
      break;
    case 'dropped':
      console.warn('Message dropped:', event.meta?.reason);
      break;
  }
});

// Process messages
(async () => {
  for await (const message of socket.messages()) {
    // message is already parsed (Incoming type)
    console.log('Received:', message);
  }
})();

// Send messages
socket.send({ type: 'ping' });

License

MIT