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

@tutti-os/ws-client

v0.2.0

Published

Thin browser WebSocket V2 client for the tsh-websocket realtime gateway.

Readme

@tutti-os/ws-client

Thin browser client for the tsh-websocket realtime gateway. It owns the parts every consumer would otherwise re-implement and get wrong — connect, app-level heartbeat, reconnect with backoff, and subscription replay on reconnect — and stays out of business semantics.

Wire contract: see ../../docs/PROTOCOL.md (V2). Uplink control messages use { action, data }. Downlink business messages use the strict V2 envelope and are dispatched by event_type. Business event names and payloads are opaque to this client, so adding a business event does not require an SDK release.

Install

pnpm add @tutti-os/ws-client

Supported Web model

The browser-native WebSocket cannot set handshake headers, so auth relies on the same-site session_id cookie. Deploy the page and the gateway on the same site. Create one client per page. When the signed-in account changes, reload the page so the old socket is closed and the new page connects with the new cookie.

deviceId is a stable routing dimension, not a credential. Generate one per page/client and keep it unchanged for that client's physical reconnects. Do not share one ID through localStorage: concurrent tabs with the same account and ID represent the same server-side slot and will replace each other's socket.

import { createWsClient } from "@tutti-os/ws-client";

const endpoint = new URL("wss://ws.tutti.sh/");
// Created once while this page initializes; the client reuses the URL on every
// reconnect. A reload (including an account switch) creates a fresh page slot.
const deviceId = `web-page:${crypto.randomUUID()}`;
endpoint.searchParams.set("deviceId", deviceId);

const ws = createWsClient({
  url: endpoint.toString()
});

// Account/user-scoped events do not require a room subscription. The gateway
// resolves the user from the authenticated session cookie.
ws.on("user.notification.changed", () => refreshUnreadCount());
ws.onError((err) => console.warn("ws error", err.code, err.message));

await ws.connect();

// `open` keeps its transport meaning for compatibility. Use `ready` when
// deciding whether Account-scoped realtime delivery can replace HTTP fallback.
ws.onReadyChange((ready) => setAccountRealtimeHealthy(ready));

// Room-scoped events require an ACKed room registration.
const room = ws.acquireRoom("room_123");
room.onReadyChange((ready) => setRoomRealtimeHealthy("room_123", ready));
ws.on("room.presence", (payload) => renderPresence(payload));

// Release when the page no longer needs room-scoped delivery.
room.release();

Room acquisition is reference counted inside a page: multiple components can acquire the same room, and the SDK removes it from the desired set only after the final handle is released. A page can hold at most 45 distinct rooms, matching the server-side device limit.

Delivery is best effort. A received event is validated and deduplicated, but the WebSocket channel is not durable storage and does not replace the business API's normal reconciliation/read path.

Node and standalone clients are outside the supported 0.1.x surface; use the Go client for those environments.

API

createWsClient(options)WsClient

| option | default | notes | |---|---|---| | url | — | ws:/wss: V2 endpoint including a valid deviceId query | | init | — | sent as {action:"init", data:init} on every (re)connect | | heartbeat | { intervalMs: 180000, timeoutMs: 360000 } | ping cadence / pong deadline | | reconnect | { initialDelayMs: 1000, maxDelayMs: 15000, factor: 1.5 } | false to disable | | maxQueuedMessages | 100 | maximum business frames buffered before V2 is ready |

WsClient:

  • connect(): Promise<void> — concurrent calls share one attempt; resolves on first open, including after automatic retries.
  • send(action, data?) — uplink; buffered until init+replay, with a 30 KiB frame limit and bounded outbox.
  • on(type, handler) => unsubscribe — downlink dispatch by type.
  • onError(handler) => unsubscribe — reserved type:"error" frames.
  • onStateChange(handler) => unsubscribeidle|connecting|open|reconnecting|closed.
  • ready / onReadyChange(handler) — Account-channel readiness after connection.ready; this does not redefine open.
  • acquireRoom(roomId) => { subscriptionId, ready, onReadyChange, release } — reference-counts one desired room; Room readiness follows the exact ACKed registration.
  • refreshSubscriptions() — reauthorizes the complete desired room set.
  • onSubscriptionsAck(handler) => unsubscribe — observes authoritative room subscription ACKs.
  • replacePresenceSubscriptions(subscriptions) — atomically replaces up to 100 observed users; stable tokens are owned by the caller/coordinator.
  • onPresenceSubscriptionsAck(handler) => unsubscribe — observes the compact Presence ACK used to trigger the initial BatchGet snapshot.
  • presenceSessionEpoch — process-scoped epoch replayed across physical reconnects.
  • close() — stop reconnect, clear timers, drop the socket.

What this client deliberately does NOT do

  • No business type/action enumeration — keep them in your app layer.
  • No store/UI binding — wrap it in your own service if you need that.
  • No account-switch API — reload the page after the session account changes.
  • No supported Node/standalone adapter in 0.1.x.

Adding any of the above into this package breaks its transport-only contract; that is what keeps "upper layers onboard without touching the client" true.

Dev

pnpm test       # node --test, no network
pnpm typecheck
pnpm build      # emits dist/