diff --git a/ui/src/hooks/useInfiniteStream.ts b/ui/src/hooks/useInfiniteStream.ts new file mode 100644 index 00000000..1285e9fa --- /dev/null +++ b/ui/src/hooks/useInfiniteStream.ts @@ -0,0 +1,65 @@ +import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +export interface StreamPage { + data: T[]; + nextCursor: string | null; + hasMore: boolean; +} + +export interface UseInfiniteStreamArgs { + queryKey: readonly unknown[]; + fetchPage: (cursor: string | undefined) => Promise>; + enabled?: boolean; + /** When true, the query auto-refetches every refetchMs ms. When false, polling pauses. */ + isAtTop: boolean; + refetchMs?: number; + staleTime?: number; +} + +export interface UseInfiniteStreamResult { + items: T[]; + fetchNextPage: () => void; + hasNextPage: boolean; + isFetchingNextPage: boolean; + isLoading: boolean; + refresh: () => void; +} + +/** + * Thin wrapper over tanstack useInfiniteQuery that: + * - flattens pages into a single items[] array (newest first) + * - gates auto-refetch on isAtTop (so a user scrolled down does not lose their viewport) + * - exposes refresh() that invalidates the query (reset to page 1 on next render) + */ +export function useInfiniteStream(args: UseInfiniteStreamArgs): UseInfiniteStreamResult { + const { queryKey, fetchPage, enabled = true, isAtTop, refetchMs = 15_000, staleTime = 300 } = args; + const queryClient = useQueryClient(); + + const query = useInfiniteQuery, Error>({ + queryKey: [...queryKey], + initialPageParam: undefined as string | undefined, + queryFn: ({ pageParam }) => fetchPage(pageParam as string | undefined), + getNextPageParam: (last) => (last.hasMore && last.nextCursor ? last.nextCursor : undefined), + enabled, + refetchInterval: isAtTop ? refetchMs : false, + staleTime, + placeholderData: (prev) => prev, + }); + + const items = useMemo( + () => (query.data?.pages ?? []).flatMap((p) => p.data), + [query.data], + ); + + return { + items, + fetchNextPage: () => { + if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage(); + }, + hasNextPage: !!query.hasNextPage, + isFetchingNextPage: query.isFetchingNextPage, + isLoading: query.isLoading, + refresh: () => queryClient.invalidateQueries({ queryKey: [...queryKey] }), + }; +}