Files
photo-live/apps/admin/src/utils/system/syncPolling.ts
T

93 lines
2.7 KiB
TypeScript
Raw Normal View History

/**
* 用户同步页轮询逻辑(纯函数/可注入依赖,vitest 用 fake timers 覆盖)
*
* - waitSyncTerminal:手动补拉 202 受理后等待批次离开 running(间隔轮询 +
* 超时上限;终态即 resolve,避免页面无限转圈)
* - createAutoRefresh:页面打开期间的定时状态刷新(10s);立即执行一次 +
* 间隔触发 + 重入保护(慢响应不堆叠)+ 幂等 start/stop(组件
* onUnmounted 清理,防路由离开后仍在请求)
*/
export interface WaitSyncOptions {
/** 轮询间隔(默认 1.5s:批次通常秒级完成) */
intervalMs?: number
/** 总超时(默认 45s:超过视为异常,交调用方展示错误) */
timeoutMs?: number
}
const DEFAULT_INTERVAL_MS = 1500
const DEFAULT_TIMEOUT_MS = 45_000
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* 轮询同步状态直到批次离开 running(首次查询即终态则立刻 resolve)。
* 超时 reject(Error.message = 'timeout');fetchStatus 的网络错误向上抛。
*/
export function waitSyncTerminal(
fetchStatus: () => Promise<Api.UserSync.Status>,
options: WaitSyncOptions = {}
): Promise<Api.UserSync.Status> {
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
const deadline = Date.now() + timeoutMs
return (async function poll(): Promise<Api.UserSync.Status> {
const status = await fetchStatus()
if (status.state !== 'running') return status
if (Date.now() >= deadline) {
throw new Error('timeout')
}
await delay(intervalMs)
return poll()
})()
}
export interface AutoRefreshHandle {
/** 开始(立即执行一次 + 间隔触发;重复调用幂等) */
start: () => void
/** 停止并清理定时器(重复调用幂等) */
stop: () => void
/** 是否在刷新周期内 */
isRunning: () => boolean
}
/**
* 周期刷新:start 立即触发一次;执行中的回调未完成时不堆叠下一次。
* 返回的 handle 由组件持有,onUnmounted 时 stop()。
*/
export function createAutoRefresh(
fn: () => void | Promise<unknown>,
intervalMs = 10_000
): AutoRefreshHandle {
let timer: ReturnType<typeof setInterval> | null = null
let inFlight = false
async function run(): Promise<void> {
if (inFlight) return
inFlight = true
try {
await fn()
} finally {
inFlight = false
}
}
return {
start() {
if (timer !== null) return
void run()
timer = setInterval(() => void run(), intervalMs)
},
stop() {
if (timer === null) return
clearInterval(timer)
timer = null
},
isRunning() {
return timer !== null
}
}
}