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

@siran/chat-client

v0.1.8

Published

Package client du système de chat.

Readme

client

Package client du système de chat.

Le ChatClient est le runtime qui connecte le ChatEngine aux interfaces utilisateur via un transport temps réel.

Responsabilités

Le ChatClient est responsable de :

  • L'abonnement aux événements temps réel
  • La réception des événements depuis le transport
  • La distribution des événements aux callbacks appropriés
  • La mise en cache des conversations et messages

Important : Le ChatClient ne contient aucune règle métier. Toute la logique métier est dans le ChatEngine.

Installation

bun install

Utilisation

Exemple basique avec Supabase

Pour utiliser le ChatClient avec Supabase, vous devez installer les packages d'implémentation Supabase :

bun install @package/chat-supabase-repo @package/chat-supabase-events
import { createClient } from "@supabase/supabase-js";
import { createChatClient } from "@siran/chat-client";
import { createChatEngine } from "@siran/chat-core";
import {
  createSupabaseConversationRepository,
  createSupabaseChatMessageRepository,
} from "@package/chat-supabase-repo";
import {
  createSupabaseRealTimeEventEmitter,
  createSupabaseRealTimeEventSubscriber,
} from "@package/chat-supabase-events";

// Initialiser le client Supabase
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

// Créer les repositories Supabase
const conversationRepository = createSupabaseConversationRepository(supabase);
const messageRepository = createSupabaseChatMessageRepository(supabase);

// Créer l'event emitter Supabase (pour le serveur/backend)
const eventEmitter = createSupabaseRealTimeEventEmitter(supabase);

// Créer le ChatEngine avec les repositories Supabase
const engine = createChatEngine({
  conversationRepository,
  messageRepository,
  eventEmitter,
});

// Créer le subscriber Supabase (pour le client/frontend)
const eventSubscriber = createSupabaseRealTimeEventSubscriber(supabase);

// Créer le ChatClient avec le ChatEngine et le subscriber Supabase
const client = createChatClient({ engine, eventSubscriber });

// Définir les callbacks pour les événements avec la nouvelle API
// Les handlers reçoivent maintenant l'événement complet pour permettre l'utilisation de middlewares
client.on("message.received", (event) => {
  console.log("Nouveau message reçu:", event.payload.message);
  console.log("Conversation:", event.conversationId);
  console.log("Timestamp:", event.timestamp);
});

client.on("message.updated", (event) => {
  console.log("Message mis à jour:", event.payload.message);
});

client.on("typing", (event) => {
  console.log(`Utilisateur ${event.payload.userId} ${event.payload.isTyping ? "est en train de taper" : "a arrêté de taper"}`);
});

client.on("presence.changed", (event) => {
  console.log(`Utilisateur ${event.payload.userId} est maintenant ${event.payload.status}`);
});

// S'abonner à une conversation
const unsubscribe = client.subscribeToConversation("conv-123");

// Utiliser les méthodes déléguées du ChatEngine
const result = await client.sendMessage({
  conversationId: "conv-123",
  senderId: "user-456",
  content: "Hello!",
});

if (result.ok) {
  console.log("Message envoyé:", result.value);
}

// Plus tard, se désabonner
unsubscribe();

Exemple basique (sans Supabase)

Si vous utilisez une autre implémentation que Supabase :

import { createChatClient } from "@siran/chat-client";
import { createChatEngine } from "@siran/chat-core";
import type { RealTimeEventSubscriber } from "@siran/chat-core";

// Créer le ChatEngine d'abord
const engine = createChatEngine({
  conversationRepository: myConversationRepository,
  messageRepository: myMessageRepository,
  eventEmitter: myEventEmitter,
});

// Créer le subscriber (implémentation dans la couche infrastructure)
const eventSubscriber: RealTimeEventSubscriber = {
  subscribeToConversation: (conversationId, callback) => {
    // Implémentation de l'abonnement (WebSocket, SSE, etc.)
    return () => {
      // Fonction de désabonnement
    };
  },
  subscribeToUser: (userId, callback) => {
    // Implémentation de l'abonnement utilisateur
    return () => {
      // Fonction de désabonnement
    };
  },
};

// Créer le ChatClient avec le ChatEngine
const client = createChatClient({ engine, eventSubscriber });

Use Case : Intégration React avec Store Observable et Supabase

Voici un exemple complet d'intégration dans une application React utilisant le store observable :

