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

rsocket-browser

v5.0.0

Published

Browser-first RSocket client over WebSocket or WebTransport with reconnect and Resume.

Readme

rsocket-browser

rsocket-browser is a browser-first TypeScript RSocket requester for backends that expose RSocket over WebSocket or the project's WebTransport mapping. It provides all four RSocket interaction models, Reactive Streams backpressure, typed metadata codecs, reconnect, Resume, fragmentation, and class-based route controllers.

npm install rsocket-browser

rsocket-client-ts, rsocket-core-ts, reactor-core-ts, rsocket-frames-ts, and bebyte are installed automatically. Import the client and controllers from rsocket-browser; import MIME codecs and protocol metadata helpers from rsocket-frames-ts.

The client is not tied to Spring Boot or any other server framework.

The URL selects the transport: ws: and wss: use WebSocket, while https: uses WebTransport. Reconnect is enabled by default, with browser online, focus, page restore, and sleep/wake signals helping restart interrupted connections.

Quick Start

import { RSocket } from "rsocket-browser";
import { WellKnownMimeType } from "rsocket-frames-ts";

const routing = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING;
const route = (name: string) => routing.toMetadata([name]);

const socket = new RSocket("wss://api.example.com/rsocket", {
  setup: {
    keepAlive: 20_000,
    lifetime: 90_000,
    mimetype: {
      data: WellKnownMimeType.APPLICATION_JSON,
      metadata: WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA
    }
  }
});

const connection = await socket.connect().block();
if (!connection) throw new Error("RSocket connection did not open");

const response = await connection
  .requestResponse({ id: 42 }, route("user.find"))
  .block();

console.log(response?.data);

The routing entry is automatically wrapped in composite metadata because the SETUP metadata MIME is composite. You do not need to construct a composite container for a single route.

API At A Glance

Direct interactions use one compact positional shape:

connection.fireAndForget(data?, metadata?, mimetype?);
connection.requestResponse(data?, metadata?, mimetype?);
connection.requestStream(data?, metadata?, mimetype?);
connection.requestChannel(source?, metadata?, mimetype?);
  • data is a value encoded by the data MIME, or an already encoded Payload.
  • metadata is a value encoded by the metadata MIME, or a typed Metadata.
  • mimetype optionally overrides codecs as { data, metadata } for that call.
  • source is a Publisher, Flux, iterable, or async iterable of channel items.
  • Calling requestChannel() without a source returns an imperative channel sink.

| Interaction | Result | Purpose | | --- | --- | --- | | fireAndForget | Mono<void> | Send one message without a response. | | requestResponse | Mono<PayloadFrame> | Send one request and receive at most one response. | | requestStream | Flux<PayloadFrame> | Receive a demand-controlled response stream. | | requestChannel | Flux<PayloadFrame> or channel sink | Exchange demand-controlled streams in both directions. |

connect() and every interaction are lazy. A frame is sent only after the returned Mono or Flux is subscribed, blocked, or consumed with for await ... of.

Direct Interactions

Fire And Forget

await connection
  .fireAndForget(
    { page: "/pricing", at: Date.now() },
    route("analytics.page-view")
  )
  .block();

Completion means the frame was written to the active transport. It does not mean the server processed the message.

Request Response

const response = await connection
  .requestResponse({ id: 42 }, route("user.find"))
  .block();

console.log(response?.data);
console.log(response?.metadata);

The result also exposes the decoded frame, dataPayload, and metadataPayload when lower-level protocol access is needed.

Request Stream

const users = connection.requestStream(
  { teamId: 7 },
  route("users.watch")
);

users.subscribe({
  onSubscribe(subscription) {
    subscription.request(20);
  },
  onNext(payload) {
    console.log(payload.data);
  },
  onError(error) {
    console.error(error);
  },
  onComplete() {
    console.log("stream complete");
  }
});

The initial and later request(n) calls become RSocket demand. The responder cannot deliver more items than the client has requested. Cancelling the subscription sends CANCEL.

Request Channel

Use the sink form when UI actions produce outbound messages over time:

const channel = connection.requestChannel(
  undefined,
  route("chat.messages")
);

channel.subscribe({
  onSubscribe(subscription) {
    subscription.request(50);
  },
  onNext(payload) {
    console.log("received", payload.data);
  },
  onError(error) {
    console.error(error);
  },
  onComplete() {
    console.log("channel complete");
  }
});

channel.next({ room: "general", text: "Hello" });
channel.next({ room: "general", text: "World" });
channel.complete();

Outbound items are buffered and sent only when the responder requests them. Inbound items are delivered only against local demand.

