SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
3.4 KB · 91 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    apps/api/src/pollers/usgs.ts6 * Purpose: USGS FDSN poller (5 min) — true event-driven RT for earthquakes_24h, never interpolated, never crashes the server7 */89import { buildStaticRtModel } from "@earth-now/models";10import { type ModelStore, type QuakesSnapshot, displayHintsOf } from "../store.js";11import { type PollerHandle, NOOP_POLLER, fetchJson, pollersEnabled } from "./poller.js";1213const METRIC_ID = "earthquakes_24h";14const SOURCE_ID = "usgs_fdsn";15const BASE = "https://earthquake.usgs.gov/fdsnws/event/1";16const HOUR_MS = 3_600_000;1718export const USGS_POLL_INTERVAL_MS = 5 * 60_000;1920function readCount(data: unknown): number {21  const count = (data as { count?: unknown }).count;22  if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {23    throw new Error("USGS count response malformed");24  }25  return count;26}2728function readLastMajor(data: unknown): QuakesSnapshot["lastMajor"] {29  const features = (data as { features?: unknown }).features;30  if (!Array.isArray(features) || features.length === 0) return null;31  const props = (features[0] as { properties?: { mag?: unknown; place?: unknown; time?: unknown } })32    .properties;33  if (!props || typeof props.mag !== "number" || typeof props.time !== "number") return null;34  return {35    mag: props.mag,36    place: typeof props.place === "string" ? props.place : "unknown location",37    timeIso: new Date(props.time).toISOString(),38  };39}4041async function pollOnce(store: ModelStore): Promise<void> {42  const nowMs = Date.now();43  const nowIso = new Date(nowMs).toISOString();44  const countUrl = `${BASE}/count?format=geojson&starttime=${new Date(nowMs - 24 * HOUR_MS).toISOString()}&minmagnitude=2.5`;45  const majorUrl = `${BASE}/query?format=geojson&starttime=${new Date(nowMs - 7 * 24 * HOUR_MS).toISOString()}&minmagnitude=5&orderby=time&limit=1`;4647  const [countRes, majorRes] = await Promise.allSettled([fetchJson(countUrl), fetchJson(majorUrl)]);48  if (countRes.status === "rejected") {49    throw new Error(`USGS count fetch failed: ${String(countRes.reason)}`);50  }51  const count24h = readCount(countRes.value);52  // The "last major quake" leg is optional — keep the previous one on failure.53  const lastMajor =54    majorRes.status === "fulfilled" ? readLastMajor(majorRes.value) : store.quakes.lastMajor;5556  const entry = store.registry.byId.get(METRIC_ID);57  if (!entry) throw new Error(`registry has no '${METRIC_ID}' entry`);5859  store.updateRtModel(60    METRIC_ID,61    buildStaticRtModel(62      {63        metricId: METRIC_ID,64        sourceId: SOURCE_ID,65        observedAt: nowIso,66        displayHints: displayHintsOf(entry),67      },68      { value: count24h, at: nowIso },69    ),70  );71  store.setQuakesSnapshot({ count24h, lastMajor, fetchedAt: nowIso });72}7374export function startUsgsPoller(75  store: ModelStore,76  intervalMs: number = USGS_POLL_INTERVAL_MS,77): PollerHandle {78  if (!pollersEnabled()) return NOOP_POLLER;79  const tick = (): void => {80    void pollOnce(store).catch((err: unknown) => {81      // Keep serving the previous model — a counter must never freeze silently or show NaN.82      store.markStale(METRIC_ID);83      console.error(`[usgs] poll failed (previous model keeps serving): ${String(err)}`);84    });85  };86  tick();87  const timer = setInterval(tick, intervalMs);88  timer.unref();89  return { stop: () => clearInterval(timer) };90}91