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

sleepy-socket

v0.9.0

Published

A dependency-free WebSocket client for sleepy-serv

Readme

sleepy-socket

A WebSocket client for talking to sleepy-serv servers

Important Notes

  • This package has zero dependencies, and runs in browsers as well as in bun.sh.
  • This package is the client half of sleepy-serv. It expects a sleepy-serv server on the other end.
  • Requests are made over a single WebSocket connection, but they're modeled as REST-ful calls with methods, routes, headers, and status codes.

Installation

bun add sleepy-socket

Getting Started

Here's a minimalist example on how to connect and make a request:

import SleepySocketClient from 'sleepy-socket'

const client = await SleepySocketClient.connect('localhost', 3000)
const res = await client.get('/users')

console.log(res.status) // 200
console.log(res.body) // the parsed response body

await client.close()

connect() is the only supported way to create a client. It's async because it doesn't resolve until the connection is fully established: it requests a ticket over HTTP, opens the WebSocket, and waits for the server's welcome message. Once it resolves, the client is ready to make requests.

Making Requests

There's one method per HTTP verb: head(), get(), post(), put(), patch(), and delete(). They all take the same two parameters:

  • route: the route to call, such as /users or /users/123
  • opts: an optional object containing headers, query, and body

Each one returns a promise that resolves to the full response message, not just the body:

const res = await client.get('/users/123')

console.log(res)

That gives you an object shaped like this:

{
  id: '2b1f...',           // uuid, matches the request that produced it
  clientId: '9c4e...',     // same as `client.id`
  type: 'response',
  status: 200,
  timestamp: '2026-07-19T...',
  headers: { 'content-type': 'application/json;charset=utf-8' },
  body: { name: 'ada' },
}

Note that a failing status does not reject the promise. A 404 NotFound or 500 InternalServerError resolves normally, with the status on res.status. Only transport-level problems reject, such as a timeout or the socket closing mid-flight. This means you check res.status rather than wrapping calls in try/catch:

const res = await client.get('/users/123')

if (res.status === 404) {
  console.log('no such user')
}

Request Options

The second parameter to any request method can contain these optional properties:

  • headers: a Headers instance. Passing anything else throws a TypeError.
  • query: a plain object of query string values
  • body: the request body, which can be any JSON-serializable value

Here's an example that uses all three:

const res = await client.post('/users', {
  headers: new Headers({
    authorization: `Bearer ${token}`,
  }),
  query: { dryRun: true },
  body: {
    name: 'ada',
    count: 3,
  },
})

When body is a non-null object, the client sets content-type to application/json;charset=utf-8 for you, unless you already set a content-type header yourself.

Notifications

Servers can push messages that aren't replies to anything. Those arrive as notifications, and you subscribe to them with on():

client.on('notification', message => {
  console.log(message.event) // 'state_changed'
  console.log(message.body) // { score: 1 }
})

There's one thing worth pointing out here: 'notification' is the only event name the client emits. The server's own event name lives on the message's event property, so you branch on that inside your handler:

client.on('notification', message => {
  switch (message.event) {
    case 'state_changed':
      return applyState(message.body)

    case 'user_joined':
      return addUser(message.body)
  }
})

If one of your handlers throws, the error is caught and logged, and the remaining handlers still receive the message.

Reconnection

The client reconnects automatically when the socket drops. It reclaims its previous session, so client.id stays the same across a reconnect and you don't need to re-establish application state.

You can tune the backoff:

const client = await SleepySocketClient.connect('localhost', 3000, {
  reconnect: {
    minDelay: 1_000,
    maxDelay: 10_000,
    factor: 2,
  },
})

Set reconnect to false to turn it off entirely:

const client = await SleepySocketClient.connect('localhost', 3000, {
  reconnect: false,
})

Note that only the literal value false disables reconnection. Any other value falls back to the defaults.

Response Queueing

Requests are sent over one socket, so responses can come back in a different order than they were sent. The queue option controls how the client hands those responses back to you.

For example, if you fire three requests that take 300ms, 100ms, and 200ms:

import SleepySocketClient, { Queue } from 'sleepy-socket'

const client = await SleepySocketClient.connect('localhost', 3000, {
  queue: Queue.Fifo,
})

const results = []

await Promise.all([
  client.get('/', { query: { delay: 300 } }).then(() => results.push(1)),
  client.get('/', { query: { delay: 100 } }).then(() => results.push(2)),
  client.get('/', { query: { delay: 200 } }).then(() => results.push(3)),
])

