Revoir le stream !

Pour un Youtubeur à 500k abonnés, je réalise un quiz, avec Angular, où les participants répondent en temps réel. C'est l'occasion, en direct, de réaliser et de voir ensemble des développements d'Angular: Streaming Resources, gestion du temps réel, utilisation de l'IA pour générer l'UI, etc.

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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 :

  1. 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
  2. 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 texte
    • Y.Array : pour les listes modifiables
    • Y.Map : pour les objets avec des propriétés
    • Y.XmlFragment : pour les documents structurés

FONCTIONNEMENT

Quand vous effectuez une modification avec YJS :

  1. La modification est appliquée immédiatement en local
  2. Elle est propagée en arrière-plan aux autres utilisateurs
  3. 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
});