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.

Streaming Resources dans Angular : Gestion élégante des données en temps réel

Version

Cette fonctionnalité est disponible depuis Angular 19.2

Imaginez que vous développez une application de chat en temps réel. Traditionnellement, vous auriez utilisé des WebSockets avec RxJS pour gérer les messages entrants. Mais avec l'introduction des Streaming Resources dans Angular 19.2, nous avons maintenant une approche plus élégante et plus "Angular-native" pour gérer ces flux de données.

Comprendre les Streaming Resources

Les Streaming Resources sont une nouvelle façon de gérer les flux de données en temps réel dans Angular. Contrairement aux Observables RxJS qui représentent un flux continu d'événements individuels, les Streaming Resources représentent un état qui évolue dans le temps.

Différence clé

Alors qu'un Observable WebSocket traditionnel émet chaque message individuellement, un Streaming Resource maintient un état complet qui inclut tous les messages reçus. C'est particulièrement utile pour les applications qui ont besoin de conserver un historique des données reçues.

Implémentation d'un chat avec Streaming Resources

Voyons comment implémenter un chat simple en utilisant les Streaming Resources :

ts
import { resource, signal, computed, Injectable } from '@angular/core';

// Types simplifiés
interface Message {
  id: number;
  user: string;
  text: string;
  timestamp: number;
}

@Injectable({
  providedIn: 'root',
})
export class ChatService {
  // État local
  private userName = signal<string>('');
  private connection = signal<WebSocket | null>(null);

  // État de connexion
  readonly isConnected = computed(() => !!this.connection());

  // Resource de chat
  readonly messages = resource({
    request: () => this.userName(),
    stream: async ({ request: userName, abortSignal }) => {
      const messages: Message[] = [];
      const resultSignal = signal({
        value: messages,
      });

      if (!userName) return resultSignal;

      // Création de la connexion WebSocket
      const ws = new WebSocket('ws://chat-server.com');
      this.connection.set(ws);

      // Gestion des messages
      ws.addEventListener('message', (event: any) => {
        const message = JSON.parse(event.data) as Message;
        messages.push(message);
        resultSignal.set({
          value: [...messages],
        });
      });

      // Nettoyage à l'arrêt
      abortSignal.addEventListener('abort', () => {
        ws.close();
        this.connection.set(null);
      });

      return resultSignal;
    },
  });

  // Méthodes publiques
  connect(userName: string) {
    this.userName.set(userName);
  }

  sendMessage(text: string) {
    const ws = this.connection();
    if (!ws) return;

    const message = {
      text,
      user: this.userName(),
      timestamp: Date.now(),
    };

    ws.send(JSON.stringify(message));
  }
}
ts
import { Component, inject } from '@angular/core';
import { ChatService } from './chat.service';

@Component({
  selector: 'app-chat',
  standalone: true,
  template: `
    @if (!isConnected()) {
     <h1>Connexion</h1>
      <div class="login">
        <input #nameInput type="text" placeholder="Votre nom">
        <button (click)="connect(nameInput.value)">
          Se connecter
        </button>
      </div>
    }

    @if (isConnected()) {
      <h1>Messages</h1>
      <div class="chat">
        <!-- Messages -->
        <div class="messages">
          @if (messages.isLoading()) {
            <p>Chargement...</p>
          }
          
          @for (message of messages.value() ?? []; track message.id) {
            <div class="message">
              <strong>{{ message.user }}</strong>
              <p>{{ message.text }}</p>
            </div>
          }
        </div>

        <!-- Formulaire d'envoi -->
        <div class="send-form">
          <input #msgInput type="text" placeholder="Votre message">
          <button (click)="sendMessage(msgInput.value); msgInput.value = ''">
            Envoyer
          </button>
        </div>
      </div>
    }
  `,
})
export class ChatComponent {
  private chatService = inject(ChatService);
  messages = this.chatService.messages;
  isConnected = this.chatService.isConnected;

  connect(userName: string) {
    this.chatService.connect(userName);
  }

  sendMessage(text: string) {
    if (!text.trim()) return;
    this.chatService.sendMessage(text);
  }
}

La fonction stream dans le Resource

