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.6 KB · 90 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    apps/ingest/src/sources/usgs.ts6 * Purpose: USGS FDSN event service — count endpoint fetcher and pure GeoJSON parsers (event-driven, no interpolation)7 */89import { type FetchLike, defaultFetch } from "../fetch-like.js";1011export const USGS_FDSN_BASE = "https://earthquake.usgs.gov/fdsnws/event/1";1213/** Build the count-endpoint URL for the trailing window. nowMs is a parameter so tests stay deterministic. */14export function usgsCountUrl(15  windowHours: number,16  minMagnitude: number,17  nowMs: number = Date.now(),18): string {19  const start = new Date(nowMs - windowHours * 3_600_000).toISOString();20  return (21    `${USGS_FDSN_BASE}/count?format=geojson` +22    `&starttime=${encodeURIComponent(start)}&minmagnitude=${minMagnitude}`23  );24}2526/**27 * Pure parser for the count endpoint payload: { count: n, maxAllowed: m }.28 * Throws a descriptive error on anything malformed.29 */30export function parseUsgsCount(json: unknown): number {31  if (typeof json !== "object" || json === null)32    throw new Error("USGS count parse error: payload is not an object");33  const count = (json as { count?: unknown }).count;34  if (typeof count !== "number" || !Number.isInteger(count) || count < 0)35    throw new Error(36      `USGS count parse error: 'count' must be a non-negative integer, got ${JSON.stringify(count)}`,37    );38  return count;39}4041export interface UsgsLastMajor {42  mag: number;43  place: string;44  timeIso: string;45}4647/**48 * Pure parser for the query endpoint (orderby=time&limit=1): features[0] → last major quake.49 * Throws a descriptive error on malformed GeoJSON.50 */51export function parseUsgsLastMajor(geojson: unknown): UsgsLastMajor {52  if (typeof geojson !== "object" || geojson === null)53    throw new Error("USGS query parse error: payload is not an object");54  const features = (geojson as { features?: unknown }).features;55  if (!Array.isArray(features) || features.length === 0)56    throw new Error("USGS query parse error: 'features' is missing or empty");57  const first = features[0] as { properties?: unknown };58  if (typeof first !== "object" || first === null || typeof first.properties !== "object" || first.properties === null)59    throw new Error("USGS query parse error: features[0].properties is missing");60  const props = first.properties as { mag?: unknown; place?: unknown; time?: unknown };61  if (typeof props.mag !== "number" || !Number.isFinite(props.mag))62    throw new Error("USGS query parse error: features[0].properties.mag is not a finite number");63  if (typeof props.place !== "string" || props.place.length === 0)64    throw new Error("USGS query parse error: features[0].properties.place is not a string");65  if (typeof props.time !== "number" || !Number.isFinite(props.time))66    throw new Error(67      "USGS query parse error: features[0].properties.time is not a millisecond timestamp",68    );69  return { mag: props.mag, place: props.place, timeIso: new Date(props.time).toISOString() };70}7172/** Fetch the earthquake count for the trailing window (M >= minMagnitude). Inject fetchImpl in tests. */73export async function fetchUsgsQuakeCount(74  windowHours: number,75  minMagnitude: number,76  fetchImpl: FetchLike = defaultFetch,77): Promise<number> {78  const url = usgsCountUrl(windowHours, minMagnitude);79  const res = await fetchImpl(url);80  if (!res.ok) throw new Error(`USGS count fetch failed: HTTP ${res.status} for ${url}`);81  const text = await res.text();82  let json: unknown;83  try {84    json = JSON.parse(text);85  } catch {86    throw new Error("USGS count fetch failed: response is not valid JSON");87  }88  return parseUsgsCount(json);89}90