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

@wemany/chat-client

v0.3.0

Published

Wemany chat client SDK — WebSocket real-time messaging, E2EE for private channels, multi-device sync. Replaces stream-chat.

Readme

@wemany/chat-client

Wemany chat client SDK — WebSocket real-time messaging + REST history + E2EE for private channels + multi-device sync. Drop-in replacement for stream-chat in any Wemany frontend (Next.js web app, Expo mobile app, browser POC, Node).

Status

v0.2 — production usable. WebSocket gateway + REST history + attachments

  • voice notes + E2EE (Olm/Megolm) + React hooks + offline queue all shipped.

Distributed only inside the Wemany monorepo / private registry — not on public npm. Requires authenticated access to install.

Install

pnpm add @wemany/chat-client socket.io-client
# react opcional, solo si usas los hooks
pnpm add react

Quickstart — connect with mode

The simplest way: pass mode: 'test' | 'live' and the SDK resolves the right Wemany cluster URLs for you.

import { createChatClient } from '@wemany/chat-client';

const chat = createChatClient({
  mode: process.env.NODE_ENV === 'production' ? 'live' : 'test',
  userId: currentUser.id,
  deviceId: 'web-chrome-1234',
  getAccessToken: async () => fetchFreshJwt(),
  onError: (err) => console.error(err),
});

await chat.connect();

const channel = chat.channel('channel-uuid');
await channel.join();

// Subscribe — re-renders whenever the channel state changes
const unsub = channel.subscribe((state) => {
  console.log('messages:', state.messages.length);
  console.log('typing:', Array.from(state.typing.values()));
});

// Send — returns clientMessageId. The optimistic placeholder is in store
// until the server confirms with `message:confirmed` or `message:new`.
const id = await channel.sendMessage('hola mundo');

| mode | WS endpoint | REST API base | |--------|-------------|---------------| | 'test' | wss://chat-staging.wemany.com | https://chat-staging.wemany.com | | 'live' | wss://chat.wemany.com | https://chat.wemany.com |

Pointing at a custom cluster

For local dev or preview environments, pass endpoint + apiBase directly. They override mode.

const chat = createChatClient({
  endpoint: 'ws://127.0.0.1:3000',
  apiBase: 'http://127.0.0.1:3000',
  userId: '...',
  getAccessToken: async () => fetchFreshJwt(),
});

Authentication — JWT only

The SDK identifies the user via the JWT returned by getAccessToken(). The same JWT that the rest of the Wemany platform issues — no separate API key, no second auth layer.

getAccessToken is called on:

  • initial connect()
  • every reconnect (so token refresh is transparent)
  • every REST call from Channel / fetchChannelMessages / etc.

If the server emits connection:reauth_required, the SDK refreshes the token and reconnects automatically.

Server-side validations (free)

Every WebSocket event the SDK emits is validated against:

  1. JWT signature + expiry (WsJwtGuard on handshake)
  2. Channel membership (channelsRepo.getMember() — must be a member)
  3. Mute state (rejects if member.isMuted)
  4. Per-user rate limit (Redis-backed)
  5. Per-channel rate limit (prevents flooding)
  6. Friendship requirement for DMs (must be friends both ways)
  7. Block / ban check (banned users can't write)
  8. Mod-only operations (pin, delete-other-message) require role

You don't need to implement any of these client-side. The server returns a shaped Ack like { ok: false, error: 'not a member of this channel' } that you can surface to the user.

React hooks

import { createChatClient } from '@wemany/chat-client';
import {
  ChatProvider,
  useChannel,
  useMessages,
  useTyping,
  usePresence,
  useAttachmentUpload,
  useVoiceRecorder,
  usePinnedMessages,
} from '@wemany/chat-client/react';

const chat = createChatClient({ mode: 'test', userId, getAccessToken });
await chat.connect();

function App() {
  return (
    <ChatProvider client={chat}>
      <ChannelView channelId="abc-123" />
    </ChatProvider>
  );
}

function ChannelView({ channelId }: { channelId: string }) {
  useChannel(channelId);
  const { messages, sendMessage, editMessage, deleteMessage } = useMessages(channelId);
  const { typingUsers, startTyping, stopTyping } = useTyping(channelId);
  const presence = usePresence(messages.map((m) => m.senderId));

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          [{presence[m.senderId]?.status ?? 'unknown'}] {m.senderId}:{' '}
          {m.content.type === 'plain' ? m.content.text : '🔒'}
          {m.optimistic?.status === 'pending' && ' (sending…)'}
        </div>
      ))}
      {typingUsers.length > 0 && (
        <em>{typingUsers.map((u) => u.name).join(', ')} escribiendo…</em>
      )}
      <input
        onChange={(e) => (e.target.value ? startTyping() : stopTyping())}
        onBlur={stopTyping}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            sendMessage((e.target as HTMLInputElement).value);
            (e.target as HTMLInputElement).value = '';
            stopTyping();
          }
        }}
      />
    </div>
  );
}

