import { AcInfinityApiClient, AcInfinityApiError } from "../http/AcInfinityApiClient"; import { TyphonMetrics } from "../metrics/TyphonMetrics"; import { Logger } from "../observability/Logger"; export class ClimatePollingService { private timer: NodeJS.Timeout | null = null; private inFlight = false; public constructor( private readonly client: AcInfinityApiClient, private readonly metrics: TyphonMetrics, private readonly logger: Logger, private readonly pollIntervalSeconds: number ) {} public start(): void { void this.pollOnce(); this.timer = setInterval(() => { void this.pollOnce(); }, this.pollIntervalSeconds * 1000); } public stop(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } } private async pollOnce(): Promise { if (this.inFlight) { this.logger.warn("skipping poll because previous poll is still running"); return; } this.inFlight = true; try { const snapshot = await this.client.fetchSnapshot(); this.metrics.updateFromSnapshot(snapshot); this.logger.info("poll succeeded", { controller_count: snapshot.controllers.length }); } catch (error) { const { reason, code } = this.classifyError(error); this.metrics.markPollFailure(reason, code); this.logger.error("poll failed", { reason, code, error_message: error instanceof Error ? error.message : String(error) }); } finally { this.metrics.refreshDataAgeGauge(); this.inFlight = false; } } private classifyError(error: unknown): { reason: string; code: string } { if (error instanceof AcInfinityApiError) { if (error.apiCode === "10001" || error.apiCode === "invalid_auth") { return { reason: "auth", code: error.apiCode }; } return { reason: "api", code: error.apiCode }; } if (error instanceof Error && error.name === "AbortError") { return { reason: "timeout", code: "timeout" }; } return { reason: "unknown", code: "unknown" }; } }