// hooks/useChatClient.ts
import { useSyncExternalStore } from "react";
import { createClient } from "@supabase/supabase-js";
import { createChatClient } from "@siran/chat-client";
import { createChatEngine } from "@siran/chat-core";
import type { ChatStoreSnapshot } from "@siran/chat-client";
import {
  createSupabaseConversationRepository,
  createSupabaseChatMessageRepository,
} from "@package/chat-supabase-repo";
import {
  createSupabaseRealTimeEventEmitter,
  createSupabaseRealTimeEventSubscriber,
} from "@package/chat-supabase-events";

// Instance singleton du client (ou via Context)
let chatClient: ReturnType<typeof createChatClient> | null = null;

export function initializeChatClient(supabaseUrl: string, supabaseAnonKey: string) {
  // Initialiser Supabase
  const supabase = createClient(supabaseUrl, supabaseAnonKey);

  // Créer les repositories Supabase
  const conversationRepository = createSupabaseConversationRepository(supabase);
  const messageRepository = createSupabaseChatMessageRepository(supabase);
  const eventEmitter = createSupabaseRealTimeEventEmitter(supabase);
  const eventSubscriber = createSupabaseRealTimeEventSubscriber(supabase);

  // Créer le ChatEngine avec les repositories Supabase
  const engine = createChatEngine({
    conversationRepository,
    messageRepository,
    eventEmitter,
  });

  // Créer le ChatClient
  chatClient = createChatClient({ engine, eventSubscriber });
  return chatClient;
}

export function useChatClient() {
  if (!chatClient) {
    throw new Error("ChatClient not initialized");
  }
  return chatClient;
}

// Hook pour s'abonner au store
export function useChatStore() {
  const client = useChatClient();
  
  return useSyncExternalStore(
    (onStoreChange) => {
      // S'abonner aux changements du store
      return client.store.subscribe(onStoreChange);
    },
    () => {
      // Obtenir le snapshot actuel
      return client.store.getSnapshot();
    }
  );
}

// Hook pour accéder aux messages d'une conversation
export function useChatMessages(conversationId: string) {
  const snapshot = useChatStore();
  return snapshot.messages[conversationId] || [];
}

// Hook pour accéder aux conversations
export function useChatConversations() {
  const snapshot = useChatStore();
  return snapshot.conversations;
}

// Hook pour accéder aux utilisateurs en train de taper
export function useChatTyping(conversationId: string) {
  const snapshot = useChatStore();
  return snapshot.typing[conversationId] || [];
}

// Hook pour accéder à la présence d'un utilisateur
export function useChatPresence(userId: string) {
  const snapshot = useChatStore();
  return snapshot.presence[userId];
}
// components/ChatConversation.tsx
import { useEffect, useState } from "react";
import { useChatClient, useChatMessages, useChatTyping } from "../hooks/useChatClient";

interface ChatConversationProps {
  conversationId: string;
  currentUserId: string;
}

export function ChatConversation({ conversationId, currentUserId }: ChatConversationProps) {
  const client = useChatClient();
  const messages = useChatMessages(conversationId);
  const typingUsers = useChatTyping(conversationId);
  const [inputValue, setInputValue] = useState("");

  // S'abonner à la conversation au montage du composant
  useEffect(() => {
    const unsubscribe = client.subscribeToConversation(conversationId);
    
    // Écouter les nouveaux messages
    client.on("message.received", (event) => {
      if (event.conversationId === conversationId) {
        // Le store sera automatiquement mis à jour via le système observable
        console.log("Nouveau message:", event.payload.message);
      }
    });

    // Écouter les événements de frappe
    client.on("typing", (event) => {
      if (event.conversationId === conversationId) {
        console.log(`User ${event.payload.userId} is ${event.payload.isTyping ? "typing" : "not typing"}`);
      }
    });

    return () => {
      unsubscribe();
      // Note: Les handlers `on()` ne retournent pas de fonction de désabonnement
      // Pour une vraie implémentation, vous devriez stocker les handlers et les retirer
      // ou utiliser un système de cleanup plus sophistiqué
    };
  }, [conversationId, client]);

  const handleSendMessage = async () => {
    if (!inputValue.trim()) return;

    const result = await client.sendMessage({
      conversationId,
      senderId: currentUserId,
      content: inputValue,
    });

    if (result.ok) {
      setInputValue("");
    } else {
      console.error("Erreur lors de l'envoi:", result.error);
    }
  };

  return (
    <div className="chat-conversation">
      <div className="messages">
        {messages.map((message) => (
          <div key={message.id} className="message">
            <div className="message-content">{message.content}</div>
            <div className="message-meta">
              {new Date(message.sentAt).toLocaleTimeString()}
            </div>
          </div>
        ))}
      </div>

      {typingUsers.length > 0 && (
        <div className="typing-indicator">
          {typingUsers.map((userId) => (
            <span key={userId}>User {userId} is typing...</span>
          ))}
        </div>
      )}

      <div className="input-area">
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
          placeholder="Tapez votre message..."
        />
        <button onClick={handleSendMessage}>Envoyer</button>
      </div>
    </div>
  );
}
// components/ChatList.tsx
import { useEffect } from "react";
import { useChatClient, useChatConversations } from "../hooks/useChatClient";

