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

@mustafakurtt/bun-sockets

v0.5.0

Published

Bun-native WebSocket server with type-safe events, rooms, middleware, and zero dependencies. The Socket.io DX you love, powered by Bun's native speed.

Downloads

67

Readme

@mustafakurtt/bun-sockets

Bun-native WebSocket server & client with type-safe events, rooms, middleware, auto-reconnect, and zero dependencies.

The Socket.io DX you love, powered by Bun's native C++ WebSocket engine.

npm version License: MIT

Why This Package?

| | Socket.io | Raw Bun WS | bun-sockets | |--|-----------|------------|-----------------| | Speed | ❌ Engine.io overhead | ✅ Native C++ | ✅ Native C++ | | Bundle size | ~100 KB | 0 KB | ~13 KB (server + client) | | Type-safe events | ⚠️ Manual generics | ❌ None | ✅ Built-in | | Rooms | ✅ Built-in | ❌ DIY | ✅ Built-in | | Middleware | ✅ After handshake | ❌ DIY | ✅ Before handshake | | Dependencies | 17+ packages | 0 | 0 | | Auto-reconnect | ✅ Built-in | ❌ DIY | ✅ Built-in | | Event buffering | ❌ None | ❌ None | ✅ Built-in | | Bun-native | ❌ Node.js polyfills | ✅ | ✅ |

bun-sockets sits in the sweet spot: Socket.io's developer experience with Bun's raw performance, zero dependencies, and full TypeScript support.

Install

bun add @mustafakurtt/bun-sockets

Quick Start

import { createServer } from '@mustafakurtt/bun-sockets'

const io = createServer()

io.on('connection', (socket) => {
  console.log(`Connected: ${socket.id}`)

  socket
    .join('general')
    .emit('welcome', { message: 'Hello!' })

  socket.on('chat_message', (payload) => {
    socket.broadcast('general', 'new_message', {
      from: socket.id,
      text: payload.text,
    })
  })
})

io.on('disconnect', (socket, code, reason) => {
  console.log(`Disconnected: ${socket.id} (${code})`)
})

Bun.serve({
  port: 3000,
  fetch: io.handler,
  websocket: io.websocket,
})

console.log('🚀 Server running on ws://localhost:3000/ws')

Client

The client works in both browser and Bun environments:

import { createClient } from '@mustafakurtt/bun-sockets/client'

const socket = createClient({ url: 'ws://localhost:3000' })

socket
  .on('welcome', (payload) => {
    console.log(payload.message) // 'Hello!'
  })
  .on('new_message', (payload) => {
    console.log(`${payload.from}: ${payload.text}`)
  })
  .onConnect(() => console.log('Connected!'))
  .onDisconnect((code) => console.log(`Disconnected: ${code}`))
  .onReconnect((attempt) => console.log(`Reconnected after ${attempt} attempts`))
  .connect()

// Send events to server
socket.emit('chat_message', { text: 'Hello everyone!' })

Shared Type Safety (Server + Client)

Define event contracts once, share between server and client — full autocomplete on both sides:

// shared/events.ts
export type ClientEvents = {
  send_message: { text: string; roomId: string }
  join_room: { roomId: string }
}

export type ServerEvents = {
  new_message: { user: string; text: string; timestamp: number }
  user_joined: { userId: string; roomId: string }
}
// server.ts
import { createServer } from '@mustafakurtt/bun-sockets'
import type { ClientEvents, ServerEvents } from './shared/events'

const io = createServer<ClientEvents, ServerEvents>()

io.on('connection', (socket) => {
  socket.on('send_message', (payload) => {
    // ✅ payload is { text: string; roomId: string }
    io.to(payload.roomId).emit('new_message', {
      user: socket.id,
      text: payload.text,
      timestamp: Date.now(),
    })
  })
})
// client.ts
import { createClient } from '@mustafakurtt/bun-sockets/client'
import type { ClientEvents, ServerEvents } from './shared/events'

