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

snub-ws

v4.2.1

Published

WebSocket server middleware for snub

Readme

snub-ws

WebSocket server middleware for snub.

Built on uWebSockets.js. Requires Redis.


Install

npm install snub snub-ws

Quick start

const Snub = require('snub');
const SnubWS = require('snub-ws');

const snub = new Snub({ host: 'localhost' });

snub.use(SnubWS({ port: 8585, auth: false }));

snub.on('ws:hello', function (event, reply) {
  console.log('message from', event.from.username, ':', event.payload);
  reply('hi back');
});

A WebSocket client connects to ws://localhost:8585, sends ["hello", "world"], and the server logs message from null : world and replies ["hello:reply", "hi back"].


Config

SnubWS({
  port: 8585,

  // Authentication — see Auth section
  auth: false,

  // Log verbose internal info
  debug: false,

  // Allow the same username to have multiple simultaneous connections
  multiLogin: true,

  // Milliseconds before an unauthenticated client is kicked (AUTH_TIMEOUT)
  authTimeout: 3000,

  // Rate limiting: [maxMessages, windowMs] — false to disable
  // e.g. [50, 5000] = max 50 messages per 5 seconds
  throttle: [50, 5000],

  // Milliseconds of inactivity before a client is kicked (IDLE_TIMEOUT)
  // Minimum: 5 minutes. Maximum: 960 seconds (uWS v20 limit).
  idleTimeout: 960000,

  // Restrict WebSocket upgrades to listed origins. null = allow all.
  // e.g. ['https://example.com', 'https://app.example.com']
  allowedOrigins: null,

  // Maximum simultaneous connections. 0 = unlimited.
  maxConnections: 0,

  // Maximum inbound message size in bytes (default 16 MB)
  maxPayloadLength: 16777216,

  // Maximum outbound buffer in bytes before backpressure kicks in (default 1 MB)
  maxBackpressure: 1048576,

  // Maximum queued messages per client under backpressure.
  // When exceeded the queue is cleared and the client is kicked (QUEUE_OVERFLOW).
  maxQueueSize: 100,

  // Messages larger than this (bytes) are offloaded to HTTP. 0 = disabled.
  // Default: 0.5 MB
  offloadToHttpSize: 524288,

  // Include the raw message string in the snub payload for debugging.
  // true = all events, or pass an array of specific event names.
  includeRaw: false,

  // Additional event names that clients are blocked from sending
  internalWsEvents: [],
})

Auth

No auth

snub.use(SnubWS({ auth: false }));

All clients are accepted immediately on connect. username will be null.

Function

snub.use(SnubWS({
  auth: function (authPayload, accept) {
    if (authPayload.password === 'secret')
      return accept(true);
    accept(false);
  }
}));

accept can be called with:

  • true — accept, username is taken from authPayload.username
  • false — deny (client is kicked with AUTH_FAIL)
  • object — accept and merge into the _acceptAuth reply sent to client

The authPayload argument is the object the client sent in its _auth message, merged with the current client state (so authPayload.remoteAddress etc. are available).

Snub event

Delegate auth to any listener in your app:

snub.use(SnubWS({ auth: 'authenticate-client' }));

snub.on('ws:authenticate-client', function (authPayload, reply) {
  // authPayload includes username, remoteAddress, etc.
  if (authPayload.username === 'admin')
    return reply({ role: 'admin' }); // merged into _acceptAuth
  reply(false);
});

HTTP Basic Auth

If the WebSocket upgrade request includes an Authorization: Basic … header, it is decoded and used as the auth payload automatically — no _auth message required.


Client protocol

Messages are JSON arrays: [eventName, payload?, replyId?]

Authenticate

Send this as the first message after connecting (required unless auth: false):

["_auth", { "username": "alice", "password": "secret" }]

On success the server responds:

["_acceptAuth", { "_id": "connectionId" }]

Additional keys from the accept(object) call are included in this response.

On failure the client is kicked with reason AUTH_FAIL.

Send a message

["event-name", { "any": "payload" }]

With a reply ID (the server will reply to this ID):

["event-name", { "any": "payload" }, "my-reply-id"]

The server replies with ["my-reply-id", replyData], or ["my-reply-id:error", { error: "..." }] if nothing was listening.

Built-in client events

| Event | Sent by | Description | |-------|---------|-------------| | _auth | client | Authenticate with the server | | _ping | client | Ping the server — server responds with _pong | | _pong | client | Response to server-initiated _ping — ignored |

Built-in server events

| Event | Sent by | Description | |-------|---------|-------------| | _acceptAuth | server | Authentication accepted | | _kickConnection | server | Server is about to close the connection — includes reason string | | _offload | server | Message too large; fetch from HTTP — see Large messages | | _ping | server | Server keepalive ping | | _pong | server | Response to client _ping |


Client → Server (receiving messages)

Inbound client messages are forwarded to snub with the ws: prefix.

snub.on('ws:my-event', function (event, reply) {
  console.log(event.from);     // client state object
  console.log(event.payload);  // the payload the client sent
  console.log(event._ts);      // server receive timestamp

  reply({ ok: true });         // sends ["replyId", { ok: true }] back to client
                               // (only if the client included a replyId)
});

The event.from object:

{
  id: 'instanceId;key_uid',   // unique connection ID
  username: 'alice',          // from auth payload (null if auth: false)
  channels: ['room1'],
  authenticated: true,
  connectTime: 1710000000000,
  remoteAddress: '127.0.0.1',
  lastMsgTime: 1710000001234,
  meta: {}                    // arbitrary key/value — see Meta
}

