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