Conceitos
Paginação e sincronização
As listas respondem uma página de cada vez com um cursor; updatedSince obtém apenas o que mudou desde a última execução.
Páginas e cursores
Todas as listas respondem da mesma forma: os itens, o cursor para a página seguinte e se ela existe.
{
"data": [ { "sessionId": "b3ac0004-0000-4000-8000-000000000000", "title": "Registration and coffee" } ],
"nextCursor": "YzE6MDYzOTI3NjEwODQ4MjgyMDAwMHxiM2Fj...",
"hasMore": true
}Devolva nextCursor como cursor, com os mesmos filtros, até ser null. limit define o tamanho da página: 100 por predefinição e 500 no máximo. Um cursor é opaco: guarde-o tal como veio e não construa um por sua conta. Um cursor que o servidor não criou é recusado com invalidCursor, em vez de recomeçar silenciosamente.
As listas têm uma ordem fixa, a alteração mais antiga primeiro, com o id a desempatar, por isso uma página nunca salta nem repete um item que não mudou enquanto lê. Um item que muda enquanto pagina passa para o fim e volta a vê-lo, que é o que uma sincronização quer.
As listas de participantes, encomendas e registos de entrada também aceitam os page e pageSize que aceitavam antes de existirem cursores. Continuam a funcionar; prefira o cursor, que nunca conta.
Sincronizar: só o que mudou
As listas de coisas que mudam indicam quando cada item mudou pela última vez, em updatedUtc, e aceitam updatedSince. Uma sincronização guarda o updatedUtc mais recente que viu e pergunta a partir daí da vez seguinte:
- Leia todas as páginas com
updatedSincedefinido para o que guardou da última vez (nada na primeira vez). - Guarde ou atualize cada item pelo seu id.
- Guarde o
updatedUtcmais recente da execução para a próxima vez.
updatedSince é inclusivo: um item alterado no mesmo milissegundo da sua marca volta a aparecer em vez de se perder. Atualizar um item que já tem não faz mal; perder um, sim.
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 $newestAs horas são ISO 8601 em UTC com três casas decimais e um Z, por isso também se comparam corretamente como texto.
O que não tem hora de alteração
Salas, áreas temáticas, oradores, patrocinadores e níveis de patrocínio são listas pequenas sem hora de alteração própria: leia-as por inteiro, com o cursor se quiser. Os seus recursos individuais e o próprio evento respondem com um ETag, por isso perguntar de novo com If-None-Match responde 304 Not Modified, sem corpo, quando nada mudou.
Os registos de entrada nunca mudam depois de feitos: percorra-os por cursor, ou por after, o último checkInId que tem, que só aumenta.
Cancelamentos e eliminação
Um bilhete cancelado continua na lista com "status": "cancelled", para que uma sincronização veja o cancelamento. Quando alguém elimina os seus dados na app do evento, o seu participante mantém-se com "erased": true e sem nada pessoal; o webhook attendee.erased avisa-o de imediato, para que possa eliminar a sua própria cópia. Consulte Privacidade e eliminação.
Webhooks ou consultas periódicas
Consultar com updatedSince a cada poucos minutos é simples e apanha tudo. Os webhooks avisam em segundos. Muitas integrações fazem as duas coisas: webhooks pela rapidez, uma sincronização noturna por segurança.