Conceptos
Paginación y sincronización
Las listas responden página a página con un cursor; updatedSince obtiene solo lo que ha cambiado desde tu última ejecución.
Páginas y cursores
Todas las listas responden igual: los elementos, el cursor de la página siguiente y si hay una.
{
"data": [ { "sessionId": "b3ac0004-0000-4000-8000-000000000000", "title": "Registration and coffee" } ],
"nextCursor": "YzE6MDYzOTI3NjEwODQ4MjgyMDAwMHxiM2Fj...",
"hasMore": true
}Devuelve nextCursor como cursor, con los mismos filtros, hasta que sea null. limit fija el tamaño de página: 100 por defecto y 500 como máximo. Un cursor es opaco: consérvalo tal como llegó y no construyas uno tú. Uno que no haya generado el servidor se rechaza con invalidCursor, en lugar de volver a empezar sin avisar.
Las listas tienen un orden fijo, primero el cambio más antiguo, con el id para desempatar, así que una página nunca se salta ni repite un elemento que no haya cambiado mientras lees. Un elemento que cambia mientras paginas pasa al final y lo vuelves a ver, que es justo lo que quiere una sincronización.
Las listas de asistentes, pedidos y registros de entrada también aceptan page y pageSize, como antes de que existieran los cursores. Siguen funcionando; es mejor usar el cursor, que nunca cuenta.
Sincronizar: solo lo que ha cambiado
Las listas de cosas que cambian indican cuándo cambió cada elemento por última vez, en updatedUtc, y aceptan updatedSince. Una sincronización guarda el updatedUtc más reciente que ha visto y pregunta desde ahí la próxima vez:
- Lee todas las páginas con
updatedSinceigual a lo que guardaste la última vez (nada la primera vez). - Guarda o actualiza cada elemento por su id.
- Guarda el
updatedUtcmás reciente de la ejecución para la próxima vez.
updatedSince es inclusivo: un elemento que cambió en el mismo milisegundo que tu marca vuelve a llegar en lugar de perderse. Actualizar un elemento que ya tienes no hace daño; perder uno, sí.
const base = "https://attendbeacon.com/api/v1/events/b3ac0000-0000-4000-8000-000000000001/attendees";
const headers = { Authorization: `Bearer ${process.env.ATTENDBEACON_KEY}` };
let since = await loadMark(); // null on the first run
let newest = since;
let cursor = null;
do {
const url = new URL(base);
url.searchParams.set("limit", "500");
if (since) url.searchParams.set("updatedSince", since);
if (cursor) url.searchParams.set("cursor", cursor);
const page = await fetch(url, { headers }).then((r) => r.json());
for (const attendee of page.data) {
await upsert(attendee); // by attendee.attendeeId
if (!newest || attendee.updatedUtc > newest) newest = attendee.updatedUtc;
}
cursor = page.nextCursor;
} while (cursor);
await saveMark(newest);var since = await LoadMarkAsync(); // null on the first run
var newest = since;
string? cursor = null;
do
{
var query = $"events/b3ac0000-0000-4000-8000-000000000001/attendees?limit=500"
+ (since is null ? "" : $"&updatedSince={Uri.EscapeDataString(since)}")
+ (cursor is null ? "" : $"&cursor={cursor}");
var page = await client.GetFromJsonAsync<JsonElement>(query);
foreach (var attendee in page.GetProperty("data").EnumerateArray())
{
await UpsertAsync(attendee); // by attendeeId
var updated = attendee.GetProperty("updatedUtc").GetString()!;
if (newest is null || string.CompareOrdinal(updated, newest) > 0) newest = updated;
}
cursor = page.GetProperty("nextCursor").GetString();
}
while (cursor is not null);
await SaveMarkAsync(newest);base = "https://attendbeacon.com/api/v1/events/b3ac0000-0000-4000-8000-000000000001/attendees"
headers = {"Authorization": f"Bearer {os.environ['ATTENDBEACON_KEY']}"}
since = load_mark() # None on the first run
newest = since
cursor = None
while True:
params = {"limit": 500}
if since: params["updatedSince"] = since
if cursor: params["cursor"] = cursor
page = requests.get(base, params=params, headers=headers).json()
for attendee in page["data"]:
upsert(attendee) # by attendee["attendeeId"]
if newest is None or attendee["updatedUtc"] > newest:
newest = attendee["updatedUtc"]
cursor = page["nextCursor"]
if cursor is None:
break
save_mark(newest)$base = "https://attendbeacon.com/api/v1/events/b3ac0000-0000-4000-8000-000000000001/attendees"
$since = Get-Content mark.txt -ErrorAction SilentlyContinue # nothing on the first run
$newest = $since
$cursor = $null
do {
$query = "?limit=500"
if ($since) { $query += "&updatedSince=" + [uri]::EscapeDataString($since) }
if ($cursor) { $query += "&cursor=$cursor" }
$page = Invoke-RestMethod -Uri ($base + $query) -Headers $headers
foreach ($attendee in $page.data) {
Save-Attendee $attendee # by attendeeId
if (-not $newest -or $attendee.updatedUtc -gt $newest) { $newest = $attendee.updatedUtc }
}
$cursor = $page.nextCursor
} while ($cursor)
Set-Content mark.txt $newestLas horas son ISO 8601 en UTC con tres decimales y una Z, así que también se comparan correctamente como texto.
Lo que no tiene hora de cambio
Las salas, los itinerarios, los ponentes, los patrocinadores y los niveles de patrocinio son listas pequeñas sin hora de cambio propia: léelas enteras, con el cursor si quieres. Sus recursos individuales y el propio evento responden con un ETag, así que si vuelves a preguntar con If-None-Match se responde 304 Not Modified, sin cuerpo, cuando nada ha cambiado.
Los registros de entrada nunca cambian una vez hechos: recórrelos por cursor, o por after, el último checkInId que tengas, que solo crece.
Eliminaciones y borrado de datos
Una entrada cancelada sigue en la lista con "status": "cancelled", así que una sincronización ve la cancelación. Cuando alguien elimina sus datos desde la app del evento, su asistente se queda con "erased": true y sin nada personal; el webhook attendee.erased te avisa de inmediato para que puedas eliminar tu propia copia. Consulta Privacidad y borrado de datos.
Webhooks o consultas periódicas
Consultar con updatedSince cada pocos minutos es sencillo y lo capta todo. Los webhooks te avisan en segundos. Muchas integraciones hacen las dos cosas: webhooks para la rapidez y una sincronización nocturna para estar seguros.