Appearance
Collaboration en temps réel avec YJS et Angular
La collaboration en temps réel est devenue un élément essentiel des applications modernes. Imaginez que vous travaillez sur un document partagé comme Google Docs, où plusieurs personnes peuvent modifier le contenu simultanément. C'est exactement ce que nous allons apprendre à faire avec YJS et Angular !
Qu'est-ce que YJS ?
YJS est une bibliothèque de structures de données CRDT (Conflict-free Replicated Data Types) qui permet de créer des applications collaboratives. Pour comprendre YJS, prenons un exemple concret :
Imaginez que vous et vos collègues travaillez sur une liste de tâches partagée. Alice ajoute une tâche "Préparer la réunion" pendant que Bob ajoute "Envoyer le rapport", et ce, exactement au même moment. Sans un système adapté, ces modifications simultanées pourraient créer des conflits et l'une des modifications pourrait être perdue.
C'est là qu'interviennent les CRDT et YJS :
Les CRDT sont des structures de données spéciales qui garantissent que :
- Chaque utilisateur peut modifier les données localement sans attendre de réponse du serveur
- Toutes les modifications sont fusionnées automatiquement et de manière cohérente
- Le résultat final est identique pour tous les utilisateurs, peu importe l'ordre des modifications
YJS implémente ces CRDT de manière optimisée et fournit plusieurs types de données collaboratives :
Y.Text
: pour l'édition collaborative de texteY.Array
: pour les listes modifiablesY.Map
: pour les objets avec des propriétésY.XmlFragment
: pour les documents structurés
FONCTIONNEMENT
Quand vous effectuez une modification avec YJS :
- La modification est appliquée immédiatement en local
- Elle est propagée en arrière-plan aux autres utilisateurs
- YJS fusionne automatiquement les modifications, même en cas de conflit
À NOTER
Contrairement aux systèmes traditionnels basés sur un serveur central qui décide de l'ordre des modifications, YJS utilise une approche décentralisée. Chaque client peut modifier les données indépendamment, ce qui permet une meilleure réactivité et une utilisation hors ligne.
Installation
Commençons par installer les dépendances nécessaires :
bash
npm install yjs y-websocket
Configuration du serveur WebSocket
Avant de commencer à coder notre application, nous devons démarrer un serveur WebSocket qui permettra la synchronisation entre les clients :
bash
npx y-websocket
ASTUCE
Par défaut, le serveur démarre sur le port 1234. Vous pouvez modifier cela en utilisant la variable d'environnement PORT.
Création d'un service pour YJS
Créons d'abord un service qui gérera notre connexion YJS :
ts
import { Injectable, signal } from '@angular/core';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
@Injectable({
providedIn: 'root'
})
export class SharedDocumentService {
// Le document YJS qui contiendra nos données partagées
private doc = new Y.Doc();
// Le provider WebSocket qui gère la connexion
private wsProvider = new WebsocketProvider(
'ws://localhost:1234',
'mon-document',
this.doc
);
// Un signal pour suivre l'état de la connexion
public connected = signal(false);
constructor() {
// On écoute l'état de la connexion
this.wsProvider.on('status', ({ status }: { status: 'connected' | 'disconnected' }) => {
this.connected.set(status === 'connected');
});
}
// Méthode pour obtenir un tableau partagé
public getSharedArray(name: string): Y.Array<any> {
return this.doc.getArray(name);
}
// Méthode pour obtenir un objet partagé
public getSharedMap(name: string): Y.Map<any> {
return this.doc.getMap(name);
}
}
Utilisation dans un composant
Voici comment utiliser notre service dans un composant pour créer une liste collaborative d'utilisateurs :
ts
import { Component, inject, OnInit } from '@angular/core';
import { SharedDocumentService } from './shared-document.service';
import { User } from './user.interface';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
@if (connected()) {
<div class="status connected">Connecté</div>
} @else {
<div class="status disconnected">Déconnecté</div>
}
<div class="user-list">
@for (user of users; track user.id) {
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
}
</div>
<button (click)="addUser()">Ajouter un utilisateur</button>
`
})
export class UserListComponent implements OnInit {
private sharedDoc = inject(SharedDocumentService);
private sharedUsers = this.sharedDoc.getSharedArray('users');
users: User[] = [];
connected = this.sharedDoc.connected;
ngOnInit() {
// On observe les changements sur le tableau partagé
this.sharedUsers.observe(() => {
this.users = this.sharedUsers.toArray();
});
}
addUser() {
const newUser: User = {
id: Date.now(),
name: 'Nouveau utilisateur',
username: 'user' + Date.now(),
email: '[email protected]'
};
// L'ajout est automatiquement synchronisé avec tous les clients
this.sharedUsers.push([newUser]);
}
}
Gestion de la présence (Awareness)
YJS permet aussi de gérer la présence des utilisateurs, comme les curseurs dans Google Docs :
ts
import { Injectable, inject } from '@angular/core';
import { SharedDocumentService } from './shared-document.service';
import { WebsocketProvider } from 'y-websocket';
@Injectable({
providedIn: 'root'
})
export class AwarenessService {
private sharedDoc = inject(SharedDocumentService);
private provider = this.sharedDoc.getProvider();
// Mettre à jour l'état de l'utilisateur courant
updateUserState(state: any) {
this.provider.awareness.setLocalState(state);
}
// Observer les changements d'état des autres utilisateurs
onStateChange(callback: (states: Map<number, any>) => void) {
this.provider.awareness.on('change', () => {
const states = this.provider.awareness.getStates();
callback(states);
});
}
}
BONNES PRATIQUES
- Utilisez toujours des signaux pour gérer l'état de la connexion
- Gérez proprement la déconnexion dans le
ngOnDestroy
- Pensez à la gestion des erreurs et à la reconnexion automatique
ATTENTION
Les modifications sur les structures de données YJS doivent toujours être faites à l'intérieur d'une transaction pour garantir la cohérence :
ts
this.doc.transact(() => {
// Modifications ici
});