typhon/src/services/ClimatePollingService.ts

72 lines
2.1 KiB
TypeScript

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<void> {
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" };
}
}