@tanstack/react-db 0.1.96 → 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/dist/cjs/useLiveInfiniteQuery.cjs +76 -71
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +1 -1
- package/dist/esm/useLiveInfiniteQuery.js +77 -72
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveInfiniteQuery.ts +110 -118
|
@@ -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
|
|
7
|
+
const pageSize = db.normalizeLiveQueryWindowPageSize(config.pageSize);
|
|
11
8
|
const initialPageParam = config.initialPageParam ?? 0;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
75
|
+
const currentRenderState = renderState;
|
|
76
|
+
const controller = currentRenderState.controller;
|
|
80
77
|
const subscribe = react.useCallback(
|
|
81
|
-
(onStoreChange) =>
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 {
|
|
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
|
|
5
|
+
const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize);
|
|
9
6
|
const initialPageParam = config.initialPageParam ?? 0;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
73
|
+
const currentRenderState = renderState;
|
|
74
|
+
const controller = currentRenderState.controller;
|
|
78
75
|
const subscribe = useCallback(
|
|
79
|
-
(onStoreChange) =>
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
44
|
+
"@tanstack/db": "0.7.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=16.8.0"
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useCallback, useRef, useSyncExternalStore } from 'react'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
assertLiveQueryWindowManyResult,
|
|
4
|
+
compareLiveQueryWindowDependencies,
|
|
4
5
|
createLiveQueryCollection,
|
|
5
6
|
createLiveQueryWindowController,
|
|
6
|
-
|
|
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
|
|
163
|
+
const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)
|
|
162
164
|
const initialPageParam = config.initialPageParam ?? 0
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const inputIsCollection =
|
|
167
|
+
getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
const dependencyComparison = compareLiveQueryWindowDependencies(
|
|
174
|
+
committed?.dependencies,
|
|
175
|
+
deps,
|
|
176
|
+
)
|
|
177
|
+
const dependenciesChanged = !inputIsCollection && dependencyComparison.changed
|
|
191
178
|
const dependenciesStructurallyEqual =
|
|
192
|
-
!
|
|
193
|
-
depsRef.current !== null &&
|
|
194
|
-
deepEquals(depsRef.current, deps)
|
|
179
|
+
!inputIsCollection && dependencyComparison.structurallyEqual
|
|
195
180
|
const needsNewCollection =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
(
|
|
181
|
+
committed === null ||
|
|
182
|
+
committed.inputKind !== inputKind ||
|
|
183
|
+
(inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||
|
|
199
184
|
dependenciesChanged
|
|
200
185
|
const pageShapeChanged =
|
|
201
|
-
|
|
202
|
-
|
|
186
|
+
committed === null ||
|
|
187
|
+
committed.pageSize !== pageSize ||
|
|
188
|
+
committed.initialPageParam !== initialPageParam
|
|
203
189
|
const needsNewController =
|
|
204
|
-
|
|
190
|
+
committed === null || needsNewCollection || pageShapeChanged
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
255
|
+
const currentRenderState = renderState!
|
|
256
|
+
const controller = currentRenderState.controller
|
|
271
257
|
|
|
272
258
|
const subscribe = useCallback(
|
|
273
|
-
(onStoreChange: () => void) =>
|
|
274
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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>,
|