82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { onScopeDispose, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
import { errorMessage } from '../lib/http'
|
|
|
|
/** 串行轮询:请求完成后才计时;切换项目立即取消,过期响应不会覆盖新项目。 */
|
|
export function usePolling<T>(
|
|
key: Ref<string>,
|
|
loader: (key: string, signal: AbortSignal) => Promise<T>,
|
|
interval: MaybeRefOrGetter<number | false> = 6000
|
|
) {
|
|
const data = ref<T | null>(null) as Ref<T | null>
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const updatedAt = ref('')
|
|
let generation = 0
|
|
let controller: AbortController | undefined
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
let disposed = false
|
|
|
|
/** 隐藏页面只安排下一次检查,不继续拉取完整 checkpoint。 */
|
|
function schedule() {
|
|
const delay = toValue(interval)
|
|
if (disposed || delay === false) return
|
|
timer = setTimeout(() => {
|
|
if (toValue(interval) === false) return
|
|
if (document.visibilityState === 'hidden') schedule()
|
|
else void refresh()
|
|
}, delay)
|
|
}
|
|
|
|
/** 手动刷新也取消上一次查询,避免同时存在多个轮询链。 */
|
|
async function refresh() {
|
|
if (disposed) return
|
|
const current = ++generation
|
|
clearTimeout(timer)
|
|
controller?.abort()
|
|
controller = new AbortController()
|
|
const signal = controller.signal
|
|
loading.value = true
|
|
try {
|
|
const result = await loader(key.value, signal)
|
|
if (current !== generation || disposed) return
|
|
data.value = result
|
|
error.value = ''
|
|
updatedAt.value = new Date().toISOString()
|
|
} catch (cause) {
|
|
if (current === generation && !signal.aborted && !disposed) error.value = errorMessage(cause)
|
|
} finally {
|
|
if (current === generation && !disposed) {
|
|
loading.value = false
|
|
schedule()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 页面可关闭定时刷新;首次读取、切换查询目标和手动刷新仍然有效。
|
|
watch(
|
|
() => toValue(interval),
|
|
() => {
|
|
clearTimeout(timer)
|
|
if (!loading.value) schedule()
|
|
}
|
|
)
|
|
watch(
|
|
key,
|
|
() => {
|
|
data.value = null
|
|
error.value = ''
|
|
updatedAt.value = ''
|
|
void refresh()
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
onScopeDispose(() => {
|
|
disposed = true
|
|
generation++
|
|
clearTimeout(timer)
|
|
controller?.abort()
|
|
})
|
|
return { data, loading, error, updatedAt, refresh }
|
|
}
|
|
|