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%
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