const socket = createClient<ClientEvents, ServerEvents>({
  url: 'ws://localhost:3000'
})

socket.on('new_message', (payload) => {
  // ✅ payload is { user: string; text: string; timestamp: number }
  console.log(`${payload.user}: ${payload.text}`)
})

socket.emit('send_message', { text: 'Hello!', roomId: 'general' })
// ❌ TypeScript error: socket.emit('invalid_event', {})

Type-Safe Events

Define your event contracts once, get full IDE autocomplete and compile-time checks everywhere:

import { createServer } from '@mustafakurtt/bun-sockets'

type ClientEvents = {
  send_message: { text: string; roomId: string }
  join_room: { roomId: string }
}

type ServerEvents = {
  new_message: { user: string; text: string; timestamp: number }
  user_joined: { userId: string; roomId: string }
}

const io = createServer<ClientEvents, ServerEvents>()

io.on('connection', (socket) => {
  // ✅ IDE knows payload is { text: string; roomId: string }
  socket.on('send_message', (payload) => {
    io.to(payload.roomId).emit('new_message', {
      user: socket.id,
      text: payload.text,        // ← autocomplete works
      timestamp: Date.now(),
    })
  })

  // ❌ TypeScript error: 'invalid_event' doesn't exist in ClientEvents
  // socket.on('invalid_event', () => {})
})

Rooms

Rooms are powered by Bun's native publish/subscribe — no polling, no overhead:

io.on('connection', (socket) => {
  // Fluent API — chain as many as you want
  socket
    .join('global-chat')
    .join('vip-lounge')
    .join(`user-${socket.id}`)

  // Leave a specific room
  socket.leave('vip-lounge')

  // Leave all rooms at once
  socket.leaveAll()

  // Check which rooms this socket is in
  console.log(socket.rooms) // ReadonlySet<string>
})

// Broadcast to a room from server level
io.to('global-chat').emit('announcement', { text: 'Server restarting in 5 min' })

// Broadcast to a room from a socket (all subscribers receive it)
socket.broadcast('global-chat', 'new_message', { from: socket.id, text: 'Hi!' })

// Inspect rooms
console.log(io.rooms)           // Map<roomName, Set<socketId>>
console.log(io.connectionCount) // number
console.log(io.sockets)         // Map<socketId, BunSocket>

Middleware (Authentication)

Middleware runs at HTTP upgrade — before the WebSocket handshake. If auth fails, the socket never opens, saving server resources:

io.use(async (req, next) => {
  const token = req.headers.get('authorization')?.split(' ')[1]

  if (!token) {
    throw new Error('No token provided') // → HTTP 401, socket never opens
  }

  const user = await verifyJWT(token)

  if (!user) {
    throw new Error('Invalid token')
  }

  // Pass data to socket.data
  next({ userId: user.id, role: user.role })
})

io.on('connection', (socket) => {
  // Access middleware data
  console.log(socket.data.userId) // 'user-123'
  console.log(socket.data.role)   // 'admin'
})

Multiple middlewares run in order — each can enrich socket.data:

io.use(async (req, next) => {
  const user = await authenticate(req)
  next({ user })
})

io.use(async (req, next) => {
  const permissions = await loadPermissions(req)
  next({ permissions })
})

io.on('connection', (socket) => {
  // Both middleware results available
  console.log(socket.data.user)
  console.log(socket.data.permissions)
})

Server Options