export function ChatList({ currentUserId }: { currentUserId: string }) {
  const client = useChatClient();
  const conversations = useChatConversations();

  // Charger les conversations au montage
  useEffect(() => {
    client.getConversations({ userId: currentUserId }).then((result) => {
      if (result.ok) {
        // Les conversations seront automatiquement mises en cache
        // et le store sera mis à jour via le système observable
        console.log("Conversations chargées:", result.value);
      }
    });
  }, [currentUserId, client]);

  // Écouter les nouvelles conversations
  useEffect(() => {
    client.on("conversation.created", (event) => {
      console.log("Nouvelle conversation créée:", event.payload.conversation);
      // Le store sera automatiquement mis à jour
    });

    return () => {
      // Note: Pour une vraie implémentation, vous devriez stocker les handlers
      // et les retirer lors du cleanup
    };
  }, [client]);

  return (
    <div className="chat-list">
      <h2>Conversations</h2>
      {conversations.map((conversation) => (
        <div key={conversation.id} className="conversation-item">
          <h3>{conversation.name || `Conversation ${conversation.id}`}</h3>
          <p>Participants: {conversation.participantIds.length}</p>
        </div>
      ))}
    </div>
  );
}
// components/UserPresence.tsx
import { useChatPresence } from "../hooks/useChatClient";

interface UserPresenceProps {
  userId: string;
}

export function UserPresence({ userId }: UserPresenceProps) {
  const presence = useChatPresence(userId);

  if (!presence) {
    return <span className="presence unknown">Inconnu</span>;
  }

  return (
    <span className={`presence ${presence.status}`}>
      {presence.status === "online" && "🟢 En ligne"}
      {presence.status === "offline" && "⚫ Hors ligne"}
      {presence.status === "away" && "🟡 Absent"}
      {presence.status === "busy" && "🔴 Occupé"}
      {presence.lastSeenAt && (
        <span className="last-seen">
          Vu il y a {Math.floor((Date.now() - presence.lastSeenAt.getTime()) / 60000)} min
        </span>
      )}
    </span>
  );
}

API

Méthodes déléguées vers le ChatEngine

Toutes les opérations métier sont disponibles directement sur le ChatClient :

  • createConversation(input): Crée une nouvelle conversation
  • sendMessage(input): Envoie un message dans une conversation
  • editMessage(input): Édite un message existant
  • deleteMessage(input): Supprime un message (soft delete)
  • markMessageAsRead(input): Marque un message comme lu
  • getConversations(input): Récupère les conversations d'un utilisateur
  • getMessages(input): Récupère les messages d'une conversation

Méthodes d'abonnement

  • subscribeToConversation(conversationId): S'abonne aux événements d'une conversation spécifique. Retourne une fonction pour se désabonner.
  • subscribeToUser(userId): S'abonne aux événements d'un utilisateur spécifique. Retourne une fonction pour se désabonner.

Callbacks d'événements

La nouvelle API utilise une méthode générique on(type, handler) avec typage fort. Les handlers reçoivent maintenant l'événement complet pour permettre l'utilisation de middlewares :

client.on("message.received", (event) => {
  // event.type === "message.received"
  // event.conversationId === string
  // event.payload.message === ChatMessage
  // event.timestamp === Date
  console.log("Message:", event.payload.message);
});

client.on("message.updated", (event) => { ... });
client.on("message.deleted", (event) => { ... });
client.on("typing", (event) => { ... });
client.on("presence.changed", (event) => { ... });
client.on("conversation.created", (event) => { ... });
client.on("conversation.updated", (event) => { ... });

Middlewares

Le système de middlewares permet d'intercepter et de traiter les événements avant qu'ils n'atteignent les handlers. Cela permet d'ajouter des fonctionnalités comme le logging, la persistance, le replay, ou des plugins.

// Middleware de logging
client.use((event, next) => {
  console.log(`[${event.type}]`, event);
  next(); // Appeler next() pour passer au middleware suivant ou au handler
});

// Middleware de persistance
client.use(async (event, next) => {
  await saveEventToDatabase(event);
  next();
});

// Middleware de replay
const eventQueue: ChatClientEvent[] = [];
client.use((event, next) => {
  eventQueue.push(event);
  next();
});

