import { onScopeDispose, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue' import { errorMessage } from '../lib/http' /** 串行轮询:请求完成后才计时;切换项目立即取消,过期响应不会覆盖新项目。 */ export function usePolling( key: Ref, loader: (key: string, signal: AbortSignal) => Promise, interval: MaybeRefOrGetter = 6000 ) { const data = ref(null) as Ref const loading = ref(false) const error = ref('') const updatedAt = ref('') let generation = 0 let controller: AbortController | undefined let timer: ReturnType | 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 } }