const io = createServer({
  path: '/ws',                    // WebSocket endpoint path (default: '/ws')
  idleTimeout: 120,               // Seconds before idle socket is dropped (default: 120)
  maxPayloadLength: 16 * 1024 * 1024, // Max message size in bytes (default: 16 MB)
  perMessageDeflate: false,       // Enable compression (default: false)
  heartbeat: true,                // Enable heartbeat (default: true)
  // heartbeat: {                 // Or fine-tune:
  //   interval: 25000,           //   Ping interval in ms (default: 25000)
  //   timeout: 10000,            //   Max wait for pong before close (default: 10000)
  // },
  recovery: true,                 // Enable connection state recovery (default: true)
  // recovery: {                  // Or fine-tune:
  //   maxBufferSize: 100,        //   Messages to keep per socket (default: 100)
  //   maxBufferAge: 30000,       //   Buffer TTL after disconnect in ms (default: 30000)
  // },
})

Heartbeat / Ping-Pong

The server automatically sends __system:ping messages to all connected sockets at the configured interval. The client automatically responds with __system:pong. If a socket fails to respond within the timeout window, the server closes it with code 4000 (heartbeat timeout).

Server ──[__system:ping]──▶ Client
Server ◀──[__system:pong]── Client   ✅ alive

Server ──[__system:ping]──▶ Client
         ... no pong ...             ❌ close(4000)

The client also detects stale connections: if no ping is received for an extended period, it closes the connection and triggers auto-reconnect.

const io = createServer({
  heartbeat: {
    interval: 15000,   // Send ping every 15s
    timeout: 5000,     // Allow 5s for pong response
  },
})

Disable heartbeat entirely:

const io = createServer({ heartbeat: false })

Connection State Recovery

When a client disconnects and reconnects, the server can replay missed messages automatically. Each emitted message carries a sequence number (seq). On reconnect, the client sends its last known seq and the server replays everything after it.

1. Client receives messages with seq: 1, 2, 3
2. Connection drops at seq 3
3. Server continues buffering: seq 4, 5
4. Client reconnects → sends __system:recover { lastSeq: 3 }
5. Server replays seq 4, 5 → sends __system:recovery_complete

This is fully automatic when both heartbeat and recovery are enabled (the defaults). No code changes needed.

// Fine-tune recovery buffer
const io = createServer({
  recovery: {
    maxBufferSize: 200,    // Keep last 200 messages per socket
    maxBufferAge: 60000,   // Keep buffer for 60s after disconnect
  },
})

Disable recovery:

const io = createServer({ recovery: false })

History Adapters

Store room message history with pluggable adapters. Two built-in adapters: MemoryAdapter (in-memory, great for dev) and SqliteAdapter (persistent, powered by bun:sqlite).

import { createServer, MemoryAdapter, SqliteAdapter } from '@mustafakurtt/bun-sockets'

// In-memory (development)
const io = createServer({
  history: new MemoryAdapter({ maxPerRoom: 1000 }),
})

// SQLite (production — persistent, WAL mode)
const io = createServer({
  history: new SqliteAdapter({
    path: './chat-history.db',  // ':memory:' for in-memory SQLite
    maxPerRoom: 10000,
  }),
})

Automatic storage — messages sent via io.to(room).emit() and socket.broadcast() are automatically stored.

Query history with pagination:

// Latest 50 messages (default)
const messages = await io.history('chat-room')

// Paginate — get older messages
const page1 = await io.history('chat-room', { limit: 20 })
const page2 = await io.history('chat-room', {
  limit: 20,
  before: page1[page1.length - 1].timestamp,
})

// Filter by event type
const chatOnly = await io.history('chat-room', { event: 'chat_message' })

// Ascending order
const oldest = await io.history('chat-room', { order: 'asc', limit: 10 })

HistoryQuery options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | limit | number | 50 | Max entries to return | | before | number | — | Return entries before this timestamp | | after | number | — | Return entries after this timestamp | | order | 'asc' \| 'desc' | 'desc' | Sort order by timestamp | | event | string | — | Filter by event name |

Custom adapter — implement the HistoryAdapter interface:

import type { HistoryAdapter } from '@mustafakurtt/bun-sockets'

