live-cursors

Par rivet-dev · skills

Curseurs en temps réel et présence multijoueur avec les Rivet Actors : état du curseur par connexion, mises à jour en temps réel via des événements ou des WebSockets bruts, et limitation du débit.

npx skills add https://github.com/rivet-dev/skills --skill live-cursors

Curseurs en direct et présence

IMPORTANT : Avant de faire quoi que ce soit, vous DEVEZ lire BASE_SKILL.md dans le répertoire de cette compétence. 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 modèles s'appliquent à tous les travaux RivetKit. Tout ce qui suit suppose que vous avez déjà lu et compris ce document.

Exemples fonctionnels

Si vous avez besoin d'une implémentation de référence, lisez le code d'exemple brut et fonctionnel dans ces modèles :

Modèles pour construire des curseurs en direct, la présence multijoueur et le partage de curseur en temps réel avec RivetKit. Un actor de salle distribue les positions des curseurs à chaque client connecté, classé par salle avec clés d'actor.

Code de démarrage

Commencez par l'une des deux variantes fonctionnelles sur GitHub. Les deux implémentent le même canevas collaboratif de curseur avec étiquettes de texte persistantes ; elles diffèrent uniquement dans le transport.

Variante Code de démarrage Transport Stockage de présence
cursors GitHub actions et événements typés sur la connexion RivetKit connState par connexion
cursors-raw-websocket GitHub gestionnaire onWebSocket brut avec un protocole de message JSON personnalisé Carte de socket dans createVars

Utilisez cursors par défaut : les actions typées, les événements typés et le suivi automatique des connexions couvrent la plupart des applications avec moins de code. Utilisez cursors-raw-websocket quand vous avez besoin d'un contrôle total du format sur le câble, par exemple un protocole JSON ou binaire personnalisé, ou des clients qui n'utilisent pas la bibliothèque client RivetKit.

État de connexion vs État persistant

La présence est éphémère par définition. Une position de curseur n'a de sens que tant que sa connexion est active, elle appartient donc au stockage par connexion, pas à l'état persistant de l'actor. L'état persistant est réservé aux données qui doivent survivre aux déconnexions et redémarrages d'actor.

Données Où elles résident Pourquoi
Position du curseur connState (cursors) ou la carte de socket createVars (cursors-raw-websocket) Limité à une connexion et supprimé avec elle. La présence obsolète ne peut pas s'accumuler dans le stockage.
Étiquettes de texte (textLabels) État persistant de l'actor dans les deux variantes Le contenu du canevas doit survivre aux déconnexions et redémarrages d'actor.

Dans la variante cursors, updateCursor écrit c.conn.state.cursor et getRoomState reconstruit la snapshot de présence en itérant c.conns.values(), donc la carte de curseur est toujours dérivée des connexions actives plutôt que stockée. Voir Connections pour connState et State pour la sémantique de persistance.

Cycle de vie de la présence

  • Rejoindre : La variante cursors-raw-websocket pousse un message init avec la snapshot actuelle { cursors, textLabels } dès qu'un socket se connecte. La variante cursors n'a pas de broadcast d'adhésion explicite ; le client appelle l'action getRoomState une fois après connexion pour amorcer ses cartes locales, et les pairs voient d'abord un nouvel utilisateur sur le premier broadcast cursorMoved de cet utilisateur.
  • Déplacement : Chaque appel updateCursor écrit l'entrée de présence de la connexion, puis diffuse cursorMoved à toutes les connexions, y compris l'expéditeur.
  • Quitter : La variante cursors gère la déconnexion dans onDisconnect, diffusant cursorRemoved avec le dernier curseur de la connexion. La variante brute fait de même à partir de l'écouteur de close du socket, puis supprime la session de la carte vars.websockets. Les clients suppriment cet utilisateur de leur carte de curseur locale, donc les curseurs obsolètes disparaissent au moment où un onglet se ferme.

Voir Lifecycle pour onDisconnect et createVars.

Limitation de la mise à jour

Aucun des deux exemples ne limite. Les deux interfaces utilisateur envoient une mise à jour du curseur sur chaque événement brut mousemove sans débounce ou plafond d'intervalle. C'est correct pour une démo, mais une souris rapide sur un affichage haute fréquence peut émettre des centaines d'événements par seconde par utilisateur. Les modèles ci-dessous sont les durcissements de production recommandés au-dessus du code de démarrage, et non quelque chose que les exemples implémentent.

Couche Modèle Conseils
Client (fluidité) Limitation à 20-30 Hz Échantillonnez la dernière position du pointeur toutes les 33-50 ms et envoyez uniquement cela. Supprimez les mouvements intermédiaires, mais toujours videz la position finale afin que les curseurs se stabilisent à l'emplacement réel. Interpolez entre les positions reçues du côté du rendu.
Serveur (application) Limite de débit par connexion Suivez le dernier timestamp de mise à jour accepté par connexion et supprimez ou fusionnez les mises à jour arrivant plus rapidement que votre plafond. Les limitations du client sont coopératives ; l'actor est la limite d'application.

