@tanstack/react-db 0.1.96 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,90 +3,95 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
4
  const db = require("@tanstack/db");
5
5
  const DEFAULT_GC_TIME_MS = 1;
6
- function hasSetWindow(collection) {
7
- return typeof collection.utils?.setWindow === `function`;
8
- }
9
6
  function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
10
- const pageSize = config.pageSize || 20;
7
+ const pageSize = db.normalizeLiveQueryWindowPageSize(config.pageSize);
11
8
  const initialPageParam = config.initialPageParam ?? 0;
12
- const isCollection = queryFnOrCollection instanceof db.CollectionImpl;
13
- if (!isCollection && typeof queryFnOrCollection !== `function`) {
14
- throw new Error(
15
- `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) or a query function. Received: ${typeof queryFnOrCollection}`
16
- );
17
- }
18
- const collectionRef = react.useRef(null);
19
- const controllerRef = react.useRef(null);
20
- const configRef = react.useRef(null);
21
- const depsRef = react.useRef(null);
22
- const pageSizeRef = react.useRef(pageSize);
23
- const initialPageParamRef = react.useRef(initialPageParam);
24
- const validatedCollectionRef = react.useRef(null);
25
- const inputKind = isCollection ? `collection` : `query`;
26
- const inputKindRef = react.useRef(null);
27
- const previousInputKind = inputKindRef.current;
28
- const dependenciesChanged = !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, index) => dep !== deps[index]));
29
- const dependenciesStructurallyEqual = !isCollection && depsRef.current !== null && db.deepEquals(depsRef.current, deps);
30
- const needsNewCollection = !collectionRef.current || inputKindRef.current !== inputKind || isCollection && configRef.current !== queryFnOrCollection || dependenciesChanged;
31
- const pageShapeChanged = pageSizeRef.current !== pageSize || initialPageParamRef.current !== initialPageParam;
32
- const needsNewController = !controllerRef.current || needsNewCollection || pageShapeChanged;
33
- if (needsNewCollection) {
34
- inputKindRef.current = inputKind;
35
- if (isCollection) {
36
- const collection = queryFnOrCollection;
37
- if (!hasSetWindow(collection)) {
38
- throw new Error(
39
- `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
40
- );
41
- }
42
- if (validatedCollectionRef.current !== collection) {
43
- validatedCollectionRef.current = collection;
44
- const currentWindow = collection.utils.getWindow?.();
45
- if (currentWindow && (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)) {
46
- console.warn(
47
- `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`
48
- );
49
- }
9
+ const inputIsCollection = db.getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
10
+ const committedRef = react.useRef(null);
11
+ const committed = committedRef.current;
12
+ const inputKind = inputIsCollection ? `collection` : `query`;
13
+ const dependencyComparison = db.compareLiveQueryWindowDependencies(
14
+ committed?.dependencies,
15
+ deps
16
+ );
17
+ const dependenciesChanged = !inputIsCollection && dependencyComparison.changed;
18
+ const dependenciesStructurallyEqual = !inputIsCollection && dependencyComparison.structurallyEqual;
19
+ const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
20
+ const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
21
+ const needsNewController = committed === null || needsNewCollection || pageShapeChanged;
22
+ let renderState = committed;
23
+ if (needsNewController) {
24
+ let collection = committed?.collection;
25
+ let warning = null;
26
+ if (needsNewCollection) {
27
+ const input = db.resolveLiveQueryWindowInput(queryFnOrCollection);
28
+ if (input.kind === `collection`) {
29
+ collection = input.collection;
30
+ } else {
31
+ collection = db.createLiveQueryCollection({
32
+ query: input.query.limit(pageSize + 1).offset(0),
33
+ // Construction happens during render. Synchronization starts only when
34
+ // useSyncExternalStore commits the controller subscription.
35
+ startSync: false,
36
+ gcTime: DEFAULT_GC_TIME_MS
37
+ });
50
38
  }
51
- collectionRef.current = collection;
52
- configRef.current = queryFnOrCollection;
39
+ }
40
+ if (!collection) {
41
+ throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`);
42
+ }
43
+ if (inputIsCollection) {
44
+ warning = db.getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null;
53
45
  } else {
54
- collectionRef.current = db.createLiveQueryCollection({
55
- query: (q) => queryFnOrCollection(q).limit(pageSize + 1).offset(0),
56
- // Construction happens during render. Synchronization starts only when
57
- // useSyncExternalStore commits the controller subscription.
58
- startSync: false,
59
- gcTime: DEFAULT_GC_TIME_MS
60
- });
61
- depsRef.current = [...deps];
46
+ db.assertLiveQueryWindowManyResult(collection);
62
47
  }
63
- }
64
- if (needsNewController) {
65
- const previousController = controllerRef.current;
66
- const canPreservePageCount = previousController !== null && (!needsNewCollection || previousInputKind === `query` && dependenciesStructurallyEqual);
67
- const initialPageCount = canPreservePageCount ? Math.max(1, previousController.getSnapshot().pages.length) : 1;
68
- pageSizeRef.current = pageSize;
69
- initialPageParamRef.current = initialPageParam;
70
- controllerRef.current = db.createLiveQueryWindowController(
71
- collectionRef.current,
72
- {
48
+ const canPreservePageCount = db.shouldPreserveLiveQueryWindowPageCount({
49
+ hasPreviousController: committed !== null,
50
+ previousInputKind: committed?.inputKind,
51
+ inputKind,
52
+ sameCollection: inputIsCollection && committed?.inputCollection === collection,
53
+ dependenciesChanged,
54
+ dependenciesStructurallyEqual,
55
+ pageShapeChanged
56
+ });
57
+ const previousPageCount = committed ? Math.max(1, committed.controller.getSnapshot().pages.length) : 1;
58
+ const initialPageCount = canPreservePageCount ? previousPageCount : 1;
59
+ renderState = {
60
+ inputKind,
61
+ inputCollection: inputIsCollection ? collection : null,
62
+ dependencies: inputIsCollection ? null : [...deps],
63
+ pageSize,
64
+ initialPageParam,
65
+ collection,
66
+ controller: db.createLiveQueryWindowController(collection, {
73
67
  pageSize,
74
68
  initialPageParam,
75
69
  initialPageCount
76
- }
77
- );
70
+ }),
71
+ warning,
72
+ warned: false
73
+ };
78
74
  }
79
- const controller = controllerRef.current;
75
+ const currentRenderState = renderState;
76
+ const controller = currentRenderState.controller;
80
77
  const subscribe = react.useCallback(
81
- (onStoreChange) => controller.subscribe(onStoreChange),
82
- [controller]
78
+ (onStoreChange) => {
79
+ const unsubscribe = controller.subscribe(onStoreChange);
80
+ committedRef.current = currentRenderState;
81
+ if (currentRenderState.warning && !currentRenderState.warned) {
82
+ currentRenderState.warned = true;
83
+ console.warn(currentRenderState.warning);
84
+ }
85
+ return unsubscribe;
86
+ },
87
+ [controller, currentRenderState]
83
88
  );
84
89
  const getSnapshot = react.useCallback(() => controller.getSnapshot(), [controller]);
85
90
  const snapshot = react.useSyncExternalStore(subscribe, getSnapshot);
86
- const fetchNextPage = react.useCallback(() => {
87
- void controller.fetchNextPage().catch(() => {
88
- });
89
- }, [controller]);
91
+ const fetchNextPage = react.useCallback(
92
+ () => db.fetchNextLiveQueryWindowPage(controller),
93
+ [controller]
94
+ );
90
95
  return {
91
96
  data: snapshot.data,
92
97
  state: snapshot.state,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n CollectionImpl,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n deepEquals,\n} from '@tanstack/db'\n// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.\nimport type { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\n\ntype WindowedCollection = Collection<any, any, any> & {\n utils: {\n setWindow: (options: {\n offset: number\n limit: number\n }) => true | Promise<void>\n }\n}\n\n/** Type guard: does this collection expose `setWindow` (i.e. has an orderBy)? */\nfunction hasSetWindow(\n collection: Collection<any, any, any>,\n): collection is WindowedCollection {\n return typeof collection.utils?.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 error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\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 const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const controllerRef = useRef<LiveQueryWindowController<any, any> | null>(null)\n const configRef = useRef<unknown>(null)\n const depsRef = useRef<Array<unknown> | null>(null)\n const pageSizeRef = useRef(pageSize)\n const initialPageParamRef = useRef(initialPageParam)\n const validatedCollectionRef = useRef<unknown>(null)\n const inputKind = isCollection ? `collection` : `query`\n const inputKindRef = useRef<typeof inputKind | null>(null)\n const previousInputKind = inputKindRef.current\n\n const dependenciesChanged =\n !isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, index) => dep !== deps[index]))\n const dependenciesStructurallyEqual =\n !isCollection &&\n depsRef.current !== null &&\n deepEquals(depsRef.current, deps)\n const needsNewCollection =\n !collectionRef.current ||\n inputKindRef.current !== inputKind ||\n (isCollection && configRef.current !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n pageSizeRef.current !== pageSize ||\n initialPageParamRef.current !== initialPageParam\n const needsNewController =\n !controllerRef.current || needsNewCollection || pageShapeChanged\n\n if (needsNewCollection) {\n inputKindRef.current = inputKind\n if (isCollection) {\n const collection = queryFnOrCollection as Collection<any, any, any>\n if (!hasSetWindow(collection)) {\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 // Warn once per collection instance if its current window doesn't match\n // the first page the hook is about to enforce.\n if (validatedCollectionRef.current !== collection) {\n validatedCollectionRef.current = collection\n const currentWindow = collection.utils.getWindow?.()\n if (\n currentWindow &&\n (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`,\n )\n }\n }\n collectionRef.current = collection\n configRef.current = queryFnOrCollection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collectionRef.current = createLiveQueryCollection({\n query: (q: InitialQueryBuilder) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n depsRef.current = [...deps]\n }\n }\n\n if (needsNewController) {\n const previousController = controllerRef.current\n const canPreservePageCount =\n previousController !== null &&\n (!needsNewCollection ||\n (previousInputKind === `query` && dependenciesStructurallyEqual))\n const initialPageCount = canPreservePageCount\n ? Math.max(1, previousController.getSnapshot().pages.length)\n : 1\n pageSizeRef.current = pageSize\n initialPageParamRef.current = initialPageParam\n controllerRef.current = createLiveQueryWindowController(\n collectionRef.current,\n {\n pageSize,\n initialPageParam,\n initialPageCount,\n },\n )\n }\n const controller = controllerRef.current!\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => controller.subscribe(onStoreChange),\n [controller],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n\n const fetchNextPage = useCallback(() => {\n void controller.fetchNextPage().catch(() => {\n // Pagination errors are exposed through the controller snapshot. The\n // hook's void callback has no promise error channel, so consume it here.\n })\n }, [controller])\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled:\n snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":["CollectionImpl","useRef","deepEquals","createLiveQueryCollection","createLiveQueryWindowController","useCallback","useSyncExternalStore"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\n// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.\nimport type { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\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: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: 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 = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n const inputKind = inputIsCollection ? `collection` : `query`\n\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n deps,\n )\n const dependenciesChanged = !inputIsCollection && dependencyComparison.changed\n const dependenciesStructurallyEqual =\n !inputIsCollection && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n\n if (needsNewCollection) {\n const input = resolveLiveQueryWindowInput<TContext>(queryFnOrCollection)\n if (input.kind === `collection`) {\n collection = input.collection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(pageSize + 1).offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === collection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n dependencies: inputIsCollection ? null : [...deps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled:\n snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":["normalizeLiveQueryWindowPageSize","getLiveQueryWindowInputKind","useRef","compareLiveQueryWindowDependencies","resolveLiveQueryWindowInput","createLiveQueryCollection","getLiveQueryWindowCollectionWarning","assertLiveQueryWindowManyResult","shouldPreserveLiveQueryWindowPageCount","createLiveQueryWindowController","useCallback","useSyncExternalStore","fetchNextLiveQueryWindowPage"],"mappings":"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"}
@@ -14,7 +14,7 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
14
14
  data: InferResultType<TContext>;
15
15
  pages: Array<Array<InferResultType<TContext>[number]>>;
16
16
  pageParams: Array<number>;
17
- fetchNextPage: () => void;
17
+ fetchNextPage: () => Promise<void>;
18
18
  hasNextPage: boolean;
19
19
  isFetchingNextPage: boolean;
20
20
  error: unknown;
@@ -14,7 +14,7 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
14
14
  data: InferResultType<TContext>;
15
15
  pages: Array<Array<InferResultType<TContext>[number]>>;
16
16
  pageParams: Array<number>;
17
- fetchNextPage: () => void;
17
+ fetchNextPage: () => Promise<void>;
18
18
  hasNextPage: boolean;
19
19
  isFetchingNextPage: boolean;
20
20
  error: unknown;
@@ -1,90 +1,95 @@
1
1
  import { useRef, useCallback, useSyncExternalStore } from "react";
2
- import { deepEquals, createLiveQueryCollection, createLiveQueryWindowController, CollectionImpl } from "@tanstack/db";
2
+ import { normalizeLiveQueryWindowPageSize, getLiveQueryWindowInputKind, compareLiveQueryWindowDependencies, resolveLiveQueryWindowInput, createLiveQueryCollection, getLiveQueryWindowCollectionWarning, assertLiveQueryWindowManyResult, shouldPreserveLiveQueryWindowPageCount, createLiveQueryWindowController, fetchNextLiveQueryWindowPage } from "@tanstack/db";
3
3
  const DEFAULT_GC_TIME_MS = 1;
4
- function hasSetWindow(collection) {
5
- return typeof collection.utils?.setWindow === `function`;
6
- }
7
4
  function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
8
- const pageSize = config.pageSize || 20;
5
+ const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize);
9
6
  const initialPageParam = config.initialPageParam ?? 0;
