@tanstack/react-db 0.1.95 → 0.2.0

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.
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source
4
+ media="(prefers-color-scheme: dark)"
5
+ srcset="https://tanstack.com/api/readme/db.png?framework=react&theme=dark"
6
+ />
7
+ <source
8
+ media="(prefers-color-scheme: light)"
9
+ srcset="https://tanstack.com/api/readme/db.png?framework=react"
10
+ />
11
+ <img
12
+ src="https://tanstack.com/api/readme/db.png?framework=react"
13
+ alt="TanStack React DB"
14
+ width="900"
15
+ />
16
+ </picture>
17
+ </div>
1
18
  # @tanstack/react-db
2
19
 
3
20
  React hooks for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
@@ -2,136 +2,113 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
4
  const db = require("@tanstack/db");
5
- const useLiveQuery = require("./useLiveQuery.cjs");
6
- function isLiveQueryCollectionUtils(utils) {
7
- return typeof utils.setWindow === `function`;
8
- }
5
+ const DEFAULT_GC_TIME_MS = 1;
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 [loadedPageCount, setLoadedPageCount] = react.useState(1);
19
- const [isFetchingNextPage, setIsFetchingNextPage] = react.useState(false);
20
- const collectionRef = react.useRef(isCollection ? queryFnOrCollection : null);
21
- const hasValidatedCollectionRef = react.useRef(false);
22
- let depsKey;
23
- try {
24
- depsKey = JSON.stringify(deps);
25
- } catch {
26
- throw new Error(
27
- `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). Ensure all dependency values are JSON-serializable.`
28
- );
29
- }
30
- const prevDepsKeyRef = react.useRef(depsKey);
31
- react.useEffect(() => {
32
- let shouldReset = false;
33
- if (isCollection) {
34
- if (collectionRef.current !== queryFnOrCollection) {
35
- collectionRef.current = queryFnOrCollection;
36
- hasValidatedCollectionRef.current = false;
37
- shouldReset = true;
38
- }
39
- } else {
40
- if (prevDepsKeyRef.current !== depsKey) {
41
- prevDepsKeyRef.current = depsKey;
42
- shouldReset = true;
43
- }
44
- }
45
- if (shouldReset) {
46
- setLoadedPageCount(1);
47
- }
48
- }, [isCollection, queryFnOrCollection, depsKey]);
49
- const queryResult = isCollection ? useLiveQuery.useLiveQuery(queryFnOrCollection) : useLiveQuery.useLiveQuery(
50
- (q) => queryFnOrCollection(q).limit(pageSize + 1).offset(0),
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,
51
15
  deps
52
16
  );