Use a publisher when the outbound source already exists:

async function* messages() {
  yield { room: "general", text: "Hello" };
  yield { room: "general", text: "World" };
}

const replies = connection.requestChannel(
  messages(),
  route("chat.messages")
);

for await (const reply of replies) {
  console.log(reply.data);
}

Setup And Types

The two constructor forms are equivalent:

new RSocket("wss://api.example.com/rsocket", options);
new RSocket({ url: "wss://api.example.com/rsocket", ...options });

The public options are intentionally small:

| Option | Meaning | | --- | --- | | setup.keepAlive | Milliseconds between requester KEEPALIVE frames. | | setup.lifetime | Maximum time without a responder KEEPALIVE before the connection is dead. | | setup.lease | Requests and enforces responder-issued LEASE credit. | | setup.fragmentSize | Maximum raw RSocket frame size before automatic fragmentation. | | setup.mimetype.data | Codec used for data payloads. | | setup.mimetype.metadata | Codec used for metadata payloads. | | setup.payload | Optional payload included in the SETUP frame. | | setup.protocols | Optional WebSocket subprotocol or ordered preference list. | | setup.transport | Optional (url) => WebSocketLike factory for WebSocket URLs. | | webTransport | Optional WebTransport factory, mapping limits, datagram mode, and media receiver. | | reconnect | Enables/disables reconnect and optionally configures Resume. | | events | Constructor-time connection lifecycle handlers. | | log | Frame and connection lifecycle logging. |

The browser WebSocket constructor is used by default. A compatible WebSocket wrapper can be supplied when the runtime needs one:

const socket = new RSocket("wss://api.example.com/rsocket", {
  setup: {
    transport: (url) => new WebSocket(url)
  }
});

WebTransport

Use an HTTPS endpoint to select the browser's global WebTransport constructor:

const socket = new RSocket("https://api.example.com/rsocket", {
  webTransport: {
    media(payload) {
      playMediaChunk(payload);
    }
  }
});

const connection = await socket.connect().block();
await connection?.media(encodedMediaChunk).block();

The versioned RSWT/1 mapping uses one bidirectional stream for connection control, one bidirectional stream per request-response, request-stream, or request-channel interaction, and a reliable unidirectional stream for fire-and-forget and metadata-push frames. Frame ordering, fragmentation, backpressure, and Resume remain automatic.

This mapping is supplied by the project; RSocket 1.0 does not define a standard WebTransport binding. The responder must use the same mapping, for example rsocket-server-ts.acceptWebTransport(session).

Complete fire-and-forget requests may opt into best-effort datagrams:

const socket = new RSocket("https://api.example.com/rsocket", {
  webTransport: {
    unreliableFireAndForget: true
  }
});

Oversized or fragmented fire-and-forget requests automatically stay on the reliable stream. A reliable marker preserves the RSocket stream sequence when a datagram is lost. Best-effort fire-and-forget cannot be combined with Resume; media datagrams are always outside RSocket Resume positions. The native constructor requests unreliable capability automatically in this mode.

Set setup.lease: true only when the responder sends LEASE frames. New interactions wait for valid credit and consume one request from the current lease.

The generic data and metadata types are inferred from typed SETUP MIME codecs. For example, TEXT_PLAIN makes direct data arguments and response data string-typed. APPLICATION_JSON intentionally supports any JSON shape, so use explicit socket generics or controllers when strict application types are required:

type ApiData =
  | { id: number }
  | { id: number; name: string }
  | { login: string; password: string }
  | { token: string };

const typedSocket = new RSocket<ApiData>("wss://api.example.com/rsocket", {
  setup: {
    mimetype: {
      data: WellKnownMimeType.APPLICATION_JSON,
      metadata: WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA
    }
  }
});

A per-call codec override gives the outbound value its own type:

connection.fireAndForget<string>(
  "ready",
  route("status.update"),
  { data: WellKnownMimeType.TEXT_PLAIN }
);

The responder must already expect that data codec for the route. RSocket does not attach a separate data MIME declaration to every request frame. Responses remain typed and decoded by the SETUP codecs. An already encoded Payload<T> can also be passed directly without changing the socket's response type.

See rsocket-frames-ts for the complete MIME, frame, payload, routing, composite metadata, and authentication codec API.

Metadata And Authentication

Routing

Create protocol metadata with a MIME codec, not with untyped strings:

import { WellKnownMimeType } from "rsocket-frames-ts";

const route = (name: string) =>
  WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.toMetadata([name]);

