58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
/**
|
||
* 轮询器(指数退避+超时+可取消)
|
||
*
|
||
*/
|
||
|
||
import { PaymentApi } from "./PaymentApi";
|
||
import type { QueryOrderData } from "./types";
|
||
import { ApiCode } from "../network/client/types";
|
||
|
||
export interface PollOptions {
|
||
maxDurationMs: number; // 轮询总时长(例如 2分钟)
|
||
startIntervalMs: number; // 初始间隔(例如 2s)
|
||
maxIntervalMs: number; // 最大间隔(例如 10s)
|
||
jitter?: boolean; // 抖动
|
||
}
|
||
|
||
export class PaymentPoller {
|
||
private cancelled = false;
|
||
|
||
// 取消轮询
|
||
cancel() { this.cancelled = true; }
|
||
|
||
// 轮询订单
|
||
async poll(orderId: string, opt: PollOptions, onTick?: (d: QueryOrderData) => void): Promise<QueryOrderData | null> {
|
||
const t0 = Date.now();
|
||
let interval = opt.startIntervalMs;
|
||
while (!this.cancelled) {
|
||
// 查询
|
||
const r = await PaymentApi.I.queryOrder(orderId);
|
||
if (r.code === ApiCode.OK && r.data) {
|
||
// 回调
|
||
onTick?.(r.data);
|
||
if (["SUCCESS", "FAILED", "CANCELED", "EXPIRED"].indexOf(r.data.status) !== -1) {
|
||
return r.data;
|
||
}
|
||
}
|
||
// 退出条件
|
||
if (Date.now() - t0 > opt.maxDurationMs) return null;
|
||
// 等待
|
||
await this.sleep(this.jitter(interval, opt));
|
||
// 每次轮询后,interval 会按照 1.5 倍增长,直到达到最大间隔 maxIntervalMs,避免在短时间内重复请求
|
||
interval = Math.min(interval * 1.5, opt.maxIntervalMs);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 用来暂停一定的时间(ms 毫秒),并让轮询进入等待状态
|
||
private sleep(ms: number) { return new Promise(res => setTimeout(res, ms)); }
|
||
|
||
// 用于实现抖动(随机化轮询间隔)
|
||
private jitter(base: number, opt: PollOptions) {
|
||
if (!opt.jitter) return base;
|
||
const delta = Math.min(500, Math.max(100, base * 0.1));
|
||
return base + (Math.random() * 2 - 1) * delta;
|
||
}
|
||
}
|
||
export default PaymentPoller;
|