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

@marianmeres/ws

v0.3.0

Published

[![NPM](https://img.shields.io/npm/v/@marianmeres/ws)](https://www.npmjs.com/package/@marianmeres/ws) [![JSR](https://jsr.io/badges/@marianmeres/ws)](https://jsr.io/@marianmeres/ws) [![License](https://img.shields.io/npm/l/@marianmeres/ws)](LICENSE)

Readme

@marianmeres/ws

NPM JSR License

A WebSocket client with namespaces, rooms, presence and reconnect that actually survives real networks — plus a mountable reference server implementing the same protocol.

Features

  • Reconnects forever — capped exponential backoff with jitter, plus instant retry when the browser comes back online or the tab regains focus
  • Detects half-open connections — the failure where the peer vanishes, no onclose ever fires, and a naive client sits "connected" receiving nothing
  • Namespaces and rooms — namespace isolates, rooms are channels within it
  • Presence — opt-in per room; membership snapshot plus join/leave deltas, re-synced automatically after every reconnect
  • Buffered sends — publishes issued while offline are queued (capped, never unbounded) and flushed after re-subscribe
  • Acknowledged publishespublish() resolves with a recipient count
  • Runs everywhereWebSocket is a global in browsers, Deno, Node 22+, Bun and Workers, so there is no polyfill and no transport dependency
  • Svelte-store compatible reactive connection state

Installation

npm install @marianmeres/ws
deno add jsr:@marianmeres/ws

npm ships the client only. The reference server needs Deno.upgradeWebSocket, so it exists solely as jsr:@marianmeres/ws/server. The client is fully runtime-agnostic, so npm consumers lose nothing they could have used.

ws or sse?

@marianmeres/sse is the sibling package: same shape of API, different transport, different strengths. In short —

  • Reach for ws when traffic is genuinely bidirectional and chatty (collaborative editing, games, chat with typing indicators), when you need presence, or when you need binary frames.
  • Reach for sse when traffic is mostly server → client (notifications, live dashboards, progress, activity feeds), or when losing messages across a reconnect is not acceptable — SSE resumes from Last-Event-ID, WebSocket has no equivalent.

They are not drop-in replacements for one another and are not meant to be. See COMPARISON.md in the sse package for the full table.

Usage

Client

import { createWSClient } from "@marianmeres/ws";

const ws = createWSClient({
	url: "/ws",
	namespace: "org-123",
	auth: () => session.token, // called on every (re)connect, so refresh works
});

// Subscribe and handle in one call; the returned function detaches the handler
// and unsubscribes the room when it was the last one.
const unsub = await ws.subscribe("chat", (msg) => {
	console.log(msg.from, msg.payload, msg.timestamp);
});

const { recipients } = await ws.publish("chat", { text: "hello" });

unsub();
ws.dispose();

connect() is optional — the first subscribe() or publish() starts the connection. Call it explicitly when you want a readiness gate:

await ws.connect(); // resolves once connected; rejects only if retrying cannot help

Presence

Presence is enabled by providing a presence handler, and is opt-in per room — a room with thousands of subscribers does not want a join event per peer every time the fleet reconnects.

await ws.subscribe("room", onMessage, {
	presence: (e) => {
		// e.event is "sync" | "join" | "leave"
		// "sync" carries the full snapshot, and fires again after every reconnect
		console.log(e.event, e.clientId, e.members);
	},
});

ws.members("room"); // last known membership

Reactive state (Svelte)

<script>
    import { createWSClient } from "@marianmeres/ws";
    const ws = createWSClient({ url: "/ws" });
    const state = ws.state;
</script>

{#if $state.connected}
    <Online />
{:else if $state.attempt > 0}
    <p>Reconnecting… (attempt {$state.attempt})</p>
{/if}

Server

import { createWSApp } from "@marianmeres/ws/server";

const { app, service } = createWSApp("/ws", [], {
	verify: async (payload, req) => {
		const user = await authenticate(payload?.token);
		// Returning null closes the socket with a terminal code.
		return user ? { clientId: user.id, namespace: user.orgId } : null;
	},
});

// Push to connected clients from anywhere in your app.
await service.publish("notifications", { text: "deploy finished" }, "org-123");

Deno.serve(app);

Mounted routes, relative to the mount path:

| Method | Path | Notes | | ------ | ----------------------------- | ----------------------------------------- | | GET | / | WebSocket upgrade | | GET | /stats | Guarded by httpAuth when supplied | | POST | /publish/[namespace]/[room] | Requires httpAuth, else not mounted | | POST | /broadcast/[room] | Requires httpAuth, else not mounted |

Example

A complete room chat — demino server plus a plain HTML client — lives in example/:

deno task example    # builds the client bundle, then serves on :8000

It exercises the handshake, namespaces, rooms, presence, acknowledged publishes, the broadcast gate, reconnect with buffered sends, HTTP injection and the pub/sub adapter seam. See example/README.md.

Concepts

Namespace — the isolation boundary. Clients in different namespaces can subscribe to identically named rooms without ever seeing each other's messages. A client may only publish into its own namespace.

Room — a channel within a namespace. Subscribe to receive its messages.

Broadcast — the one operation that crosses namespaces. It is a separate method rather than a flag on publish() precisely because crossing an isolation boundary deserves its own name and its own server-side check: allowBroadcast denies by default.

Behaviour worth knowing

Reconnect classification. Everything reconnects except a local disconnect() and an explicit terminal close code (4001 AUTH_FAILED, 4003 FORBIDDEN by default). A server-sent 1000 Normal Closure does reconnect — a graceful shutdown or rolling deploy is exactly when clients must come back.

Terminal failures are loud. Giving up is the only non-retrying exit, so it rejects any pending connect(), emits terminated, and logs at error level. A silent one would be indistinguishable from a network that never recovered.

Delivery is at-most-once. A publish that was transmitted but unacknowledged when the socket died is not resent — that would risk duplicates, and the server has no deduplication. It rejects on sendTimeout. At-least-once would need server-side replay, which this version does not do.

Sends are bounded. Every publish carries one deadline covering queue, flight and acknowledgement. Without it, a publish issued while offline would pend forever behind an infinite retry.

disconnect() is resumable; dispose() is terminal. Handlers, rooms and buffered sends survive a disconnect(), so a later connect() picks up where it left off.

API

See API.md for complete API documentation.

License

MIT