feat(client): 引入共享 use-polling 轮询原语
四处站点(SubagentView 活动预览 / SideChatView 转录 / GitLens 状态 / ChangesTab 会话事件)各自手写取消安全与代际失效,机制各异。先落地共享 原语:间隔与调度模式(fixed-interval / self-scheduling)参数化、每代一个 AbortSignal + 卸载/依赖变更即失效(迟到响应靠 signal.aborted 拦截,不信任 abort 必然触发 reject)、任务异常静默保留旧值由下一 tick 重试。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* The sidebar's ONE polling loop: every timed client fetch — subagent live
|
||||
* previews, side-chat transcript deltas, git status, session ops — funnels
|
||||
* here so the cancellation-safety rules are written once instead of four
|
||||
* times. The contract every poller gets:
|
||||
*
|
||||
* - While `enabled`, the task runs on the chosen cadence; the loop restarts
|
||||
* (and the old one is torn down) whenever `enabled`, the task identity, or
|
||||
* any option primitive changes — callers express "scope changed" through
|
||||
* the task's `useCallback` deps, exactly like an effect's dep array.
|
||||
* - Each enabled run owns ONE AbortSignal for its whole lifetime; teardown
|
||||
* (unmount, `enabled` flip, identity change) aborts it AND stops all
|
||||
* scheduling. A task whose fetch settles late must check the signal
|
||||
* before writing state: an aborted fetch is NOT guaranteed to reject
|
||||
* (the transport may deliver the response anyway), so the signal is the
|
||||
* only reliable staleness guard.
|
||||
* - A rejected task never breaks the loop — the scheduler swallows it and
|
||||
* the next tick retries. Sites that surface errors (setError banners and
|
||||
* friends) do so inside the task body; the scheduler always stays silent.
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/** One poll tick. See the module doc for the signal contract. */
|
||||
export type PollingTask = (signal: AbortSignal) => Promise<void>
|
||||
|
||||
export interface UsePollingOptions {
|
||||
/** Tick cadence in milliseconds. */
|
||||
intervalMs: number
|
||||
/**
|
||||
* Scheduling mode. `'fixed-interval'` (default) fires ticks on a plain
|
||||
* `setInterval` cadence — an in-flight task never delays the next tick,
|
||||
* overlapping tasks guard their own writes (abort controllers or
|
||||
* generation counters inside the task). `'self-scheduling'` arms the next
|
||||
* tick only after the previous task settles: at most ONE request in
|
||||
* flight, ever, so a slow host never sees request storms.
|
||||
*/
|
||||
mode?: 'fixed-interval' | 'self-scheduling'
|
||||
/** Run one task immediately when the poller (re)starts, before the first
|
||||
* scheduled tick. */
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
export function usePolling(
|
||||
enabled: boolean,
|
||||
task: PollingTask,
|
||||
{ intervalMs, mode = 'fixed-interval', immediate = false }: UsePollingOptions,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
const controller = new AbortController()
|
||||
if (mode === 'self-scheduling') {
|
||||
let disposed = false
|
||||
let timer: number | undefined
|
||||
const tick = async (): Promise<void> => {
|
||||
if (disposed) return
|
||||
try {
|
||||
await task(controller.signal)
|
||||
} catch {
|
||||
// A failed poll keeps the last view; the next tick retries.
|
||||
}
|
||||
if (!disposed) timer = window.setTimeout(() => { void tick() }, intervalMs)
|
||||
}
|
||||
if (immediate) void tick()
|
||||
else timer = window.setTimeout(() => { void tick() }, intervalMs)
|
||||
return () => {
|
||||
disposed = true
|
||||
if (timer !== undefined) window.clearTimeout(timer)
|
||||
controller.abort()
|
||||
}
|
||||
}
|
||||
const run = (): void => {
|
||||
// A failed tick keeps the last view; the next tick retries.
|
||||
task(controller.signal).catch(() => {})
|
||||
}
|
||||
if (immediate) run()
|
||||
const timer = window.setInterval(run, intervalMs)
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
controller.abort()
|
||||
}
|
||||
// Option primitives only: a churned options object must not restart the loop.
|
||||
}, [enabled, task, intervalMs, mode, immediate])
|
||||
}
|
||||
Reference in New Issue
Block a user