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

@zakkster/lite-ws

v1.0.0

Published

Reactive raw-WebSocket primitive: coarse lifecycle signals (status/reconnectAttempts/latency), reconnect with exponential backoff + jitter, an outbound buffer, a transport pipe that bypasses signals (batched once per frame), and a subscribe(topic) async-g

Readme

@zakkster/lite-ws

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads lite-signal peer TypeScript Dependencies License: MIT

Reactive raw-WebSocket primitive — lifecycle manager and transport pipe.

A WebSocket helper that manages the connection reactively (reconnect, exponential backoff with jitter, heartbeat, outbound buffering) and treats incoming data as a transport pipe, not a per-message signal. The reactive-socket trap is firing signal.set() on every frame of a 120Hz feed — lite-ws avoids it by design.

npm install @zakkster/lite-ws

Peer: @zakkster/lite-signal (the coarse lifecycle signals). ESM only. MIT. Optional wiring (not dependencies): @zakkster/lite-raf to drive poll() once per frame; @zakkster/lite-stream to consume subscribe() generators.

Raw WebSocket only — no Socket.IO. Twitch EventSub / IRC / PubSub are all raw WS; bundling a 30KB+ client with its own reconnect/ack/room lifecycle would negate the zero-dependency, zero-GC point and duplicate the lifecycle this module owns.


The trap it avoids

// THE ENEMY OF 120FPS
socket.onmessage = (e) => latestSignal.set(JSON.parse(e.data));
//   ^ allocates + reconciles the whole graph (lite-map, CRDT) on every network tick

lite-ws splits the two concerns that the naive version conflates:

  • Lifecycle is coarse signalsstatus(), reconnectAttempts(), latency(). They change rarely, so they're a perfect reactive fit: render an "Offline" banner or disable Save with no per-message graph churn.
  • Data is a pipeonMessage(data) runs per message (hand the raw frame to a CRDT/buffer in place; nothing allocates on this side). poll() coalesces every message since the last frame into one onBatch().

Lifecycle

import { createSocket } from "@zakkster/lite-ws";
import { effect } from "@zakkster/lite-signal";

const socket = createSocket("wss://eventsub.wss.twitch.tv/ws", {
  backoff: { min: 250, max: 10000, factor: 2, jitter: 0.5 },
});

effect(() => { banner.hidden = socket.status() === "open"; });
effect(() => { saveBtn.disabled = !socket.isOpen(); });

status() moves through "connecting" → "open" → "reconnecting" → "closed", with reconnects following exponential backoff with jitter; close() is intentional and does not reconnect. Sends issued while down are buffered (bounded, drop-oldest) and flushed in order on the next open.


How data flows

flowchart LR
    WS["WebSocket<br>(120 Hz)"] -->|onmessage| PIPE["onMessage(data)<br>-> CRDT / buffer, in place"]
    PIPE --> CNT["+1 (no signal)"]
    F["frame tick<br>(lite-raf)"] -->|poll| B{"any since last frame?"}
    CNT --> B
    B -->|yes| ONE["onBatch(n) -- ONE coarse UI bump"]
    B -->|no| SKIP["skip"]
    LC["status / attempts / latency"] -.->|change rarely| UI["reactive UI"]
const socket = createSocket(url, {
  onMessage: (buf) => crdt.applySyncMessage(buf),   // per message, in place
  onBatch:   ()    => room.notifyUI(),              // once per frame
});

import { frameDelta } from "@zakkster/lite-raf";
frameDelta.subscribe(socket.poll);                  // drive the batch once per frame

The firehose never touches the reactive graph; the UI wakes once per frame no matter how many messages arrived.


Event streams: subscribe(topic)

For specific lower-rate feeds, subscribe() returns an async generator (the lite-stream bridge) with an explicit drop policy — because a socket is push-only and you can't back-pressure the server:

const socket = createSocket(url, { topicOf: (m) => m.type });

for await (const tx of socket.subscribe("order", { policy: "ring", capacity: 512 })) {
  render(tx);     // if you lag, oldest are dropped — bounded memory
}
  • "ring" (default) — bounded queue, drop oldest.
  • "latest" — keep only the newest.
  • "all" — unbounded (memory risk if the consumer lags; use deliberately).

Returning/breaking the generator unsubscribes it.


Latency is application-level, by necessity

The browser WebSocket API handles protocol ping/pong frames invisibly — JS can't send or observe them. So heartbeat sends a payload your server echoes, and isPong identifies it:

const socket = createSocket(url, {
  heartbeat: {
    interval: 15000,
    timeout: 5000,                                  // no pong in time -> force reconnect
    isPong: (m) => (m.type === "pong" ? m.t : false),   // return the echoed send-timestamp
  },
});
effect(() => { pingLabel.textContent = `${socket.latency()} ms`; });   // smoothed RTT, -1 if unknown

Zero-GC

The message firehose is onMessage(data) plus a counter increment — no allocation on this side, and the lifecycle signals do not fire per message. Verified: 1,000 messages leave the engine pool counters flat and a status() effect un-re-run. (subscribe() queues do allocate on push — use them for event streams, not the CRDT firehose.)


Testability

Everything is injectable — the WebSocket constructor, timers, clock, RNG — so the entire state machine (connect/open, backoff geometry, reconnect, heartbeat RTT and timeout, the batch coalesce, the stream drop policies) is tested headlessly with a mock socket and a fake clock. No browser needed.


API

createSocket(url, opts?): Socket
createSocketFactory(registry): { createSocket }     // non-default registry

interface Socket {
  status(): "connecting" | "open" | "reconnecting" | "closed"   // reactive
  isOpen(): boolean                                             // reactive
  reconnectAttempts(): number                                   // reactive
  latency(): number                                             // reactive, -1 if unknown
  send(data): boolean             // sent now, or buffered (bounded, drop-oldest)
  poll(): number                  // coalesce -> one onBatch(); drive once per frame
  subscribe(topic?, { policy?: "ring"|"latest"|"all", capacity? }): AsyncGenerator
  connect(): void
  close(code?, reason?): void     // intentional — no reconnect
  readonly raw                    // underlying WebSocket (null while down)
  dispose(): void
}

Created inside an owning scope it auto-disposes with it (onCleanup); otherwise call dispose().


License

MIT (c) 2026 Zahary Shinikchiev <[email protected]>