import { useEffect, useMemo, useRef, useState } from 'react'; import { knowledgeBaseApi } from '@/api'; import type { KnowledgeDocumentView } from '@/api'; import { useUploadContext } from '@/context/UploadContext'; const POLL_INTERVAL_MS = 1500; /** * Document ids the polling loop should track each tick. Combines: * * - documents the local `UploadProvider` is still expecting transitions * for (so a freshly-uploaded file polls without waiting for the next * list refresh); * - documents the caller knows are non-terminal from a server list * (so a page reload mid-indexing keeps polling without lifting the * upload through the provider). * * Ids that resolve to a terminal state (`ready` / `error`) drop out * automatically — the polling loop stops as soon as the union becomes * empty. */ interface PollingInput { knowledgeBaseId: string | null; /** Currently-listed server documents. */ documents: KnowledgeDocumentView[]; } interface PollingResult { /** Most-recent status views, keyed by document id. */ statuses: Record; /** True while the loop is actively scheduling refreshes. */ polling: boolean; } /** * Conditional polling — only schedules a request when there is at least * one non-terminal document id to watch. The loop self-stops as soon as * every tracked id reaches `ready` / `error`, and rewires automatically * when new ids enter the set (a fresh upload, a re-mount, etc.). * * Single source of truth for the `UploadProvider`'s server-side phase * mirror: every tick that returns items is fanned out through * `applyServerStatuses`, so the in-flight upload cards reflect the * worker's progress without any extra wiring at the call site. */ export function useDocumentStatusPolling({ knowledgeBaseId, documents, }: PollingInput): PollingResult { const { pollableDocumentIds, applyServerStatuses } = useUploadContext(); // Server-side non-terminal docs from the list (catches reload state). const docIds = useMemo(() => { const out: string[] = []; for (const d of documents) { if (d.status !== 'ready' && d.status !== 'error') { out.push(d.id); } } return out; }, [documents]); // Stable, sorted union of server-side non-terminal docs (catches // reload state) and locally-tracked in-flight docs from the upload // provider. Used both as the request payload and as the effect // dependency key so dropping/adding a single id triggers exactly // one effect re-run. const watchIdsKey = useMemo(() => { const set = new Set(docIds); if (knowledgeBaseId) { for (const id of pollableDocumentIds(knowledgeBaseId)) { set.add(id); } } return Array.from(set).sort().join(','); }, [docIds, knowledgeBaseId, pollableDocumentIds]); const [statuses, setStatuses] = useState>({}); // Mutable refs keep the running interval alive across re-renders // without forcing the effect to restart on every state change. const inflightRef = useRef(null); useEffect(() => { if (!knowledgeBaseId) return; const ids = watchIdsKey ? watchIdsKey.split(',') : []; if (ids.length === 0) return; let cancelled = false; const tick = async () => { if (cancelled) return; inflightRef.current?.abort(); const controller = new AbortController(); inflightRef.current = controller; try { const { items } = await knowledgeBaseApi.getDocumentStatus(knowledgeBaseId, ids); if (cancelled) return; setStatuses((prev) => { const next = { ...prev }; for (const item of items) next[item.id] = item; return next; }); applyServerStatuses( knowledgeBaseId, items.map((i) => ({ id: i.id, status: i.status, error: i.error, })), ); } catch { // Swallow transient errors — the next tick will retry. // We deliberately don't surface a toast: a flaky network // shouldn't spam the user while indexing is in the // background. } }; // Fire immediately so the first reading lands without waiting a // full poll interval (important right after an upload). void tick(); const handle = window.setInterval(tick, POLL_INTERVAL_MS); return () => { cancelled = true; inflightRef.current?.abort(); window.clearInterval(handle); }; }, [knowledgeBaseId, watchIdsKey, applyServerStatuses]); return { statuses, polling: watchIdsKey.length > 0, }; }