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

@mmgt-cloud/realtime-client

v1.0.1

Published

Universal TypeScript client for the realtime service.

Downloads

173

Readme

@mmgt-cloud/realtime-client

Universal TypeScript client for the MMGT realtime service. It targets browser frontends and backend/BFF runtimes with standards-based WebSocket and fetch implementations.

The package exports:

  • RealtimeClient for authenticated browser WebSocket connections.
  • RealtimeAppClient for backend grant creation and event publishing.

Install

pnpm add @mmgt-cloud/realtime-client

No .npmrc, GitHub account, or access token is required. Browser use requires WebSocket and fetch; Node.js 22+ callers can supply a WebSocket factory. Public exports include both clients, channel helpers, cursor stores, typed errors, and protocol types. Licensed under MIT.

Browser client

import { RealtimeClient, userChannel } from "@mmgt-cloud/realtime-client";
import { AuthClient } from "@mmgt-cloud/auth-client";

const auth = new AuthClient({
  baseUrl: "https://api.mmgt.cloud/auth",
  appId: "00000000-0000-0000-0000-000000000001"
});

const realtime = new RealtimeClient({
  baseUrl: "https://api.mmgt.cloud/realtime",
  appId: "00000000-0000-0000-0000-000000000001",
  tokenProvider: async () => (await auth.getTokens())?.accessToken
});

realtime.onEvent<{ todoId: string }>("todo.updated", (event) => {
  console.log(event.id, event.payload.todoId);
});

const ready = await realtime.connect();
await realtime.subscribe(userChannel(ready.user_id));

The auth token is sent in the first WebSocket frame:

{ "type": "auth", "app_id": "<app-id>", "access_token": "<auth-access-token>" }

It is never put in the WebSocket URL.

Group channels

Group channels require a short-lived grant minted by an app backend or BFF:

await realtime.subscribe("project:alpha", {
  grant: grantFromBackend,
  presence: true
});

Client-side publishing also requires a grant with publish permission:

realtime.publish("project:alpha", "todo.updated", { todoId: "todo-1" }, { grant });

Backend/BFF client

import { RealtimeAppClient, targetChannel, targetUser } from "@mmgt-cloud/realtime-client";

const realtime = new RealtimeAppClient({
  baseUrl: "https://api.mmgt.cloud/realtime",
  appId: process.env.REALTIME_APP_ID!,
  apiKey: process.env.REALTIME_APP_API_KEY!
});

const grant = await realtime.createSubscriptionGrant({
  userId: "00000000-0000-0000-0000-000000000001",
  channels: ["project:alpha"],
  permissions: ["subscribe", "presence"],
  ttlSeconds: 300
});

await realtime.publish({
  targets: [
    targetChannel("project:alpha"),
    targetUser("00000000-0000-0000-0000-000000000001")
  ],
  eventType: "todo.updated",
  payload: { todoId: "todo-1" }
});

Never expose REALTIME_APP_API_KEY to browser code.

Backend and BFF code can also inspect current channel presence and persisted transport acknowledgement state:

const presence = await realtime.getPresence("project:alpha");
const ack = await realtime.getAckState({
  channel: "project:alpha",
  userId: "00000000-0000-0000-0000-000000000001"
});

Reconnect and replay

connect() resolves after the server's ready frame. Each attempt has a 15-second deadline covering the token provider, WebSocket handshake and ready. It rejects with connection_timeout on expiry, connection_closed if the socket closes before ready, or connection_cancelled on manual disconnect. Handle the returned promise, including when unmount/logout cancels a pending connection. Automatic reconnect uses the configured backoff after transient failures; manual disconnect cancels it. invalid_auth, invalid_token and auth_required stop handshake reconnect until an explicit new connection.

Each attempt owns its socket and callbacks. Late events and token-provider completions from a retired attempt cannot replace the current connection or send frames on it. Retiring a socket aborts a pending WebSocket handshake too. An already-running custom cursor-store operation cannot be cancelled by the client; implementations must handle concurrent writes themselves. Close and discard the client and use user-scoped storage when changing accounts.

Delivery is at-least-once. A backend publication retried after losing its response can receive a different transport event.id. Include a stable domain event or message ID and deduplicate application effects by that identity.

The browser client stores the last event ID per appId + channel in localStorage by default and sends it as resume_after when it reconnects or resubscribes. You can replace this with MemoryRealtimeCursorStore or any custom RealtimeCursorStore.

If the server emits replay_gap, the cursor is older than the bounded replay buffer. Treat that as a signal to refresh the full application state.

Presence and transport acknowledgements

Presence is per app and channel. Subscribe with presence: true and a grant that includes the presence permission to receive presence_snapshot, presence_joined, and presence_left.

realtime.onPresence("project:alpha", (users) => {
  console.log(users.map((user) => user.user_id));
});

ack(channel, eventId) persists the last acknowledged event ID server-side for the authenticated user and channel. The server confirms persisted state with ack_confirmed. This is transport progress, not proof that a person read a domain message. Domain read receipts require an authorized message and recipient.

The default ackMode is manual. Use ackMode: "auto" only when your UI is ready to acknowledge transport events after handlers run:

await realtime.subscribe("project:alpha", {
  grant,
  ackMode: "auto"
});

Future extensions

Unknown server message types are emitted as RealtimeUnknownMessage, so future messages such as richer presence diffs or read-receipt confirmations can be added without replacing the dispatcher.