10
- const isCollection = queryFnOrCollection instanceof CollectionImpl;
11
- if (!isCollection && typeof queryFnOrCollection !== `function`) {
12
- throw new Error(
13
- `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) or a query function. Received: ${typeof queryFnOrCollection}`
14
- );
15
- }
16
- const collectionRef = useRef(null);
17
- const controllerRef = useRef(null);
18
- const configRef = useRef(null);
19
- const depsRef = useRef(null);
20
- const pageSizeRef = useRef(pageSize);
21
- const initialPageParamRef = useRef(initialPageParam);
22
- const validatedCollectionRef = useRef(null);
23
- const inputKind = isCollection ? `collection` : `query`;
24
- const inputKindRef = useRef(null);
25
- const previousInputKind = inputKindRef.current;
26
- const dependenciesChanged = !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, index) => dep !== deps[index]));
27
- const dependenciesStructurallyEqual = !isCollection && depsRef.current !== null && deepEquals(depsRef.current, deps);
28
- const needsNewCollection = !collectionRef.current || inputKindRef.current !== inputKind || isCollection && configRef.current !== queryFnOrCollection || dependenciesChanged;
29
- const pageShapeChanged = pageSizeRef.current !== pageSize || initialPageParamRef.current !== initialPageParam;
30
- const needsNewController = !controllerRef.current || needsNewCollection || pageShapeChanged;
31
- if (needsNewCollection) {
32
- inputKindRef.current = inputKind;
33
- if (isCollection) {
34
- const collection = queryFnOrCollection;
35
- if (!hasSetWindow(collection)) {
36
- throw new Error(
37
- `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
38
- );
39
- }
40
- if (validatedCollectionRef.current !== collection) {
41
- validatedCollectionRef.current = collection;
42
- const currentWindow = collection.utils.getWindow?.();
43
- if (currentWindow && (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)) {
44
- console.warn(
45
- `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`
46
- );
47
- }
7
+ const inputIsCollection = getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
8
+ const committedRef = useRef(null);
9
+ const committed = committedRef.current;
10
+ const inputKind = inputIsCollection ? `collection` : `query`;
11
+ const dependencyComparison = compareLiveQueryWindowDependencies(
12
+ committed?.dependencies,
13
+ deps
14
+ );
15
+ const dependenciesChanged = !inputIsCollection && dependencyComparison.changed;
16
+ const dependenciesStructurallyEqual = !inputIsCollection && dependencyComparison.structurallyEqual;
17
+ const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
18
+ const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
19
+ const needsNewController = committed === null || needsNewCollection || pageShapeChanged;
20
+ let renderState = committed;
21
+ if (needsNewController) {
22
+ let collection = committed?.collection;
23
+ let warning = null;
24
+ if (needsNewCollection) {
25
+ const input = resolveLiveQueryWindowInput(queryFnOrCollection);
26
+ if (input.kind === `collection`) {
27
+ collection = input.collection;
28
+ } else {
29
+ collection = createLiveQueryCollection({
30
+ query: input.query.limit(pageSize + 1).offset(0),
31
+ // Construction happens during render. Synchronization starts only when
32
+ // useSyncExternalStore commits the controller subscription.
33
+ startSync: false,
34
+ gcTime: DEFAULT_GC_TIME_MS
35
+ });
48
36
  }
49
- collectionRef.current = collection;
50
- configRef.current = queryFnOrCollection;
37
+ }
38
+ if (!collection) {
39
+ throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`);
40
+ }
41
+ if (inputIsCollection) {
42
+ warning = getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null;
51
43
  } else {
52
- collectionRef.current = createLiveQueryCollection({
53
- query: (q) => queryFnOrCollection(q).limit(pageSize + 1).offset(0),
54
- // Construction happens during render. Synchronization starts only when
55
- // useSyncExternalStore commits the controller subscription.
56
- startSync: false,
57
- gcTime: DEFAULT_GC_TIME_MS
58
- });
59
- depsRef.current = [...deps];
44
+ assertLiveQueryWindowManyResult(collection);
60
45
  }
61
- }
62
- if (needsNewController) {
63
- const previousController = controllerRef.current;
64
- const canPreservePageCount = previousController !== null && (!needsNewCollection || previousInputKind === `query` && dependenciesStructurallyEqual);
65
- const initialPageCount = canPreservePageCount ? Math.max(1, previousController.getSnapshot().pages.length) : 1;
66
- pageSizeRef.current = pageSize;
67
- initialPageParamRef.current = initialPageParam;
68
- controllerRef.current = createLiveQueryWindowController(
69
- collectionRef.current,
70
- {
46
+ const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({
47
+ hasPreviousController: committed !== null,
48
+ previousInputKind: committed?.inputKind,
49
+ inputKind,
50
+ sameCollection: inputIsCollection && committed?.inputCollection === collection,
51
+ dependenciesChanged,
52
+ dependenciesStructurallyEqual,
53
+ pageShapeChanged
54
+ });
55
+ const previousPageCount = committed ? Math.max(1, committed.controller.getSnapshot().pages.length) : 1;
56
+ const initialPageCount = canPreservePageCount ? previousPageCount : 1;
57
+ renderState = {
58
+ inputKind,
59
+ inputCollection: inputIsCollection ? collection : null,
60
+ dependencies: inputIsCollection ? null : [...deps],
61
+ pageSize,
62
+ initialPageParam,
63
+ collection,
64
+ controller: createLiveQueryWindowController(collection, {
71
65
  pageSize,
72
66
  initialPageParam,
73
67
  initialPageCount
74
- }
75
- );
68
+ }),
69
+ warning,
70
+ warned: false
71
+ };
76
72
  }
77
- const controller = controllerRef.current;
73
+ const currentRenderState = renderState;
74
+ const controller = currentRenderState.controller;
78
75
  const subscribe = useCallback(
79
- (onStoreChange) => controller.subscribe(onStoreChange),
80
- [controller]
76
+ (onStoreChange) => {
77
+ const unsubscribe = controller.subscribe(onStoreChange);
78
+ committedRef.current = currentRenderState;
79
+ if (currentRenderState.warning && !currentRenderState.warned) {
80
+ currentRenderState.warned = true;
81
+ console.warn(currentRenderState.warning);
82
+ }
83
+ return unsubscribe;
84
+ },
85
+ [controller, currentRenderState]
81
86
  );
82
87
  const getSnapshot = useCallback(() => controller.getSnapshot(), [controller]);
83
88
  const snapshot = useSyncExternalStore(subscribe, getSnapshot);
84
- const fetchNextPage = useCallback(() => {
85
- void controller.fetchNextPage().catch(() => {
86
- });
87
- }, [controller]);
89
+ const fetchNextPage = useCallback(
90
+ () => fetchNextLiveQueryWindowPage(controller),
91
+ [controller]
92
+ );
88
93
  return {
89
94
  data: snapshot.data,
90
95
  state: snapshot.state,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n CollectionImpl,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n deepEquals,\n} from '@tanstack/db'\n// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.\nimport type { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\n\ntype WindowedCollection = Collection<any, any, any> & {\n utils: {\n setWindow: (options: {\n offset: number\n limit: number\n }) => true | Promise<void>\n }\n}\n\n/** Type guard: does this collection expose `setWindow` (i.e. has an orderBy)? */\nfunction hasSetWindow(\n collection: Collection<any, any, any>,\n): collection is WindowedCollection {\n return typeof collection.utils?.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 error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\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 const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const controllerRef = useRef<LiveQueryWindowController<any, any> | null>(null)\n const configRef = useRef<unknown>(null)\n const depsRef = useRef<Array<unknown> | null>(null)\n const pageSizeRef = useRef(pageSize)\n const initialPageParamRef = useRef(initialPageParam)\n const validatedCollectionRef = useRef<unknown>(null)\n const inputKind = isCollection ? `collection` : `query`\n const inputKindRef = useRef<typeof inputKind | null>(null)\n const previousInputKind = inputKindRef.current\n\n const dependenciesChanged =\n !isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, index) => dep !== deps[index]))\n const dependenciesStructurallyEqual =\n !isCollection &&\n depsRef.current !== null &&\n deepEquals(depsRef.current, deps)\n const needsNewCollection =\n !collectionRef.current ||\n inputKindRef.current !== inputKind ||\n (isCollection && configRef.current !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n pageSizeRef.current !== pageSize ||\n initialPageParamRef.current !== initialPageParam\n const needsNewController =\n !controllerRef.current || needsNewCollection || pageShapeChanged\n\n if (needsNewCollection) {\n inputKindRef.current = inputKind\n if (isCollection) {\n const collection = queryFnOrCollection as Collection<any, any, any>\n if (!hasSetWindow(collection)) {\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 // Warn once per collection instance if its current window doesn't match\n // the first page the hook is about to enforce.\n if (validatedCollectionRef.current !== collection) {\n validatedCollectionRef.current = collection\n const currentWindow = collection.utils.getWindow?.()\n if (\n currentWindow &&\n (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`,\n )\n }\n }\n collectionRef.current = collection\n configRef.current = queryFnOrCollection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collectionRef.current = createLiveQueryCollection({\n query: (q: InitialQueryBuilder) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n depsRef.current = [...deps]\n }\n }\n\n if (needsNewController) {\n const previousController = controllerRef.current\n const canPreservePageCount =\n previousController !== null &&\n (!needsNewCollection ||\n (previousInputKind === `query` && dependenciesStructurallyEqual))\n const initialPageCount = canPreservePageCount\n ? Math.max(1, previousController.getSnapshot().pages.length)\n : 1\n pageSizeRef.current = pageSize\n initialPageParamRef.current = initialPageParam\n controllerRef.current = createLiveQueryWindowController(\n collectionRef.current,\n {\n pageSize,\n initialPageParam,\n initialPageCount,\n },\n )\n }\n const controller = controllerRef.current!\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => controller.subscribe(onStoreChange),\n [controller],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n\n const fetchNextPage = useCallback(() => {\n void controller.fetchNextPage().catch(() => {\n // Pagination errors are exposed through the controller snapshot. The\n // hook's void callback has no promise error channel, so consume it here.\n })\n }, [controller])\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled:\n snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\n// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.\nimport type { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\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: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: 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 = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n const inputKind = inputIsCollection ? `collection` : `query`\n\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n deps,\n )\n const dependenciesChanged = !inputIsCollection && dependencyComparison.changed\n const dependenciesStructurallyEqual =\n !inputIsCollection && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n\n if (needsNewCollection) {\n const input = resolveLiveQueryWindowInput<TContext>(queryFnOrCollection)\n if (input.kind === `collection`) {\n collection = input.collection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(pageSize + 1).offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === collection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n dependencies: inputIsCollection ? null : [...deps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled:\n snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
- "version": "0.1.96",
3
+ "version": "0.2.1",
4
4
  "description": "React integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "use-sync-external-store": "^1.6.0",
44
- "@tanstack/db": "0.7.0"
44
+ "@tanstack/db": "0.7.2"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": ">=16.8.0"
@@ -13,7 +13,7 @@ description: >
13
13
  type: framework
14
14
  library: db
15
15
  framework: react
16
- library_version: '0.6.0'
16
+ library_version: '0.6.17'
17
17
  requires:
18
18
  - db-core
19
19
  sources:
@@ -229,7 +229,7 @@ const { data: projects } = useLiveQuery((q) =>
229
229
  ),
230
230
  })),
