Spaces:
Paused
Paused
File size: 4,598 Bytes
0b9dc2e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | 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<string, KnowledgeDocumentView>;
/** 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<string>(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<Record<string, KnowledgeDocumentView>>({});
// Mutable refs keep the running interval alive across re-renders
// without forcing the effect to restart on every state change.
const inflightRef = useRef<AbortController | null>(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,
};
}
|