connection.requestResponse({ id: 42 }, route("user.find"));

Use composite metadata when one request may contain more than one metadata type, such as routing plus authentication. A fully manual composite is also accepted:

import {
  WellKnownAuthType,
  WellKnownMimeType
} from "rsocket-frames-ts";

const authMime = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION;
const compositeMime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA;

const metadata = compositeMime.toMetadata([
  route("user.find"),
  authMime.toMetadata(WellKnownAuthType.BEARER.auth("access-token"))
]);

connection.requestResponse({ id: 42 }, metadata);

Persistent Metadata

metadataUpdate changes client-side defaults merged into all later interactions. This is the simplest way to attach an access token:

const authMime = WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION;

socket.metadataUpdate((metadata) => {
  metadata.set(
    authMime,
    WellKnownAuthType.BEARER.auth("access-token")
  );
});

Use the RSocket simple authentication type when needed:

socket.metadataUpdate((metadata) => {
  metadata.set(
    authMime,
    WellKnownAuthType.SIMPLE.auth({
      username: "daniel",
      password: "secret"
    })
  );
});

Remove authentication without changing other defaults:

socket.metadataUpdate((metadata) => {
  metadata.remove(authMime);
});

Metadata is merged by MIME type:

  • Persistent authentication and a controller/request route are both retained.
  • Request metadata replaces only a persistent entry with the same MIME type.
  • A controller route therefore overrides a persistent route for that request.
  • Unrelated persistent metadata is never removed by a request override.

The SETUP metadata MIME controls what can be stored. Composite metadata accepts any MIME-keyed entries. A direct routing MIME accepts only routing entries; a direct authentication MIME accepts only authentication entries. Unsupported updates throw immediately instead of creating an invalid wire payload.

Metadata Push

metadataPush sends an RSocket METADATA_PUSH frame to the server. It does not replace the persistent client defaults managed by metadataUpdate:

await connection
  .metadataPush(
    authMime.toMetadata(
      WellKnownAuthType.BEARER.auth("refreshed-access-token")
    )
  )
  .block();

Server support for METADATA_PUSH is optional. Use metadataUpdate when the goal is only to attach metadata to subsequent client requests.

Declarative Controllers

Controllers provide endpoint-specific request and response types while keeping the direct interaction methods available. Declare a class, select its interaction model through extends, and assign a protected route.

import { RequestResponseController } from "rsocket-browser";

interface SignInRequest {
  login: string;
  password: string;
}

interface TokenResponse {
  token: string;
}

export default class SignInController extends RequestResponseController<
  SignInRequest,
  TokenResponse
> {
  protected readonly route = "account.sign-in";
}

process infers its arguments and return type from the controller:

const token = await connection
  .process(SignInController, {
    login: "[email protected]",
    password: "password"
  })
  .map((response) => response.token)
  .block();

The other interaction models follow the same declaration style:

import {
  FireAndForgetController,
  RequestChannelController,
  RequestStreamController
} from "rsocket-browser";

class DeactivateAccountController extends FireAndForgetController<{
  reason: string;
}> {
  protected readonly route = "account.deactivate";
}

class OnlineController extends RequestStreamController<
  { accountId: number },
  { accountId: number; lastOnlineAt: number; online: boolean }
> {
  protected readonly route = "account.online";
}

class ChatController extends RequestChannelController<
  { room: string; text: string },
  { id: string; room: string; text: string }
> {
  protected readonly route = "chat.messages";
}

connection.process(DeactivateAccountController, { reason: "privacy" });
connection.process(OnlineController, { accountId: 42 });
connection.process(ChatController, [
  { room: "general", text: "Hello" }
]);

Class declarations are instantiated once per socket, which reuses encoded route metadata. Pass an instance when request options, logging, or instance state are needed:

const signIn = new SignInController({ timeout: 5_000 }).log({
  payload: false
});

connection.process(signIn, {
  login: "[email protected]",
  password: "password"
});

Override data or response only when the wire shape differs from the public controller type:

class FindUserController extends RequestResponseController<number, string> {
  protected readonly route = "user.find";

  protected override data(id: number) {
    return { id };
  }

  protected override response(payload: { data?: unknown }) {
    const user = payload.data as { name: string };
    return user.name;
  }
}

Those overrides, route, and public log(...) are the complete controller extension surface. Transport, reconnect, and dispatch hooks stay internal.

Controller routes work with direct RSocket routing metadata or composite metadata. With composite metadata, a route is merged with persistent entries such as authentication.

Reconnect And Resume

