npm package: @siran/chat-client
Description: Package client du système de chat.
Latest version: 0.1.8

# 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

```bash
bun install
```

## Utilisation

### Exemple basique avec Supabase

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

```bash
bun install @package/chat-supabase-repo @package/chat-supabase-events
```

```typescript
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 :

```typescript
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 :

```typescript
// 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];
}
```

```typescript
// 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>
  );
}
```

```typescript
// 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>
  );
}
```

```typescript
// 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 :

```typescript
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.

```typescript
// 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

```typescript
// 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 :
- [Documentation `@package/chat-supabase-repo`](../supabase-repo/README.md)
- [Documentation `@package/chat-supabase-events`](../supabse-events/README.md)

### 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
```