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

internset-messaging-platform-sdk

v0.15.0

Published

TypeScript client SDK for the Messaging Platform — REST + WebSocket wrapper with auto-reconnect, auto-resync, and message dedupe.

Readme

internset-messaging-platform-sdk

TypeScript client SDK for the Messaging Platform — a thin, batteries-included wrapper over its REST API and realtime WebSocket. Handles auth token refresh, auto-reconnect, gap resync after sleep/offline, message dedupe, offline queueing, and attachment URL renewal so your app doesn't have to.

Works in the browser and in Node (WebSocket realtime requires a socket.io v4 compatible environment).

Install

npm install internset-messaging-platform-sdk

Quick start

Your backend exchanges its API key for short-lived user tokens (server-side, POST /v1/auth/user-tokens with X-Api-Key). The browser only ever sees user tokens — never the API key.

import { MessagingClient } from 'internset-messaging-platform-sdk';

const client = new MessagingClient({
  baseUrl: 'https://api.yourapp.com/chat', // platform origin, or a reverse-proxy path
  getToken: async () => {
    const res = await fetch('/api/messaging/token'); // your backend endpoint
    const { token } = await res.json();
    return token;
  },
  lifecycle: true, // reconnect + resync on tab focus / network regain
});

await client.connect();

client.on('message:created', (message) => {
  console.log('new message', message);
});

const conversation = await client.createConversation({
  type: 'direct',
  memberIds: [otherMessagingUserId],
});
await client.sendMessage(conversation.id, {
  content: { text: 'Hello!' },
});

