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

@rivalis/browser

v6.0.0

Published

πŸ”— Browser client for rivalis server

Readme

@rivalis/browser

GitHub npm version npm downloads

The browser WebSocket client for Rivalis. Connects to a @rivalis/core server, decodes binary frames into typed events, and handles reconnection.

⭐ Features

  • Tiny surface β€” connect, disconnect, send, on / once / off. That's it.
  • Typed events β€” client:connect, client:disconnect, client:kicked, client:reconnecting, client:reconnect_failed with their actual payload shapes; user topics typed via an optional generic.
  • Exponential-backoff reconnect with jitter (opt-in).
  • Token-refresh hook β€” getTicket is called before every reconnect attempt; perfect for short-lived JWTs.
  • Two ticket-delivery modes β€” query string (default, back-compat) or Sec-WebSocket-Protocol header (recommended for production β€” keeps credentials out of access logs and browser history).
  • Native browser WebSocket only β€” no ws dependency, no polyfill.

πŸš€ Install

npm install @rivalis/browser

@rivalis/browser declares its dependencies as peers:

npm install @toolcase/base @toolcase/logging @toolcase/serializer

πŸš€ Hello world

import { WSClient } from '@rivalis/browser'

const ws = new WSClient('ws://localhost:8080')
const encoder = new TextEncoder()
const decoder = new TextDecoder()

ws.on('client:connect', () => console.log('connected'))
ws.on('client:disconnect', (reason) => console.log('disconnected:', decoder.decode(reason)))
ws.on('chat', (payload) => console.log('chat:', decoder.decode(payload)))

ws.connect('alice')                                // ticket = "alice"
ws.send('chat', encoder.encode('hello world'))     // payload is opaque bytes

payload is always a Uint8Array. The framework never inspects it β€” encode it however you like (JSON + TextEncoder, protobuf, msgpack…).

🧠 API

Constructor

new WSClient<TTopics extends string = string>(baseURL: string, options?: WSClientOptions)
type WSClientOptions = {
    reconnect?: boolean | WSClientReconnectOptions
    ticketSource?: 'query' | 'protocol'      // default 'query'
    getTicket?: () => string | Promise<string>
}

type WSClientReconnectOptions = {
    maxAttempts?: number      // default Infinity
    baseDelayMs?: number      // default 500
    maxDelayMs?: number       // default 10_000
}

Methods

| Method | Description | |---|---| | connect(ticket?) | Open a new connection. The ticket is what your server's AuthMiddleware.authenticate receives. | | disconnect() | Close gracefully. Cancels pending reconnects, nulls lastTicket. | | send(topic, payload?) | Send a frame. payload: Uint8Array \| string (strings are UTF-8 encoded for you). Drops with a warning if not in OPEN state. | | on(event, listener, context?) | Subscribe to an event. | | once(event, listener, context?) | One-shot subscribe. | | off(event, listener, context?) | Unsubscribe. |

Events

| Event | Payload | When it fires | |---|---|---| | client:connect | – | WebSocket handshake completed (auth may still kick the connection right after). | | client:disconnect | Uint8Array (close-frame reason, UTF-8) | Socket closed for any reason. | | client:kicked | { code: number, reason: string } | Server closed with a 4xxx app-level code. Fires before client:disconnect. | | client:reconnecting | Uint8Array (attempt number as UTF-8 string) | Reconnect attempt scheduled; backoff is already running. | | client:reconnect_failed | – | maxAttempts exhausted, or getTicket threw. Terminal β€” no further attempts. | | <your topic> | Uint8Array | Inbound frame on a server-broadcast topic. |

The default β€” non-reconnecting β€” flow is client:connect β†’ … β†’ client:disconnect (with a client:kicked in between if the server actively closed with a 4xxx code).

Typed topics generic

Constrain user-topic listeners to a known set:

type AppTopics = 'lobby:state' | 'chat' | 'game:tick'

const ws = new WSClient<AppTopics>('ws://localhost:8080')

ws.on('chat', (payload) => { ... })           // βœ“
ws.on('typo:state', (payload) => { ... })     // type error
ws.on('client:kicked', ({ code, reason }) => { ... })  // built-in events still typed

Built-in client:* events keep their typed payload shape regardless of the generic.

πŸ” Reconnection

const ws = new WSClient(url, { reconnect: true })   // exp backoff with jitter, no attempt limit

const ws2 = new WSClient(url, {
    reconnect: { maxAttempts: 8, baseDelayMs: 250, maxDelayMs: 5000 }
})

ws2.on('client:reconnecting', (n) => {
    console.log('attempt', new TextDecoder().decode(n))
})
ws2.on('client:reconnect_failed', () => {
    console.log('gave up β€” surface a "reconnect" button to the user')
})

Reconnect skips terminal close codes. If the server kicked the connection with INVALID_TICKET, KICKED, or ROOM_REJECTED, the client treats it as terminal and won't reconnect β€” those mean the server doesn't want you back, so retrying is just noise.

πŸͺͺ Refreshing tickets across reconnects

If your tickets are short-lived (signed JWTs that expire), reconnects must fetch a fresh one. The getTicket hook is called before every reconnect attempt:

const ws = new WSClient(url, {
    reconnect: true,
    getTicket: async () => {
        const res = await fetch('/api/realtime-token', { credentials: 'include' })
        if (!res.ok) throw new Error('token endpoint failed')
        return await res.text()
    }
})

ws.connect(initialTicket)   // first call still uses its argument verbatim

If getTicket throws or rejects, the loop terminates with client:reconnect_failed (you can't reconnect without a ticket).

πŸͺ§ Putting the ticket in the subprotocol header

Production deployments should keep credentials out of URL access logs and browser history. Set ticketSource: 'protocol' on both the server (WSTransportOptions) and the client (WSClientOptions):

const ws = new WSClient('wss://api.example.com/ws', {
    ticketSource: 'protocol',
    reconnect: true
})
ws.connect(jwt)   // sent via Sec-WebSocket-Protocol, NOT ?ticket=

The ticket must conform to the WebSocket subprotocol token grammar β€” no spaces, no commas, no padding =. Standard base64url JWTs satisfy this. Empty tickets in protocol mode throw a clear error before opening the socket.

πŸ“‘ Sending and receiving structured payloads

The framework treats payload as opaque bytes. Most apps wrap a JSON encode/decode helper:

const encoder = new TextEncoder()
const decoder = new TextDecoder()

export const encode = <T>(value: T): Uint8Array => encoder.encode(JSON.stringify(value))
export const decode = <T>(payload: Uint8Array): T => JSON.parse(decoder.decode(payload)) as T

// usage
ws.send('chat', encode({ text: 'hello' }))
ws.on('chat', (payload) => console.log(decode<{ text: string }>(payload)))

For higher-throughput / lower-overhead protocols, swap in protobufjs, @bufbuild/protobuf, msgpackr, etc. β€” the wire layer doesn't care.

πŸ›‘ Close codes

CloseCode is re-exported from @rivalis/handshake (bundled into @rivalis/browser β€” no extra install).

import { CloseCode } from '@rivalis/browser'

CloseCode.INVALID_TICKET   // 4001 β€” bad / missing ticket
CloseCode.INVALID_FRAME    // 4002 β€” non-binary frame
CloseCode.KICKED           // 4003 β€” server-initiated kick
CloseCode.ROOM_REJECTED    // 4004 β€” room_full / room_not_joinable
CloseCode.RATE_LIMITED     // 4005 β€” pre-handshake connection limiter

client:kicked fires for any 4xxx code with the parsed { code, reason } so you don't have to peek into the close payload yourself.

License

MIT β€” see LICENSE.