WSKit — Universal WebSocket Client

One script tag. Zero dependencies. Works with any backend.

wskit.js is a standalone, universal WebSocket client that handles the boilerplate of working with WebSockets: auto-reconnect, message queuing, channel-based routing, request/response with Promises, heartbeat keep-alive, and automatic JSON serialization.

🔄
Auto-Reconnect

Exponential backoff from 1s up to 30s. Configurable max attempts.

📨
Message Queue

Buffers sends while disconnected and flushes automatically on reconnect.

💬
Channels

Route messages by type. Subscribe, unsubscribe, and handle with ease.

🤝
Request / Response

Send a message and await a matching response. Built-in timeout.

💓
Heartbeat

Configurable keep-alive ping to detect stale connections.

🔧
Auto-JSON

Automatically parse incoming and stringify outgoing JSON messages.

Installation

npm
npm install wskit-client
ES Modules
import WSKit from 'wskit-client';
CommonJS
const WSKit = require('wskit-client');
Script Tag (CDN)
<script src="https://unpkg.com/wskit-client/wskit.js"></script>
Self-Hosted
<script src="https://rte.whitneys.co/wskit.js"></script>
Zero dependencies. WSKit works in any browser with native WebSocket support. ~5 KB gzipped.

Quick Start

<script src="https://unpkg.com/wskit-client/wskit.js"></script>
<script>
  const ws = WSKit.connect('wss://yourserver.com/ws', {
    onOpen: () => console.log('Connected'),
    onMessage: (msg) => console.log('Received:', msg),
    onClose: () => console.log('Disconnected'),
  });

  ws.send({ type: 'chat', text: 'Hello!' });
</script>

That's it. WSKit connects, auto-serializes JSON, and starts reconnecting if the connection drops.

Channels

Route incoming messages by their type field (configurable via typeField):

// Subscribe — returns an unsubscribe function
const off = ws.on('chat', (msg) => {
  console.log(msg.user + ':', msg.text);
});

// Unsubscribe later
off();

// Or unsubscribe by type
ws.off('chat');

You can subscribe multiple handlers to the same type. The global onMessage callback still fires for every message regardless of channel subscriptions.

Example: Chat + Notifications
ws.on('chat', (msg) => {
  appendMessage(msg.user, msg.text);
});

ws.on('notification', (msg) => {
  showToast(msg.title, msg.body);
});

ws.on('presence', (msg) => {
  updateUserList(msg.users);
});

Request / Response

Send a message and wait for a matching response using Promises:

const user = await ws.request({ type: 'getUser', id: 123 });
console.log(user.name); // "John"

WSKit auto-generates a unique _id field on the outgoing message. Your server must echo the same _id in its response:

// Server-side (Node.js)
socket.on('message', (raw) => {
  const msg = JSON.parse(raw);
  if (msg.type === 'getUser') {
    socket.send(JSON.stringify({
      _id: msg._id,  // echo back the _id
      name: 'John',
      email: 'john@example.com',
    }));
  }
});
Custom Timeout
// Override the default 10s timeout
const result = await ws.request({ type: 'slowQuery', q: 'data' }, 30000);
The _id field name is configurable via the idField option.

Message Queue

Messages sent while disconnected are buffered and automatically flushed when the connection is restored:

const ws = WSKit.connect(url, {
  queueWhileDisconnected: true,  // default
  maxQueueSize: 100,             // default
});

// These will be queued if not yet connected
ws.send({ type: 'init', token: 'abc' });
ws.send({ type: 'subscribe', channel: 'updates' });

// Check queue size
console.log(ws.queueSize); // 2

// Clear queue if needed
ws.clearQueue();

ws.send() returns true if the message was sent immediately, or false if it was queued. When the queue is full, new messages are dropped.

Front-End Configuration

Pass an options object as the second argument to WSKit.connect():

