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

@grpc-webnext/client

v0.1.1

Published

Full bidirectional gRPC client for the browser and Node — real HTTP/2 tunneled over WebSockets via h2ts, plus JSON over Fetch + WebSocket.

Readme

@grpc-webnext/client

Full bidirectional gRPC in the browser (and Node) — real HTTP/2 tunneled over WebSockets via h2ts, plus REST and JSON.

npm license

The TypeScript client for grpc-webnext — all four call types, deadlines, metadata, trailers, and cancellation, with no translation on the default path. The browser runs a real HTTP/2 stack tunneled over a WebSocket, straight into an unmodified gRPC server. Want plaintext instead? Flip one switch for JSON over Fetch + WebSocket. The API mirrors @grpc/grpc-js and Connect — a callback/EventEmitter flavor and a promise/async-iterable flavor.

Install

npm install @grpc-webnext/client

Works in the browser out of the box. In Node, pass the ws package as webSocketImpl (see Node.js below).

Quickstart

Generate a typed service definition with ts-proto (outputServices=generic-definitions), then pick a client flavor:

import { makeClient, makePromiseClient } from "@grpc-webnext/client";
import { GreeterDefinition } from "./gen/greeter.js";

// Callback / EventEmitter — mirrors @grpc/grpc-js
const cb = makeClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
cb.sayHello({ name: "world" }, (err, reply) => console.log(reply.message));

// Promise / async-iterable — mirrors Connect / nice-grpc
const rpc = makePromiseClient(GreeterDefinition, { baseUrl: "https://api.example.com" });

const reply = await rpc.sayHello({ name: "world" });          // unary → Promise
for await (const t of rpc.countdown({ from: 3 })) log(t);     // server-stream → AsyncIterable
const sum = await rpc.concat(source);                         // client-stream → Promise
for await (const m of rpc.chat(source, { signal })) log(m);   // bidi, cancel via AbortSignal

Both flavors share one transport and every gRPC feature — grpc-timeout deadlines, metadata (ASCII + -bin), canonical status codes, and cancellation via AbortSignal.

Transports & codecs

The transport is a per-client config { codec, unary, streaming }. The default is real gRPC over h2ts; a single switch drops to plaintext JSON you can read in the browser's Network tab.

| Client options | Unary | Streaming | On the wire | |---|---|---|---| | {}  (default) | h2ts | h2ts | Real HTTP/2 over one WebSocket — multiplexed, unmodified gRPC | | { unary: "fetch" } | Fetch | h2ts | Fetch unary + multiplexed HTTP/2 streams | | { streaming: "ws" } | Fetch | one WS / stream | Custom Frame protocol — binary | | { codec: "json" } | Fetch | one WS / stream | Custom Frame protocol — plaintext JSON |

// Plaintext JSON — debuggable in the Network tab
const json = makePromiseClient(GreeterDefinition, { baseUrl, codec: "json" });

codec: "json" is locked to { unary: "fetch", streaming: "ws" } — JSON stays plaintext and never rides h2ts.

REST (google.api.http) URLs

If your server annotates methods with google.api.http, those URLs are not part of this client, deliberately: an annotated URL exists so that a caller needs no SDK, and this client already reaches every method with JSON over the base endpoints. Call one directly — the generated types still give you a typed result:

const res = await fetch(`${baseUrl}/v1/things/${encodeURIComponent(id)}`);
const thing = Thing.fromJSON(await res.json());   // ts-proto's decoder

Worth doing when the annotated route buys something the base endpoints cannot — a GET the browser or a CDN will cache, or an API gateway that only admits documented paths.

Node.js

Browsers provide fetch and WebSocket globally. In Node, supply a WebSocket implementation (and, on older runtimes, fetch):

import { makePromiseClient } from "@grpc-webnext/client";
import WebSocket from "ws";

const rpc = makePromiseClient(GreeterDefinition, {
  baseUrl: "http://localhost:8080",
  webSocketImpl: WebSocket as unknown as typeof globalThis.WebSocket,
});

Options

