EnglishBac à sable

Mise en cache des données

Des données qui ne changent pas à chaque visite ne devraient pas être chargées à chaque visite. Fluixi garde les résultats par nom, partage une seule requête entre les lecteurs, et rafraîchit ce qu'un changement a rendu obsolète, y compris dans les autres onglets de votre site.

import { cache } from '@fluixi/start/router';

const organisations = cache(() => api.get('/api/v1/organizations'), 'orgs', { ttl: 30_000 });

export const routeData = () => organisations();

Revenir sur cette route dans les 30 secondes affiche la liste sans requête. Au-delà, l'ancienne liste s'affiche tout de suite et une nouvelle est chargée derrière.

Où vivent les entrées

Sur le serveur, chaque requête a son propre cache : deux visiteurs ne lisent jamais les entrées l'un de l'autre, et rien ne survit à la requête. Dans le navigateur, les entrées vivent le temps de la page. Les lecteurs d'une même clé partagent une seule requête en cours, des deux côtés.

cache

cache(fn, name, options) renvoie une fonction qui prend les mêmes arguments que fn. La clé est le nom plus les arguments : user('a') et user('b') sont deux entrées.

Option Par défaut
ttl jusqu'à revalidation millisecondes pendant lesquelles une valeur reste fraîche
staleWhileRevalidate true une fois périmée, renvoyer l'ancienne valeur tout de suite et rafraîchir derrière ; les routes à l'écran se rechargent à l'arrivée de la nouvelle
cache la requête ou la page où vivent les entrées (ResourceCache)

Ne mettez pas routeData lui-même en cache : le routeur l'appelle avec des arguments qui changent à chaque navigation. Mettez en cache une fonction nommée, appelez-la depuis routeData, et passez-lui ce dont les données dépendent.

Revalider

Après un changement, retirez ce qu'il a rendu obsolète :

import { action, revalidate } from '@fluixi/start/router';

const createOrg = action(createOrganization, 'org.create', { invalidates: ['orgs'] });

// ou à la main
revalidate('orgs');                 // toutes les entrées orgs:*
revalidate(['orgs', 'members']);
revalidate((key) => key.startsWith('user:'));

Les entrées correspondantes sont retirées et les routes à l'écran rechargent leurs données. Ce qui reste en cache est servi depuis le cache : seul ce qui a été retiré est rechargé. La page reste à l'écran avec ses données actuelles jusqu'à l'arrivée des nouvelles.

invalidates peut aussi être une fonction du résultat de l'action : { invalidates: (org) => ['orgs', org:${org.id}] }.

Une revalidation par nom atteint aussi les autres onglets ouverts de votre site, qui retirent les mêmes entrées et rechargent leurs routes : créer une organisation dans un onglet met à jour la liste dans un autre.

Dans le chargement des données d'une route, ou dans tout ce qui recharge déjà, retirez les entrées sans recharger : clearCachedResources('orgs') depuis @fluixi/reactive/cached-resource. À la connexion, videz tout avec clearCachedResources(), pour que rien de ce qui a été chargé pour l'utilisateur précédent ne soit servi au suivant.

Quand le serveur sait

Parfois seul le serveur sait ce qu'une requête a changé : accepter une invitation ajoute une organisation à la liste sans que la page appelle la route des organisations. Déclarez-le sur la route d'API :

import { route } from '@fluixi/api';
import { signedIn } from '@fluixi/auth/server';

export const POST = route({
  guard: signedIn(),
  body: acceptInvite,
  revalidates: ['orgs', 'members'],
  handler: ({ body, auth }) => invitations.accept(body.token, auth.user.id),
});

Une réponse réussie porte x-fx-revalidate: orgs, members, une réponse refusée ne porte rien. @fluixi/api/client la lit après chaque appel et, dans le navigateur, revalide ces noms. Passez onRevalidate à createClient pour les traiter vous-même, ou false pour ignorer l'en-tête.

Les valeurs dont dépendent les données

Les données d'une route se chargent avant que le composant de la page existe : elles ne peuvent pas dépendre de l'état d'un composant. Ce qu'une route charge devrait venir de l'URL (paramètres, recherche), qui arrive à routeData et sert de clé au cache.

Pour des valeurs propres à toute l'application qui ne sont pas dans l'URL, comme l'utilisateur connecté ou l'organisation active, exportez routeDeps à côté de routeData :

export const routeDeps = () => [session.userId(), workspace.id()];

export const routeData = ({ deps }) => projects(...deps);

Les valeurs arrivent à routeData sous args.deps, le cache garde donc une entrée par combinaison. Quand elles changent, la route recharge ses données, en gardant la page à l'écran entre-temps.

Dans un composant

cachedResource est une ressource dont les résultats sont gardés de la même façon :

import { cachedResource } from '@fluixi/reactive/cached-resource';

const workspace = cachedResource(workspaceId, (id) => api.get(`/workspaces/${id}`), {
  name: 'workspace',
  ttl: 5 * 60_000,
  deps: [userId],
});

workspace();            // la valeur ; suspend sous <Suspense> au premier chargement
workspace.latest;       // la dernière valeur, sans suspendre
workspace.stale;        // true pendant un rafraîchissement en arrière-plan
workspace.refetch();
workspace.invalidate(); // retire l'entrée et recharge

deps sont des accesseurs dont les valeurs entrent dans la clé : chaque utilisateur a sa propre entrée, et revenir à l'un d'eux la sert depuis le cache. Le fetcher reçoit toujours la seule clé de la source.

Une valeur chargée pendant le rendu serveur voyage vers le navigateur avec la page, comme celle d'une ressource ordinaire, et la première lecture là-bas la prend au lieu de recharger. Dans une application start, $cachedResource n'a pas besoin d'import.

Garder le cache d'un rechargement à l'autre

storageCache(storage) garde les entrées dans un stockage plutôt qu'en mémoire :

import { storageCache, useStorage } from '@fluixi/core/storage';

const cache = storageCache(useStorage({ scope: 'session', namespace: 'cache' }), { ttl: 10 * 60_000 });
const orgs = cachedResource(() => api.get('/orgs'), { name: 'orgs', ttl: 60_000, cache });

Les valeurs passent par le sérialiseur du stockage : elles doivent être des données simples. Donnez-lui son propre espace de noms et un ttl, pour que les entrées ne s'accumulent pas après la disparition du code qui les a écrites, et videz-le à la déconnexion, puisqu'il survit désormais à la page.