API y empresas

Cursores estables para paginar resultados que pueden cambiar mientras se leen

Si una colección cambia entre la primera y la última página, el offset puede repetir u omitir resultados. Un cursor ligado a una revisión conserva un corte auditable.

Por Marco Ferreiro ·

Monitores de operaciones mostrando secuencias abstractas de resultados de una flota

Una API entrega la primera página de infracciones y, antes de que el cliente pida la segunda, ingresa un acta nueva y se corrige otra. Si el cliente usa offset=50, la posición 50 puede ya no representar el mismo borde. El resultado será una fila repetida, una omitida o una mezcla imposible de reproducir.

El cursor estable resuelve el problema sólo cuando identifica tanto la posición como el corte de actas que se está recorriendo.

Por qué el offset se mueve

Supongamos un listado de actas ordenado por observed_at DESC, id DESC, con páginas de tres registros. Al iniciar existen F9, F8, F7, F6, F5, F4.

La primera página devuelve F9, F8, F7. Antes de pedir offset=3, entra F10 al comienzo. La nueva lista es F10, F9, F8, F7, F6, F5, F4. El offset tres devuelve F7, F6, F5: F7 se repite y F4 queda para otra página.

No es un error matemático del cliente. El índice numérico fue aplicado a dos versiones distintas del listado de infracciones.

Posición y revisión son dos datos diferentes

Un cursor robusto puede representar:

El cliente no necesita leer esos campos. Recibe una cadena opaca y la devuelve sin modificar. El servidor valida que pertenece a la misma flota, consulta, filtros y revisión.

Codificar sólo F7 permite continuar “después de F7” sobre el estado actual, pero no garantiza que una corrección haya conservado el orden ni que un acta retirada siga siendo localizable.

Dos modelos válidos de estabilidad

El primer modelo conserva una instantánea materializada. Todas las páginas leen la misma revisión hasta completar o vencer. Ofrece semántica clara, pero consume almacenamiento y exige una política de retención.

El segundo usa un corte temporal o de versión: por ejemplo, sólo filas cuya revisión sea menor o igual a R42, con historial suficiente para reconstruir valores. Puede ser más barato si el sistema ya versiona cambios, pero un campo mutable sin historial rompe la promesa.

En ambos casos debe existir un orden total. Ordenar sólo por fecha de infracción permite empates; agregar un identificador estable como segundo criterio evita que dos filas intercambien posición arbitrariamente.

Cómo publicar la página siguiente

RFC 8288 define enlaces tipados y su serialización en el encabezado HTTP Link. Una respuesta puede publicar la URI siguiente con relación next, además de incluirla en el cuerpo si el contrato lo necesita. RFC 8977 muestra una aplicación concreta de enlaces para paginar resultados RDAP.

La API debería devolver el enlace completo o el cursor opaco, no pedir al cliente que reconstruya parámetros internos. OpenAPI permite documentar el esquema de la respuesta, ejemplos y errores esperados.

Una forma posible es:

{
  "revision": "R42",
  "items": [
    { "id": "F9" },
    { "id": "F8" },
    { "id": "F7" }
  ],
  "nextCursor": "opaque-value",
  "hasMore": true
}

Los identificadores son ficticios. El contrato público no debería revelar cómo firmar o decodificar el cursor.

Qué papel cumplen ETag y HTTP condicional

RFC 9110 define validadores como ETag y solicitudes condicionales. Un ETag puede identificar la representación de una página o la revisión del conjunto y ayudar a detectar cambios. No reemplaza automáticamente al cursor: su semántica debe quedar documentada.

Por ejemplo, la primera respuesta puede declarar un ETag de revisión. Si el cliente reanuda con un cursor incompatible, el servidor rechaza la solicitud en vez de devolver silenciosamente otro conjunto de actas. Para descargas largas, también puede permitir verificar que una representación en caché sigue vigente.

No conviene usar una fecha de servidor redondeada como único validador si dos cambios pueden ocurrir dentro de ese intervalo.

Altas, correcciones y retiros durante la lectura

En la revisión R42, un acta nueva pertenece a R43 y no aparece a mitad del recorrido. Una corrección posterior tampoco debe cambiar la posición histórica ya entregada; se verá al iniciar una nueva lectura. Si un acta fue retirada por una corrección sensible, la política puede invalidar la revisión completa en lugar de seguir sirviéndola.

La consistencia no significa ocultar novedades para siempre. Significa elegir un corte explícito. Al terminar, el cliente puede iniciar otra enumeración o consumir eventos desde R42 para conocer cambios.

Errores que forman parte del contrato

Definí respuestas distintas para cursor mal formado, firma inválida, filtros incompatibles, revisión vencida y acceso a otro tenant. No conviertas todos los casos en una página vacía: una lista vacía podría interpretarse como que la flota no tiene multas.

También fijá un máximo de tamaño, duración de revisión y comportamiento de reintentos. Los logs deben usar identificadores opacos; no incluyas patentes en el cursor ni en métricas.

Cómo probarlo de manera reproducible

El test de contrato crea un listado de actas ficticias, obtiene la primera página, inserta y corrige filas y continúa con ambos métodos. La aserción no es que el cursor devuelva la versión más nueva: debe devolver cada elemento de la revisión inicial exactamente una vez y en el orden documentado.

Después se inicia un recorrido nuevo y se comprueba que incluye las actas nuevas. Finalmente, se prueba expiración, cambio de filtro, cursor alterado y aislamiento entre empresas.

Un cursor estable no es una cadena misteriosa: es una promesa de consistencia. Cuando esa promesa incluye revisión, orden y errores explícitos, una integración puede auditar qué actas leyó incluso mientras el sistema siguió cambiando.

Metodología

Alcance
Comparación lógica de paginación por offset y cursor sobre una colección sintética que recibe altas y correcciones durante la lectura.
Unidad de análisis
Una ejecución completa de paginado identificada por revisión, orden total, tamaño de página y secuencia de enlaces siguientes.
Cobertura
Semántica de enlaces y validadores basada en RFC 8288, RFC 9110 y un ejemplo estandarizado de paginación en RFC 8977.

Limitaciones

Fuentes

Preguntas frecuentes

¿Un cursor es simplemente el último identificador recibido?

Puede contener esa posición, pero no alcanza si la colección cambia. Para una lectura estable también debe quedar ligada una revisión, instantánea o regla de corte que el servidor pueda aplicar en páginas posteriores.

¿El cursor debería ser legible y editable por el cliente?

No hace falta. Conviene tratarlo como opaco, validarlo en el servidor y proteger su integridad. El contrato documenta cómo usarlo y sus errores, no su representación interna.

¿Qué debe ocurrir cuando una revisión ya no está disponible?

La API debe responder un error explícito de cursor vencido o inválido e indicar cómo reiniciar. Continuar sobre otra revisión sin avisar mezcla cortes y vuelve imposible auditar la enumeración.

Notas relacionadas