La fonction stream est le cœur du Streaming Resource. Elle reçoit deux paramètres importants :

  1. request : La valeur actuelle de la requête (dans notre cas, le nom d'utilisateur)
  2. abortSignal : Un signal permettant de gérer le nettoyage des ressources

Structure et type de retour

ts
stream: async ({ 
  request: userName,  // La valeur retournée par la fonction request
  abortSignal        // Signal pour le nettoyage
}): PromiseLike<Signal<{ value: T; } | { error: unknown; }>> => {
  // ... code ...
  return resultSignal; // Doit retourner un signal avec le bon type
}

Le resultSignal est un élément crucial que stream doit retourner. Il doit respecter strictement le type suivant :

  • Être une Promise (ou PromiseLike) qui résout vers
  • Un Signal qui contient soit :
    • Un objet avec une propriété value contenant les données de type T
    • OU un objet avec une propriété error contenant l'erreur

Exemple de création du signal :

ts
// Pour un succès
const resultSignal = signal({ value: messages });

// Pour une erreur
const resultSignal = signal({ error: new Error('Erreur de connexion') });

Important

  • Le signal DOIT toujours contenir soit value, soit error, jamais les deux
  • Ne jamais retourner un signal sans ces propriétés
  • En cas d'erreur, toujours utiliser la propriété error plutôt que de lancer une exception

La gestion du nettoyage avec abortSignal

L'abortSignal est un mécanisme crucial fourni par Angular pour gérer proprement le nettoyage des ressources dans les Streaming Resources. Il s'agit d'une instance de AbortSignal, une API web standard.

ts
stream: async ({ abortSignal }) => {
  // 1. Création des ressources
  const ws = new WebSocket('ws://chat-server.com');
  
  // 2. Configuration du nettoyage
  abortSignal.addEventListener('abort', () => {
    // Fermeture de la connexion WebSocket
    ws.close();
    
    // Réinitialisation de l'état
    this.connection.set(null);
  });

  // ... reste du code ...
}

Imaginez que vous quittez une salle de chat. Vous devez :

  1. Fermer la connexion WebSocket
  2. Nettoyer les données en mémoire
  3. Réinitialiser l'état de connexion

C'est exactement ce que fait l'abortSignal automatiquement !

Quand l'AbortSignal est-il déclenché ?

L'événement 'abort' est émis automatiquement par Angular dans plusieurs cas :

  • Quand le composant est détruit
  • Quand la fonction request() renvoie une nouvelle valeur
  • Quand le Resource est explicitement arrêté

Bonnes pratiques

  • Toujours utiliser l'abortSignal pour nettoyer les ressources externes (WebSocket, EventSource, etc.)
  • Ne pas oublier de réinitialiser les états internes
  • Éviter les fuites mémoire en nettoyant les listeners et les timers

L'objet messages dans le composant

Dans le composant, messages est un Streaming Resource qui expose trois propriétés principales :

  1. value() : Un signal contenant les messages actuels
  2. isLoading() : Un signal indiquant si le stream est en cours de chargement
  3. error() : Un signal contenant l'erreur éventuelle

Bonnes pratiques

  • Toujours vérifier messages.value() avec l'opérateur ?? pour gérer le cas où les données sont undefined
  • Utiliser messages.isLoading() pour afficher un état de chargement
  • Gérer les erreurs avec messages.error() pour une meilleure expérience utilisateur

Avantages des Streaming Resources

Les Streaming Resources offrent plusieurs avantages par rapport à l'approche traditionnelle avec RxJS :

  1. Gestion d'état intégrée : Les Streaming Resources maintiennent naturellement l'état des données reçues, ce qui est parfait pour les cas d'utilisation comme un chat où vous voulez conserver l'historique des messages.

  2. Intégration native avec les Signals : Pas besoin de convertir des Observables en Signals, tout fonctionne nativement avec le système de réactivité d'Angular.

  3. Nettoyage automatique : Le système gère automatiquement la fermeture des connexions et le nettoyage des ressources via l'abortSignal.

Bonne pratique

Utilisez les Streaming Resources quand :

  • Vous avez besoin de maintenir un état qui évolue dans le temps
  • Vous voulez une intégration native avec les Signals d'Angular
  • Vous gérez des connexions en temps réel comme WebSocket