mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
feat: refactor document fetching and improve comment batching
- Replaced the useDocuments hook with React Query for better caching and deduplication of document requests. - Updated the ConnectorIndicator component to fetch document type counts using a new atom for real-time updates. - Enhanced the useComments hook to manage batch requests more effectively, reducing race conditions and improving performance. - Set default query options in the query client to optimize stale time and refetch behavior.
This commit is contained in:
parent
cc0d8ad4d7
commit
1bb9f479e1
4 changed files with 80 additions and 57 deletions
|
|
@ -5,6 +5,7 @@ import { AlertTriangle, Cable, Settings } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
|
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||||
import {
|
import {
|
||||||
globalNewLLMConfigsAtom,
|
globalNewLLMConfigsAtom,
|
||||||
llmPreferencesAtom,
|
llmPreferencesAtom,
|
||||||
|
|
@ -19,7 +20,6 @@ import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
|
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
|
||||||
import { useDocuments } from "@/hooks/use-documents";
|
|
||||||
import { useInbox } from "@/hooks/use-inbox";
|
import { useInbox } from "@/hooks/use-inbox";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
||||||
|
|
@ -62,10 +62,9 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger
|
||||||
|
|
||||||
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
||||||
|
|
||||||
// Fetch document type counts using Electric SQL + PGlite for real-time updates
|
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
|
||||||
const { typeCounts: documentTypeCounts, loading: documentTypesLoading } = useDocuments(
|
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
|
||||||
searchSpaceId ? Number(searchSpaceId) : null
|
useAtomValue(documentTypeCountsAtom);
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch notifications to detect indexing failures
|
// Fetch notifications to detect indexing failures
|
||||||
const { inboxItems = [] } = useInbox(
|
const { inboxItems = [] } = useInbox(
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,26 @@ interface UseCommentsOptions {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Module-level coordination: when a batch request is in-flight, individual
|
// Module-level coordination: when a batch request is in-flight, individual
|
||||||
// useComments queryFns piggy-back on it instead of making their own requests.
|
// useComments queryFns piggy-back on it instead of making their own requests.
|
||||||
|
//
|
||||||
|
// _batchReady is a promise that resolves once the batch useEffect has had a
|
||||||
|
// chance to set _batchInflight. Individual queryFns await this gate before
|
||||||
|
// deciding whether to piggy-back or fetch on their own, eliminating the
|
||||||
|
// previous race where setTimeout(0) was not enough.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
let _batchInflight: Promise<void> | null = null;
|
let _batchInflight: Promise<void> | null = null;
|
||||||
let _batchTargetIds = new Set<number>();
|
let _batchTargetIds = new Set<number>();
|
||||||
|
let _batchReady: Promise<void> | null = null;
|
||||||
|
let _resolveBatchReady: (() => void) | null = null;
|
||||||
|
|
||||||
|
function resetBatchGate() {
|
||||||
|
_batchReady = new Promise<void>((r) => {
|
||||||
|
_resolveBatchReady = r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the initial gate immediately (no batch pending yet)
|
||||||
|
resetBatchGate();
|
||||||
|
_resolveBatchReady?.();
|
||||||
|
|
||||||
export function useComments({ messageId, enabled = true }: UseCommentsOptions) {
|
export function useComments({ messageId, enabled = true }: UseCommentsOptions) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -21,9 +38,11 @@ export function useComments({ messageId, enabled = true }: UseCommentsOptions) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: cacheKeys.comments.byMessage(messageId),
|
queryKey: cacheKeys.comments.byMessage(messageId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// Yield one macro-task so the batch prefetch useEffect (which sets
|
// Wait for the batch gate so the useEffect in useBatchCommentsPreload
|
||||||
// _batchInflight) has a chance to fire before we decide to fetch.
|
// has a chance to set _batchInflight before we decide.
|
||||||
await new Promise<void>((r) => setTimeout(r, 0));
|
if (_batchReady) {
|
||||||
|
await _batchReady;
|
||||||
|
}
|
||||||
|
|
||||||
if (_batchInflight && _batchTargetIds.has(messageId)) {
|
if (_batchInflight && _batchTargetIds.has(messageId)) {
|
||||||
await _batchInflight;
|
await _batchInflight;
|
||||||
|
|
@ -57,6 +76,9 @@ export function useBatchCommentsPreload(messageIds: number[]) {
|
||||||
if (key === prevKeyRef.current) return;
|
if (key === prevKeyRef.current) return;
|
||||||
prevKeyRef.current = key;
|
prevKeyRef.current = key;
|
||||||
|
|
||||||
|
// Open a new gate so individual queryFns wait for us
|
||||||
|
resetBatchGate();
|
||||||
|
|
||||||
_batchTargetIds = new Set(messageIds);
|
_batchTargetIds = new Set(messageIds);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
|
@ -80,6 +102,9 @@ export function useBatchCommentsPreload(messageIds: number[]) {
|
||||||
|
|
||||||
_batchInflight = promise;
|
_batchInflight = promise;
|
||||||
|
|
||||||
|
// Release the gate — individual queryFns can now check _batchInflight
|
||||||
|
_resolveBatchReady?.();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (_batchInflight === promise) {
|
if (_batchInflight === promise) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||||
|
|
@ -183,56 +184,47 @@ export function useDocuments(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// EFFECT 1: Load ALL documents from API (PRIMARY source of truth)
|
// STEP 1: Load ALL documents from API (PRIMARY source of truth).
|
||||||
// No type filter — always fetches everything so typeCounts stay complete
|
// Uses React Query for automatic deduplication, caching, and staleTime so
|
||||||
useEffect(() => {
|
// multiple components mounting useDocuments(sameId) share a single request.
|
||||||
if (!searchSpaceId) {
|
const {
|
||||||
setLoading(false);
|
data: apiResponse,
|
||||||
return;
|
isLoading: apiLoading,
|
||||||
}
|
error: apiError,
|
||||||
|
} = useQuery({
|
||||||
// Capture validated value for async closure
|
queryKey: ["documents", "all", searchSpaceId],
|
||||||
const spaceId = searchSpaceId;
|
queryFn: () =>
|
||||||
|
documentsApiService.getDocuments({
|
||||||
let mounted = true;
|
|
||||||
apiLoadedRef.current = false;
|
|
||||||
|
|
||||||
async function loadFromApi() {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
console.log("[useDocuments] Loading from API (source of truth):", spaceId);
|
|
||||||
|
|
||||||
const response = await documentsApiService.getDocuments({
|
|
||||||
queryParams: {
|
queryParams: {
|
||||||
search_space_id: spaceId,
|
search_space_id: searchSpaceId!,
|
||||||
page: 0,
|
page: 0,
|
||||||
page_size: -1, // Fetch all documents (unfiltered)
|
page_size: -1,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
enabled: !!searchSpaceId,
|
||||||
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mounted) return;
|
// Seed local state from API response (runs once per fresh fetch)
|
||||||
|
useEffect(() => {
|
||||||
populateUserCache(response.items);
|
if (!apiResponse) return;
|
||||||
const docs = response.items.map(apiToDisplayDoc);
|
populateUserCache(apiResponse.items);
|
||||||
|
const docs = apiResponse.items.map(apiToDisplayDoc);
|
||||||
setAllDocuments(docs);
|
setAllDocuments(docs);
|
||||||
apiLoadedRef.current = true;
|
apiLoadedRef.current = true;
|
||||||
setError(null);
|
setError(null);
|
||||||
console.log("[useDocuments] API loaded", docs.length, "documents");
|
}, [apiResponse, populateUserCache, apiToDisplayDoc]);
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
console.error("[useDocuments] API load failed:", err);
|
|
||||||
setError(err instanceof Error ? err : new Error("Failed to load documents"));
|
|
||||||
} finally {
|
|
||||||
if (mounted) setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFromApi();
|
// Propagate loading / error from React Query
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(apiLoading);
|
||||||
|
}, [apiLoading]);
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
mounted = false;
|
if (apiError) {
|
||||||
};
|
setError(apiError instanceof Error ? apiError : new Error("Failed to load documents"));
|
||||||
}, [searchSpaceId, populateUserCache, apiToDisplayDoc]);
|
}
|
||||||
|
}, [apiError]);
|
||||||
|
|
||||||
// EFFECT 2: Start Electric sync + live query for real-time updates
|
// EFFECT 2: Start Electric sync + live query for real-time updates
|
||||||
// No type filter — syncs and queries ALL documents; filtering is client-side
|
// No type filter — syncs and queries ALL documents; filtering is client-side
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const queryClient = new QueryClient();
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue