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

@alexify/wrpc

v1.0.0

Published

Fast, zero-dependency, WebSocket-based RPC protocol for Node.js and browsers: one router over WebSocket, HTTP, SSE and REST - typed client, subscriptions, rooms, cluster and binary streams

Readme

npm CI node dependencies docs license

A fast, zero-dependency WebSocket-based RPC protocol for Node.js and browsers. Router and procedures, subscriptions that resume, rooms that scale across processes, and binary streams with backpressure that reaches all the way into TCP — under 15 KB min+gzip in a browser bundle, and nothing at all in your lockfile.

const { Server, WrpcClient, defineRouter, procedure } = require('@alexify/wrpc');

const router = defineRouter({
  greeting: {
    hello: procedure({
      access: 'public',
      handler: async (context, { name }) => `Hello, ${name}`,
    }),
  },
});

const server = new Server({ router, host: '127.0.0.1', port: 8000, protocol: 'http' });
await server.listen();

const client = await WrpcClient.connect('ws://127.0.0.1:8000/api');
await client.load('greeting');
await client.api.greeting.hello({ name: 'World' }); // → 'Hello, World'

📖 Full documentation · Getting started · Wire protocol

Why zero dependencies?

Nothing extra in the process handling your RPC traffic. package.json has no dependencies field at all — and no peerDependencies or optionalDependencies either, because those are dependencies with extra steps. The WebSocket engine is written from scratch (RFC 6455 + 7692), and the handful of utilities borrowed from elsewhere were copied in and adapted rather than installed.

Optional integrations are injected, not depended on. uWebSockets.js, Redis, fastify, express and TanStack Query all work — you require them and hand them over, and wrpc duck-types the injection at the boundary. So the integrations you do not use cost you nothing, and the ones you do use are on your version, not ours.

One protocol, both sides. The browser build contains zero Node builtins, so the client that talks to your server is the same implementation, not a reimplementation.

Positioning

| | wrpc | tRPC | Socket.IO | | --- | --- | --- | --- | | Transport | WebSocket, HTTP, SSE, worker port | HTTP, WebSocket | WebSocket + long-poll fallback | | Types | contract-first + codegen, no build step | TypeScript inference | none built in | | Realtime | subscriptions, events, rooms, acks | subscriptions | events, rooms, acks | | Binary | streams with backpressure | ❌ | ❌ (messages only) | | Scaling | rooms backplane (any pub/sub) | your own | adapters | | Cluster ops | fetchClients, commands, presence (local read) | your own | fetchSockets (round-trip) | | Runtime deps | 0 | a few | several | | Needs TypeScript | ❌ | effectively yes | ❌ |

When NOT to use wrpc

  • You want a public HTTP API. wrpc has a REST mode, but it is a convenience for reaching procedures, not an API design. If third parties consume it, write an HTTP API and document it.
  • Your stack is polyglot. The protocol is documented and small enough to reimplement, but the only implementation today is JavaScript. gRPC exists for a reason.
  • You need guaranteed delivery. Rooms are at-most-once fan-out, not a queue. Subscriptions with tracked() values and an event log let a client catch up; if you need a broker's guarantees, use a broker.
  • You want end-to-end type inference from server code. tRPC's model — where the client's types come from the server's implementation with nothing written twice — is genuinely nicer if your whole stack is TypeScript. wrpc's contract is declared or generated, deliberately, because it must also work for JavaScript users and across a network boundary the compiler cannot see.

Table of Contents

Installation

npm install @alexify/wrpc

Requires Node.js ≥ 22 (or any modern browser through a bundler). The package is CommonJS and ships as-is — no build step and no transpile; require('@alexify/wrpc') and import { Server } from '@alexify/wrpc' both work.

Bundle size

Measured with pnpm size (esbuild, fully minified with identifier mangling, then gzipped):

| Entry | min | min+gzip | budget | | ----- | ---:| --------:| ------:| | @alexify/wrpc — browser (client, streams, chunks) | 44.3 KB | 14.9 KB | 15.0 KB | | @alexify/wrpc — node (client + server) | 158.2 KB | 52.2 KB | — | | @alexify/wrpc/ws (WebSocket engine) | 20.7 KB | 7.5 KB | — | | @alexify/wrpc/engine (engine port) | 21.2 KB | 7.7 KB | — | | @alexify/wrpc/uws (uWebSockets.js adapter) | 16.0 KB | 6.4 KB | — | | @alexify/wrpc/fastify | 135.3 KB | 46.2 KB | — | | @alexify/wrpc/express | 122.9 KB | 41.3 KB | — | | @alexify/wrpc/scaling (rooms backplane) | 4.1 KB | 1.7 KB | — | | @alexify/wrpc/sse — browser (client transport) | 47.2 KB | 15.9 KB | 16.0 KB | | @alexify/wrpc/sse — node | 86.1 KB | 28.6 KB | — | | @alexify/wrpc/query (TanStack bindings) | 2.7 KB | 1.1 KB | 2.0 KB | | @alexify/wrpc/auth (token strategies) | 3.3 KB | 1.5 KB | 2.0 KB |

The Node-only rows are reported for visibility into what each subpath pulls in — they never ship to a browser, and the adapter rows include the whole core because they bundle it. Only the browser-reachable entries carry a budget, and exceeding one fails CI: it is a ratchet against accidental bloat, raised deliberately in the same change that earns the bytes.

Injected packages (uWebSockets.js, ioredis, fastify, express, @tanstack/query-core) are never bundled — you install what you use.

Browser usage

The package ships a browser entry (browser.js), resolved automatically by any bundler that honours the package.json browser field and the browser condition in exports — webpack 5, Vite, esbuild (platform: 'browser'), Parcel, Bun. Rollup users need @rollup/plugin-node-resolve with browser: true.

It contains WrpcClient/WrpcClientProxy, the stream and chunk helpers, and zero Node builtins — the server half is not in it. The two platform-specific pairs (chunks, runtime) are swapped automatically, and scripts/size.js fails the build if any browser entry ever resolves a node: import or a package.

Quick Start

A router of procedures, a server, a client:

const { Server, WrpcClient, defineRouter, procedure, tracked } = require('@alexify/wrpc');

const router = defineRouter({
  chat: {
    // a call
    send: procedure({
      access: 'session',
      input: (args) => { if (!args?.text) throw new Error('text is required'); },
      handler: async (context, { text }) => {
        const sent = context.server.to('lobby').emit('chat/message', { text });
        return { sent };
      },
    }),
    // a subscription: many values, one connection
    onMessage: procedure.subscription({
      access: 'session',
      handler: async function* (context, args, { lastEventId, signal }) {
        for await (const message of feed({ signal })) yield tracked(message.id, message);
      },
    }),
    // an inbound, fire-and-forget event
    on: {
      typing: procedure({ handler: async (context, data) => announce(data) }),
    },
  },
  auth: {
    login: procedure({
      access: 'public',
      handler: async (context, { user }) => {
        context.client.startSession(undefined, { user });
        return { ok: true };
      },
    }),
  },
});

const server = new Server({ router, host: '127.0.0.1', port: 8000, protocol: 'http' });
await server.listen();
const client = await WrpcClient.connect('ws://127.0.0.1:8000/api');
await client.load('chat', 'auth');

await client.api.auth.login({ user: 'ada' });
client.api.chat.on('message', ({ text }) => console.log(text));
client.sendEvent('chat/typing', { who: 'ada' });

for await (const message of client.api.chat.onMessage.iterate()) {
  console.log(message);
}

Features

| Area | What you get | | ---- | ------------ | | RPC | Router and procedures with access control, Standard Schema validators, timeouts, concurrency queues and versioned units | | Realtime | Events both ways, rooms, acks (client.ask, to(room).ask), subscriptions with resume, cancellation, call batching | | Hooks | Lifecycle phases in the fastify tradition — router, unit and procedure levels flattened into one pipeline; no (ctx, next) middleware | | Streams | Binary upload/download interleaved on one connection, with end-to-end backpressure | | REST | Declarative endpoints on the same procedures — verb, path, status, fastify-shaped schemas, /vN version paths, OpenAPI via wrpc types --openapi | | Sessions & auth | Cookie-backed sessions restored on reconnect, pluggable store, CSRF-aware REST dispatch; bearer/payload token carriers, client stores and the authenticate/refresh lifecycle | | Scaling | Rooms backplane over any pub/sub; Redis and in-memory adapters included; cluster layer — replicated presence (count with no round-trip), fetchClients, cross-instance commands, node-to-node ask | | Transports | WebSocket, plain HTTP, Server-Sent Events, Service Worker MessagePort; pluggable wire codec and request metadata (x-wrpc-meta-<key>) | | Hosts | Batteries-included server, or fastify / express / uWebSockets.js / bare node:http | | Client | Exponential backoff with full jitter, app-level heartbeat, automatic re-load() and re-subscribe, offline/online | | Observability | Structured logging into your pino, OpenTelemetry spans and metrics, W3C trace context across the wire | | Operations | Security hardening, graceful drain and shutdown, benchmarks you can reproduce, testing patterns | | DX | Contract-first typed client, wrpc types codegen, TanStack Query bindings, hand-maintained .d.ts for every subpath |

Observability

Both are injected, never depended on — the package still has no dependencies. Your logger and your OpenTelemetry SDK are duck-typed.

const pino = require('pino');
const api = require('@opentelemetry/api');

const server = new Server({ router, logger: pino(), telemetry: { api } });

logger takes a structured logger (pino, bunyan, winston), a Console, or false to go silent. wrpc binds children for you — component, peer and a per-call callId — and context.log inside a handler is already scoped to that call.

telemetry takes the @opentelemetry/api module or your own { tracer, meter }. Spans follow the OTel rpc.* convention, fourteen metrics cover calls, connections, subscriptions, broadcasts and streams, and call packets carry W3C trace context so a client span parents the server's across the network hop.

Neither can break a request: a logger that throws, a broken exporter or a meter that dies is contained at the call site.

See Logging and OpenTelemetry.

Exports

| Subpath | Exports | Docs | | --- | --- | --- | | @alexify/wrpc | Server, RpcServer, WrpcClient, connect, defineRouter, procedure, tracked, createEventLog, createEventStream, MemorySessionStore, WrpcReadable, WrpcWritable, WrpcError, chunkEncode/chunkDecode | Server · Client | | @alexify/wrpc/ws | WebsocketServer, Connection, Frame, FrameParser, OPCODES, CLOSE_CODES | Wire format | | @alexify/wrpc/engine | createNodeEngine, isEngine, the Engine/WrpcSocket contracts | Engine port | | @alexify/wrpc/uws | createUwsEngine, UwsSocket | uWebSockets.js | | @alexify/wrpc/fastify | wrpcFastify, findUwsApp | Fastify | | @alexify/wrpc/express | createWrpc | Express | | @alexify/wrpc/scaling | MemoryBackplane, createRedisAdapter, isBackplane | Scaling | | @alexify/wrpc/sse | SseChannels, ServerSseTransport, ClientSseTransport, SseParser | Server-Sent Events | | @alexify/wrpc/query | createQueryUtils | TanStack Query | | @alexify/wrpc/auth | bearerAuth, memoryStore, webStorage, cookieStorage, bearerTransport, payloadTransport | Authentication | | wrpc (bin) | wrpc types <url> --out api.d.ts | Codegen CLI |

Every subpath ships hand-maintained TypeScript declarations — no generation, no any where a real type belongs.

Documentation

Contributing

See CONTRIBUTING.md for the development workflow, the house rules (zero dependencies, .d.ts and tsd in the same change) and the release checklist.

pnpm test           # node --test, recursive
pnpm test:coverage  # c8, thresholds 95/95/90/95
pnpm test:types     # tsd
pnpm lint           # oxlint
pnpm size           # bundle-size budgets
pnpm docs:dev       # the documentation site

Changelog

See CHANGELOG.md.

Security

See SECURITY.md for how to report a vulnerability.

License

MIT — see LICENSE.