231
231
  )
232
- // project.issues is string[] — no subcomponent needed
232
+ // project.issues is Array<{ id: string; title: string }> — no subcomponent needed
233
233
  ```
234
234
 
235
235
  See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).
@@ -238,7 +238,8 @@ See db-core/live-queries/SKILL.md for full includes rules (correlation condition
238
238
 
239
239
  Live query results include computed, read-only virtual properties on every row:
240
240
 
241
- - `$synced`: `true` when the row is confirmed by sync; `false` when it is still optimistic.
241
+ - `$synced`: `true` when no pending local optimistic write affects the row;
242
+ `false` while one does. It does not prove backend confirmation.
242
243
  - `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`.
243
244
  - `$key`: the row key for the result.
244
245
  - `$collectionId`: the source collection ID.
@@ -253,7 +254,7 @@ const { data } = useLiveQuery(
253
254
  .where(({ todo }) => eq(todo.$synced, false)),
254
255
  [],
255
256
  )
256
- // Shows only optimistic (unconfirmed) todos
257
+ // Shows rows with pending local optimistic writes
257
258
  ```
258
259
 
259
260
  ## React-Specific Patterns
@@ -354,7 +355,12 @@ Source: docs/guides/live-queries.md
354
355
 
355
356
  ### HIGH "Not a Collection" error from duplicate @tanstack/db