baseUrl may include a path prefix (e.g. the platform hidden behind https://api.yourapp.com/chat) — the SDK derives the WebSocket engine path from it automatically. Override with wsUrl / wsPath if your proxy layout is unusual.

Client options

| Option | Default | Purpose | | --- | --- | --- | | baseUrl | — | Platform base URL (origin or origin + path prefix) | | getToken | — | Returns a user token; called on connect and re-auth | | refreshToken | — | Returns a fresh token (bypass caches); on a 401 the SDK calls it and retries the failed REST call / socket connect once | | lifecycle | off | true (or options) wakes the client on focus/online | | queueOfflineSends | true | Queue sends/edits/deletes/reactions made offline | | dedupeCacheSize | 10000 | Seen-message-ID cache size | | persistDedupe | browser: true | Persist the dedupe cache in storage | | wakeMaxAttempts | 3 | Reconnect attempts per wake | | onBeforeWake | — | Hook to clear cached tokens before a reconnect | | wsUrl / wsPath | derived | Override WebSocket origin / engine path | | attachmentUrlRenewSkewMs | 60s | Renew signed URLs this long before expiry |

Realtime events

client.on(event, handler) returns an unsubscribe function.

  • message:created, message:updated, message:deleted, message:restored
  • message:reaction — reactions / saved-by changes (metadata patch)
  • message:delivered, message:read — peer receipt cursors
  • message:queued, message:sent, message:send-failed — offline outbox lifecycle
  • operation:queued, operation:failed — offline edit/delete/react/save lifecycle
  • typing:started, typing:stopped, presence:changed
  • conversation:created, conversation:updated, member:changed
  • connected, disconnected, connection:failed, sync:completed, error

Offline behaviour

With queueOfflineSends (default on):

  • sendMessage while offline returns a pending message (metadata.pending === true, id prefixed pending:) and emits message:queued. On reconnect the queue flushes in order — each item emits message:sent (with the real message) or message:send-failed.
  • editMessage / deleteMessage / reactToMessage / toggleSaveMessage while offline throw OfflineQueuedError (catch it to keep your optimistic UI) and replay after reconnect.
  • client.getQueuedMessages(conversationId?) returns pending messages so a thread can render unsent bubbles after a reload; client.queuedSendCount is the queue size.
  • The queue is persisted in localStorage, so it survives page reloads, tab closes, and browser restarts; entries older than 24h are dropped. Flushing the same item from two tabs is safe — the platform is idempotent per clientMessageId.
  • Failed sends can be retried by calling sendMessage again with the same clientMessageId — the platform is idempotent per (conversation, sender, clientMessageId).

After sleep or connection loss the client resyncs automatically: it asks the platform for everything after its last-known sequence numbers and replays the gap through message:created, then emits sync:completed.

In-app notifications

The platform doubles as a generic notification service — usable together with messaging or entirely on its own. Your backend sends; the browser lists and listens:

// Backend (API key) — recipients are YOUR user ids; unknown ones are provisioned
await server.sendNotification({
  recipients: [userId],
  type: 'ORDER_SHIPPED',            // free string — you define your types
  title: 'Your order has shipped',
  body: 'Arriving Thursday.',
  href: '/orders/123',              // your app's route, navigated on click
  metadata: { orderId: '123' },
});

// Browser (user token)
const page = await client.getNotifications({ limit: 20 });        // CursorPage
const { count } = await client.getUnreadNotificationCount();
await client.markNotificationRead(id);
await client.markAllNotificationsRead();
await client.deleteNotification(id);
const n = await client.getNotificationByMetadata('orderId', '123'); // or null

client.on('notification:created', (notification) => { /* toast + badge */ });
client.on('notification:sync', ({ unreadCount }) => { /* badge correction */ });

Reliability matches messages: stored in the platform DB, delivered live over the same socket, and after every reconnect (laptop wake, network regain) the SDK automatically emits a notification:sync with the authoritative unread count — no refresh needed. React: useUnreadNotificationCount(client). Withdrawing server-side: server.deleteNotificationsByMetadata({ type?, metadataKey, metadataValue }).

Inbox controller

InboxController maintains a live conversation list (ordering, unread counts, last-message previews, presence) on top of a client — subscribe once and render its snapshots.

import { InboxController } from 'internset-messaging-platform-sdk';

const inbox = new InboxController(client);
inbox.attach(); // wires client events
const unsubscribe = inbox.subscribe((snapshot) => {
  render(snapshot.conversations, snapshot.lastMessagePreview);
});
await inbox.refreshInbox();

Concurrent refreshInbox calls are coalesced; non-forced refreshes are throttled (default 60s, inboxRefreshMinIntervalMs).

Server-side usage (Node / Next.js)

Both clients work server-side on Node >= 18 (global fetch).

User scope — the regular MessagingClient works in route handlers, server actions, and server components exactly like in the browser (browser conveniences like the offline queue and tab-lifecycle wake simply switch off). Use it with a user token to prefetch conversations for SSR. Calling REST methods does not require connect() — the WebSocket is only opened when you ask for it.

API-key scopeMessagingServerClient is for the things only a server may do, starting with minting user tokens. It refuses to run in a browser.

// app/api/messaging/token/route.ts (Next.js route handler)
import { MessagingServerClient } from 'internset-messaging-platform-sdk/server';

const server = new MessagingServerClient({
  baseUrl: process.env.MESSAGING_PLATFORM_URL!, // e.g. http://127.0.0.1:6100
  apiKey: process.env.MESSAGING_PLATFORM_API_KEY!,
});

export async function GET() {
  const me = await getAuthenticatedUser(); // your own auth
  const result = await server.issueUserToken({
    externalUserId: me.id,
    displayName: me.name,
    avatarUrl: me.avatarUrl,
  });
  return Response.json(result); // { token, expiresIn, user }
}

Also available: upsertUser, getUserByExternalId, and a generic request(method, path, body) escape hatch for the rest of the API-key surface (e.g. /v1/admin/* moderation endpoints). Keep the API key in server env only — never expose it with a NEXT_PUBLIC_ prefix.

React

import {
  useInbox,
  useMessagingConnection,
  useMessagingEvent,
  useTotalUnread,
} from 'internset-messaging-platform-sdk/react';

function Badge({ controller }) {
  const unread = useTotalUnread(controller);
  return unread > 0 ? <span>{unread}</span> : null;
}

Requires react >= 18 (optional peer dependency).

Attachments

One attachment per message, uploaded straight from the browser to storage via a presigned URL — the file never passes through your servers.

const attachment = await client.uploadAttachment(conversationId, file);
await client.sendAttachmentMessage(conversationId, attachment);

// Rendering: signed download URLs are cached and renewed automatically.
const url = await client.getAttachmentUrl({
  objectKey: attachment.objectKey,
  fileName: attachment.fileName,
});

Validation helpers: validateAttachment(file), ALLOWED_ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BYTES, ATTACHMENT_ACCEPT.

Messages

await client.editMessage(messageId, { text: 'edited' });   // within the modify window
await client.deleteMessage(messageId);                      // soft delete
await client.reactToMessage(messageId, '👍');               // toggles per user
await client.toggleSaveMessage(messageId);                  // bookmark
await client.markAsRead(conversationId, upToSeq);
const results = await client.searchMessages('hello', { conversationId });
await client.reportMessage(messageId, { reason: 'harassment' });

Contract helpers exported for UI logic: canEditMessage, canModifyMessage (15-minute window), getMessageReactions, isMessageSavedByUser, getMessageReceiptStatus, sortThreadMessages, maxMessageSeq, REPORT_REASONS, and more — see dist/index.d.ts for the full surface.

Errors

  • ApiError — the platform rejected the request (has status and code).
  • OfflineQueuedError — the request never reached the platform but was queued for replay (err.queued === true).
  • formatMessagingError(err) — human-readable message for either.