Éditeur de texte collaboratif
IMPORTANT : Avant de faire quoi que ce soit, vous DEVEZ 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 modèles s'appliquent à tout le travail RivetKit. Tout ce qui suit suppose que vous l'ayez déjà lu et compris.
Exemples fonctionnels
Si vous avez besoin d'une implémentation de référence, lisez le code d'exemple de travail brut dans ces modèles :
Modèles de construction d'un serveur Yjs sur RivetKit : synchronisation de document CRDT, présence et curseurs, et persistance d'instantané, avec un Actor Rivet par document agissant comme relais.
Code de démarrage
Commencez par l'exemple de travail sur GitHub et adaptez-le à votre éditeur. Il intègre un frontend React avec une simple zone de texte, des superpositions de curseur distant et un index de document d'espace de travail.
| Cas d'usage | Code de démarrage | Exemples courants |
|---|---|---|
| Édition de document partagé | GitHub | Documents de type Notion, notes partagées, outils d'écriture en pair, co-édition de formulaires |
CRDT vs OT
Deux familles d'algorithmes résolvent l'édition de texte concurrente. Le choix détermine ce que votre serveur doit faire.
| Dimension | CRDT (Yjs) | Transformation opérationnelle |
|---|---|---|
| Modèle de résolution de conflits | Fusions commutatives. Les mises à jour s'appliquent dans n'importe quel ordre sur n'importe quel pair et convergent vers le même résultat. | Le serveur transforme chaque opération par rapport à chaque opération concurrente. La correction dépend d'un séquenceur central. |
| Support hors ligne | Fort. Les clients continuent à éditer localement et fusionnent les mises à jour mises en buffer à la reconnexion. | Faible. La divergence de longue durée rend les chaînes de transformation complexes et fragiles. |
| Rôle du serveur | Relais plus persistance. Le serveur applique les mises à jour opaques et les rediffuse. Il n'a jamais besoin de comprendre la sémantique du document. | Transformateur faisant autorité. Le serveur doit implémenter la logique de transformation pour chaque type d'opération. |
| Maturité de la bibliothèque | Yjs est mature et largement déployé, avec des liaisons pour ProseMirror, CodeMirror, Monaco et autres. | Les implémentations de qualité production sont principalement propriétaires (Google Docs) ou vieillissantes (ShareDB). |
L'exemple utilise Yjs car les CRDT permettent au serveur de rester un Actor Rivet de style relais. L'actor applique chaque mise à jour entrante à un Y.Doc côté serveur afin qu'il puisse persister l'état fusionné et servir les arrivants tardifs, mais il ne transforme jamais les opérations ni n'arbitre les conflits. L'ordre n'a pas d'importance car les fusions Yjs sont commutatives.
Modèle d'Actor de document
| Sujet | Résumé |
|---|---|
| Topologie | Un actor document[workspaceId, documentId] par document plus un coordinateur documentList[workspaceId] par espace de travail. |
| Modèle de synchronisation | Chaque client détient un Y.Doc local. L'actor de document relaye les mises à jour Yjs incrémentielles en tant que événements de diffusion et conserve une copie fusionnée côté serveur dans les vars. |
| Persistance | Instantané Yjs complet réécrit dans une clé actor KV binaire (yjs:doc) à chaque mise à jour de synchronisation. Les métadonnées du document se trouvent dans l'état JSON. |
| Files d'attente | Aucune. L'exemple est purement actions plus événements de diffusion. |
| Présence | Awareness Yjs relayé par la même action applyUpdate. Le connState par connexion suit les clientIds d'awareness assertés pour le nettoyage à la déconnexion. |
La division en deux actors suit le modèle coordinateur de Design Patterns : le coordinateur possède la découverte et la création, et chaque actor de document possède l'état temps réel d'un document. Les clés multi-parties limitent les deux actors à un espace de travail.
Actors
-
Clé :
document[workspaceId, documentId] -
Responsabilité : Applique les mises à jour de synchronisation et d'awareness entrantes à un
Y.Docet uneAwarenesscôté serveur, persiste l'instantané Yjs fusionné dans actor KV, et diffuse les mises à jour à tous les collaborateurs connectés. -
Actions
getContentapplyUpdategetAwareness
-
Files d'attente
- Aucune
-
État
- Métadonnées JSON uniquement :
title,createdAt,updatedAt - Clé KV binaire
yjs:doccontenant l'instantané Yjs complet fusionné - Vars éphémères : le
Y.DocetAwarenessactifs, créés danscreateVarset réhydratés à partir de KV au démarrage de l'actor connStatepar connexion :clientIdsdes clients d'awareness assertés par cette connexion
- Métadonnées JSON uniquement :
-
Clé :
documentList[workspaceId] -
Responsabilité : Coordinateur pour un espace de travail. Crée des actors de document via le client actor-to-actor et maintient l'index des résumés de documents.
-
Actions
createDocumentlistDocumentsdeleteDocument
-
Files d'attente
- Aucune
-
État
- JSON
- Tableau
documentsd'entréesDocumentSummary(id,title,createdAt,updatedAt)
La createDocument du coordinateur génère un UUID, puis crée explicitement l'actor de document avec c.client<typeof registry>() et transmet { title, createdAt } en tant qu'entrée de création, que le createState de l'actor de document consomme. Voir Communicating Between Actors pour le client actor-to-actor.
Relais de mise à jour
Une seule action applyUpdate(update, kind, clientId?) gère les deux types de mises à jour. Les mises à jour franchissent la limite d'action en tant que tableaux d'octets number[] et sont reconverties en Uint8Array de chaque côté.
| Type | Le serveur applique à | Persiste | Diffuse |
|---|---|---|---|
"sync" |
c.vars.doc via Y.applyUpdate avec origine "client" |
Instantané fusionné complet dans la clé KV yjs:doc, puis augmente updatedAt |
Événement sync portant la mise à jour incrémentiell |
"awareness" |
c.vars.awareness via applyAwarenessUpdate avec origine "client" |
Rien. La présence est éphémère. | Événement awareness portant la mise à jour |
Notez l'asymétrie sur la branche de synchronisation : la diffusion porte uniquement la petite mise à jour incrémentiielle, tandis que l'écriture KV stocke le document complet fusionné ré-encodé avec Y.encodeStateAsUpdate.
Les étiquettes d'origine Yjs sont les gardes d'écho qui maintiennent la boucle de relais libre :
| Étiquette d'origine | Défini où | Effet |
|---|---|---|
"local" |
Éditions client à l'intérieur de doc.transact(..., "local") |
L'écouteur de mise à jour du client s'exécute et envoie applyUpdate à l'actor. |
"client" |
Serveur appliquant une mise à jour entrante à son Y.Doc ou Awareness |
Marque le changement comme originaire du client sur la copie du serveur. |
"remote" |
Client appliquant des événements de diffusion ou des données de synchronisation initiale | Les écouteurs de mise à jour reviennent tôt sur "remote", donc un client ne renvoie jamais son propre écho. |
À la connexion ou reconnexion, le client appelle getContent et getAwareness, puis applique les deux résultats à son Y.Doc et Awareness locaux avec origine "remote". Après cela, chaque changement passe par applyUpdate et les événements de diffusion.
Awareness et présence
La présence (noms d'utilisateurs, couleurs, positions du curseur) s'appuie sur le protocole Yjs Awareness plutôt que sur l'état des actors :
- Les clients définissent la présence avec
awareness.setLocalStateFieldpour les champsuseretcursor. L'écouteur de mise à jour d'awareness encode la modification et envoieapplyUpdate(update, "awareness", awareness.clientID). - L'actor enregistre chaque
clientIdasserté dans leconnState.clientIdsde cette connexion, applique la mise à jour à l'Awarenesscôté serveur, et diffuse l'événementawarenessà tous les pairs. Voir Connections pour l'état par connexion. onDisconnectlit lesclientIdsde la connexion, appelleremoveAwarenessStatessur l'Awarenesscôté serveur, et diffuse la suppression encodée afin que chaque client restant abandonne le curseur de l'utilisateur parti. Voir Lifecycle pour le crochet.
Parce que l'actor suit quels clientIds d'awareness appartiennent à quelle connexion, le nettoyage de la présence est automatique à la déconnexion sans coopération client requise.
Persistance et compaction
L'exemple persiste avec une réécriture d'instantané complet : à chaque mise à jour "sync", l'actor ré-encode le document complet fusionné avec Y.encodeStateAsUpdate et réécrit la clé KV binaire unique yjs:doc. Il n'y a pas de log d'mises à jour ajoutables et pas de tâche de compaction séparée. La compaction est implicite car Y.encodeStateAsUpdate émet une représentation fusionnée compacte du document, donc la sémantique de fusion Yjs maintient le blob stocké compact par elle-même.
| Propriété | Réécriture d'instantané complet (l'exemple) |
|---|---|
| Coût d'écriture | Une écriture KV de document complet par mise à jour de synchronisation, donc chaque frappe réécrit le blob entier. |
| Coût de lecture | Une lecture KV binaire dans createVars réhydrate le document au démarrage de l'actor. |
| Sécurité après crash | Le dernier applyUpdate complété est durable. Aucune relecture de log requise. |
| Point sweet | Petits à moyens documents où la simplicité l'emporte sur l'amplification d'écriture. |
Extension recommandée (pas dans l'exemple) : pour les gros documents ou les taux d'édition très élevés, basculez vers l'ajout de mises à jour incrémentielles à un log d'mises à jour KV et la rédaction d'un instantané fusionné uniquement périodiquement (par exemple tous les N mises à jour). Le démarrage devient instantané plus relecture de log, et l'écriture d'instantané devient l'étape de compaction explicite qui tronque le log. Adoptez ceci seulement quand les écritures d'instantané complet deviennent le goulot d'étranglement mesuré ou que le blob s'approche des limites de taille de valeur KV.
Cycle de vie
sequenceDiagram
participant A as Client A
participant B as Client B
participant DL as documentList
participant D as document
A->>DL: listDocuments()
A->>DL: createDocument(title)
DL->>D: create([workspaceId, documentId], input)
DL-->>A: DocumentSummary
A->>D: connect
B->>D: connect
Note over D: createVars réhydrate Y.Doc à partir de KV "yjs:doc"
A->>D: getContent() + getAwareness()
D-->>A: doc encodé + état awareness
Note over A: édition locale avec origine "local"
A->>D: applyUpdate(update, "sync")
Note over D: appliquer avec origine "client", réecrire instantané KV
D-->>B: événement sync (mise à jour incrémentiielle)
Note over B: appliquer avec origine "remote", pas d'écho
A->>D: applyUpdate(update, "awareness", clientId)
D-->>B: événement awareness
B-->>D: disconnect
Note over D: onDisconnect supprime les clientIds d'awareness de B
D-->>A: événement awareness (suppression)
Liste de contrôle de sécurité
L'exemple est expédié sans authentification ni autorisation. Renforcez-le avec cette base avant la production. Aucun de ces éléments n'est implémenté dans l'exemple.
- Authentifier avant la connexion : Quiconque connaît ou devinez un ID d'espace de travail peut se connecter, et parce que
useActorcrée implicitement getOrCreate, la connexion avec un ID d'espace de travail inexistant crée silencieusement un coordinateurdocumentListvierge. Ajoutez une authentification de connexion afin que les clients non authentifiés ne rejoignent jamais un actor. Voir Authentication. - Contrôle d'accès par document : Validez que l'utilisateur authentifié est autorisé à accéder à la clé
[workspaceId, documentId]spécifique, et pas seulement à n'importe quel document. - Cap et limite de débit
applyUpdate: Les charges utiles de mise à jour sont des tableauxnumber[]non validés sans limite de taille, et le client exemple envoie une action par frappe et par mouvement de curseur sans limitation. Appliquez des limites de taille de charge utile et des limites de débit par connexion côté serveur, et déthrottlez côté client. - Ne faites pas confiance aux clientIds d'awareness assertés par le client : L'argument
clientIddeapplyUpdateest fourni par le client et accepté tel quel. Dérivez ou vérifiez l'identité de présence à partir de l'état serveur limité à la connexion. - Détruisez les actors et KV à la suppression :
deleteDocumentfiltre uniquement l'entrée de l'index du coordinateur. L'actor de document et son instantané KV sont orphelins. À la suppression, détruisez aussi l'actor de document et son stockage, avec une vérification d'autorisation sur qui peut supprimer.
Carte de référence
Actors
- Contrôle d'accès
- Actions
- Clés d'Actor
- Planification d'Actor
- Statuts d'Actor
- Actors Rivet générés par IA et utilisateurs
- 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'aide
- Icônes et noms
- Paramètres d'entrée
- Cycle de vie
- Limites
- Gestionnaire de requête HTTP bas niveau
- Stockage KV bas niveau
- Gestionnaire WebSocket bas niveau
- Métadonnées
- Démarrage rapide Next.js
- Démarrage rapide Node.js & 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 jointure d'état
- SQLite
- SQLite + Drizzle
- État et stockage
- Essais
- Dépannage
- Types
- HTTP API vanilla
- Versions et mises à niveau
- Workflows
Agent Os
- Communication agent à agent
- agentOS vs Sandbox
- Authentification
- Benchmarks
- Configuration
- Paquet central
- Tâches Cron
- Déploiement
- Passerelle LLM intégrée
- Événements
- Système de fichiers
- Limitations
- Identifiants LLM
- Multijoueur
- Réseaux et aperçus
- Aperçu
- 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 de workflow
Clients
Connexion
- 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 VM et matériel nu
Cookbook
- Agent IA
- Espaces de travail d'agent IA
- Salon de chat
- Éditeur de texte collaboratif
- Tâches Cron et tâches planifiées
- 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 entre origines
- Documentation pour les LLM et l'IA
- Réseaux périphériques
- Endpoints
- Variables d'environnement
- Serveur HTTP
- Journalisation
- Configuration du pool
- Liste de contrôle de production
- Configuration du registre
- Modes d'exécution