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

@mongrov/collab

v0.2.2

Published

Collaboration adapter for real-time messaging — supports RocketChat DDP and custom backends

Readme

@mongrov/collab

Collaboration adapter for real-time messaging in @mongrov apps.

Features

  • Adapter Pattern — Swap between backends (RocketChat, custom WebSocket, etc.)
  • XState v5 State Machine — Robust connection management with auto-reconnect
  • React Integration — Provider and hooks for easy component access
  • Real-time Updates — Messages, typing indicators, presence

Installation

pnpm add @mongrov/collab @mongrov/types xstate

Quick Start

1. Create an Adapter

Extend BaseAdapter or RocketChatAdapter with your existing client:

import { BaseAdapter } from '@mongrov/collab'
import type { AdapterCredentials, SendMessageParams } from '@mongrov/collab'

class MyAdapter extends BaseAdapter {
  readonly id = 'my-adapter'

  private client: MyExistingClient

  constructor(client: MyExistingClient) {
    super()
    this.client = client
  }

  async connect(credentials: AdapterCredentials) {
    await this.client.connect(credentials.token)
    this.setStatus('connected')
    this.emit('connection:connected', undefined)
  }

  async disconnect() {
    await this.client.disconnect()
    this.setStatus('disconnected')
    this.emit('connection:disconnected', {})
  }

  async sendMessage(params: SendMessageParams) {
    const result = await this.client.send(params)
    return { messageId: result.id, message: result }
  }

  // ... implement other methods
}

2. Set Up Provider

import { CollabProvider } from '@mongrov/collab'

function App() {
  const adapter = useMemo(() => new MyAdapter(existingClient), [])

  return (
    <CollabProvider config={{ adapter, autoConnect: false }}>
      <ChatApp />
    </CollabProvider>
  )
}

3. Use Hooks

import { useCollab, useMessages, useTyping } from '@mongrov/collab'

function ChatScreen({ conversationId }) {
  const { status, isConnected, connect, sendMessage } = useCollab()
  const { messages, isLoading, loadMore } = useMessages(conversationId)
  const { typingUsers, sendTyping } = useTyping(conversationId)

  // Connect on mount
  useEffect(() => {
    if (!isConnected) {
      connect({ serverUrl: 'wss://chat.example.com', token: authToken })
    }
  }, [])

  const handleSend = async (text: string) => {
    await sendMessage({
      conversationId,
      content: { type: 'text', text },
    })
  }

  return (
    <View>
      <MessageList messages={messages} onEndReached={loadMore} />
      {typingUsers.length > 0 && <TypingIndicator users={typingUsers} />}
      <MessageInput
        onSend={handleSend}
        onTyping={(isTyping) => sendTyping(isTyping)}
      />
    </View>
  )
}

API Reference

CollabProvider

<CollabProvider config={config}>
  {children}
</CollabProvider>

Config options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | adapter | CollabAdapter | required | The adapter instance | | autoConnect | boolean | false | Connect automatically on mount | | reconnect.enabled | boolean | true | Enable auto-reconnect | | reconnect.maxAttempts | number | 10 | Max reconnection attempts | | reconnect.baseDelay | number | 1000 | Base delay (ms) | | reconnect.maxDelay | number | 30000 | Max delay (ms) |

useCollab()

const {
  status,           // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
  isConnected,      // boolean
  isConnecting,     // boolean
  error,            // Error | null
  connect,          // (credentials) => void
  disconnect,       // () => void
  retry,            // () => void
  sendMessage,      // (params) => Promise<SendMessageResult>
  sendTyping,       // (conversationId, isTyping) => Promise<void>
  setPresence,      // (status) => Promise<void>
  addReaction,      // (messageId, emoji) => Promise<void>
  removeReaction,   // (messageId, emoji) => Promise<void>
  editMessage,      // (messageId, content) => Promise<void>
  deleteMessage,    // (messageId) => Promise<void>
  markAsRead,       // (conversationId, messageId?) => Promise<void>
  on,               // (event, handler) => Unsubscribe
  adapter,          // CollabAdapter
} = useCollab()

useMessages(conversationId)

const {
  messages,    // Message[]
  isLoading,   // boolean
  error,       // Error | null
  hasMore,     // boolean
  loadMore,    // () => Promise<void>
  refresh,     // () => Promise<void>
} = useMessages(conversationId)

useTyping(conversationId)

const {
  typingUsers,  // TypingUser[]
  sendTyping,   // (isTyping: boolean) => void
} = useTyping(conversationId)

usePresence(userIds)

const {
  presence,   // Map<string, UserPresence>
  isLoading,  // boolean
} = usePresence(['user-1', 'user-2'])

Creating Custom Adapters

Extend BaseAdapter and implement all abstract methods:

import { BaseAdapter } from '@mongrov/collab'

class CustomAdapter extends BaseAdapter {
  readonly id = 'custom'

  // Required methods:
  async connect(credentials) { /* ... */ }
  async disconnect() { /* ... */ }
  async sendMessage(params) { /* ... */ }
  async sendTyping(conversationId, isTyping) { /* ... */ }
  async setPresence(status) { /* ... */ }
  async addReaction(messageId, emoji) { /* ... */ }
  async removeReaction(messageId, emoji) { /* ... */ }
  async editMessage(messageId, content) { /* ... */ }
  async deleteMessage(messageId) { /* ... */ }
  async markAsRead(conversationId, messageId?) { /* ... */ }
  async subscribeToConversation(conversationId) { /* ... */ }
  async subscribeToPresence(userIds) { /* ... */ }
  async fetchMessages(conversationId, options?) { /* ... */ }
  async fetchConversations(options?) { /* ... */ }
  async getUser(userId) { /* ... */ }
  async searchMessages(query, options?) { /* ... */ }
}

Use this.setStatus() to update connection status and this.emit() to dispatch events.

License

MIT