The three queue types resolve those promises differently:

  • Queue.None: each promise resolves the moment its response arrives, so results is [2, 3, 1]. This is the default.
  • Queue.Fifo: responses are held back until every earlier request has resolved, so results is [1, 2, 3], matching the order you sent them.
  • Queue.Lifo: responses drain from the most recent request backwards, so results is [3, 2, 1].

Queue.None is the right choice most of the time. Queue.Fifo is useful when responses have to be applied in the order they were requested.

Mount Paths

If the server was created with a mountPath, give the client the same value:

const client = await SleepySocketClient.connect('localhost', 3000, {
  mountPath: '/api/v2',
})

const res = await client.get('/users')

The routes you pass to request methods stay mount-relative. The client joins the prefix on internally, so /users above is sent as /api/v2/users.

API

SleepySocketClient.connect(host, port, opts)

This static method creates a client, connects it, and resolves once the server has acknowledged the connection. It's the only supported way to construct a client.

The parameters are:

  • host: the hostname, without a scheme, such as 'localhost'
  • port: the port number
  • opts: an optional options object

The opts object can contain these optional properties:

  • queue: how responses are handed back, one of Queue.None, Queue.Fifo, or Queue.Lifo. Defaults to Queue.None. An unrecognized value throws a RangeError.
  • secure: set to true to use https and wss instead of http and ws. Defaults to false.
  • timeout: how long to wait, in milliseconds, both for the initial connection and for each individual request. Defaults to 30_000.
  • serverTimeout: how long the client tolerates silence from the server, in milliseconds, before it considers the connection dead and closes it. Defaults to 120_000.
  • mountPath: the server's mount path prefix. Defaults to ''.
  • reconnect: an options object for reconnection behavior, or false to disable it

The reconnect object can contain these optional properties:

  • minDelay: the starting backoff delay in milliseconds. Defaults to 500.
  • maxDelay: the maximum backoff delay in milliseconds. Defaults to 30_000.
  • factor: the exponential multiplier applied to the delay after each failed attempt. Defaults to 2.
  • random: the jitter source. Defaults to Math.random.

Request Methods

head(route, opts), get(route, opts), post(route, opts), put(route, opts), patch(route, opts), and delete(route, opts) all send a request and return a promise resolving to the response message.

They throw synchronously if the client isn't connected, and their promises reject on timeout or if the socket closes before the response arrives.

on(event, handler)

Registers a handler for an event. The only event emitted is 'notification'. Registering the same function twice is a no-op, since handlers are stored in a set.

off(event, handler)

Removes a previously registered handler. It's safe to call with a handler that was never registered.

close()

Closes the connection and rejects any in-flight requests. It returns a promise, so it's worth awaiting before your process exits.

Note that closing is permanent. There's no reopen, and calling close() a second time throws. If you're calling it in a finally block, guard it with isConnected:

try {
  await doWork(client)
} finally {
  if (client.isConnected) {
    await client.close()
  }
}

Properties

All of these are read-only:

  • id: the server-assigned client id, which survives reconnects
  • isConnected: whether the client is currently connected and ready for requests
  • socket: the underlying WebSocket, or null while disconnected
  • connectionData: whatever payload the server attached when the connection was established. This is where application data such as an auth token shows up.
  • token: the reclaim token used internally to restore the session after a drop. This is not an application auth token; that would be on connectionData.
  • queueType: the configured queue type
  • isSecure: whether the connection uses wss
  • timeout: the configured request timeout
  • serverTimeout: the configured server silence timeout
  • heartbeatInterval: how often the client sends heartbeats. This is dictated by the server, not configured by you.
  • mountPath: the configured mount path

Queue

Contains the valid values for the queue option: Queue.None, Queue.Fifo, and Queue.Lifo.

MessageType

Contains the message type names used on the wire: MessageType.Welcome, MessageType.Heartbeat, MessageType.Request, MessageType.Response, and MessageType.Notification. A response message's type is always MessageType.Response, and a notification's is always MessageType.Notification.

Errors

Most failures surface as thrown errors or rejected promises:

  • Invalid queue type: <value>: a RangeError thrown by connect() when queue isn't a valid Queue value. This is thrown before any network call is made.
  • Connection failed.: the connection couldn't be established
  • Connection timed out.: the connection wasn't established within timeout milliseconds
  • opts.headers must be a Headers instance: a TypeError thrown when a request's headers option isn't a Headers object
  • Socket is closed: thrown when you call a request method while disconnected, or when you call close() more than once
  • Request timed out.: a request didn't get a response within timeout milliseconds
  • Socket closed.: the socket closed while requests were still in flight. Every pending request rejects with this.

Remember that these cover transport failures only. An error response from the server, such as a 404 NotFound, resolves normally with the status on res.status.