const ws = WSKit.connect('wss://yourserver.com/ws', {
  // Reconnection
  reconnect: true,              // auto-reconnect (default: true)
  reconnectBaseMs: 1000,        // initial delay (default: 1000)
  reconnectMaxMs: 30000,        // max delay (default: 30000)
  maxReconnectAttempts: 0,      // 0 = unlimited (default: 0)

  // Heartbeat
  heartbeatMs: 30000,           // ping interval (default: 30000)
  heartbeatMessage: { type: 'ping' },

  // Message queue
  queueWhileDisconnected: true, // buffer sends (default: true)
  maxQueueSize: 100,            // max queue size (default: 100)

  // JSON
  autoJSON: true,               // auto parse/stringify (default: true)
  typeField: 'type',            // channel routing field (default: 'type')

  // Request/response
  requestTimeout: 10000,        // timeout in ms (default: 10000)
  idField: '_id',               // matching field (default: '_id')

  // Debug
  debug: false,                 // console logging (default: false)

  // Callbacks
  onOpen: () => {},
  onClose: (e) => {},
  onError: (e) => {},
  onMessage: (data) => {},
  onReconnect: (attempt) => {},
  onStateChange: (newState, prevState) => {},
});
OptionTypeDefaultDescription
reconnectBooleantrueAuto-reconnect on disconnect
reconnectBaseMsNumber1000Initial reconnect delay in ms
reconnectMaxMsNumber30000Max reconnect delay in ms
maxReconnectAttemptsNumber0Max attempts (0 = unlimited)
heartbeatMsNumber30000Ping interval in ms (0 to disable)
heartbeatMessageAny{ type: "ping" }Heartbeat payload
queueWhileDisconnectedBooleantrueBuffer sends while disconnected
maxQueueSizeNumber100Max queued messages
autoJSONBooleantrueAuto parse/stringify JSON
typeFieldString"type"Field for channel routing
requestTimeoutNumber10000Request/response timeout in ms
idFieldString"_id"Field for request/response matching
debugBooleanfalseEnable verbose console logging
Callbacks
CallbackArgumentsDescription
onOpen(none)WebSocket connected
onCloseCloseEventWebSocket closed
onErrorEvent | ErrorWebSocket error
onMessagedataEvery incoming message (parsed if autoJSON)
onReconnectattemptBefore each reconnect attempt
onStateChangenewState, prevStateConnection state changed

API Reference

WSKit.connect() returns a connection object:

Method / PropertyDescription
ws.send(data)Send data (auto-JSON if enabled). Returns true if sent, false if queued.
ws.request(data, timeout?)Send and wait for a matching response. Returns a Promise.
ws.on(type, handler)Subscribe to a message type. Returns an unsubscribe function.
ws.off(type, handler?)Unsubscribe from a type. Omit handler to remove all handlers for that type.
ws.disconnect()Close connection and stop reconnecting. Rejects all pending requests.
ws.reconnect()Manually reconnect. Resets attempt counter.
ws.clearQueue()Clear the message queue.
ws.state"connecting", "open", "closed", or "reconnecting"
ws.queueSizeNumber of messages in the queue
ws.socketThe raw WebSocket instance
ws.urlThe WebSocket URL
ws.reconnectAttemptsCurrent reconnect attempt count

Reconnection

WSKit uses exponential backoff for reconnection:

  • 1st attempt: 1 second
  • 2nd attempt: 2 seconds
  • 3rd attempt: 4 seconds
  • 4th attempt: 8 seconds
  • ...up to reconnectMaxMs (default 30s)

On successful reconnect, the message queue is flushed automatically. Set maxReconnectAttempts to limit retries:

const ws = WSKit.connect(url, {
  reconnect: true,
  reconnectBaseMs: 500,
  reconnectMaxMs: 15000,
  maxReconnectAttempts: 10,    // give up after 10 attempts
  onReconnect: (attempt) => {
    console.log('Reconnecting... attempt', attempt);
  },
  onStateChange: (state) => {
    document.getElementById('status').textContent = state;
  },
});

