chat-room

Par rivet-dev · skills

Construisez un backend de salon de chat en temps réel avec Rivet Actors : un actor par salon, historique des messages stocké dans SQLite, et diffusion WebSocket à tous les clients connectés.

npx skills add https://github.com/rivet-dev/skills --skill chat-room

Salon de Chat

IMPORTANT : Avant de faire quoi que ce soit, tu DOIS lire BASE_SKILL.md dans le répertoire de cette skill. Il contient des conseils essentiels sur le débogage, la gestion des erreurs, la gestion d'état, le déploiement et la configuration du projet. Ces règles et patterns s'appliquent à tous les travaux RivetKit. Tout ce qui suit suppose que tu as déjà lu et compris ce document.

Exemples Fonctionnels

Si tu as besoin d'une implémentation de référence, lis le code brut de l'exemple fonctionnant dans ces templates :

Patterns pour construire un backend de salon de chat avec RivetKit : acteurs scoped par salon, historique de messages persistant, et livraison en temps réel sur des connexions WebSocket.

Code de Démarrage

Commence par l'exemple fonctionnant sur GitHub et adapte-le. Le backend est un seul acteur chatRoom ; le frontend est une app React utilisant @rivetkit/react (vois le quickstart React).

Sujet Résumé
Modèle de salon Un acteur chatRoom par clé de salon. Le frontend utilise par défaut la clé general ; taper un nom de salon différent se connecte à un acteur différent.
Historique Table SQLite messages créée dans db({ onMigrate }), relue avec ORDER BY id ASC.
Livraison sendMessage insère la ligne, puis diffuse un événement typé newMessage à chaque client connecté.
Identité Aucune dans l'exemple. sender est un simple argument d'action ; la production devrait lier l'identité à la connexion.

Modèle Une Room Par Acteur

Chaque salon est une instance d'Acteur Rivet, adressée par clé. Le client appelle useActor({ name: "chatRoom", key: [roomId] }), qui crée ou récupère l'acteur pour ce salon. Cela te donne :

  • Isolation : l'historique et les connexions de chaque salon sont complètement scopés à sa clé. Changer l'entrée du salon reconfigure le hook et se connecte à un acteur différent avec un historique séparé.
  • Un seul writer sérialisé : tous les appels sendMessage pour un salon passent par un seul acteur, donc l'ordre des messages est cohérent sans verrous. L'id SQLite AUTOINCREMENT est l'ordre canonique, c'est pourquoi getHistory trie par id plutôt que par timestamp.
  • Scaling naturel : les salons se distribuent indépendamment sur le cluster. Un salon actif ne ralentit pas les autres salons.

Stockage de l'Historique des Messages

Cet exemple stocke l'historique dans la base de données SQLite de l'acteur, pas dans l'état JSON. Choisis en fonction de la taille de l'historique et des besoins de requête :

