Begreber
Sider og synkronisering
Lister svarer én side ad gangen med en cursor; updatedSince henter kun det, der er ændret siden sidste kørsel.
Sider og cursorer
Alle lister svarer på samme måde: elementerne, cursoren til næste side, og om der er en.
{
"data": [ { "sessionId": "b3ac0004-0000-4000-8000-000000000000", "title": "Registration and coffee" } ],
"nextCursor": "YzE6MDYzOTI3NjEwODQ4MjgyMDAwMHxiM2Fj...",
"hasMore": true
}Send nextCursor tilbage som cursor, med de samme filtre, indtil den er null. limit angiver sidestørrelsen, 100 som standard og højst 500. En cursor er uigennemsigtig: behold den, som den kom, og byg aldrig en selv. En cursor, som serveren ikke har lavet, afvises med invalidCursor i stedet for at starte forfra i stilhed.
Lister har en fast rækkefølge, den ældste ændring først og id'et som afgørelse ved lighed, så en side aldrig springer over eller gentager et element, der ikke er ændret, mens du læste. Et element, der ændres, mens du bladrer, flytter til slutningen, og du ser det igen, hvilket er det, en synkronisering har brug for.
Deltager-, ordre- og indtjekningslisterne tager også de page og pageSize, de tog, før der fandtes cursorer. De virker stadig; foretræk cursoren, som aldrig tæller.
Synkronisering: kun det, der er ændret
Lister over ting, der ændrer sig, angiver i updatedUtc, hvornår hvert element sidst blev ændret, og tager updatedSince. En synkronisering gemmer den nyeste updatedUtc, den har set, og spørger derfra næste gang:
- Læs alle sider med
updatedSincesat til det, du gemte sidste gang (intet første gang). - Gem eller opdater hvert element ud fra dets id.
- Gem kørslens nyeste
updatedUtctil næste gang.
updatedSince er inklusiv: et element, der er ændret i samme millisekund som dit mærke, kommer igen i stedet for at blive overset. Det skader ikke at opdatere et element, du allerede har; at overse et gør.
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 $newestTidspunkter er ISO 8601 i UTC med tre decimaler og et Z, så de også sammenlignes korrekt som tekst.
Det, der ikke har et ændringstidspunkt
Lokaler, spor, oplægsholdere, sponsorer og sponsorniveauer er små lister uden eget ændringstidspunkt: læs dem helt, gerne med cursoren. Deres enkelte ressourcer og selve eventet svarer med en ETag, så spørger du igen med If-None-Match, svares der 304 Not Modified uden body, når intet er ændret.
Indtjekninger ændres aldrig, når de først er foretaget: bladr gennem dem med cursor eller med after, det seneste checkInId, du har, som kun bliver større.
Annulleringer og slettede data
En annulleret billet bliver i listen med "status": "cancelled", så en synkronisering ser annulleringen. Når en person sletter sine data fra event-appen, bliver deltageren stående med "erased": true og intet personligt; webhooken attendee.erased giver dig besked med det samme, så du kan slette din egen kopi. Se Privatliv og sletning af data.
Webhooks eller polling
Polling med updatedSince med få minutters mellemrum er enkelt og fanger alt. Webhooks giver dig besked inden for sekunder. Mange integrationer gør begge dele: webhooks for hastighedens skyld og en natlig synkronisering for en sikkerheds skyld.