53
- react.useEffect(() => {
54
- const utils = queryResult.collection.utils;
55
- const expectedOffset = 0;
56
- const expectedLimit = loadedPageCount * pageSize + 1;
57
- if (!isLiveQueryCollectionUtils(utils)) {
58
- if (isCollection) {
59
- throw new Error(
60
- `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
61
- );
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
+ });
62
38
  }
63
- return;
64
39
  }
65
- if (isCollection && !hasValidatedCollectionRef.current) {
66
- const currentWindow = utils.getWindow();
67
- if (currentWindow && (currentWindow.offset !== expectedOffset || currentWindow.limit !== expectedLimit)) {
68
- console.warn(
69
- `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
70
- );
71
- }
72
- hasValidatedCollectionRef.current = true;
40
+ if (!collection) {
41
+ throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`);
73
42
  }
74
- if (!isCollection && !queryResult.isReady) return;
75
- let cancelled = false;
76
- const result = utils.setWindow({
77
- offset: expectedOffset,
78
- limit: expectedLimit
79
- });
80
- if (result !== true) {
81
- setIsFetchingNextPage(true);
82
- result.catch((error) => {
83
- if (!cancelled)
84
- console.error(`useLiveInfiniteQuery: setWindow failed:`, error);
85
- }).finally(() => {
86
- if (!cancelled) setIsFetchingNextPage(false);
87
- });
43
+ if (inputIsCollection) {
44
+ warning = db.getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null;
88
45
  } else {
89
- setIsFetchingNextPage(false);
90
- }
91
- return () => {
92
- cancelled = true;
93
- };
94
- }, [
95
- isCollection,
96
- queryResult.collection,
97
- queryResult.isReady,
98
- loadedPageCount,
99
- pageSize
100
- ]);
101
- const { pages, pageParams, hasNextPage, flatData } = react.useMemo(() => {
102
- const dataArray = Array.isArray(queryResult.data) ? queryResult.data : [];
103
- const totalItemsRequested = loadedPageCount * pageSize;
104
- const hasMore = dataArray.length > totalItemsRequested;
105
- const pagesResult = [];
106
- const pageParamsResult = [];
107
- for (let i = 0; i < loadedPageCount; i++) {
108
- const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize);
109
- pagesResult.push(pageData);
110
- pageParamsResult.push(initialPageParam + i);
46
+ db.assertLiveQueryWindowManyResult(collection);
111
47
  }
112
- const flatDataResult = dataArray.slice(
113
- 0,
114
- totalItemsRequested
115
- );
116
- return {
117
- pages: pagesResult,
118
- pageParams: pageParamsResult,
119
- hasNextPage: hasMore,
120
- flatData: flatDataResult
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, {
67
+ pageSize,
68
+ initialPageParam,
69
+ initialPageCount
70
+ }),
71
+ warning,
72
+ warned: false
121
73
  };
122
- }, [queryResult.data, loadedPageCount, pageSize, initialPageParam]);
123
- const fetchNextPage = react.useCallback(() => {
124
- if (!hasNextPage || isFetchingNextPage) return;
125
- setLoadedPageCount((prev) => prev + 1);
126
- }, [hasNextPage, isFetchingNextPage]);
74
+ }
75
+ const currentRenderState = renderState;
76
+ const controller = currentRenderState.controller;
77
+ const subscribe = react.useCallback(
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]
88
+ );
89
+ const getSnapshot = react.useCallback(() => controller.getSnapshot(), [controller]);
90
+ const snapshot = react.useSyncExternalStore(subscribe, getSnapshot);
91
+ const fetchNextPage = react.useCallback(
92
+ () => db.fetchNextLiveQueryWindowPage(controller),
93
+ [controller]
94
+ );
127
95
  return {
128
- ...queryResult,
129
- data: flatData,
130
- pages,
131
- pageParams,
96
+ data: snapshot.data,
97
+ state: snapshot.state,
98
+ status: snapshot.status,
99
+ isLoading: snapshot.isLoading,
100
+ isReady: snapshot.isReady,
101
+ isIdle: snapshot.isIdle,
102
+ isError: snapshot.isError,
103
+ isCleanedUp: snapshot.isCleanedUp,
104
+ collection: snapshot.collection,
105
+ isEnabled: snapshot.isEnabled,
106
+ pages: snapshot.pages,
107
+ pageParams: snapshot.pageParams,
132
108
  fetchNextPage,
133
- hasNextPage,
134
- isFetchingNextPage
109
+ hasNextPage: snapshot.hasNextPage,
110
+ isFetchingNextPage: snapshot.isFetchingNextPage,
111
+ error: snapshot.error
135
112
  };
136
113
  }
137
114
  exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
@@ -1 +1 @@
1
- {"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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"}
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,9 +14,10 @@ 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
+ error: unknown;
20
21
  };
21
22
  /**
22
23
  * Create an infinite query using a query function with live updates
@@ -10,8 +10,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
10
10
  );
11
11
  const depsRef = react.useRef(null);
12
12
  const configRef = react.useRef(null);
13
- const versionRef = react.useRef(0);
14
- const snapshotRef = react.useRef(null);
13
+ const observerRef = react.useRef(
14
+ null
15
+ );
15
16
  const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
16
17
  if (needsNewCollection) {
17
18
  if (inputIsCollection) {
@@ -67,96 +68,19 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
67
68
  }
68
69
  }
69
70
  if (needsNewCollection) {
70
- versionRef.current = 0;
71
- snapshotRef.current = null;
71
+ observerRef.current = db.createLiveQueryObserver(collectionRef.current, {
72
+ mode: `wholesale`
73
+ });
72
74
  }
75
+ const observer = observerRef.current;
73
76
  const subscribeRef = react.useRef(null);
74
77
  if (!subscribeRef.current || needsNewCollection) {
75
- subscribeRef.current = (onStoreChange) => {
76
- if (!collectionRef.current) {
77
- return () => {
78
- };
79
- }
80
- let unsubscribed = false;
81
- const subscription = collectionRef.current.subscribeChanges(() => {
82
- if (unsubscribed) return;
83
- versionRef.current += 1;
84
- onStoreChange();
85
- });
86
- if (collectionRef.current.status === `ready`) {
87
- queueMicrotask(() => {
88
- if (unsubscribed) return;
89
- versionRef.current += 1;
90
- onStoreChange();
91
- });
92
- }
93
- return () => {
94
- unsubscribed = true;
95
- subscription.unsubscribe();
96
- };
97
- };
78
+ subscribeRef.current = (onStoreChange) => observer.subscribe(() => onStoreChange());
98
79
  }
99
- const getSnapshotRef = react.useRef(null);
100
- if (!getSnapshotRef.current || needsNewCollection) {
101
- getSnapshotRef.current = () => {
102
- const currentVersion = versionRef.current;
103
- const currentCollection = collectionRef.current;
104
- if (!snapshotRef.current || snapshotRef.current.version !== currentVersion || snapshotRef.current.collection !== currentCollection) {
105
- snapshotRef.current = {
106
- collection: currentCollection,
107
- version: currentVersion
108
- };
109
- }
110
- return snapshotRef.current;
111
- };
112
- }
113
- const snapshot = react.useSyncExternalStore(
80
+ return react.useSyncExternalStore(
114
81
  subscribeRef.current,
115
- getSnapshotRef.current
82
+ () => observer.getSnapshot()
116
83
  );
117
- const returnedSnapshotRef = react.useRef(null);
118
- const returnedRef = react.useRef(null);
119
- if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
120
- if (!snapshot.collection) {
121
- returnedRef.current = {
122
- state: void 0,
123
- data: void 0,
124
- collection: void 0,
125
- status: `disabled`,
126
- isLoading: false,
127
- isReady: true,
128
- isIdle: false,
129
- isError: false,
130
- isCleanedUp: false,
131
- isEnabled: false
132
- };
133
- } else {
134
- const entries = Array.from(snapshot.collection.entries());
135
- const singleResult = db.isSingleResultCollection(snapshot.collection);
136
- let stateCache = null;
137
- let dataCache = null;
138
- returnedRef.current = {
139
- get state() {
140
- if (!stateCache) {
141
- stateCache = new Map(entries);
142
- }
143
- return stateCache;
144
- },
145
- get data() {
146
- if (!dataCache) {
147
- dataCache = entries.map(([, value]) => value);
148
- }
149
- return singleResult ? dataCache[0] : dataCache;
150
- },
151
- collection: snapshot.collection,
152
- status: snapshot.collection.status,
153
- ...db.getLiveQueryStatusFlags(snapshot.collection.status),
154
- isEnabled: true
155
- };
156
- }
157
- returnedSnapshotRef.current = snapshot;
158
- }
159
- return returnedRef.current;
160
84
  }
161
85
  exports.useLiveQuery = useLiveQuery;
162
86
  //# sourceMappingURL=useLiveQuery.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n getLiveQueryStatusFlags,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (syncMode === `on-demand`) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (isCollection(result)) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n let unsubscribed = false\n\n const subscription = collectionRef.current.subscribeChanges(() => {\n // Drop late notifies that race with unsubscribe.\n if (unsubscribed) return\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Already-ready collections won't emit an initial change. Notify React\n // ourselves, but defer to a microtask — calling onStoreChange synchronously\n // here lands during the render-to-commit window and trips React's\n // \"state update on a component that hasn't mounted yet\" warning.\n if (collectionRef.current.status === `ready`) {\n queueMicrotask(() => {\n if (unsubscribed) return\n versionRef.current += 1\n onStoreChange()\n })\n }\n return () => {\n unsubscribed = true\n subscription.unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current,\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: true,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n const singleResult = isSingleResultCollection(snapshot.collection)\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return singleResult ? dataCache[0] : dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n ...getLiveQueryStatusFlags(snapshot.collection.status),\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":["isCollection","useRef","BaseQueryBuilder","createLiveQueryCollection","useSyncExternalStore","isSingleResultCollection","getLiveQueryStatusFlags"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // The shared observer owns subscription, the ready-race, and the snapshot.\n const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(\n null,\n )\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (syncMode === `on-demand`) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (isCollection(result)) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n observerRef.current = createLiveQueryObserver(collectionRef.current, {\n mode: `wholesale`,\n })\n }\n const observer = observerRef.current!\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange) =>\n observer.subscribe(() => onStoreChange())\n }\n\n // The observer returns a stable snapshot per revision, which is the return\n // shape this hook exposes. Keep the return loose to satisfy the overloads.\n return useSyncExternalStore(subscribeRef.current, () =>\n observer.getSnapshot(),\n ) as any\n}\n"],"names":["isCollection","useRef","BaseQueryBuilder","createLiveQueryCollection","createLiveQueryObserver","useSyncExternalStore"],"mappings":";;;;AAqBA,MAAM,qBAAqB;AAuSpB,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,oBAAoBA,GAAAA,aAAa,yBAAyB;AAGhE,QAAM,gBAAgBC,MAAAA;AAAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAUA,MAAAA,OAA8B,IAAI;AAClD,QAAM,YAAYA,MAAAA,OAAgB,IAAI;AAGtC,QAAM,cAAcA,MAAAA;AAAAA,IAClB;AAAA,EAAA;AAIF,QAAM,qBACJ,CAAC,cAAc,WACd,qBAAqB,UAAU,YAAY,6BAC3C,CAAC,sBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,mBAAmB;AAIrB,YAAM,WACJ,0BACA,QAAQ;AACV,UAAI,aAAa,aAAa;AAC5B,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA;AAAA,MAMJ;AAEA,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAEL,UAAI,OAAO,8BAA8B,YAAY;AAEnD,cAAM,eAAe,IAAIC,oBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,wBAAc,UAAU;AAAA,QAC1B,WAAWF,gBAAa,MAAM,GAAG;AAE/B,iBAAO,mBAAA;AACP,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkBE,qBAAkB;AAG7C,wBAAc,UAAUC,6BAA0B;AAAA,YAChD,OAAO;AAAA,YACP,WAAW;AAAA,YACX,QAAQ;AAAA,UAAA,CACT;AAAA,QACH,WAAW,UAAU,OAAO,WAAW,UAAU;AAE/C,wBAAc,UAAUA,6BAA0B;AAAA,YAChD,WAAW;AAAA,YACX,QAAQ;AAAA,YACR,GAAG;AAAA,UAAA,CACJ;AAAA,QACH,OAAO;AAEL,gBAAM,IAAI;AAAA,YACR,qHAAqH,OAAO,MAAM;AAAA,UAAA;AAAA,QAEtI;AACA,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B,OAAO;AAEL,sBAAc,UAAUA,6BAA0B;AAAA,UAChD,WAAW;AAAA,UACX,QAAQ;AAAA,UACR,GAAG;AAAA,QAAA,CACJ;AACD,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAQA,MAAI,oBAAoB;AAOtB,gBAAY,UAAUC,2BAAwB,cAAc,SAAS;AAAA,MACnE,MAAM;AAAA,IAAA,CACP;AAAA,EACH;AACA,QAAM,WAAW,YAAY;AAI7B,QAAM,eAAeH,MAAAA,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBACtB,SAAS,UAAU,MAAM,eAAe;AAAA,EAC5C;AAIA,SAAOI,MAAAA;AAAAA,IAAqB,aAAa;AAAA,IAAS,MAChD,SAAS,YAAA;AAAA,EAAY;AAEzB;;"}
@@ -14,9 +14,10 @@ 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
+ error: unknown;
20
21
  };
21
22
  /**
22
23
  * Create an infinite query using a query function with live updates