// Middleware de filtrage (bloque certains événements)
client.use((event, next) => {
  if (event.type !== "typing") {
    next(); // Ne passer que les événements non-typing
  }
  // Si on n'appelle pas next(), l'événement est bloqué
});

Ordre d'exécution :

  1. Les middlewares sont exécutés dans l'ordre d'enregistrement
  2. Chaque middleware doit appeler next() pour continuer
  3. Si un middleware ne appelle pas next(), l'événement est bloqué
  4. Après tous les middlewares, les handlers sont appelés

Voir middlewares.example.ts pour plus d'exemples de middlewares.

Store Observable

Le store permet d'accéder aux données mises en cache et de s'abonner aux changements :

Méthodes du store

  • store.messages(conversationId): Récupère les messages d'une conversation
  • store.conversations(): Récupère toutes les conversations
  • store.typing(conversationId): Récupère les utilisateurs en train de taper dans une conversation
  • store.presence(userId): Récupère la présence d'un utilisateur

Système observable

  • store.subscribe(listener): S'abonne aux changements du store. Le listener est appelé immédiatement avec le snapshot actuel, puis à chaque changement. Retourne une fonction pour se désabonner.
  • store.getSnapshot(): Récupère un snapshot immuable de l'état actuel du store
// Exemple d'utilisation du store observable
const unsubscribe = client.store.subscribe((snapshot) => {
  console.log("Store mis à jour:", snapshot);
  // snapshot contient :
  // - conversations: Conversation[]
  // - messages: Record<string, ChatMessage[]>
  // - typing: Record<string, string[]>
  // - presence: Record<string, { status, lastSeenAt? }>
});

// Obtenir un snapshot à tout moment
const snapshot = client.store.getSnapshot();
const messages = snapshot.messages["conv-123"];

// Se désabonner
unsubscribe();

Méthodes utilitaires

  • isSubscribedToConversation(conversationId): Vérifie si une conversation est abonnée
  • isSubscribedToUser(userId): Vérifie si un utilisateur est abonné
  • getSubscribedConversations(): Liste des conversations abonnées
  • getSubscribedUsers(): Liste des utilisateurs abonnés
  • unsubscribeAll(): Se désabonne de toutes les conversations et utilisateurs

Avantages du Store Observable

Le système de store observable offre plusieurs avantages :

  1. Réactivité automatique : Les composants React se mettent à jour automatiquement lorsque les données changent
  2. Performance : Seuls les composants abonnés sont re-rendus lors des changements
  3. Snapshot immuable : getSnapshot() retourne toujours une copie des données, garantissant la cohérence
  4. Compatibilité React : Compatible avec useSyncExternalStore pour une intégration native
  5. Type-safe : TypeScript vérifie automatiquement les types du snapshot et des handlers

Architecture

Le ChatClient fait partie de la couche infrastructure du système de chat :

ChatEngine (core)     →  Logique métier (use cases, validation)
     ↓
ChatClient (client)   →  Abonnements, cache, distribution d'événements, store observable
     ↓
UI Layer              →  Interfaces utilisateur (React, React Native, etc.)

Le ChatClient utilise le RealTimeEventSubscriber (port) pour s'abonner aux événements. L'implémentation concrète du subscriber doit être fournie par la couche infrastructure (ex. WebSocket, Server-Sent Events, Supabase Realtime).

Intégration avec les packages Supabase

Le ChatClient est conçu pour fonctionner avec les packages d'implémentation Supabase :

  • @package/chat-supabase-repo : Fournit les repositories Supabase pour les conversations et messages

    • createSupabaseConversationRepository(supabaseClient) : Repository pour les conversations
    • createSupabaseChatMessageRepository(supabaseClient) : Repository pour les messages
  • @package/chat-supabase-events : Fournit les adaptateurs Supabase pour les événements temps réel

    • createSupabaseRealTimeEventEmitter(supabaseClient) : Pour émettre des événements (backend)
    • createSupabaseRealTimeEventSubscriber(supabaseClient) : Pour s'abonner aux événements (frontend)

Ces packages implémentent les ports définis dans @siran/chat-core et peuvent être utilisés directement avec le ChatEngine et le ChatClient.

Pour plus d'informations sur l'utilisation de ces packages, consultez :

Flux de données

1. Événement temps réel reçu (via Supabase Realtime) → ChatClient.handleEvent()
2. Cache mis à jour → ChatCache
3. Listeners du store notifiés → store.subscribe()
4. Composants React re-rendus → useSyncExternalStore
5. UI mise à jour automatiquement