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-websocketpousse un messageinitavec la snapshot actuelle{ cursors, textLabels }dès qu'un socket se connecte. La variantecursorsn'a pas de broadcast d'adhésion explicite ; le client appelle l'actiongetRoomStateune fois après connexion pour amorcer ses cartes locales, et les pairs voient d'abord un nouvel utilisateur sur le premier broadcastcursorMovedde cet utilisateur. - Déplacement : Chaque appel
updateCursorécrit l'entrée de présence de la connexion, puis diffusecursorMovedà toutes les connexions, y compris l'expéditeur. - Quitter : La variante
cursorsgère la déconnexion dansonDisconnect, diffusantcursorRemovedavec le dernier curseur de la connexion. La variante brute fait de même à partir de l'écouteur declosedu socket, puis supprime la session de la cartevars.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éfautroomIdà"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
updateCursorupdateTextremoveTextgetRoomState
-
Événements
cursorMovedcursorRemovedtextUpdatedtextRemoved
-
Files d'attente
- Aucune
-
État
- JSON
textLabels(persistant)connState.cursorpar connexion (éphémère)
-
Clé :
cursorRoom[roomId](résolu viaclient.cursorRoom.getOrCreate(roomId)) -
Responsabilité : Expose un endpoint WebSocket brut, suit les sockets actifs et leurs curseurs dans une carte
createVarsclassée par un paramètre de requêtesessionId, 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éthodegetOrCreate(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.websocketsdesessionIdà 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.iddans la variante actions, un ID de session généré par le serveur dans la variante brute). Ne faites jamais confiance à unuserIdfourni 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,
updateTextaccepte des arguments arbitrairesidetuserIdetremoveTextaccepte unidarbitraire, donc n'importe quel client peut modifier ou supprimer n'importe quelle étiquette. - Validation des entrées : Limitez
xetyaux limites du canevas, limitez la longueur de l'étiquette de texte, et limitez le total detextLabelsafin 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
sessionIden double au lieu de silencieusement écraser l'entrée de socket d'une autre session.
Carte de référence
Actors
- Contrôle d'accès
- Actions
- Clés d'actor
- Programmation d'actor
- États d'actor
- Actors IA et générés par l'utilisateur Rivet
- Authentification
- Communication entre Actors
- Connexions
- Onglets d'inspecteur personnalisés
- Débogage
- Modèles de conception
- Destruction d'Actors
- Erreurs
- Gestionnaire Fetch et WebSocket
- Types d'assistant
- Icônes et noms
- Paramètres d'entrée
- Cycle de vie
- Limites
- Gestionnaire de requête HTTP de bas niveau
- Stockage KV de bas niveau
- Gestionnaire WebSocket de bas niveau
- Métadonnées
- Démarrage rapide Next.js
- Démarrage rapide Node.js et Bun
- Files d'attente et boucles d'exécution
- Démarrage rapide React
- Temps réel
- Démarrage rapide Rust (aperçu)
- Actor sandbox
- Mise à l'échelle et concurrence
- Partage et fusion d'état
- SQLite
- SQLite + Drizzle
- État et stockage
- Tests
- Dépannage
- Types
- API HTTP vanille
- Versions et mises à niveau
- Flux de travail
Agent Os
- Communication agent à agent
- agentOS vs Sandbox
- Authentification
- Benchmarks
- Configuration
- Paquetage principal
- Tâches Cron
- Déploiement
- Passerelle LLM intégrée
- Événements
- Système de fichiers
- Limitations
- Identifiants LLM
- Multijoueur
- Réseau et aperçus
- Vue d'ensemble
- Autorisations
- Persistance et sommeil
- Pi
- Processus et shell
- Files d'attente
- Démarrage rapide
- Montage Sandbox
- Sécurité et authentification
- Modèle de sécurité
- Sessions
- Logiciels
- SQLite
- Invite système
- Outils
- Webhooks
- Automatisation des flux de travail
Clients
Connect
- Déployer sur AWS Lambda
- Déploiement sur AWS ECS
- Déploiement sur Cloudflare Workers
- Déploiement sur Freestyle
- Déploiement sur Google Cloud Run
- Déploiement sur Hetzner
- Déploiement sur Kubernetes
- Déploiement sur Railway
- Déploiement sur Rivet Compute
- Déploiement sur Supabase Functions
- Déploiement sur Vercel
- Déploiement sur des machines virtuelles et du matériel nu
Recettes
- Agent IA
- Espaces de travail agent IA
- Salle de discussion
- Éditeur de texte collaboratif
- Tâches planifiées et Cron Jobs
- Base de données par locataire
- Déploiement de Rivet dans un VPC ou réseau isolé
- Curseurs en direct et présence
- Jeu multijoueur
Général
- Configuration d'Actor
- Architecture
- Partage des ressources d'origine croisée
- Documentation pour les LLM et l'IA
- Réseau en périphérie
- Points de terminaison
- Variables d'environnement
- Serveur HTTP
- Journalisation
- Configuration du pool
- Liste de contrôle de production
- Configuration du registre
- Modes d'exécution