107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { getLastInteractionTime } from '../bootstrap/state.js'
|
|
import { fetchPrStatus, type PrReviewState } from '../utils/ghPrStatus.js'
|
|
|
|
const POLL_INTERVAL_MS = 60_000
|
|
const SLOW_GH_THRESHOLD_MS = 4_000
|
|
const IDLE_STOP_MS = 60 * 60_000 // stop polling after 60 min idle
|
|
|
|
export type PrStatusState = {
|
|
number: number | null
|
|
url: string | null
|
|
reviewState: PrReviewState | null
|
|
lastUpdated: number
|
|
}
|
|
|
|
const INITIAL_STATE: PrStatusState = {
|
|
number: null,
|
|
url: null,
|
|
reviewState: null,
|
|
lastUpdated: 0,
|
|
}
|
|
|
|
/**
|
|
* Polls PR review status every 60s while the session is active.
|
|
* When no interaction is detected for 60 minutes, the loop stops — no
|
|
* timers remain. React re-runs the effect when isLoading changes
|
|
* (turn starts/ends), restarting the loop. Effect setup schedules
|
|
* the next poll relative to the last fetch time so turn boundaries
|
|
* don't spawn `gh` more than once per interval. Disables permanently
|
|
* if a fetch exceeds 4s.
|
|
*
|
|
* Pass `enabled: false` to skip polling entirely (hook still must be
|
|
* called unconditionally to satisfy the rules of hooks).
|
|
*/
|
|
export function usePrStatus(isLoading: boolean, enabled = true): PrStatusState {
|
|
const [prStatus, setPrStatus] = useState<PrStatusState>(INITIAL_STATE)
|
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const disabledRef = useRef(false)
|
|
const lastFetchRef = useRef(0)
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return
|
|
if (disabledRef.current) return
|
|
|
|
let cancelled = false
|
|
let lastSeenInteractionTime = -1
|
|
let lastActivityTimestamp = Date.now()
|
|
|
|
async function poll() {
|
|
if (cancelled) return
|
|
|
|
const currentInteractionTime = getLastInteractionTime()
|
|
if (lastSeenInteractionTime !== currentInteractionTime) {
|
|
lastSeenInteractionTime = currentInteractionTime
|
|
lastActivityTimestamp = Date.now()
|
|
} else if (Date.now() - lastActivityTimestamp >= IDLE_STOP_MS) {
|
|
return
|
|
}
|
|
|
|
const start = Date.now()
|
|
const result = await fetchPrStatus()
|
|
if (cancelled) return
|
|
lastFetchRef.current = start
|
|
|
|
setPrStatus(prev => {
|
|
const newNumber = result?.number ?? null
|
|
const newReviewState = result?.reviewState ?? null
|
|
if (prev.number === newNumber && prev.reviewState === newReviewState) {
|
|
return prev
|
|
}
|
|
return {
|
|
number: newNumber,
|
|
url: result?.url ?? null,
|
|
reviewState: newReviewState,
|
|
lastUpdated: Date.now(),
|
|
}
|
|
})
|
|
|
|
if (Date.now() - start > SLOW_GH_THRESHOLD_MS) {
|
|
disabledRef.current = true
|
|
return
|
|
}
|
|
|
|
if (!cancelled) {
|
|
timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS)
|
|
}
|
|
}
|
|
|
|
const elapsed = Date.now() - lastFetchRef.current
|
|
if (elapsed >= POLL_INTERVAL_MS) {
|
|
void poll()
|
|
} else {
|
|
timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS - elapsed)
|
|
}
|
|
|
|
return () => {
|
|
cancelled = true
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
timeoutRef.current = null
|
|
}
|
|
}
|
|
}, [isLoading, enabled])
|
|
|
|
return prStatus
|
|
}
|