interface ClientOptions {
  baseUrl: string;                    // "http://localhost:8080" or "https://api.example.com"
  codec?: "proto" | "json";           // message codec (default "proto")
  unary?: "h2ts" | "fetch";           // unary transport
  streaming?: "h2ts" | "ws";          // streaming transport
  maxMessageBytes?: number;           // inbound message size limit
  readableHighWaterMark?: number;     // response messages queued before pushing back (default 32)
  retry?: RetryPolicy;                // off unless set (see below)
  retryThrottling?: RetryThrottling;  // token bucket shared by every call
  webSocketImpl?: typeof WebSocket;   // Node: pass the `ws` package
  fetch?: typeof fetch;               // override the fetch implementation
}

Connectivity

The default transport keeps one HTTP/2 tunnel, and its state is observable — gRPC's connectivity states, the same ones the Rust WASM client reports:

import { ConnectivityState } from "@grpc-webnext/client";

client.getConnectivityState();          // IDLE | CONNECTING | READY | TRANSIENT_FAILURE
const stop = client.watchConnectivityState((state) => {
  setOffline(state === ConnectivityState.TRANSIENT_FAILURE);
});
stop();                                  // unsubscribe

Every callback is a real transition — repeats are collapsed — and IDLE fires on the disconnect, not just before the redial, so a reconnect is distinguishable from a first connect.

getConnectivityState() returns undefined on streaming: "ws" and codec: "json", and that is the honest answer rather than a gap: Fetch is stateless and the custom Frame path opens one WebSocket per stream, so there is no channel that could be "down" between calls. Read undefined as "no banner to show".

Reconnecting and retries

A dropped connection heals itself. Every call on the default transport rides one HTTP/2 tunnel; if it dies — a blip, a load balancer recycling, a server restart — the next call redials, and a call that was handed to an already-dead tunnel is replayed automatically. It never reached the server, so replaying it cannot duplicate anything. That needs no configuration, and it is not the retry policy below.

Retries are off unless you ask, as in gRPC — a retry nobody asked for turns one failing server into a thundering herd.

const client = makePromiseClient(GreeterDefinition, {
  baseUrl: "https://api.example.com",
  retry: {
    maxAttempts: 5,               // including the first
    initialBackoffMs: 100,        // grows ×2, jittered, up to maxBackoffMs
    maxBackoffMs: 2_000,
    backoffMultiplier: 2,
    retryableStatusCodes: [Status.UNAVAILABLE],
  },
  retryThrottling: { maxTokens: 10, tokenRatio: 0.1 },  // recommended alongside retry
});

Behavior worth knowing:

  • The deadline wins. Retries stop when the call's deadline fires; they cannot extend it.
  • The server can override you. A grpc-retry-pushback-ms trailer sets the delay, and a negative one stops retries even for a retryable status.
  • Streams are retried only until their first message. After the caller has seen a message, replaying would contradict what it was shown, so the attempt is committed — gRPC's rule.
  • Client-streaming and bidi are not retried. Replaying them means buffering every message you have written for as long as a replay is possible; that unbounded buffer is the thing this client avoids.
  • Hedging is not implemented.

Consuming a response stream

A server-streaming or bidi call returns a ClientReadableStream, consumable two ways — for await, or a data listener. Pick one: attaching a data listener puts the stream in flowing mode (as in Node), and the async iterator then has nothing to read.

for await (const reply of client.subscribe({ topic: "prices" })) {
  await handle(reply);           // slow work here pushes back on the server (see below)
}

Backpressure. On the default h2ts transport it is real: a consumer that stops reading stops HTTP/2 window replenishment, so the server blocks instead of the tab filling with messages. Slow work inside a for await body is all it takes.

On streaming: "ws" it cannot be. The browser WebSocket API has no way to stop receiving — no pause(), no unread side, onmessage fires regardless — so a consumer that falls behind queues messages client-side, exactly as any browser WebSocket library does. Two levers:

stream.readableLength;   // messages received but not yet consumed — watch this
stream.pause();          // stop delivering (they keep queueing)
stream.resume();         // flush the backlog, in order

If you need the server throttled for you, use the default transport — that is what it is for.

Server side

This package is the client. To serve grpc-webnext, wrap a native gRPC server in-process or run the schema-agnostic proxy — see the main repository. The endpoint speaks grpc-webnext and native application/grpc on the same port, so native gRPC clients pass through untouched.

Links

  • Repository: https://github.com/debdattabasu/grpc-webnext
  • Protocol spec (normative): spec/PROTOCOL.md
  • h2ts (HTTP/2 over WebSocket): https://github.com/debdattabasu/h2ts

License

Dual-licensed under either Apache-2.0 or MIT, at your option.