Approche À Utiliser Quand Conseils d'Implémentation
SQLite (ce qu'utilise cet exemple) Historique grand ou longue durée qui a besoin de tri, limites, pagination ou recherche Crée la table messages dans db({ onMigrate }), insère avec des requêtes paramétrées (c.db.execute("INSERT ... VALUES (?, ?, ?)", ...)), et lis avec ORDER BY id ASC. L'historique survit au sommeil de l'acteur et passe à l'échelle au-delà de ce que tu veux en mémoire.
État JSON Petit historique récent, par exemple les 50 à 100 derniers messages Ajoute à un array messages dans l'état de l'acteur et réduis à une limite à chaque envoi. Option la plus simple, mais tout l'historique vit en mémoire et il n'y a pas de couche de requête, donc cela ne convient qu'aux cas d'usage limités d'historique récent.

Livraison par Broadcast

Les nouveaux messages atteignent les clients connectés via un événement typé :

  • L'acteur déclare events: { newMessage: event() }, où Message est { sender, text, timestamp }.
  • L'action sendMessage construit le message avec un timestamp côté serveur Date.now(), l'insère dans la table messages, puis appelle c.broadcast("newMessage", message) et retourne le message à l'appelant.
  • Chaque client s'abonne avec useEvent("newMessage", ...) et ajoute à sa liste locale. L'expéditeur affiche son propre message via le même chemin de broadcast que tout le monde, donc tous les clients restent sur un seul code path.
  • Le chargement de l'historique est gate de la connexion : une fois que la connexion est prête, le client appelle getHistory() une fois pour afficher le backlog, puis s'appuie sur les événements pour tout ce qui vient après.

Utilise c.broadcast(...) pour les messages au niveau du salon. Pour les payloads privés ou par destinataire (comme les DMs dans un salon), envoie sur la connexion individuelle à la place, ce qui est une extension recommandée au-delà de cet exemple.

Indicateurs de Saisie et Présence (Extension)

L'exemple n'implémente pas les indicateurs de saisie, la présence, ou la gestion des jointures/départs de quelque sorte. Il n'y a pas de createConnState, onConnect, ou onDisconnect dans le code. Si tu en as besoin, ajoute-les comme comportement de connexion éphémère :

  • Garde-le éphémère : stocke le nom d'utilisateur et le flag de saisie dans l'état per-connexion, jamais dans SQLite ou l'état d'acteur persisté. La présence est dérivée des connexions vivantes et devrait disparaître avec elles.
  • Broadcast au changement uniquement : émet un événement de saisie quand un utilisateur commence ou arrête de taper, et un événement de présence depuis onConnect / onDisconnect, plutôt que de faire du polling ou du ticking.
  • Expire côté client : efface un indicateur de saisie après un court timeout côté client pour qu'une connexion abandonnée ne laisse jamais une ligne "is typing" bloquée.

Boîte Aux Lettres Par Utilisateur (Extension)

Pour la livraison hors ligne, les DMs, les compteurs non lus, ou la fanout de notifications, ajoute un acteur userInbox[userId] par utilisateur. C'est une extension au-delà de l'exemple :

  • L'acteur du salon fait suivre chaque message à l'acteur de la boîte aux lettres de chaque membre via des appels acteur-à-acteur, ainsi les utilisateurs qui ne sont pas connectés au salon accumulent toujours les messages.
  • L'acteur de la boîte aux lettres possède l'état non lu per-utilisateur et le sert quand l'utilisateur se connecte, indépendamment des salons dans lesquels il se trouve.
  • Les DMs deviennent une salle dégénérée : soit une chatRoom clée par la paire triée de user ids, soit une livraison directe boîte-à-boîte si tu ne veux pas de sémantique d'historique partagé.

Acteurs

  • Clé : chatRoom[roomId]
  • Responsabilité : Possède un salon de chat. Persiste l'historique des messages du salon dans sa base de données SQLite et diffuse chaque nouveau message à chaque client connecté.
  • Actions
    • sendMessage
    • getHistory
  • Files d'attente
    • Aucune
  • Événements
    • newMessage
  • État
    • SQLite
    • Table messages : id (clé primaire autoincrement), sender, text, timestamp

Cycle de Vie

sequenceDiagram
    participant A as Client A
    participant B as Client B
    participant R as chatRoom

    A->>R: connect with key [roomId]
    Note over R: every start runs onMigrate (CREATE TABLE IF NOT EXISTS messages)
    A->>R: getHistory()
    R-->>A: Message[] ordered by id
    B->>R: connect with key [roomId]
    B->>R: getHistory()
    R-->>B: Message[] ordered by id
    A->>R: sendMessage(sender, text)
    Note over R: INSERT row with server timestamp
    R-->>A: newMessage (broadcast)
    R-->>B: newMessage (broadcast)
    A->>R: disconnect
    Note over R: history stays in SQLite for the next connection

Checklist de Sécurité

L'exemple est intentionnellement minimal et saute tous les éléments suivants. Ajoute-les avant la production :

  • Auth avant la jointure : n'importe quel client peut rejoindre n'importe quel salon en connaissant son nom, et sender est une entrée arbitraire du client à chaque appel. Valide un token pendant l'auth de connexion, lie l'identité à l'état de connexion, et vérifie l'adhésion au salon avant de servir l'historique. Ne fais jamais confiance à un nom d'expéditeur passé comme argument d'action.
  • Clamps de longueur de message : l'exemple accepte les messages vides et n'a pas de limite de longueur. Réduis côté serveur, rejette le texte vide, et clamp à une longueur maximale.
  • Rate limiting par connexion : rate limit sendMessage par connexion pour arrêter le spam et l'amplification de broadcast.
  • Timestamps et ids côté serveur : l'exemple fait déjà cela correctement. timestamp provient de Date.now() à l'intérieur de l'action et id de SQLite AUTOINCREMENT. Garde-le ainsi ; n'accepte jamais les timestamps ou ids fournis par le client.
  • History caps : getHistory retourne chaque ligne sans limite. Ajoute un LIMIT plus la pagination, et élagage ou archive les anciennes lignes pour qu'un salon longue durée ne puisse pas croître indéfiniment.
  • Requêtes paramétrées : l'exemple insère déjà avec des placeholders ?. Garde tout le texte fourni par l'utilisateur hors de l'interpolation de string SQL.

Reference Map

Actors

Agent Os

Clients

Connect

Cookbook

General

Self Hosting

Skills similaires