Vous chargez deux éléments, cliquez sur « Suivant » et retrouvez le dernier élément de la page précédente. Une autre fois, un élément disparaît entre deux pages. Imaginez une petite liste de tâches qui évolue pendant sa consultation : le code doit définir ce que signifie « suivant » lorsque les lignes changent de place.

Lire ensemble le filtre, le tri et la page

La liste est triée par createdAt décroissant, puis par un id numérique unique, lui aussi décroissant. L’exemple utilise de petits entiers pour représenter le temps. Ses six éléments open apparaissent dans l’ordre 6, 5, 4, 3, 2, 1 ; les éléments 5 et 4 partagent la valeur 50. Un autre élément, 0, a le statut done et la valeur temporelle 70.

Partie à lire Règle à trouver
selected Filtrer par statut avant de prendre une page
compare Le temps le plus récent d’abord ; à temps égal, le plus grand ID d’abord
offsetPage / cursorPage Quelles lignes constituent la page suivante
Code appelant Enregistrer nextCursor, ajouter les résultats valides et réinitialiser quand la requête change

Partez de l’action « Suivant » et suivez ses arguments jusqu’à la fonction de sélection. Lire uniquement le découpage final du tableau masque le filtre et le tri qui donnent un sens à ces positions.

Voir le même offset désigner d’autres lignes

Cette implémentation saute un nombre donné de lignes correspondant actuellement au filtre :

const compare = (a, b) => b.createdAt - a.createdAt || b.id - a.id;
const selected = (rows, status) =>
  rows.filter((row) => row.status === status).sort(compare);

function offsetPage(rows, offset, take = 2, status = 'open') {
  return selected(rows, status).slice(offset, offset + take);
}

La première page contient 6 et 5. Demander la deuxième avec offset = 2 produit des résultats différents après une seule modification entre les deux appels :

Modification après la première page Ordre actuel des lignes retenues Deuxième page Résultat
Aucune 6, 5, 4, 3, 2, 1 4, 3 Suite attendue
Insérer 7 avec le temps 70 7, 6, 5, 4, 3, 2, 1 5, 4 5 se répète
Supprimer 6 5, 4, 3, 2, 1 3, 2 4 est sauté

Chaque cas repart des données initiales. Le nombre de lignes à sauter reste fixe alors que les positions changent. Supprimer les ID en double dans l’interface peut masquer la répétition ; cela ne récupère pas l’élément sauté.

Comparer la clé de tri complète

Un curseur contient la clé du dernier élément renvoyé. Une condition fondée uniquement sur le temps, comme createdAt < cursor.createdAt, perd l’élément 4 après le 5, car tous deux ont la valeur 50. La remplacer par <= accepte à nouveau l’élément précédent.

Conservez les deux valeurs de (createdAt, id) et réutilisez la comparaison du tri :

function cursorPage(rows, after = null, take = 2, status = 'open') {
  const items = selected(rows, status)
    .filter((row) => after === null || compare(row, after) > 0)
    .slice(0, take);
  const last = items.at(-1);
  return {
    items,
    nextCursor: last ? { createdAt: last.createdAt, id: last.id } : null,
  };
}

Dans cet ordre décroissant, compare(row, after) > 0 signifie que la ligne se situe strictement après la position enregistrée. À temps égal, id départage les lignes. La page suivante contient 4, 3 après l’insertion comme après la suppression présentées plus haut.

L’objet nextCursor copie les valeurs au lieu de rechercher la ligne précédente. Supprimer l’élément 5 permet donc toujours de continuer depuis sa clé enregistrée. Une page vide renvoie null. Cet exemple peut nécessiter une requête supplémentaire pour découvrir la fin de la liste.

Exécuter les modifications dans des tests séparés

Téléchargez pagination-reading.mjs, puis exécutez-le avec Node.js 22 ou une version ultérieure :

node --test pagination-reading.mjs

Les dix tests passent. Les trois dont le nom commence par reproduce: vérifient volontairement les résultats erronés : doublon avec offset, omission avec offset et omission avec un curseur limité au temps. Les sept autres vérifient l’insertion, la suppression avant le curseur, la suppression de sa ligne d’origine, le parcours complet avec des temps égaux et une page finale vide, une nouvelle ligne ultérieure, le filtrage avant la limite et la réinitialisation du filtre.

Les tests utilisent des tableaux neufs et des modifications synchrones entre les appels. Ils ne nécessitent ni service, ni compte, ni minuteur, ni installation de paquets.

Lire le code appelant avant de considérer la correction terminée

Passer le filtre à done tout en gardant l’ancien curseur renvoie une page vide dans cet exemple : l’élément 0 se classe avant ce curseur. Démarrer la nouvelle requête avec after = null renvoie l’élément 0. Quand le filtre ou le tri change, le code appelant doit réinitialiser ensemble les lignes accumulées et le curseur.

Vérifiez aussi où nextCursor est enregistré et quand il est utilisé. Ici, null après une réponse vide signifie qu’il faut s’arrêter ; le repasser directement à la fonction relancerait la première page. Cherchez les requêtes répétées, les réponses reçues dans le désordre et les réponses ajoutées deux fois. Ces comportements d’interface sortent du cadre de cet exemple synchrone ; suivre les changements de sélection asynchrones présente un autre exemple consacré aux réponses périmées.

Décider comment les changements doivent apparaître

Un curseur ne fournit pas d’instantané. Insérez l’élément 8 avec le temps 49 après la lecture de la première page : la suivante devient 4, 8. La nouvelle ligne se situe après la position enregistrée et apparaît donc dans le parcours déjà commencé. Une nouvelle ligne avant le curseur attendra une actualisation.

La continuité vérifiée ici suppose des ID uniques et des clés de tri inchangées pour les éléments restants. Modifier une clé ou un statut, ou supprimer un élément encore non lu, exige une règle explicite du produit ; un export au contenu figé peut nécessiter une stratégie d’instantané distincte. Cet exemple sur tableaux ne teste ni isolation de base de données, ni index, ni transport, ni encodage du curseur. Filtrer et trier toutes les données à chaque appel n’est pas non plus une conception adaptée aux grands volumes.

En lisant votre implémentation, examinez côte à côte le filtre, la comparaison, la clé enregistrée et la réinitialisation dans le code appelant. Dans Yenpo, ouvrez les fichiers liés et suivez leurs références pour vérifier chaque étape dans le code.