import type { QuoteDetail } from "@/lib/frontend/types"; import { POLL_INTERVAL_MS, POLL_TIMEOUT_MS, } from "@/lib/frontend/constants"; import { formatQuoteErrorMessage } from "@/modules/quote/quote-error-messages"; export type PollFetchQuote = () => Promise<{ ok: boolean; data?: QuoteDetail; errorMessage?: string; }>; export type PollResult = | { type: "done"; quote: QuoteDetail } | { type: "timeout" } | { type: "error"; message: string; quote?: QuoteDetail }; const BASE_BACKOFF_MS = 500; /** 轮询超过该时长后缩短间隔,加快感知 RPA 完成 */ const POLL_FAST_AFTER_MS = 10_000; const POLL_FAST_INTERVAL_MS = 800; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** 按 wall-clock 轮询至 timeoutMs(默认与 POLL_TIMEOUT_MS 一致) */ export async function pollQuoteUntilDone( fetchQuote: PollFetchQuote, options?: { timeoutMs?: number; fastAfterMs?: number; fastIntervalMs?: number; /** processing 时每次轮询回调(用于实时进度) */ onProcessing?: (quote: QuoteDetail) => void; }, ): Promise { let networkFailures = 0; const startedAt = Date.now(); const timeoutMs = options?.timeoutMs ?? POLL_TIMEOUT_MS; const fastAfterMs = options?.fastAfterMs ?? POLL_FAST_AFTER_MS; const fastIntervalMs = options?.fastIntervalMs ?? POLL_FAST_INTERVAL_MS; const deadline = Date.now() + timeoutMs; function pollIntervalMs(elapsedMs: number): number { if (elapsedMs >= fastAfterMs) { return Math.min(fastIntervalMs, POLL_INTERVAL_MS); } return POLL_INTERVAL_MS; } while (Date.now() < deadline) { const result = await fetchQuote(); if (!result.ok) { if (result.errorMessage?.includes("询价结果接口不可用")) { return { type: "error", message: result.errorMessage }; } networkFailures += 1; const backoff = Math.min( BASE_BACKOFF_MS * 2 ** (networkFailures - 1), POLL_INTERVAL_MS, ); const remaining = deadline - Date.now(); if (remaining <= 0) { break; } await sleep(Math.min(backoff, remaining)); continue; } networkFailures = 0; const quote = result.data!; if (quote.status === "processing") { options?.onProcessing?.(quote); const remaining = deadline - Date.now(); if (remaining <= 0) { break; } const interval = pollIntervalMs(Date.now() - startedAt); await sleep(Math.min(interval, remaining)); continue; } if (quote.status === "failed") { return { type: "error", message: formatQuoteErrorMessage( quote.error_code, quote.error_message, ), quote, }; } return { type: "done", quote }; } return { type: "timeout" }; }