Actors

  • Clé : cursorRoom[roomId] (le frontend par défaut roomId à "general")

  • Responsabilité : Conserve la présence du curseur par connexion dans connState, persiste les étiquettes de texte partagées dans l'état de l'actor, et diffuse les mises à jour de curseur et de texte à toutes les connexions.

  • Actions

    • updateCursor
    • updateText
    • removeText
    • getRoomState
  • Événements

    • cursorMoved
    • cursorRemoved
    • textUpdated
    • textRemoved
  • Files d'attente

    • Aucune
  • État

    • JSON
    • textLabels (persistant)
    • connState.cursor par connexion (éphémère)
  • Clé : cursorRoom[roomId] (résolu via client.cursorRoom.getOrCreate(roomId))

  • Responsabilité : Expose un endpoint WebSocket brut, suit les sockets actifs et leurs curseurs dans une carte createVars classée par un paramètre de requête sessionId, persiste les étiquettes de texte, et distribue manuellement les trames JSON à chaque socket.

  • Actions

    • getOrCreate (stub renvoyant { status: "ok" } ; le frontend résout l'ID de l'actor avec la méthode getOrCreate(roomId).resolve() du handle client, qui crée l'actor sans dispatcher cette action)
    • getRoomState
  • Files d'attente

    • Aucune
  • État

    • JSON
    • textLabels (persistant)
    • Carte vars.websockets de sessionId à socket et curseur (en mémoire, perdue au redémarrage)

La variante brute ne définit aucun événement RivetKit. Ses noms de message sont des champs type sur les trames JSON brutes :

Direction Message type Charge utile
Client vers serveur updateCursor { userId, x, y }
Client vers serveur updateText { id, userId, text, x, y }
Client vers serveur removeText { id }
Serveur vers client init Snapshot { cursors, textLabels } à la connexion
Serveur vers client cursorMoved, textUpdated, textRemoved, cursorRemoved La charge utile correspondante de curseur, étiquette ou ID

Cycle de vie

cursors (Actions + Événements)

sequenceDiagram
    participant A as Client A
    participant R as cursorRoom
    participant B as Other Clients

    A->>R: connect via useActor (cursorRoom[roomId])
    A->>R: getRoomState()
    R-->>A: {cursors, textLabels}
    loop every mouse move
        A->>R: updateCursor(userId, x, y)
        Note over R: write c.conn.state.cursor
        R-->>B: cursorMoved (broadcast)
    end
    A->>R: updateText(id, userId, text, x, y)
    Note over R: upsert persistent state.textLabels
    R-->>B: textUpdated (broadcast)
    Note over A: tab closes
    Note over R: onDisconnect reads conn.state.cursor
    R-->>B: cursorRemoved (broadcast)

cursors-raw-websocket

sequenceDiagram
    participant A as Client A
    participant R as cursorRoom
    participant B as Other Clients

    A->>R: getOrCreate(roomId).resolve()
    R-->>A: actorId
    A->>R: open WebSocket /gateway/{actorId}/websocket?sessionId=...
    Note over R: close 1008 if sessionId is missing
    Note over R: store socket in vars.websockets
    R-->>A: init {cursors, textLabels}
    loop every mouse move
        A->>R: {type: "updateCursor"} frame
        Note over R: update session cursor in vars
        R-->>B: cursorMoved frame
    end
    Note over A: socket closes
    R-->>B: cursorRemoved frame
    Note over R: delete session from vars.websockets

Liste de contrôle de sécurité

Les deux exemples sont livrés sans authentification afin que le modèle de présence reste lisible. Tout ce qui suit est le durcissement recommandé pour la production, et non le comportement que les exemples implémentent.

  • Identité : Liez l'identité de présence à la connexion (c.conn.id dans la variante actions, un ID de session généré par le serveur dans la variante brute). Ne faites jamais confiance à un userId fourni par le client ; dans les exemples, c'est une chaîne générée aléatoirement par le client, donc n'importe quel client peut usurper l'identité d'un curseur ou en supprimer un.
  • Autorisation : Autorisez les mutations d'étiquette par propriétaire. Dans les exemples, updateText accepte des arguments arbitraires id et userId et removeText accepte un id arbitraire, donc n'importe quel client peut modifier ou supprimer n'importe quelle étiquette.
  • Validation des entrées : Limitez x et y aux limites du canevas, limitez la longueur de l'étiquette de texte, et limitez le total de textLabels afin que l'état persistant ne puisse pas croître sans limite.
  • Limitation de débit : Appliquez un plafond par connexion sur updateCursor (par exemple 30 Hz) et sur les écritures d'étiquette, comme décrit dans Limitation de la mise à jour.
  • Strictesse du protocole (variante brute) : Validez la forme du message avant utilisation et fermez le socket sur un JSON mal formé au lieu de journaliser et continuer. Rejetez les valeurs sessionId en double au lieu de silencieusement écraser l'entrée de socket d'une autre session.

Carte de référence

Actors

Agent Os

Clients

Connect

Recettes

Général

Auto-hébergement

Skills similaires