Heartbeat

WSKit sends a configurable ping message at regular intervals to keep the connection alive and detect stale connections:

const ws = WSKit.connect(url, {
  heartbeatMs: 30000,                  // every 30 seconds (default)
  heartbeatMessage: { type: 'ping' },  // payload (default)
});

// Disable heartbeat
const ws2 = WSKit.connect(url, {
  heartbeatMs: 0,  // disabled
});

Your server can respond with a pong or simply ignore the ping. The heartbeat stops when disconnected and restarts on reconnect.

Backend Examples

import { WebSocketServer } from 'ws';

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

wss.on('connection', (socket) => {
  socket.on('message', (raw) => {
    const msg = JSON.parse(raw);

    switch (msg.type) {
      case 'chat':
        // Broadcast to all clients
        wss.clients.forEach((client) => {
          if (client.readyState === 1) {
            client.send(JSON.stringify(msg));
          }
        });
        break;

      case 'getUser':
        // Request/response — echo back _id
        socket.send(JSON.stringify({
          _id: msg._id,
          name: 'John',
          email: 'john@example.com',
        }));
        break;

      case 'ping':
        socket.send(JSON.stringify({ type: 'pong' }));
        break;
    }
  });
});
import asyncio, json, websockets

connected = set()

async def handler(ws):
    connected.add(ws)
    try:
        async for raw in ws:
            msg = json.loads(raw)

            if msg.get("type") == "chat":
                for client in connected:
                    if client != ws:
                        await client.send(json.dumps(msg))

            elif msg.get("type") == "getUser":
                await ws.send(json.dumps({
                    "_id": msg["_id"],
                    "name": "John",
                    "email": "john@example.com",
                }))

            elif msg.get("type") == "ping":
                await ws.send(json.dumps({"type": "pong"}))
    finally:
        connected.discard(ws)

asyncio.run(websockets.serve(handler, "0.0.0.0", 8080))
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}

func handler(w http.ResponseWriter, r *http.Request) {
    conn, _ := upgrader.Upgrade(w, r, nil)
    defer conn.Close()

    for {
        _, raw, err := conn.ReadMessage()
        if err != nil { break }

        var msg map[string]interface{}
        json.Unmarshal(raw, &msg)

        switch msg["type"] {
        case "getUser":
            resp, _ := json.Marshal(map[string]interface{}{
                "_id":   msg["_id"],
                "name":  "John",
                "email": "john@example.com",
            })
            conn.WriteMessage(websocket.TextMessage, resp)

        case "ping":
            resp, _ := json.Marshal(map[string]string{"type": "pong"})
            conn.WriteMessage(websocket.TextMessage, resp)
        }
    }
}

func main() {
    http.HandleFunc("/ws", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
<?php
// composer require ratchet/pawl react/event-loop

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

require __DIR__ . '/vendor/autoload.php';

class Handler implements MessageComponentInterface {
    protected $clients;

    public function __construct() {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $raw) {
        $msg = json_decode($raw, true);

        switch ($msg['type'] ?? '') {
            case 'chat':
                // Broadcast to all other clients
                foreach ($this->clients as $client) {
                    if ($client !== $from) {
                        $client->send(json_encode($msg));
                    }
                }
                break;

            case 'getUser':
                // Request/response — echo back _id
                $from->send(json_encode([
                    '_id'   => $msg['_id'],
                    'name'  => 'John',
                    'email' => 'john@example.com',
                ]));
                break;

            case 'ping':
                $from->send(json_encode(['type' => 'pong']));
                break;
        }
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        $conn->close();
    }
}

$server = IoServer::factory(
    new HttpServer(new WsServer(new Handler())),
    8080
);
$server->run();

npm

WSKit is available on npm as wskit-client.

npm install wskit-client