class RedisAdapter implements HistoryAdapter {
  store(room, event, payload) { /* ... */ }
  getHistory(room, query?) { /* ... */ }
  clear(room) { /* ... */ }
  clearAll() { /* ... */ }
}

Namespaces

Namespaces let you split logic across multiple endpoints on the same server. Each namespace has its own connection handlers, middleware, rooms, and sockets.

const io = createServer({ heartbeat: false })

// Create namespaces
const chat = io.of('/chat')
const admin = io.of('/admin')

// Each namespace has independent handlers
chat.on('connection', (socket) => {
  socket.join('general')
  socket.on('message', (payload) => {
    socket.broadcast('general', 'message', payload)
  })
})

admin.use((req, next) => {
  const token = new URL(req.url).searchParams.get('token')
  if (token === 'secret') next({ role: 'admin' })
  else throw new Error('Unauthorized')
})

admin.on('connection', (socket) => {
  console.log('Admin connected:', socket.data.role)
})

Client connects to a namespace by setting path:

const chatClient = createClient({ url: 'ws://localhost:3000', path: '/chat' })
const adminClient = createClient({ url: 'ws://localhost:3000', path: '/admin' })

Namespace API — same as server: on, use, to, rooms, sockets, connectionCount, history.

Binary Messages

Send and receive raw binary data (ArrayBuffer / Uint8Array) alongside JSON events. Zero-copy wire format — no base64 overhead.

Server side:

io.on('connection', (socket) => {
  // Send binary to client
  const pixels = new Uint8Array([255, 0, 0, 255, 0, 255])
  socket.emitBinary('frame', pixels)

  // Receive binary from client
  socket.onBinary('upload', (data: ArrayBuffer) => {
    console.log('Received', data.byteLength, 'bytes')
  })
})

Client side:

const client = createClient({ url: 'ws://localhost:3000' })

// Send binary to server
client.emitBinary('upload', new Uint8Array([1, 2, 3]))

// Receive binary from server
client.onBinary('frame', (data: ArrayBuffer) => {
  const pixels = new Uint8Array(data)
})

Wire format: [0x01][2-byte event length][event name][binary payload] — efficient, no JSON encoding for binary data.

Client Options

const socket = createClient({
  url: 'ws://localhost:3000',     // Server URL (required)
  path: '/ws',                    // WebSocket endpoint path (default: '/ws')
  reconnect: true,                // Enable auto-reconnect (default: true)
  // reconnect: {                 // Or fine-tune:
  //   maxRetries: 10,            //   Max reconnection attempts (default: 10)
  //   baseDelay: 1000,           //   Initial delay in ms (default: 1000)
  //   maxDelay: 30000,           //   Max delay in ms (default: 30000)
  //   jitter: true,              //   Add randomness to prevent thundering herd (default: true)
  // },
  auth: { token: 'jwt-token' },   // Auth params sent as query string (default: {})
  bufferMessages: true,           // Buffer messages sent while disconnected (default: true)
  maxBufferSize: 100,             // Max buffered messages (default: 100)
  protocols: [],                  // WebSocket sub-protocols (default: [])
})

Auto-Reconnect

When the connection drops unexpectedly, the client automatically reconnects with exponential backoff and jitter:

Attempt 1: ~1000ms delay
Attempt 2: ~2000ms delay
Attempt 3: ~4000ms delay
Attempt 4: ~8000ms delay
...capped at maxDelay (30s)

Jitter adds ±25% randomness to each delay, preventing all clients from reconnecting at the exact same time (thundering herd problem).

const socket = createClient({
  url: 'ws://localhost:3000',
  reconnect: { maxRetries: 5, baseDelay: 500 },
})

socket
  .onReconnect((attempt) => console.log(`Reconnected on attempt ${attempt}`))
  .onReconnectFailed(() => console.log('All reconnection attempts exhausted'))
  .connect()

Event Buffering

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

const socket = createClient({ url: 'ws://localhost:3000' })