Architecture

  • createChatClient(config) — factory que devuelve el handle principal. Internamente: socket.io-client + ChatStore observable + OfflineQueue
    • opcional StorageAdapter.
  • Channel — facade per-channel devuelto por chat.channel(id). Métodos: join, leave, sendMessage, editMessage, deleteMessage, addReaction, removeReaction, markRead, startTyping, stopTyping, subscribe.
  • ChatStore — in-memory observable. Listeners por canal + presence. Maneja optimistic placeholders y reconciliación.
  • OfflineQueue — pendientes persistidos cuando socket desconectado, re-emitidos al reconnect. Usa StorageAdapter (default in-memory; pasa un adapter custom para IndexedDB / AsyncStorage / etc).
  • REST historyfetchChannelMessages({ apiBase, channelId, before, limit, getAccessToken }) paginado vía cursor. Ideal para cargar histórico al abrir un canal.
  • AttachmentsrequestUploadUrl() + uploadFileToS3() flow. El servidor firma una presigned URL S3, el cliente sube directo (no pasa por gateway).
  • Voice notesrequestVoiceUploadUrl() + uploadVoiceBlob() con límites duros (MAX_VOICE_DURATION_MS, MAX_VOICE_SIZE_BYTES, VOICE_MIME lista permitida).
  • E2EE Olm/MegolmOlmService + SessionManager + KeyRegistry para canales private. El servidor solo ve ciphertext.
  • Hooks React (opcionales)useChannel, useMessages, useTyping, usePresence, useAttachmentUpload, useVoiceRecorder, usePinnedMessages. Bindings reactivos al store.

REST history

import { fetchChannelMessages } from '@wemany/chat-client';

const { messages, nextCursor } = await fetchChannelMessages({
  apiBase: 'https://chat-staging.wemany.com',
  channelId: 'abc-123',
  limit: 50,
  before: undefined,                         // first page
  getAccessToken: () => fetchFreshJwt(),
});

// Paginate older
const older = await fetchChannelMessages({
  apiBase: 'https://chat-staging.wemany.com',
  channelId: 'abc-123',
  limit: 50,
  before: nextCursor,
  getAccessToken: () => fetchFreshJwt(),
});

When you use mode, you can read the resolved apiBase from the SDK so you don't have to hard-code:

// Coming soon: chat.endpoints — for now, mirror in the consumer.

Attachments

import { requestUploadUrl, uploadFileToS3 } from '@wemany/chat-client';

const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;

const upload = await requestUploadUrl({
  apiBase: 'https://chat-staging.wemany.com',
  channelId,
  contentType: file.type,
  size: file.size,
  filename: file.name,
  getAccessToken,
});

await uploadFileToS3({ url: upload.uploadUrl, file });

await channel.sendMessage('check this out', {
  attachments: [{
    id: upload.attachmentId,
    type: 'image',
    url: upload.publicUrl,
    name: file.name,
    size: file.size,
    mime: file.type,
  }],
});

The same flow exists for voice notes (requestVoiceUploadUrl / uploadVoiceBlob).

Offline persistence

// Web — IndexedDB via idb-keyval
import { get, set, del, keys } from 'idb-keyval';
import type { StorageAdapter } from '@wemany/chat-client';

const idbAdapter: StorageAdapter = {
  getItem: async (k) => (await get(k)) ?? null,
  setItem: async (k, v) => { await set(k, v); },
  removeItem: async (k) => { await del(k); },
  keys: async (prefix) =>
    (await keys()).filter((k): k is string => typeof k === 'string' && k.startsWith(prefix)),
};

const chat = createChatClient({ mode: 'test', userId, getAccessToken, storage: idbAdapter });
// Expo — AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';

const expoAdapter: StorageAdapter = {
  getItem: (k) => AsyncStorage.getItem(k),
  setItem: (k, v) => AsyncStorage.setItem(k, v),
  removeItem: (k) => AsyncStorage.removeItem(k),
  keys: async (prefix) => (await AsyncStorage.getAllKeys()).filter((k) => k.startsWith(prefix)),
};

Error handling

The factory accepts an onError sink. The SDK emits errors for:

  • Unhandled socket errors
  • Server-side rate-limit notifications (error:rate_limited)
  • REST call failures from Channel methods (logged then thrown)
  • E2EE setup failures
const chat = createChatClient({
  mode: 'test',
  userId,
  getAccessToken,
  onError: (err) => {
    if (err.message.startsWith('rate_limited:')) {
      // surface a toast
    } else {
      Sentry.captureException(err);
    }
  },
});

Manual POC test (2 browsers)

Ver examples/two-browser-poc.html en este package: HTML standalone que carga socket.io-client por CDN + script bundle del cliente y permite enviar mensajes entre 2 ventanas conectadas al mismo channel.

Requisitos:

  1. chat-service corriendo (pnpm --filter chat-service start:dev)
  2. Redis local en redis://localhost:6379
  3. DynamoDB Local o tablas creadas en AWS staging (ver apps/chat-service/scripts/create-chat-tables.ts)
  4. JWT válido firmado con el mismo JWT_SECRET del chat-service

Versioning

v0.x — public API may have small adjustments. v1.0.0 lands once the frontend integration is fully soaked in production. Any breaking change is documented in this README + git tag.

License

UNLICENSED — internal use only. Distribution restricted to Wemany teams and authorized contractors.