QoraClient
Constructeur
QoraClient({QoraClientConfig? config})
Crée une nouvelle instance de client. Toutes les requêtes partagent le même cache et la même map de déduplication.
// Minimal (tous les défauts)
final client = QoraClient();
// Avec configuration complète
final client = QoraClient(
config: QoraClientConfig(
defaultOptions: QoraOptions(
staleTime: Duration(minutes: 5),
retryCount: 2,
),
debugMode: kDebugMode,
maxCacheSize: 200,
),
);
Propriétés
config
final QoraClientConfig config
La configuration résolue pour cette instance de client. Lecture seule.
cachedKeys
Iterable<List<dynamic>> get cachedKeys
Toutes les clés de requête actuellement en cache (normalisées). Utile pour le débogage ou l'invalidation en masse.
for (final key in client.cachedKeys) {
print(key); // [users, 1], [posts], ...
}
Récupération
fetchQuery
Future<T> fetchQuery<T>({
required Object key,
required Future<T> Function() fetcher,
QoraOptions? options,
CancelToken? cancelToken,
})
Récupère les données une seule fois et retourne le résultat.
Comportement du cache :
| État du cache | Action |
|---|---|
| Frais (non périmé) | Retourne les données en cache : pas d'appel réseau |
| Périmé | Retourne les données périmées immédiatement ; revalide en arrière-plan |
| Absent | Fetch, attend et retourne le résultat |
Les appels concurrents avec la même clé sont dédupliqués : tous partagent une seule requête en cours. Les fetches échoués sont relancés avec backoff exponentiel jusqu'à retryCount fois.
final user = await client.fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(staleTime: Duration(minutes: 5)),
);
Lève :
StateErrorsiQoraOptions.enabledestfalseStateErrorsiQoraOptions.dependsOnest défini et que la dépendance n'a pas encore de donnéesQoraCancelExceptionsicancelTokenest annulé avant ou pendant le fetch- L'exception du fetcher (après tous les retries) en cas d'échec
watchQuery
Stream<QoraState<T>> watchQuery<T>({
required Object key,
required Future<T> Function() fetcher,
QoraOptions? options,
CancelToken? cancelToken,
})
Crée un Stream<QoraState<T>> réactif pour une requête.
À l'abonnement :
- Émet immédiatement l'état en cache actuel
- Déclenche un fetch si les données sont absentes ou périmées (selon
refetchOnMount) - Configure un timer de polling si
refetchIntervalest défini - Pousse tous les futurs changements d'état : depuis n'importe quelle source
À l'annulation (dernier abonné) :
- Annule le timer de polling
- Planifie un timer GC pour l'entrée de cache
StreamBuilder<QoraState<User>>(
stream: client.watchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(refetchInterval: Duration(seconds: 30)),
),
builder: (context, snapshot) {
return switch (snapshot.data) {
null || Initial() => const SizedBox.shrink(),
Loading(:final previousData) => previousData != null
? UserCard(previousData)
: const CircularProgressIndicator(),
Success(:final data) => UserCard(data),
Failure(:final error) => ErrorWidget('$error'),
};
},
)
prefetch
Future<void> prefetch<T>({
required Object key,
required Future<T> Function() fetcher,
QoraOptions? options,
CancelToken? cancelToken,
})
Pré-chauffe le cache sans créer de stream. Sans effet si les données sont déjà fraîches.
onEnter: (_) => client.prefetch<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
),
Lecture du cache
getQueryData
T? getQueryData<T>(Object key)
Retourne les meilleures données disponibles pour key, ou null. Retourne Success.data, ou previousData de Loading/Failure.
final user = client.getQueryData<User>(['users', userId]);
getQueryState
QoraState<T> getQueryState<T>(Object key)
Retourne le QoraState<T> complet, ou Initial<T>() si non mis en cache.
final state = client.getQueryState<User>(['users', userId]);
if (state is Success<User>) {
print('Ancienneté : ${state.age}');
}
Écriture du cache
setQueryData
void setQueryData<T>(Object key, T data)
Injecte des données directement dans le cache. Pousse instantanément Success vers tous les streams watchQuery actifs.
client.setQueryData(['users', userId], updatedUser);
restoreQueryData
void restoreQueryData<T>(Object key, T? snapshot)
Effectue un rollback vers un snapshot précédent. Si snapshot est null, l'entrée est supprimée du cache.
final snapshot = client.getQueryData<User>(['users', userId]);
client.setQueryData(['users', userId], optimisticUser);
try {
await api.updateUser(userId, payload);
} catch (_) {
client.restoreQueryData(['users', userId], snapshot);
}
Invalidation
invalidate
void invalidate(Object key)
Marque une requête comme périmée. Les streams watchQuery actifs reçoivent immédiatement Loading(previousData: ...). Le prochain accès déclenche un fetch frais.
await api.createPost(payload);
client.invalidate(['posts']);
invalidateWhere
void invalidateWhere(bool Function(List<dynamic> key) predicate)
Invalide toutes les requêtes dont la clé satisfait predicate.
// Toutes les requêtes utilisateur
client.invalidateWhere((key) => key.firstOrNull == 'users');
// Utilisateur spécifique + ses sous-ressources
client.invalidateWhere(
(key) => key.length >= 2 && key[0] == 'users' && key[1] == userId,
);
invalidatePrefix
void invalidatePrefix(Object prefix)
Invalide toutes les requêtes dont la clé commence par prefix. Les segments sont comparés avec la même égalité profonde que les clés (les maps et listes imbriquées sont comparées structurellement). Un préfixe vide correspond à toutes les requêtes. Les watchers actifs refetch.
// ['posts'], ['posts', 1], ['posts', 1, 'comments'], ...
client.invalidatePrefix(['posts']);
// Toutes les requêtes sous l'utilisateur 42
client.invalidatePrefix(['users', 42]);
invalidate(key) est exact. Utilisez invalidatePrefix pour une correspondance partielle façon TanStack.invalidateQueries
void invalidateQueries({required QueryFilter filter})
Invalidation en masse avec un QueryFilter : un prédicat plus riche qu'invalidateWhere qui reçoit également l'état actuel et les options du dernier fetch réussi.
typedef QueryFilter = bool Function(
String key,
QoraState<dynamic> state,
QoraOptions? lastOptions,
);
// Invalider toutes les requêtes en mode online pas encore en Success
client.invalidateQueries(
filter: (key, state, opts) =>
opts?.networkMode == NetworkMode.online && state is! Success,
);
// Invalider toutes les requêtes utilisateur dont les données ont plus de 10 minutes
client.invalidateQueries(
filter: (key, state, opts) {
if (state is! Success) return false;
return key.contains('users') &&
DateTime.now().difference(state.updatedAt) > const Duration(minutes: 10);
},
);
invalidateTags
void invalidateTags(List<QueryTag> tags)
Invalide toutes les requêtes en cache qui fournissent au moins un des tags donnés. Les requêtes enregistrent des tags via QoraOptions.providesTags. La correspondance suit deux règles :
- Générique :
QueryTag('post')correspond à toute requête fournissant un tag avectype: 'post', quel que soit sonid. - Spécifique :
QueryTag('post', '123')correspond uniquement aux requêtes fournissant un tag avectype: 'post'etid: '123'.
// Revalider toutes les requêtes de posts
client.invalidateTags([QueryTag('post')]);
// Revalider un post spécifique + son auteur
client.invalidateTags([QueryTag('post', postId), QueryTag('user', authorId)]);
Voir QoraOptions.providesTags et MutationOptions.invalidatesTags.
Suppression du cache
removeQuery
void removeQuery(Object key)
Supprime une entrée du cache et annule toute requête en attente. Le prochain accès repart de Initial.
await api.deletePost(postId);
client.removeQuery(['posts', postId]);
clearCache
void clearCache()
Vide uniquement le cache en mémoire. Le stockage persistant est inchangé.
Utiliser clear() pour vider les deux (si vous utilisez la persistance), ou clearStorage() pour vider uniquement le stockage.
clear
void clear()
Supprime toutes les requêtes en cache et annule toutes les requêtes en attente.
Pour QoraClient, cela vide le cache en mémoire (identique à clearCache).
Pour PersistQoraClient, cela vide À LA FOIS le cache en mémoire et le stockage.
// À la déconnexion
client.clear();
Hydratation
queueHydration
void queueHydration(Object key, dynamic data, {DateTime? updatedAt})
Met en file une valeur pré-désérialisée pour injection différée dans le cache typé. La valeur est injectée au premier appel API typé (fetchQuery<T>, watchQuery<T>, etc.) pour cette clé, reportant le cast Dart jusqu'à ce que le type T soit connu.
C'est le mécanisme partagé utilisé par PersistQoraClient.hydrate() et SsrHydrator.hydrate(). L'utilisation directe est réservée aux scénarios avancés (ex. sources d'hydratation personnalisées).
// Pré-peupler manuellement le cache avant runApp
client.queueHydration(['users', 1], User(id: 1, name: 'Ada'));
// Avec un timestamp personnalisé pour que staleTime s'évalue correctement
client.queueHydration(
['config'],
appConfig,
updatedAt: DateTime.now().subtract(const Duration(minutes: 2)),
);
hydrateQuery
void hydrateQuery<T>(Object key, T data, {DateTime? updatedAt})
Injecte un Success<T> typé directement dans une entrée Initial du cache. Sans effet si l'entrée a déjà des données. Utilisé en interne ; préférer queueHydration quand le type n'est pas connu à l'appel.
Débogage
debugInfo
Map<String, dynamic> debugInfo()
Retourne un snapshot de débogage de l'état actuel du cache.
print(client.debugInfo());
// {
// total_queries: 12,
// active_queries: 3,
// inactive_queries: 9,
// pending_requests: 1,
// max_size: 200,
// keys: [['users', 1], ['posts'], ...]
// }
Cycle de vie
attachLifecycleManager
void attachLifecycleManager(LifecycleManager manager)
Enregistre un LifecycleManager auprès du client. Quand l'application passe à LifecycleState.resumed, le client revalide toutes les entrées périmées ayant des abonnés actifs. QoraScope appelle cela automatiquement quand un FlutterLifecycleManager est fourni. L'utilisation directe est uniquement nécessaire dans des scénarios d'intégration personnalisés.
dispose
void dispose()
Libère toutes les ressources. Arrête le timer d'éviction, dispose toutes les entrées du cache, annule les requêtes en attente. Le client ne doit pas être utilisé après cet appel.
// Dans un teardown DI ou un tearDown de test
client.dispose();
Configuration recommandée
import 'package:flutter/foundation.dart';
import 'package:qora/qora.dart';
/// Singleton : créer une fois, injecter partout via DI ou QoraScope.
final qoraClient = QoraClient(
config: QoraClientConfig(
defaultOptions: QoraOptions(
staleTime: Duration(minutes: 5),
gcTime: Duration(minutes: 10),
retryCount: 3,
),
debugMode: kDebugMode,
maxCacheSize: 200,
refetchOnMount: true,
errorMapper: (error, stack) => QoraException(
'$error',
originalError: error,
stackTrace: stack,
),
),
);