Automatic reconnect is enabled by default for both browser transports. Unexpected disconnects, browser offline/online transitions, tab wakeups, and mobile sleep/wake cycles trigger a new connection attempt. Calling disconnect() stops that process.

Without Resume, reconnect opens a fresh RSocket session with SETUP. Enable protocol Resume by specifying how long the backend retains resumable state:

const socket = new RSocket("wss://api.example.com/rsocket", {
  setup: {
    keepAlive: 20_000,
    lifetime: 90_000,
    mimetype: {
      data: WellKnownMimeType.APPLICATION_JSON,
      metadata: WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA
    }
  },
  reconnect: {
    resume: {
      ttl: 5 * 60_000
    }
  }
});

The client creates and manages the Resume token and byte positions. While the TTL is valid, it waits for network/server availability and attempts RESUME. If the responder rejects Resume, or the TTL expires, the client automatically falls back to a fresh SETUP connection.

Existing streams and channels survive only when protocol Resume succeeds. A fresh session cannot recover server-side stream state, so affected interactions fail and the application must create them again. The client does not guess at application-level replay or deduplication rules.

Use lifecycle events to update frontend connection state:

const socket = new RSocket("wss://api.example.com/rsocket", {
  events: {
    disconnect(event) {
      showConnectionBanner(event.message);
    },
    reconnecting(event) {
      showConnectionBanner(event.message);
    },
    resumeRejected(event) {
      console.warn(event.message, event.error);
    },
    connected(event) {
      hideConnectionBanner();
      console.log(event.message);
    }
  }
});

Every event includes type, status, message, connected, recovering, attempt, and relevant error/retry fields.

Disable automatic reconnect explicitly when the application owns retry policy:

new RSocket("wss://api.example.com/rsocket", {
  reconnect: false
});

Connection Lifecycle

The state-specific surface prevents invalid lifecycle calls:

const socket = new RSocket("wss://api.example.com/rsocket");
const connected = await socket.connect().block();

if (connected) {
  const disconnected = connected.disconnect(1000, "Signed out");
  await disconnected.connect().block();
}
  • A disconnected socket exposes interactions, metadataPush, media, metadataUpdate, process, and connect.
  • A connected socket exposes the same request API plus disconnect, but no second connect method.
  • Requests subscribed before a connection is available wait for the current connection attempt. Calling connect() explicitly during application startup usually makes failure handling clearer.

Logging And Errors

Enable only the diagnostics needed for the current problem:

const socket = new RSocket("wss://api.example.com/rsocket", {
  log: {
    enabled: true,
    frames: true,
    lifecycle: true,
    payload: false
  }
});

Frame logging covers direct socket interactions. Higher-level interaction logging is local to one declarative controller:

const signIn = new SignInController().log({
  interactions: true,
  payload: false
});

connection.process(signIn, {
  login: "[email protected]",
  password: "password"
});

Payload logging is disabled by default because payloads may contain credentials or personal data.

RSocket ERROR frames, timeouts, transport failures, invalid demand, and codec errors are propagated through Reactor error signals. block() rejects; streaming subscribers receive onError:

try {
  await connection
    .requestResponse({ id: 42 }, route("user.find"))
    .block();
} catch (error) {
  console.error("request failed", error);
}

Server Compatibility

The responder must expose either an RSocket WebSocket endpoint or the matching RSWT/1 WebTransport endpoint and agree on the SETUP data and metadata MIME types. For Spring applications, composite metadata plus message/x.rsocket.routing.v0 is the usual choice for @MessageMapping routes. The controller route field creates that standard routing metadata.

Resume and METADATA_PUSH require server-side support. If Resume is rejected, the client reports resumeRejected and opens a fresh session. The package is a requester only and does not expose a responder/server API.

Large RSocket payloads are fragmented and incoming fragments are reassembled by the client. Backpressure, cancellation, KEEPALIVE, stream IDs, protocol errors, and Resume positions are handled at the frame layer.

Public Exports

The package root intentionally has one small runtime surface:

import {
  RSocket,
  FireAndForgetController,
  RequestResponseController,
  RequestStreamController,
  RequestChannelController
} from "rsocket-browser";

The controller implementations are shared by rsocket-client-ts, and the root re-export keeps application imports concise. Frames, MIME types, metadata, and authentication codecs remain available from the automatically installed rsocket-frames-ts package. There is no default export and no secondary package entry point.

Development

npm test
npm run build

npm test type-checks the public API and runs unit, protocol, transport, and full WebSocket and WebTransport integration tests against rsocket-server-ts. npm run build creates the ESM bundle and declarations, then rewrites internal aliases.