task-86: 将隐藏页面轮询间隔、前台恢复和退避策略统一配置化

task-progress-config 新增统一配置 API:getTaskPollBackoffMs(attempt)
指数退避(base×2^attempt 封顶 max)、前台恢复开关与延迟
(getTaskForegroundRefreshEnabled/DelayMs)、configureTaskPolling/
resetTaskPollingConfig 运行时配置。校验失败抛错且零状态变更,
document 缺失时按可见间隔降级。useTaskProgressLoop 改用退避配置
替代硬编码 500ms 重试,前台恢复走可配置开关与延迟(含 in-flight 防
并发)。8 个测试覆盖默认、多配置、幂等、空、单字段、封顶、非法输入
与故障恢复。
This commit is contained in:
2026-08-30 22:51:21 +08:00
parent 62bf1fb206
commit 58e91589df
3 changed files with 261 additions and 7 deletions
@@ -1,5 +1,10 @@
import { onBeforeUnmount, ref, watch, type Ref } from 'vue'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import {
getTaskPollIntervalMs,
getTaskPollBackoffMs,
getTaskForegroundRefreshEnabled,
getTaskForegroundRefreshDelayMs,
} from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import {
createTaskPollingBaseline,
@@ -233,8 +238,8 @@ export function useTaskProgressLoop<TDetail>(
if (disposed) return
if (!taskIds.value.length) return
if (inFlight.value) {
// 上一次还没回,500ms 后再试
pollTimer = timers.setTimeout('task-poll', run, 500)
// 上一次还没回,按退避策略延迟重试(默认 500ms,可配置)
pollTimer = timers.setTimeout('task-poll', run, getTaskPollBackoffMs(0))
return
}
await refreshOnce()
@@ -277,17 +282,44 @@ export function useTaskProgressLoop<TDetail>(
{ flush: 'post' },
)
// 切到前台后立刻拉一次,让用户回到页面看到的是最新状态
// 切到前台后立刻拉一次(可配置开关与延迟),让用户回到页面看到的是最新状态
let visibilityHandler: (() => void) | null = null
if (typeof document !== 'undefined') {
visibilityHandler = () => {
if (document.visibilityState === 'visible' && taskIds.value.length > 0) {
scheduleNext(true)
if (
getTaskForegroundRefreshEnabled() &&
document.visibilityState === 'visible' &&
taskIds.value.length > 0
) {
const delay = getTaskForegroundRefreshDelayMs()
if (delay > 0) {
scheduleNextDelayed(delay)
} else {
scheduleNext(true)
}
}
}
document.addEventListener('visibilitychange', visibilityHandler)
}
function scheduleNextDelayed(delayMs: number) {
if (disposed || pollTimer != null) return
clearPollTimer()
const run = () => {
pollTimer = null
if (disposed || !taskIds.value.length) return
if (inFlight.value) {
pollTimer = timers.setTimeout('task-poll', run, getTaskPollBackoffMs(0))
return
}
void refreshOnce()
if (!disposed && taskIds.value.length > 0) {
pollTimer = timers.setTimeout('task-poll', run, intervalMs())
}
}
pollTimer = timers.setTimeout('task-poll', run, delayMs)
}
function dispose() {
if (disposed) return
disposed = true