{"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n  Collection,\n  Context,\n  InferResultType,\n  InitialQueryBuilder,\n  LiveQueryCollectionUtils,\n  NonSingleResult,\n  QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n  utils: unknown,\n): utils is LiveQueryCollectionUtils {\n  return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n  pageSize?: number\n  initialPageParam?: number\n  /**\n   * @deprecated This callback is not used by the current implementation.\n   * Pagination is determined internally via a peek-ahead strategy.\n   * Provided for API compatibility with TanStack Query conventions.\n   */\n  getNextPageParam?: (\n    lastPage: Array<InferResultType<TContext>[number]>,\n    allPages: Array<Array<InferResultType<TContext>[number]>>,\n    lastPageParam: number,\n    allPageParams: Array<number>,\n  ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n  ReturnType<typeof useLiveQuery<TContext>>,\n  `data`\n> & {\n  data: InferResultType<TContext>\n  pages: Array<Array<InferResultType<TContext>[number]>>\n  pageParams: Array<number>\n  fetchNextPage: () => void\n  hasNextPage: boolean\n  isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n *   (q) => q\n *     .from({ posts: postsCollection })\n *     .orderBy(({ posts }) => posts.createdAt, 'desc')\n *     .select(({ posts }) => ({\n *       id: posts.id,\n *       title: posts.title\n *     })),\n *   {\n *     pageSize: 20,\n *     getNextPageParam: (lastPage, allPages) =>\n *       lastPage.length === 20 ? allPages.length : undefined\n *   }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n *   (q) => q\n *     .from({ posts: postsCollection })\n *     .where(({ posts }) => eq(posts.category, category))\n *     .orderBy(({ posts }) => posts.createdAt, 'desc'),\n *   {\n *     pageSize: 10,\n *     getNextPageParam: (lastPage) =>\n *       lastPage.length === 10 ? lastPage.length : undefined\n *   },\n *   [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n *   query: (q) => q\n *     .from({ posts: postsCollection })\n *     .orderBy(({ posts }) => posts.createdAt, 'desc')\n *     .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n *   postsQuery,\n *   {\n *     pageSize: 20,\n *     getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n *   }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n  config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n  config: UseLiveInfiniteQueryConfig<TContext>,\n  deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n  queryFnOrCollection: any,\n  config: UseLiveInfiniteQueryConfig<TContext>,\n  deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n  const pageSize = config.pageSize || 20\n  const initialPageParam = config.initialPageParam ?? 0\n\n  // Detect if input is a collection or query function\n  const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n  // Validate input type\n  if (!isCollection && typeof queryFnOrCollection !== `function`) {\n    throw new Error(\n      `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n        `or a query function. Received: ${typeof queryFnOrCollection}`,\n    )\n  }\n\n  // Track how many pages have been loaded\n  const [loadedPageCount, setLoadedPageCount] = useState(1)\n  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n  // Track collection instance and whether we've validated it (only for pre-created collections)\n  const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n  const hasValidatedCollectionRef = useRef(false)\n\n  // Track deps for query functions (stringify for comparison)\n  let depsKey: string\n  try {\n    depsKey = JSON.stringify(deps)\n  } catch {\n    throw new Error(\n      `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). ` +\n        `Ensure all dependency values are JSON-serializable.`,\n    )\n  }\n  const prevDepsKeyRef = useRef(depsKey)\n\n  // Reset pagination when inputs change\n  useEffect(() => {\n    let shouldReset = false\n\n    if (isCollection) {\n      // Reset if collection instance changed\n      if (collectionRef.current !== queryFnOrCollection) {\n        collectionRef.current = queryFnOrCollection\n        hasValidatedCollectionRef.current = false\n        shouldReset = true\n      }\n    } else {\n      // Reset if deps changed (for query functions)\n      if (prevDepsKeyRef.current !== depsKey) {\n        prevDepsKeyRef.current = depsKey\n        shouldReset = true\n      }\n    }\n\n    if (shouldReset) {\n      setLoadedPageCount(1)\n    }\n  }, [isCollection, queryFnOrCollection, depsKey])\n\n  // Create a live query with initial limit and offset\n  // Either pass collection directly or wrap query function\n  // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n  const queryResult = isCollection\n    ? useLiveQuery(queryFnOrCollection)\n    : useLiveQuery(\n        (q) =>\n          queryFnOrCollection(q)\n            .limit(pageSize + 1)\n            .offset(0),\n        deps,\n      )\n\n  // Adjust window when pagination changes\n  useEffect(() => {\n    const utils = queryResult.collection.utils\n    const expectedOffset = 0\n    const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n    // Check if collection has orderBy (required for setWindow)\n    if (!isLiveQueryCollectionUtils(utils)) {\n      // For pre-created collections, throw an error if no orderBy\n      if (isCollection) {\n        throw new Error(\n          `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n            `Please add .orderBy() to your createLiveQueryCollection query.`,\n        )\n      }\n      return\n    }\n\n    // For pre-created collections, validate window on first check\n    if (isCollection && !hasValidatedCollectionRef.current) {\n      const currentWindow = utils.getWindow()\n      if (\n        currentWindow &&\n        (currentWindow.offset !== expectedOffset ||\n          currentWindow.limit !== expectedLimit)\n      ) {\n        console.warn(\n          `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n            `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n        )\n      }\n      hasValidatedCollectionRef.current = true\n    }\n\n    // For query functions, wait until collection is ready\n    if (!isCollection && !queryResult.isReady) return\n\n    // Adjust the window\n    let cancelled = false\n    const result = utils.setWindow({\n      offset: expectedOffset,\n      limit: expectedLimit,\n    })\n\n    if (result !== true) {\n      setIsFetchingNextPage(true)\n      result\n        .catch((error: unknown) => {\n          if (!cancelled)\n            console.error(`useLiveInfiniteQuery: setWindow failed:`, error)\n        })\n        .finally(() => {\n          if (!cancelled) setIsFetchingNextPage(false)\n        })\n    } else {\n      setIsFetchingNextPage(false)\n    }\n\n    return () => {\n      cancelled = true\n    }\n  }, [\n    isCollection,\n    queryResult.collection,\n    queryResult.isReady,\n    loadedPageCount,\n    pageSize,\n  ])\n\n  // Split the data array into pages and determine if there's a next page\n  const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n    const dataArray = (\n      Array.isArray(queryResult.data) ? queryResult.data : []\n    ) as InferResultType<TContext>\n    const totalItemsRequested = loadedPageCount * pageSize\n\n    // Check if we have more data than requested (the peek ahead item)\n    const hasMore = dataArray.length > totalItemsRequested\n\n    // Build pages array (without the peek ahead item)\n    const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n    const pageParamsResult: Array<number> = []\n\n    for (let i = 0; i < loadedPageCount; i++) {\n      const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n      pagesResult.push(pageData)\n      pageParamsResult.push(initialPageParam + i)\n    }\n\n    // Flatten the pages for the data return (without peek ahead item)\n    const flatDataResult = dataArray.slice(\n      0,\n      totalItemsRequested,\n    ) as InferResultType<TContext>\n\n    return {\n      pages: pagesResult,\n      pageParams: pageParamsResult,\n      hasNextPage: hasMore,\n      flatData: flatDataResult,\n    }\n  }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n  // Fetch next page\n  const fetchNextPage = useCallback(() => {\n    if (!hasNextPage || isFetchingNextPage) return\n\n    setLoadedPageCount((prev) => prev + 1)\n  }, [hasNextPage, isFetchingNextPage])\n\n  return {\n    ...queryResult,\n    data: flatData,\n    pages,\n    pageParams,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n  } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":["CollectionImpl","useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":"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"}