Cargas dos elementos, pulsas «Siguiente» y vuelves a ver el último de la página anterior. En otra sesión, un elemento desaparece al pasar de página. Imagina una pequeña lista de tareas que cambia mientras la recorres: el código debe definir qué significa «siguiente» cuando las filas se desplazan.

Lee juntos el filtro, el orden y la página

La lista se ordena por createdAt descendente y después por un id numérico único, también descendente. El ejemplo usa enteros pequeños como marcas de tiempo. Sus seis elementos open aparecen como 6, 5, 4, 3, 2, 1; los elementos 5 y 4 comparten el tiempo 50. Hay además un elemento done, el 0, con tiempo 70.

Parte que debes leer Regla que debes buscar
selected Filtrar por estado antes de extraer una página
compare Primero el tiempo más reciente; a igual tiempo, el ID mayor
offsetPage / cursorPage Qué filas forman la página siguiente
Código que llama a la función Guardar nextCursor, añadir resultados válidos y reiniciar al cambiar la consulta

Empieza en la acción «Siguiente» y sigue sus argumentos hasta la función de selección. Leer solo el corte final del array oculta el filtro y el orden que dan sentido a esas posiciones.

Observa cómo el mismo offset apunta a otras filas

Esta implementación omite cierto número de filas que coinciden en ese momento:

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 primera página contiene 6 y 5. Pedir la segunda con offset = 2 produce resultados distintos tras un único cambio entre ambas llamadas:

Cambio tras la primera página Orden actual de las coincidencias Segunda página Resultado
Ninguno 6, 5, 4, 3, 2, 1 4, 3 Continuación esperada
Insertar 7 con tiempo 70 7, 6, 5, 4, 3, 2, 1 5, 4 Se repite 5
Eliminar 6 5, 4, 3, 2, 1 3, 2 Se omite 4

Cada caso empieza de nuevo con los datos originales. El número de filas que se omiten permanece fijo, pero las posiciones cambian. Quitar IDs duplicados en la interfaz puede ocultar la repetición; no recupera el elemento omitido.

Compara la clave de ordenación completa

El cursor guarda la clave del último elemento devuelto. Una condición basada solo en el tiempo, como createdAt < cursor.createdAt, pierde el elemento 4 después del 5: ambos tienen tiempo 50. Cambiarla por <= vuelve a admitir el elemento anterior.

Conserva los dos valores de (createdAt, id) y reutiliza la comparación del orden:

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,
  };
}

En este orden descendente, compare(row, after) > 0 significa que la fila queda estrictamente después de la posición guardada. Si el tiempo coincide, decide el id. La página siguiente contiene 4, 3 tanto después de la inserción anterior como después de la eliminación.

El objeto nextCursor copia los valores en lugar de buscar de nuevo la fila anterior; eliminar el elemento 5 permite, por tanto, continuar desde su clave guardada. Una página vacía devuelve null. El ejemplo puede necesitar una petición adicional para descubrir que la lista ha terminado.

Ejecuta los cambios como pruebas separadas

Descarga pagination-reading.mjs y ejecútalo con Node.js 22 o posterior:

node --test pagination-reading.mjs

Pasan las diez pruebas. Las tres cuyo nombre empieza por reproduce: comprueban deliberadamente el duplicado con offset, la omisión con offset y la omisión del cursor que solo compara el tiempo. Las otras siete comprueban la inserción, la eliminación anterior al cursor, la eliminación de su fila de origen, el recorrido completo con tiempos iguales y una página final vacía, una nueva fila posterior, el filtro antes del límite y el reinicio del filtro.

Las pruebas usan arrays nuevos y cambios síncronos entre llamadas. No necesitan un servicio, una cuenta, temporizadores ni instalar paquetes.

Lee el código que llama antes de dar por terminada la corrección

Cambiar el filtro a done y conservar el cursor anterior devuelve una página vacía en este ejemplo: el elemento 0 se ordena antes de ese cursor. Iniciar la consulta modificada con after = null devuelve el elemento 0. Quien llama debe reiniciar juntos las filas acumuladas y el cursor cuando cambien el filtro o el orden.

Comprueba también dónde se guarda nextCursor y cuándo se usa. Aquí, null tras una respuesta vacía significa detenerse; devolverlo directamente a la función reiniciaría la primera página. Busca envíos repetidos, respuestas desordenadas y la adición de una misma respuesta dos veces. Estos comportamientos de interfaz quedan fuera del ejemplo síncrono; seguir cambios de selección asíncronos ofrece otro ejemplo sobre respuestas obsoletas.

Decide qué cambios debe ver quien recorre la lista

Un cursor no proporciona una instantánea. Inserta el elemento 8 con tiempo 49 después de leer la primera página: la siguiente será 4, 8. La fila nueva queda después de la posición guardada, por lo que aparece en el recorrido ya iniciado. Una fila nueva anterior al cursor tendrá que esperar a que se actualice la lista.

Estas comprobaciones suponen IDs únicos y claves de ordenación sin cambios en los elementos que siguen existiendo. Editar una clave, cambiar un estado o eliminar un elemento aún no leído requiere una regla explícita del producto; una exportación de contenido fijo puede necesitar una estrategia de instantáneas aparte. Este ejemplo con arrays no prueba aislamiento de base de datos, índices, transporte ni codificación del cursor. Filtrar y ordenar todos los datos en cada llamada tampoco es un diseño de rendimiento para grandes volúmenes.

Al leer tu implementación, revisa juntos el filtro, la comparación, la clave guardada y el reinicio en quien llama. En Yenpo, abre los archivos relacionados y sigue sus referencias para comprobar cada paso en el código.