usePrStatus.ts
hooks/usePrStatus.ts
No strong subsystem tag
107
Lines
3202
Bytes
2
Exports
3
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 107 lines, 3 detected imports, and 2 detected exports.
Important relationships
Detected exports
PrStatusStateusePrStatus
Keywords
currentdatetimeoutrefreviewstatepollcancelleduserefpoll_interval_msprstatusstateenabled
Detected imports
react../bootstrap/state.js../utils/ghPrStatus.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}