356
357
 
357
- If `useLiveQuery` throws `InvalidSourceError: The value provided for alias "todo" is not a Collection`, it usually means two copies of `@tanstack/db` are installed. The collection was created by one copy, but `useLiveQuery` checks `instanceof` against the other.
358
+ If a query-builder alias throws
359
+ `InvalidSourceError: The value provided for alias "todo" is not a Collection`,
360
+ it can mean two copies of `@tanstack/db` are installed. Direct
361
+ `useLiveQuery(preCreatedCollection)` detection is structural and works across
362
+ package copies or realms, but `q.from({ todo: collection })` still validates
363
+ the source with the core collection class.
358
364
 
359
365
  In dev mode, TanStack DB also throws `DuplicateDbInstanceError` if two instances are detected.
360
366
 
@@ -372,7 +378,7 @@ If multiple versions appear, fix with one of:
372
378
  {
373
379
  "pnpm": {
374
380
  "overrides": {
375
- "@tanstack/db": "^0.6.0"
381
+ "@tanstack/db": "^0.6.17"
376
382
  }
377
383
  }
378
384
  }
@@ -1,9 +1,15 @@
1
1
  import { useCallback, useRef, useSyncExternalStore } from 'react'
2
2
  import {
3
- CollectionImpl,
3
+ assertLiveQueryWindowManyResult,
4
+ compareLiveQueryWindowDependencies,
4
5
  createLiveQueryCollection,
5
6
  createLiveQueryWindowController,
6
- deepEquals,
7
+ fetchNextLiveQueryWindowPage,
8
+ getLiveQueryWindowCollectionWarning,
9
+ getLiveQueryWindowInputKind,
10
+ normalizeLiveQueryWindowPageSize,
11
+ resolveLiveQueryWindowInput,
12
+ shouldPreserveLiveQueryWindowPageCount,
7
13
  } from '@tanstack/db'
