Concepts clés

Déduplication

Qora fusionne les requêtes concurrentes pour la même clé en un seul appel réseau, quel que soit le nombre d'appelants.

Le problème

Plusieurs widgets ou services peuvent demander indépendamment les mêmes données au même moment : au chargement de page, lors d'une navigation rapide, ou quand un feature flag est évalué. Sans déduplication, chaque appel déclenche sa propre requête réseau :

Sans déduplication :
  Widget A → GET /users/1  ←─┐
  Widget B → GET /users/1  ←─┤  3 appels réseau identiques
  Widget C → GET /users/1  ←─┘

Cela gaspille de la bande passante, augmente la charge serveur et risque des conditions de course où les réponses arrivent dans le désordre.


Comment Qora déduplique

Qora suit chaque requête en cours dans une map _pendingRequests indexée par la clé de requête normalisée. Quand un second appel arrive pendant que le premier est encore en attente :

  1. Le Future en cours est réutilisé. Aucun nouvel appel réseau n'est émis.
  2. Tous les appelants attendent le même résultat.
  3. Le cache est mis à jour une seule fois quand la réponse unique arrive.
  4. Tous les streams watchQuery actifs reçoivent la mise à jour d'état.
Avec Qora :
  Widget A ─────────────────┐
  Widget B → fetchQuery → dédup → GET /users/1  (1 requête)
  Widget C ─────────────────┘
           les trois se résolvent avec le même résultat

Exemple

main.dart
// Les trois appels se déclenchent simultanément : une seule requête HTTP est émise
final f1 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final f2 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final f3 = client.fetchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));

// f1, f2, f3 sont le même Future : un appel réseau, trois attendants
final results = await Future.wait([f1, f2, f3]);
// results = [alice, alice, alice]

La même logique s'applique aux abonnés watchQuery :

main.dart
// Montés simultanément sur la même page
final stream1 = client.watchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));
final stream2 = client.watchQuery<User>(key: ['users', 1], fetcher: () => api.getUser(1));

// Un seul fetch se déclenche ; les deux streams émettent la même séquence Loading → Success

Périmètre de la déduplication

La déduplication s'applique au sein d'une seule instance de QoraClient. Un client partagé unique par application (ou scope DI) fournit une couverture de déduplication complète sur tous les widgets et services.

main.dart
// Un client partagé : déduplication complète
final client = QoraClient();

class UserRepository {
  UserRepository(this._client);   // injecté
  final QoraClient _client;
}

class PostRepository {
  PostRepository(this._client);   // même instance
  final QoraClient _client;
}
main.dart
// Deux clients séparés : pas de déduplication entre eux
final clientA = QoraClient();
final clientB = QoraClient(); // cache différent, map pending différente

Interaction avec les retries

La déduplication et les retries se composent correctement. Si la requête en cours échoue, tous les attendants reçoivent le même état Failure après épuisement de toutes les tentatives :

fetchQuery appelé par A, B, C (dédupliqué en 1 requête)
  ↓ tentative 1 → échoue
  ↓ attente 1 s
  ↓ tentative 2 → échoue
  ↓ attente 2 s
  ↓ tentative 3 → échoue
  ↓ Failure stocké en cache
  A, B, C reçoivent tous Failure

Quand la déduplication ne s'applique pas

La déduplication fusionne uniquement les requêtes en cours simultanément. Les appels séquentiels sont indépendants et résolus via le cache :

main.dart
// Séquentiel : deux appels réseau (chacun passe par SWR ou cache indépendamment)
final u1 = await client.fetchQuery<User>(key: ['users', 1], fetcher: api.getUser);
final u2 = await client.fetchQuery<User>(key: ['users', 1], fetcher: api.getUser);
//                                                 ↑ servi depuis le cache (vérification staleTime)

Étapes suivantes

Copyright © 2026