import { sleep } from "./utils";
import { StatusAnalysis, SyncPipelineStatus } from "./types";
import { isStatusFinal, analyzeStatus } from "./status-analyzer";
export interface PollerConfig {
pollIntervalMs: number;
timeoutMinutes: number;
firstPollDelayMs?: number;
nullRecordStatus?: string;
}
const NULL_RECORD_REASON = "未找到代码仓同步流水线记录";
export interface PollerCallbacks {
onStatusChange: (
analysis: StatusAnalysis,
info: SyncPipelineStatus,
) => void | Promise<void>;
onProgress: (elapsedSeconds: number, pollCount: number) => void;
}
export interface PollerResult {
finalStatus: string;
syncPipelineUrl: string;
errorCode: string | null;
buildDetailUrl: string | null;
failReason: string | null;
totalPolls: number;
elapsedSeconds: number;
records: Array<{
time: string;
status: string;
pipelineStatus: string | undefined;
}>;
}
const APIG_RETRY_MAX = 3;
export class Poller {
private config: PollerConfig;
private startTime: number;
private apiFailCount = 0;
constructor(config: PollerConfig) {
this.config = config;
this.startTime = Date.now();
}
getElapsedSeconds(): number {
return Math.floor((Date.now() - this.startTime) / 1000);
}
isTimedOut(): boolean {
return this.getElapsedSeconds() > this.config.timeoutMinutes * 60;
}
private currentTimestamp(): string {
const now = new Date();
const cst = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const y = cst.getUTCFullYear();
const mo = String(cst.getUTCMonth() + 1).padStart(2, "0");
const d = String(cst.getUTCDate()).padStart(2, "0");
const h = String(cst.getUTCHours()).padStart(2, "0");
const m = String(cst.getUTCMinutes()).padStart(2, "0");
const s = String(cst.getUTCSeconds()).padStart(2, "0");
return `${y}-${mo}-${d} ${h}:${m}:${s}`;
}
async run(
queryFn: () => Promise<SyncPipelineStatus | null>,
callbacks: PollerCallbacks,
): Promise<PollerResult> {
const records: Array<{
time: string;
status: string;
pipelineStatus: string | undefined;
}> = [];
let finalStatus = "TIMEOUT";
let syncPipelineUrl = "";
let errorCode: string | null = null;
let buildDetailUrl: string | null = null;
let failReason: string | null = null;
let totalPolls = 0;
const firstPollDelayMs = this.config.firstPollDelayMs ?? 0;
if (firstPollDelayMs > 0) {
await sleep(firstPollDelayMs);
}
while (true) {
totalPolls++;
callbacks.onProgress(this.getElapsedSeconds(), totalPolls);
let info: SyncPipelineStatus | null;
try {
info = await queryFn();
this.apiFailCount = 0;
} catch (e) {
this.apiFailCount++;
const errMsg = e instanceof Error ? e.message : String(e);
if (this.apiFailCount >= APIG_RETRY_MAX) {
finalStatus = "FAILURE";
failReason = "APIG调用连续失败";
break;
}
await sleep(this.config.pollIntervalMs);
continue;
}
if (info === null) {
if (this.config.nullRecordStatus) {
finalStatus = this.config.nullRecordStatus;
failReason = NULL_RECORD_REASON;
break;
}
await sleep(this.config.pollIntervalMs);
continue;
}
if (info.syncPipelineUrl) {
syncPipelineUrl = info.syncPipelineUrl;
}
if (info.errorCode) {
errorCode = info.errorCode;
}
if (info.buildDetailUrl) {
buildDetailUrl = info.buildDetailUrl;
}
const analysis = analyzeStatus(
info.status,
info.pipelineStatus,
info.failReason,
);
records.push({
time: this.currentTimestamp(),
status: info.status,
pipelineStatus: info.pipelineStatus,
});
await callbacks.onStatusChange(analysis, info);
if (analysis.isFinal) {
finalStatus = info.status.toUpperCase();
failReason = info.failReason;
break;
}
if (this.isTimedOut()) {
records.push({
time: this.currentTimestamp(),
status: "TIMEOUT",
pipelineStatus: undefined,
});
break;
}
await sleep(this.config.pollIntervalMs);
}
return {
finalStatus,
syncPipelineUrl,
errorCode,
buildDetailUrl,
failReason,
totalPolls,
elapsedSeconds: this.getElapsedSeconds(),
records,
};
}
}