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

@amatiasq/socket

v2.0.0

Published

WebSockets in three layers, on both ends of the wire. Pick the one you need and ignore the rest.

Readme

@amatiasq/socket

WebSockets in three layers, on both ends of the wire. Pick the one you need and ignore the rest.

Install with npm i --save @amatiasq/socket.

| | client (browser) | server | one connection | | ------------------------------- | ----------------- | ---------------- | ------------------- | | transport, reconnection, queue | ResilientSocket | — | — | | typed JSON | JsonSocket | JsonServer | JsonConnection | | typed messages, session identity| SessionSocket | SessionServer | SessionConnection |

Client-side names come from @amatiasq/socket, server-side from @amatiasq/socket/server. The split is deliberate: importing the client surface never pulls the server code into your bundle.

This package has one dependency (@amatiasq/emitter) and no WebSocket implementation. The client uses the platform's global WebSocket; the server takes sockets you already have. That is what lets it run on Node, Bun, Deno and Cloudflare Workers without a per-runtime build.

ResilientSocket — reconnection and a send queue

import { ResilientSocket } from '@amatiasq/socket';

const socket = new ResilientSocket('wss://sockethost.com');

socket.onOpen(() => console.log('Socket open'));
socket.onClose(() => console.log('Socket closed'));
socket.onMessage(event => console.log(event.data));

// Fires when it gives up, after maxReconnectAttempts
socket.onError(() => console.log('Reconnection failed'));

socket.onReconnect(event =>
  console.log(`Disconnected for ${Date.now() - +event.disconnectedTime}ms`),
);

// Sent now if connected, queued and flushed on reconnect if not
socket.send('hello');

JsonSocket — typed JSON

import { JsonSocket } from '@amatiasq/socket';

interface Incoming {
  foo: number;
}
interface Outgoing {
  bar: string;
}

const socket = new JsonSocket<Incoming, Outgoing>('wss://sockethost.com');

socket.onMessage(message => console.log(message.foo));
socket.send({ bar: 'test' }); // a wrong shape is a compile error

A frame that is not valid JSON is dropped with a console.warn — a peer sending garbage does not take the listener down.

JsonServer — the same, server-side

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { JsonServer, fromEventEmitter } from '@amatiasq/socket/server';

const server = new JsonServer<Incoming, Outgoing>();

server.onConnection(client => {
  client.onClose(() => logout(client));
  client.onMessage(data => console.log(data.foo));
  client.send({ bar: 'hello' });
});

// You own the listening socket. `ws` here, but see "Runtimes" below.
const wss = new WebSocketServer({ server: createServer() });
wss.on('connection', ws => server.accept(fromEventEmitter(ws)));

The session layer

SessionSocket / SessionServer add an identity that outlives the socket. When a client reconnects it re-announces its ClientId, and the server rebinds it to the same SessionConnection — every listener registered on it stays attached, and onConnection does not fire again.

import { SessionServer, fromEventEmitter } from '@amatiasq/socket/server';

type ServerMessage = { type: 'tick'; data: number };
type ClientMessage = { type: 'move'; data: string };

const server = new SessionServer<ServerMessage, ClientMessage>();

server.onConnection(client => {
  client.onMessageType('move', where => console.log(client.id, where));
  client.send('tick', Date.now());
});

wss.on('connection', ws => server.accept(fromEventEmitter(ws)));

Session ids are bearer tokens

A ClientId is a v4 UUID from the platform CSPRNG — 122 random bits — issued by the server and never taken from the wire. A reconnect naming an id the server does not know gets a fresh session rather than adopting that id.

Treat it as you would a session cookie: whoever presents the id is that client. There is nothing else distinguishing a reconnecting client from someone who obtained its id, so do not log it, do not put it in a URL, and do not persist it anywhere a third party can read.

(Before 2.0.0 these were 1, 2, 3… and any client naming another's id was handed its session. If you are on 1.x, upgrade.)

Runtimes

The servers never create a socket, so they work anywhere. Two adapters cover the two socket shapes that exist:

import { fromEventTarget, fromEventEmitter } from '@amatiasq/socket/server';

fromEventEmitter(ws); //  `ws` on Node — .on() / .off()
fromEventTarget(socket); //  Deno, Cloudflare Workers, browsers — addEventListener

Bun's ServerWebSocket declares its handlers on the server rather than per socket, so build the transport by hand — it is four methods:

Bun.serve({
  websocket: {
    open(ws) {
      server.accept({
        send: data => ws.send(data),
        close: () => ws.close(),
        onMessage: listener => ((ws.data.onMessage = listener), () => {}),
        onClose: listener => ((ws.data.onClose = listener), () => {}),
      });
    },
    message: (ws, data) => ws.data.onMessage?.(String(data)),
    close: ws => ws.data.onClose?.(),
  },
});

Reconnection, in detail

A dropped socket is retried after reconnectionDelay (100ms by default), then 200ms, then 400ms… After maxReconnectAttempts (14) it stops and fires onError. Both are per-instance options:

new ResilientSocket(uri, { reconnectionDelay: 500, maxReconnectAttempts: 3 });

Anything sent while disconnected is queued and flushed, in order, once the connection is back.

History

This package is the merge of four: @amatiasq/socket, @amatiasq/json-socket, @amatiasq/nice-socket and @amatiasq/resilient-socket. The three absorbed names are deprecated on npm and will not get further versions.

Renames, if you are coming from one of them:

| before | now | | ------------------------------- | ------------------- | | ClientSocket | SessionSocket | | WebSocketServer | SessionServer | | ServerSocket | SessionConnection | | NiceSocketServer | JsonServer | | NiceSocket | JsonConnection | | sendJson() / onJsonMessage()| send() / onMessage() |

JsonSocket used to carry its own copy of the reconnection logic and no send queue; it is now a layer over ResilientSocket, so it gained the queue. Its onMessage/onOpen/onReconnect are subscribe functions now, matching the rest of the package — call them, do not reach for .subscribe.

The server classes no longer take an http.Server. See "Runtimes".