Spaces:
Sleeping
Sleeping
Download frontend/src/hooks/useJobPolling.ts from junyoung137/image-tool: direct link, hf CLI and curl.
- Browser
- Download file 5.24 kB
-
https://huggingface.co/spaces/junyoung137/image-tool/resolve/main/frontend/src/hooks/useJobPolling.ts
- Command line
-
hf download hf://spaces/junyoung137/image-tool/frontend/src/hooks/useJobPolling.ts
-
curl -L -o useJobPolling.ts https://huggingface.co/spaces/junyoung137/image-tool/resolve/main/frontend/src/hooks/useJobPolling.ts
5.24 kB
| /** | |
| * useJobPolling.ts — recursive-setTimeout polling for a backend job. | |
| * | |
| * Why setTimeout and not setInterval? | |
| * ------------------------------------ | |
| * setInterval fires at a fixed wall-clock cadence regardless of whether the | |
| * previous request has finished. setTimeout-based recursion only schedules the | |
| * NEXT poll after the CURRENT one resolves, so there is never more than one | |
| * in-flight request at a time. | |
| * | |
| * steps[] persistence | |
| * ------------------- | |
| * The steps array accumulates across poll cycles — startPolling() only resets | |
| * pollPhase and errorMessage, not steps/currentStep. This lets the history | |
| * navigator keep showing all known steps while a new step is being processed. | |
| * | |
| * refreshResult() | |
| * --------------- | |
| * A lightweight one-shot GET that updates steps/currentStep in state without | |
| * touching pollPhase. Used after goToStep() to sync local state without | |
| * triggering the phase-transition effects that startPolling() would cause. | |
| */ | |
| import { useCallback, useEffect, useRef, useState } from "react"; | |
| import { getJobResult } from "../api/client"; | |
| import type { StepSummary } from "../api/client"; | |
| const POLL_INTERVAL_MS = 2000; | |
| export type PollPhase = "idle" | "polling" | "completed" | "failed"; | |
| interface PollState { | |
| pollPhase: PollPhase; | |
| errorMessage: string | null; | |
| steps: StepSummary[]; | |
| currentStep: number; | |
| } | |
| const INITIAL_STATE: PollState = { | |
| pollPhase: "idle", | |
| errorMessage: null, | |
| steps: [], | |
| currentStep: 0, | |
| }; | |
| type JobResultFetcher = typeof getJobResult; | |
| export function useJobPolling(fetcher: JobResultFetcher = getJobResult) { | |
| const [state, setState] = useState<PollState>(INITIAL_STATE); | |
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| const isMountedRef = useRef(true); | |
| useEffect(() => { | |
| isMountedRef.current = true; | |
| return () => { | |
| isMountedRef.current = false; | |
| if (timeoutRef.current !== null) clearTimeout(timeoutRef.current); | |
| }; | |
| }, []); | |
| const stopPolling = useCallback(() => { | |
| if (timeoutRef.current !== null) { | |
| clearTimeout(timeoutRef.current); | |
| timeoutRef.current = null; | |
| } | |
| }, []); | |
| /** | |
| * Begin polling for the given jobId. | |
| * Resets pollPhase/errorMessage but preserves steps[] so history stays visible | |
| * while the new step is processing. | |
| */ | |
| const startPolling = useCallback((jobId: string) => { | |
| stopPolling(); | |
| if (!isMountedRef.current) return; | |
| // Only reset phase/error — keep steps/currentStep from prior polls. | |
| setState(prev => ({ ...prev, pollPhase: "polling", errorMessage: null })); | |
| const poll = async () => { | |
| try { | |
| const result = await fetcher(jobId); | |
| if (!isMountedRef.current) return; | |
| if (result.status === "completed") { | |
| setState({ | |
| pollPhase: "completed", | |
| errorMessage: null, | |
| steps: result.steps, | |
| currentStep: result.current_step, | |
| }); | |
| } else if (result.status === "failed") { | |
| setState(prev => ({ | |
| ...prev, | |
| pollPhase: "failed", | |
| errorMessage: result.error_message ?? "Job failed with no error details.", | |
| steps: result.steps, | |
| currentStep: result.current_step, | |
| })); | |
| } else { | |
| // "pending" or "processing" — keep polling | |
| timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS); | |
| } | |
| } catch (err) { | |
| if (!isMountedRef.current) return; | |
| setState(prev => ({ | |
| ...prev, | |
| pollPhase: "failed", | |
| errorMessage: `Network error while polling: ${(err as Error).message}`, | |
| })); | |
| } | |
| }; | |
| poll(); | |
| }, [stopPolling, fetcher]); | |
| /** | |
| * Silently refresh steps[] and currentStep from the backend without touching | |
| * pollPhase. Call this after goToStep() to sync local state. | |
| */ | |
| const refreshResult = useCallback(async (jobId: string) => { | |
| try { | |
| const result = await fetcher(jobId); | |
| if (!isMountedRef.current) return; | |
| setState(prev => ({ | |
| ...prev, | |
| steps: result.steps, | |
| currentStep: result.current_step, | |
| })); | |
| } catch { /* ignore — stale state is acceptable here */ } | |
| }, [fetcher]); | |
| /** Stop polling and reset ALL state back to idle, including steps[]. */ | |
| const reset = useCallback(() => { | |
| stopPolling(); | |
| if (isMountedRef.current) setState(INITIAL_STATE); | |
| }, [stopPolling]); | |
| /** | |
| * Reset pollPhase to "idle" WITHOUT clearing steps[] or currentStep. | |
| * Use this when the user initiates "Do More Work" — we want the history | |
| * strip to stay visible but the pollPhase→phase sync effect must stop | |
| * fighting against the manually-set "painting" phase. | |
| */ | |
| const resetPollPhase = useCallback(() => { | |
| stopPolling(); | |
| if (isMountedRef.current) { | |
| setState(prev => ({ ...prev, pollPhase: "idle", errorMessage: null })); | |
| } | |
| }, [stopPolling]); | |
| return { | |
| pollPhase: state.pollPhase, | |
| errorMessage: state.errorMessage, | |
| steps: state.steps, | |
| currentStep: state.currentStep, | |
| startPolling, | |
| refreshResult, | |
| stopPolling, | |
| reset, | |
| resetPollPhase, | |
| }; | |
| } | |