Begrepp
Sidindelning och synkronisering
Listor svarar en sida i taget med en markör; updatedSince hämtar bara det som har ändrats sedan din senaste körning.
Sidor och markörer
Varje lista svarar på samma sätt: posterna, markören för nästa sida och om det finns en.
{
"data": [ { "sessionId": "b3ac0004-0000-4000-8000-000000000000", "title": "Registration and coffee" } ],
"nextCursor": "YzE6MDYzOTI3NjEwODQ4MjgyMDAwMHxiM2Fj...",
"hasMore": true
}Skicka tillbaka nextCursor som cursor, med samma filter, tills den är null. limit anger sidstorleken, 100 som standard och högst 500. En markör är ogenomskinlig: behåll den som den kom och bygg aldrig en själv. En markör som servern inte har skapat avvisas med invalidCursor i stället för att i tysthet börja om.
Listorna har en fast ordning, den äldsta ändringen först, med id:t som avgör vid lika, så en sida hoppar aldrig över eller upprepar en post som inte ändrades medan du läste. En post som ändras medan du bläddrar flyttas till slutet och du ser den igen, vilket är precis vad en synkronisering vill.
Listorna över deltagare, beställningar och incheckningar tar också page och pageSize, som de tog innan markörerna fanns. De fungerar fortfarande; använd hellre markören, som aldrig räknar.
Synkronisera: bara det som har ändrats
Listor med saker som ändras anger när varje post senast ändrades, i updatedUtc, och tar updatedSince. En synkronisering sparar det senaste updatedUtc den såg och frågar därifrån nästa gång:
- Läs varje sida med
updatedSincesatt till det du sparade förra gången (inget första gången). - Spara eller uppdatera varje post utifrån dess id.
- Spara körningens senaste
updatedUtctill nästa gång.
updatedSince är inkluderande: en post som ändrades under samma millisekund som ditt märke kommer igen i stället för att missas. Att uppdatera en post du redan har är ofarligt; att missa en är det inte.
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 $newestTider anges i ISO 8601 i UTC med tre decimaler och ett Z, så de jämförs också korrekt som text.
Det som saknar ändringstid
Rum, spår, talare, sponsorer och sponsornivåer är små listor utan egen ändringstid: läs dem i sin helhet, gärna med markören. Deras enskilda resurser och själva evenemanget svarar med en ETag, så om du frågar igen med If-None-Match blir svaret 304 Not Modified, utan brödtext, när inget har ändrats.
Incheckningar ändras aldrig när de väl har gjorts: bläddra igenom dem med cursor, eller med after, det senaste checkInId du har, som bara växer.
Borttagningar och radering
En avbokad biljett ligger kvar i listan med "status": "cancelled", så att en synkronisering ser avbokningen. När någon raderar sina uppgifter i evenemangsappen ligger deltagaren kvar med "erased": true och inget personligt; webhooken attendee.erased meddelar dig direkt, så att du kan radera din egen kopia. Se Integritet och radering.
Webhooks eller att fråga
Att fråga med updatedSince med några minuters mellanrum är enkelt och fångar allt. Webhooks meddelar dig inom några sekunder. Många integrationer gör båda: webhooks för snabbheten, en nattlig synkronisering för säkerhets skull.