Server → Client (sending messages)

Send to specific clients

Target by username or connection ID. Comma-separate to target multiple.

// by username
snub.poly('ws:send:alice', ['event-name', payload]).send();

// by connection ID
snub.poly('ws:send:' + connectionId, ['event-name', payload]).send();

// multiple targets
snub.poly('ws:send:alice,bob', ['event-name', payload]).send();

Send to all clients

snub.poly('ws:send-all', ['event-name', payload]).send();

// optionally filter to specific usernames/IDs
snub.poly('ws:send-all', ['event-name', payload, ['alice', 'bob']]).send();

Send to a channel

// via event name suffix
snub.poly('ws:send-channel:room1', ['event-name', payload]).send();

// multiple channels in suffix
snub.poly('ws:send-channel:room1,room2', ['event-name', payload]).send();

// or pass channel list as payload element
snub.poly('ws:send-channel', ['event-name', payload, ['room1', 'room2']]).send();

Channels

Channels are sets of string tags on a client. Use them to group clients for targeted broadcasts.

// Add channels to a client (by username or ID)
snub.poly('ws:add-channel:alice', ['room1', 'room2']).send();

// Remove specific channels
snub.poly('ws:del-channel:alice', ['room2']).send();

// Replace the entire channel set
snub.poly('ws:set-channel:alice', ['room1']).send();

Kick

// by username or ID
snub.poly('ws:kick:alice', 'reason string').send();

// multiple targets
snub.poly('ws:kick:alice,bob', 'reason string').send();

// with a custom WebSocket close code (default 1000)
snub.poly('ws:kick', ['alice', 'reason string', 1008]).send();

// kick everyone
snub.poly('ws:kick-all', 'reason string').send();

Before closing, the server sends ["_kickConnection", "reason string"] to the client so it can handle the reason before the socket closes.

Automatic kick reasons:

| Reason | Cause | |--------|-------| | AUTH_TIMEOUT | Client did not authenticate within authTimeout ms | | AUTH_FAIL | Auth check returned false | | DUPE_LOGIN | Second connection from same username when multiLogin: false | | IDLE_TIMEOUT | No messages received within idleTimeout ms | | THROTTLE_LIMIT | Client exceeded the rate limit | | QUEUE_OVERFLOW | Outbound queue exceeded maxQueueSize | | SERVER_SHUTDOWN | Server received SIGINT / SIGTERM / SIGUSR2 |


Meta

Arbitrary key/value data attached to a client. Included in all from payloads and query results. Updated values are broadcast via ws:client-updated.

// set by username or ID
snub.poly('ws:set-meta:alice', { role: 'admin', plan: 'pro' }).send();

// set for multiple clients via payload list
snub.poly('ws:set-meta', [{ role: 'guest' }, ['alice', 'bob']]).send();

Allowed value types: string, number, boolean, or array of string/number/boolean.

  • Strings/numbers: max 128 characters
  • Arrays: max 64 items, each item max 64 characters
  • Other types are silently dropped

Query

All query events use snub.mono(...).awaitReply() since they need a response.

// Get clients by username or connection ID
const clients = await snub.mono('ws:get-clients:alice').awaitReply();
const clients = await snub.mono('ws:get-clients:alice,bob').awaitReply();

// Pass IDs/usernames as payload instead of suffix
const clients = await snub.mono('ws:get-clients', ['alice', 'bob']).awaitReply();

// Get all connected clients across all instances
const all = await snub.mono('ws:connected-clients').awaitReply();

// Filter connected-clients to specific usernames/IDs
const some = await snub.mono('ws:connected-clients', ['alice']).awaitReply();

// Get all clients subscribed to one or more channels
const inRoom = await snub.mono('ws:channel-clients', ['room1']).awaitReply();
const inRooms = await snub.mono('ws:channel-clients', ['room1', 'room2']).awaitReply();

All queries return an array of client state objects (see shape above). Queries fan out to all running instances and aggregate the results.


Server lifecycle events

These are emitted by snub-ws itself — listen with snub.on(...).

snub.on('ws:client-authenticated', function (state) {
  console.log('connected:', state.username, state.id);
});

snub.on('ws:client-disconnected', function (state) {
  console.log('disconnected:', state.username);
});

snub.on('ws:client-updated', function (state) {
  // fired when meta or channels change
  console.log('updated:', state.username, state.meta);
});

snub.on('ws:client-failedauth', function (state) {
  console.log('auth failed from', state.remoteAddress);
});

Large message offloading

When offloadToHttpSize is set and an outbound message exceeds that size, the payload is stored in Redis with a 30-second TTL and the client receives a redirect instead:

["_offload", "a3f9...hex32chars"]

The client fetches the full payload over HTTP:

GET http://hostname:port/?offload=a3f9...hex32chars

The server responds with the original JSON message payload (Content-Type: application/json). The ID is 128-bit cryptographically random.


Multi-instance

Each snub-ws instance registers itself in Redis. Query events (connected-clients, channel-clients, get-clients) fan out to all live instances and aggregate results. Send events target clients on whichever instance holds them.

Each instance is identified by config.instanceId (defaults to PID + random suffix). Use a unique instanceId per process if running multiple instances on the same host.


Graceful shutdown

On SIGINT, SIGTERM, or SIGUSR2:

  1. All connected clients are kicked with SERVER_SHUTDOWN
  2. 500 ms drain window allows close handshakes to complete
  3. The instance is removed from Redis
  4. process.exit(0)