8
14
  // Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.
9
15
  import type { useLiveQuery } from './useLiveQuery'
@@ -20,22 +26,6 @@ import type {
20
26
  // Live queries created here are cleaned up immediately (0 disables GC).
21
27
  const DEFAULT_GC_TIME_MS = 1
22
28
 
23
- type WindowedCollection = Collection<any, any, any> & {
24
- utils: {
25
- setWindow: (options: {
26
- offset: number
27
- limit: number
28
- }) => true | Promise<void>
29
- }
30
- }
31
-
32
- /** Type guard: does this collection expose `setWindow` (i.e. has an orderBy)? */
33
- function hasSetWindow(
34
- collection: Collection<any, any, any>,
35
- ): collection is WindowedCollection {
36
- return typeof collection.utils?.setWindow === `function`
37
- }
38
-
39
29
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
40
30
  pageSize?: number
41
31
  initialPageParam?: number
@@ -59,7 +49,7 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<
59
49
  data: InferResultType<TContext>
60
50
  pages: Array<Array<InferResultType<TContext>[number]>>
61
51
  pageParams: Array<number>
62
- fetchNextPage: () => void
52
+ fetchNextPage: () => Promise<void>
63
53
  hasNextPage: boolean
64
54
  isFetchingNextPage: boolean
65
55
  error: unknown
@@ -69,6 +59,18 @@ type EnabledLiveQueryReturn<TContext extends Context> = ReturnType<
69
59
  typeof useLiveQuery<TContext>
70
60
  >
71
61
 
62
+ type InfiniteQueryRenderState = {
63
+ inputKind: `collection` | `query`
64
+ inputCollection: Collection<any, any, any> | null
65
+ dependencies: Array<unknown> | null
66
+ pageSize: number
67
+ initialPageParam: number
68
+ collection: Collection<any, any, any>
69
+ controller: LiveQueryWindowController<any, any>
70
+ warning: string | null
71
+ warned: boolean
72
+ }
73
+
72
74
  /**
73
75
  * Create an infinite query using a query function with live updates
74
76
  *
@@ -158,130 +160,120 @@ export function useLiveInfiniteQuery<TContext extends Context>(
158
160
  config: UseLiveInfiniteQueryConfig<TContext>,
159
161
  deps: Array<unknown> = [],
160
162
  ): UseLiveInfiniteQueryReturn<TContext> {
161
- const pageSize = config.pageSize || 20
163
+ const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)
162
164
  const initialPageParam = config.initialPageParam ?? 0
163
165
 
164
- // Detect if input is a collection or query function
165
- const isCollection = queryFnOrCollection instanceof CollectionImpl
166
+ const inputIsCollection =
167
+ getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`
166
168
 
167
- // Validate input type
168
- if (!isCollection && typeof queryFnOrCollection !== `function`) {
169
- throw new Error(
170
- `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +
171
- `or a query function. Received: ${typeof queryFnOrCollection}`,
172
- )
173
- }
174
-
175
- const collectionRef = useRef<Collection<any, any, any> | null>(null)
176
- const controllerRef = useRef<LiveQueryWindowController<any, any> | null>(null)
177
- const configRef = useRef<unknown>(null)
178
- const depsRef = useRef<Array<unknown> | null>(null)
179
- const pageSizeRef = useRef(pageSize)
180
- const initialPageParamRef = useRef(initialPageParam)
181
- const validatedCollectionRef = useRef<unknown>(null)
182
- const inputKind = isCollection ? `collection` : `query`
183
- const inputKindRef = useRef<typeof inputKind | null>(null)
184
- const previousInputKind = inputKindRef.current
169
+ const committedRef = useRef<InfiniteQueryRenderState | null>(null)
170
+ const committed = committedRef.current
171
+ const inputKind = inputIsCollection ? `collection` : `query`
185
172
 
186
- const dependenciesChanged =
187
- !isCollection &&
188
- (depsRef.current === null ||
189
- depsRef.current.length !== deps.length ||
190
- depsRef.current.some((dep, index) => dep !== deps[index]))
173
+ const dependencyComparison = compareLiveQueryWindowDependencies(
174
+ committed?.dependencies,
175
+ deps,
176
+ )
177
+ const dependenciesChanged = !inputIsCollection && dependencyComparison.changed
191
178
  const dependenciesStructurallyEqual =
192
- !isCollection &&
193
- depsRef.current !== null &&
194
- deepEquals(depsRef.current, deps)
179
+ !inputIsCollection && dependencyComparison.structurallyEqual
195
180
  const needsNewCollection =
196
- !collectionRef.current ||
197
- inputKindRef.current !== inputKind ||
198
- (isCollection && configRef.current !== queryFnOrCollection) ||
181
+ committed === null ||
182
+ committed.inputKind !== inputKind ||
183
+ (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||
199
184
  dependenciesChanged
200
185
  const pageShapeChanged =
201
- pageSizeRef.current !== pageSize ||
202
- initialPageParamRef.current !== initialPageParam
186
+ committed === null ||
187
+ committed.pageSize !== pageSize ||
188
+ committed.initialPageParam !== initialPageParam
203
189
  const needsNewController =
204
- !controllerRef.current || needsNewCollection || pageShapeChanged
190
+ committed === null || needsNewCollection || pageShapeChanged
205
191
 
206
- if (needsNewCollection) {
207
- inputKindRef.current = inputKind
208
- if (isCollection) {
209
- const collection = queryFnOrCollection as Collection<any, any, any>
210
- if (!hasSetWindow(collection)) {
211
- throw new Error(
212
- `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
213
- `Please add .orderBy() to your createLiveQueryCollection query.`,
214
- )
215
- }
216
- // Warn once per collection instance if its current window doesn't match
217
- // the first page the hook is about to enforce.
218
- if (validatedCollectionRef.current !== collection) {
219
- validatedCollectionRef.current = collection
220
- const currentWindow = collection.utils.getWindow?.()
221
- if (
222
- currentWindow &&
223
- (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)
224
- ) {
225
- console.warn(
226
- `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +
227
- `but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`,
228
- )
229
- }
192
+ let renderState = committed
193
+ if (needsNewController) {
194
+ let collection = committed?.collection
195
+ let warning: string | null = null
196
+
197
+ if (needsNewCollection) {
198
+ const input = resolveLiveQueryWindowInput<TContext>(queryFnOrCollection)
199
+ if (input.kind === `collection`) {
200
+ collection = input.collection
201
+ } else {
202
+ // Wrap the query with the first page's peek-ahead window; the controller
203
+ // grows the limit from here via setWindow.
204
+ collection = createLiveQueryCollection({
205
+ query: input.query.limit(pageSize + 1).offset(0),
206
+ // Construction happens during render. Synchronization starts only when
207
+ // useSyncExternalStore commits the controller subscription.
208
+ startSync: false,
209
+ gcTime: DEFAULT_GC_TIME_MS,
210
+ })
230
211
  }
231
- collectionRef.current = collection
232
- configRef.current = queryFnOrCollection
212
+ }
213
+
214
+ if (!collection) {
215
+ throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)
216
+ }
217
+
218
+ if (inputIsCollection) {
219
+ warning =
220
+ getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null
233
221
  } else {
234
- // Wrap the query with the first page's peek-ahead window; the controller
235
- // grows the limit from here via setWindow.
236
- collectionRef.current = createLiveQueryCollection({
237
- query: (q: InitialQueryBuilder) =>
238
- queryFnOrCollection(q)
239
- .limit(pageSize + 1)
240
- .offset(0),
241
- // Construction happens during render. Synchronization starts only when
242
- // useSyncExternalStore commits the controller subscription.
243
- startSync: false,
244
- gcTime: DEFAULT_GC_TIME_MS,
245
- })
246
- depsRef.current = [...deps]
222
+ assertLiveQueryWindowManyResult(collection)
247
223
  }
248
- }
249
224
 
250
- if (needsNewController) {
251
- const previousController = controllerRef.current
252
- const canPreservePageCount =
253
- previousController !== null &&
254
- (!needsNewCollection ||
255
- (previousInputKind === `query` && dependenciesStructurallyEqual))
256
- const initialPageCount = canPreservePageCount
257
- ? Math.max(1, previousController.getSnapshot().pages.length)
225
+ const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({
226
+ hasPreviousController: committed !== null,
227
+ previousInputKind: committed?.inputKind,
228
+ inputKind,
229
+ sameCollection:
230
+ inputIsCollection && committed?.inputCollection === collection,
231
+ dependenciesChanged,
232
+ dependenciesStructurallyEqual,
233
+ pageShapeChanged,
234
+ })
235
+ const previousPageCount = committed
236
+ ? Math.max(1, committed.controller.getSnapshot().pages.length)
258
237
  : 1
259
- pageSizeRef.current = pageSize
260
- initialPageParamRef.current = initialPageParam
261
- controllerRef.current = createLiveQueryWindowController(
262
- collectionRef.current,
263
- {
238
+ const initialPageCount = canPreservePageCount ? previousPageCount : 1
239
+ renderState = {
240
+ inputKind,
241
+ inputCollection: inputIsCollection ? collection : null,
242
+ dependencies: inputIsCollection ? null : [...deps],
243
+ pageSize,
244
+ initialPageParam,
245
+ collection,
246
+ controller: createLiveQueryWindowController(collection, {
264
247
  pageSize,
265
248
  initialPageParam,
266
249
  initialPageCount,
267
- },
268
- )
250
+ }),
251
+ warning,
252
+ warned: false,
253
+ }
269
254
  }
270
- const controller = controllerRef.current!
255
+ const currentRenderState = renderState!
256
+ const controller = currentRenderState.controller
271
257
 
272
258
  const subscribe = useCallback(
273
- (onStoreChange: () => void) => controller.subscribe(onStoreChange),
274
- [controller],
259
+ (onStoreChange: () => void) => {
260
+ const unsubscribe = controller.subscribe(onStoreChange)
261
+ committedRef.current = currentRenderState
262
+ if (currentRenderState.warning && !currentRenderState.warned) {
263
+ currentRenderState.warned = true
264
+ console.warn(currentRenderState.warning)
265
+ }
266
+ return unsubscribe
267
+ },
268
+ [controller, currentRenderState],
275
269
  )
276
270
  const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])
277
271
  const snapshot = useSyncExternalStore(subscribe, getSnapshot)
278
272
 
279
- const fetchNextPage = useCallback(() => {
280
- void controller.fetchNextPage().catch(() => {
281
- // Pagination errors are exposed through the controller snapshot. The
282
- // hook's void callback has no promise error channel, so consume it here.
283
- })
284
- }, [controller])
273
+ const fetchNextPage = useCallback(
274
+ () => fetchNextLiveQueryWindowPage(controller),
275
+ [controller],
276
+ )
285
277
 
286
278
  return {
287
279
  data: snapshot.data as InferResultType<TContext>,