Den Katalog spiegeln und ehrlich halten
Eine Kopie ist erlaubt und oft richtig. Dies ist die Reihenfolge: befüllen, Änderungen holen, revalidieren und abgleichen, was die ersten drei verpasst haben.
Brauchen Sie überhaupt eine Kopie?
Ein Spiegel ist eine zweite Datenbank, die ehrlich gehalten werden muss. Nehmen Sie ihn nur, wenn Sie etwas brauchen, das die API nicht für Sie tun kann.
- Sie brauchen ihn, wenn Sie unsere Daten mit Ihren verbinden (Bestand, Preise, Besitz) oder so abfragen, wie es kein Endpunkt ausdrückt.
- Für Geschwindigkeit allein brauchen Sie ihn nicht. Datensätze sind stundenlang cachebar, und ein wiederholter Lesezugriff kostet nichts vom Kontingent; sehen Sie zuerst Caching.
- Auch als Ausfallschutz nicht. Ein Cache der Seiten, die Sie tatsächlich ausliefern, leistet das mit einem Bruchteil des Aufwands.
Die Reihenfolge, die keine Zeilen verliert
Jeder Schritt schließt eine Lücke des vorherigen. Einen auszulassen ist der Grund, warum ein Spiegel abdriftet.
- Einmal befüllenDen Katalog in Erstellungsreihenfolge durchlaufen und speichern, samt Cursor, an dem Sie stehen geblieben sind.
- Vorher abonnierenDen Webhook-Endpunkt registrieren, bevor der Durchlauf endet. Sonst geht ein zwischenzeitlich geänderter Datensatz zwischen beiden verloren.
- Änderungen per Webhook holenEreignisse zu neuen, geänderten und gelöschten Datensätzen treffen binnen etwa einer Viertelstunde ein und tragen den Datensatz.
- Regelmäßig revalidierenDen Bestand mit dem gespeicherten ETag prüfen. Unveränderte Datensätze antworten 304 und kosten nichts.
- Periodisch abgleichenIn langem Takt erneut durchlaufen, um alles zu fangen, was verloren ging, während Ihr Endpunkt nicht erreichbar war.
Der erste Durchlauf
Nach Erstellung sortieren, nicht nach Namen. Ein währenddessen angelegter Datensatz landet dann am Ende, statt eine bereits gelesene Seite zu verschieben. Die Seitentiefe ist je Tarif begrenzt, studio, enterprise ohne Obergrenze; die Paginierungsseite nennt die Grenzen.
// Walk the catalog once, in creation order, and remember where// you stopped. Ordering by `created_at` means a record added// mid-walk lands at the end rather than shifting a page you have// already read. A name-ordered walk silently skips rows.let page = 1let newest = loadCursor() // the created_at you last stored, or null for (;;) { const url = new URL('https://api.gunspec.io/v1/firearms') url.searchParams.set('sort', 'created_at') url.searchParams.set('order', 'asc') url.searchParams.set('per_page', '100') url.searchParams.set('page', String(page)) // Only what is new since the last run. On a first run, omitted. if (newest) url.searchParams.set('created_after', newest) const res = await fetch(url, { headers: { 'X-API-Key': key } }) const { data, pagination } = await res.json() if (data.length === 0) break await upsertAll(data) newest = data[data.length - 1].createdAt saveCursor(newest) // A short page is the last page. `pagination.totalPages` is not // sent to every plan, so ending on it works on some keys and // loops forever on others. if (data.length < pagination.per_page) break page++}Änderungen kommen als Webhooks
Die Datenbank selbst verzeichnet, was sich geändert hat, und ein Job leert diese Liste viertelstündlich. So erreicht Sie auch eine Änderung aus einem Import, den Sie nie sehen. Die Webhook-Seite listet alle Ereignisse und die Zustellregeln.
| Ereignis | Was es für Ihre Kopie bedeutet |
|---|---|
firearm.created | Ein Datensatz, den Sie nicht haben. Einfügen; das Ereignis trägt ihn so, wie die API ihn liefern würde. |
firearm.updated | Ein ausgeliefertes Feld hat sich geändert. Kopie ersetzen und die neue Version speichern. |
firearm.deleted | Der Datensatz ist fort. Die Nutzlast ist allein die ID; mehr hat eine Löschung nicht zu sagen. |
firearm.confidence.changed | Der Datensatz ist derselbe, seine Belegbarkeit nicht. Alles neu rendern, was Konfidenz anzeigt. |
image.created | Neue Medien zu einem Datensatz, den Sie vielleicht schon halten. Die Medienliste auffrischen, nicht den Datensatz. |
// Your endpoint. Verify first, answer 2xx fast, work afterwards.import { verifyWebhookSignature } from '@gunspec/sdk' export async function POST(request) { const raw = await request.text() // the exact bytes, not a re-serialised object try { await verifyWebhookSignature(raw, request.headers.get('X-Webhook-Signature'), secret) } catch { // Unsigned or stale. Never act on it, and do not 200 it either. return new Response('bad signature', { status: 401 }) } const event = JSON.parse(raw) // Deliveries retry, so the same event can arrive twice. Key the // work on the event id and make the second one a no-op. await enqueueOnce(event.id, event) // Acknowledge now; fetch the record on your own time. A handler // that fetches before answering is a handler that times out and // gets redelivered. return new Response(null, { status: 204 })}Revalidieren ist die günstige Hälfte
Senden Sie das gespeicherte ETag als If-None-Match. Ein unveränderter Datensatz antwortet 304 ohne Rumpf, und ein 304 zählt nicht gegen Ihr Tageslimit. Ein kompletter Spiegeldurchlauf kostet damit einen Bruchteil eines einzigen Neu-Downloads. Caching und Aktualisierung erklärt, welcher Wert wofür zu speichern ist.
// A record you hold, checked without downloading it again.const res = await fetch(`${API}/v1/firearms/${row.id}`, { headers: { 'X-API-Key': key, ...(row.etag ? { 'If-None-Match': row.etag } : {}) },}) // Unchanged. No body crossed the wire and a 304 is not counted// against your daily cap, so a nightly sweep of the whole mirror// costs a fraction of one re-download.if (res.status === 304) return row const { data } = await res.json()// Store `version` beside the record: it is a hash of the record's// own fields, so it survives a plan change and a response-shape// change that would both move the ETag.return { ...data, etag: res.headers.get('ETag'), version: data.version }Abgleichen, weil Zustellungen scheitern
Ihr Endpunkt wird irgendwann nicht erreichbar sein, und Wiederholungen dauern nicht ewig. Ein langsamer Volldurchlauf macht daraus eine Verzögerung statt eines Datenverlusts.
- Deduplizieren Sie über die Ereignis-ID, nicht über die Zustell-ID: eine Wiederholung sendet dasselbe Ereignis, die Zustell-ID ist jedes Mal neu.
- Antworten Sie
2xx, bevor Sie arbeiten. Ein Handler, der zuerst den Datensatz holt, läuft in den Timeout und bekommt eine erneute Zustellung. - Was Ihre Kopie führt und der Katalog nicht mehr ausliefert, gehört beim Abgleich gelöscht statt versteckt. Ein nach seinem Löschereignis behaltener Datensatz ist genau der, der auf einer Seite landet.