// These are buffered — not lost
socket.emit('message', { text: 'sent while offline 1' })
socket.emit('message', { text: 'sent while offline 2' })

socket.connect()
// → on connect, both messages are delivered in order

API Reference

createServer<ClientEvents, ServerEvents>(options?)

Creates a new WebSocket server instance.

Server (io)

| Method / Property | Description | |-------------------|-------------| | io.on('connection', handler) | Handle new connections | | io.on('disconnect', handler) | Handle disconnections | | io.use(middleware) | Add middleware (runs at HTTP upgrade) | | io.to(room).emit(event, data) | Broadcast to all sockets in a room | | io.rooms | ReadonlyMap<string, ReadonlySet<string>> — all rooms | | io.sockets | ReadonlyMap<string, BunSocket> — all connected sockets | | io.connectionCount | Number of connected sockets | | io.handler | Pass to Bun.serve({ fetch }) | | io.websocket | Pass to Bun.serve({ websocket }) |

Socket (socket)

| Method / Property | Description | |-------------------|-------------| | socket.id | Unique socket identifier (UUID) | | socket.rooms | ReadonlySet<string> — rooms this socket is in | | socket.data | Record<string, unknown> — middleware context | | socket.join(room) | Subscribe to a room (fluent) | | socket.leave(room) | Unsubscribe from a room (fluent) | | socket.leaveAll() | Leave all rooms (fluent) | | socket.emit(event, payload) | Send event to this socket only (fluent) | | socket.on(event, handler) | Listen for client events (fluent) | | socket.broadcast(room, event, payload) | Publish to all sockets in a room (fluent) | | socket.disconnect(code?, reason?) | Close the connection |

Middleware

type MiddlewareFn = (req: Request, next: MiddlewareNext) => void | Promise<void>
type MiddlewareNext = (context?: Record<string, unknown>) => void

createClient<ClientEvents, ServerEvents>(options)

Creates a new WebSocket client instance.

Client (socket)

| Method / Property | Description | |-------------------|-------------| | socket.id | Socket ID (null until connected) | | socket.state | 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | | socket.connected | true if currently connected | | socket.connect() | Open the WebSocket connection (fluent) | | socket.disconnect(code?, reason?) | Close the connection (fluent) | | socket.emit(event, payload) | Send event to server (fluent, buffered) | | socket.on(event, handler) | Listen for server events (fluent) | | socket.off(event, handler?) | Remove event handler(s) (fluent) | | socket.onConnect(handler) | Connection opened callback (fluent) | | socket.onDisconnect(handler) | Connection closed callback (fluent) | | socket.onReconnect(handler) | Successful reconnection callback (fluent) | | socket.onReconnectFailed(handler) | All retries exhausted callback (fluent) | | socket.onError(handler) | WebSocket error callback (fluent) |

Wire Protocol

Messages between client and server use a simple JSON protocol:

{ "event": "event_name", "payload": { ... } }

Client → Server example:

ws.send(JSON.stringify({ event: 'chat_message', payload: { text: 'Hello!' } }))

Server → Client example (received):

{ "event": "new_message", "payload": { "from": "uuid", "text": "Hello!" } }

Requirements

  • Bun >= 1.0.0

This package uses Bun's native WebSocket server. It does not work with Node.js.

Roadmap

  • [x] ~~Client package~~ — auto-reconnect, backoff, event buffering, type-safe ✅
  • [x] ~~Heartbeat / ping-pong~~ — zombie socket detection and cleanup ✅
  • [x] ~~Connection State Recovery~~ — replay missed messages after reconnect ✅
  • [x] ~~History adapters (Memory + bun:sqlite)~~ — room message history with pagination ✅
  • [x] ~~Namespace support~~ — multiple endpoints on one server ✅
  • [x] ~~Binary message support~~ — ArrayBuffer / Uint8Array, zero-copy wire format ✅

License

MIT — Mustafa Kurt