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,
	};
}