GunSpec
Praxis

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.

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.

Jeder Schritt schließt eine Lücke des vorherigen. Einen auszulassen ist der Grund, warum ein Spiegel abdriftet.

  1. Einmal befüllenDen Katalog in Erstellungsreihenfolge durchlaufen und speichern, samt Cursor, an dem Sie stehen geblieben sind.
  2. Vorher abonnierenDen Webhook-Endpunkt registrieren, bevor der Durchlauf endet. Sonst geht ein zwischenzeitlich geänderter Datensatz zwischen beiden verloren.
  3. Änderungen per Webhook holenEreignisse zu neuen, geänderten und gelöschten Datensätzen treffen binnen etwa einer Viertelstunde ein und tragen den Datensatz.
  4. Regelmäßig revalidierenDen Bestand mit dem gespeicherten ETag prüfen. Unveränderte Datensätze antworten 304 und kosten nichts.
  5. Periodisch abgleichenIn langem Takt erneut durchlaufen, um alles zu fangen, was verloren ging, während Ihr Endpunkt nicht erreichbar war.

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.

backfill.ts
javascript
// 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++}

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.

EreignisWas es für Ihre Kopie bedeutet
firearm.createdEin Datensatz, den Sie nicht haben. Einfügen; das Ereignis trägt ihn so, wie die API ihn liefern würde.
firearm.updatedEin ausgeliefertes Feld hat sich geändert. Kopie ersetzen und die neue Version speichern.
firearm.deletedDer Datensatz ist fort. Die Nutzlast ist allein die ID; mehr hat eine Löschung nicht zu sagen.
firearm.confidence.changedDer Datensatz ist derselbe, seine Belegbarkeit nicht. Alles neu rendern, was Konfidenz anzeigt.
image.createdNeue Medien zu einem Datensatz, den Sie vielleicht schon halten. Die Medienliste auffrischen, nicht den Datensatz.
webhook-endpoint.ts
javascript
// 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 })}

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.

revalidate.ts
javascript
// 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 }

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.