Ccuishiying修改文档
594fd9d1Add on Sep 9History
import { sleep } from "./utils";
import { StatusAnalysis, SyncPipelineStatus } from "./types";
import { isStatusFinal, analyzeStatus } from "./status-analyzer";

export interface PollerConfig {
    pollIntervalMs: number;
    timeoutMinutes: number;
    /** 设置后,首次查询前先等待该时长(用于等待流水线记录生成);未设置时不等待 */
    firstPollDelayMs?: number;
    /** 设置后,查询返回 null(无流水线记录)即以该状态立即结束轮询;未设置时 null 视为未完成继续轮询 */
    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;
  /** 轮询期间最后一次返回的错误码(extension.errorCode) */
  errorCode: string | null;
  /** 轮询期间最后一次返回的构建详情链接(extension.buildDetailUrl) */
  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) {
        // 规范为大写枚举(APIG 可能返回小写 failure/success),保证 outputs 与文档契约一致
        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,
    };
  }
}