/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/api/src/pollers/open-notify.ts * Purpose: Open Notify poller (1 h) for humans_in_space — extremely failure-tolerant, fixture value stays as fallback */ import { buildStaticRtModel } from "@earth-now/models"; import { type ModelStore, displayHintsOf } from "../store.js"; import { type PollerHandle, NOOP_POLLER, fetchJson, pollersEnabled } from "./poller.js"; const METRIC_ID = "humans_in_space"; const SOURCE_ID = "open_notify"; const ASTROS_URL = "http://api.open-notify.org/astros.json"; // The API is known to be flaky: only flag the metric stale after repeated failures. const MAX_CONSECUTIVE_FAILURES = 3; export const OPEN_NOTIFY_POLL_INTERVAL_MS = 60 * 60_000; async function pollOnce(store: ModelStore): Promise { const data = await fetchJson(ASTROS_URL); const count = (data as { number?: unknown }).number; if (typeof count !== "number" || !Number.isInteger(count) || count < 0 || count > 100) { throw new Error("astros.json malformed or implausible 'number' field"); } const entry = store.registry.byId.get(METRIC_ID); if (!entry) throw new Error(`registry has no '${METRIC_ID}' entry`); const nowIso = new Date().toISOString(); store.updateRtModel( METRIC_ID, buildStaticRtModel( { metricId: METRIC_ID, sourceId: SOURCE_ID, observedAt: nowIso, displayHints: displayHintsOf(entry), }, { value: count, at: nowIso }, ), ); } export function startOpenNotifyPoller( store: ModelStore, intervalMs: number = OPEN_NOTIFY_POLL_INTERVAL_MS, ): PollerHandle { if (!pollersEnabled()) return NOOP_POLLER; let consecutiveFailures = 0; const tick = (): void => { void pollOnce(store) .then(() => { consecutiveFailures = 0; }) .catch((err: unknown) => { consecutiveFailures += 1; if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) store.markStale(METRIC_ID); console.error( `[open-notify] poll failed ${consecutiveFailures}× (fixture/previous value keeps serving): ${String(err)}`, ); }); }; tick(); const timer = setInterval(tick, intervalMs); timer.unref(); return { stop: () => clearInterval(timer) }; }