@tanstack/react-db 0.2.1 → 0.3.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.
Files changed (52) hide show
  1. package/README.md +12 -0
  2. package/dist/cjs/DbProvider.cjs +23 -0
  3. package/dist/cjs/DbProvider.cjs.map +1 -0
  4. package/dist/cjs/DbProvider.d.cts +9 -0
  5. package/dist/cjs/HydrationBoundary.cjs +16 -0
  6. package/dist/cjs/HydrationBoundary.cjs.map +1 -0
  7. package/dist/cjs/HydrationBoundary.d.cts +7 -0
  8. package/dist/cjs/index.cjs +6 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +4 -1
  11. package/dist/cjs/live-query-internals.cjs +19 -0
  12. package/dist/cjs/live-query-internals.cjs.map +1 -0
  13. package/dist/cjs/live-query-internals.d.cts +9 -0
  14. package/dist/cjs/useLiveInfiniteQuery.cjs +76 -8
  15. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  16. package/dist/cjs/useLiveInfiniteQuery.d.cts +12 -58
  17. package/dist/cjs/useLiveQuery.cjs +263 -41
  18. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  19. package/dist/cjs/useLiveQuery.d.cts +79 -33
  20. package/dist/cjs/useLiveSuspenseQuery.cjs +19 -5
  21. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  22. package/dist/cjs/useLiveSuspenseQuery.d.cts +22 -19
  23. package/dist/esm/DbProvider.d.ts +9 -0
  24. package/dist/esm/DbProvider.js +23 -0
  25. package/dist/esm/DbProvider.js.map +1 -0
  26. package/dist/esm/HydrationBoundary.d.ts +7 -0
  27. package/dist/esm/HydrationBoundary.js +16 -0
  28. package/dist/esm/HydrationBoundary.js.map +1 -0
  29. package/dist/esm/index.d.ts +4 -1
  30. package/dist/esm/index.js +6 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/live-query-internals.d.ts +9 -0
  33. package/dist/esm/live-query-internals.js +19 -0
  34. package/dist/esm/live-query-internals.js.map +1 -0
  35. package/dist/esm/useLiveInfiniteQuery.d.ts +12 -58
  36. package/dist/esm/useLiveInfiniteQuery.js +76 -8
  37. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  38. package/dist/esm/useLiveQuery.d.ts +79 -33
  39. package/dist/esm/useLiveQuery.js +265 -43
  40. package/dist/esm/useLiveQuery.js.map +1 -1
  41. package/dist/esm/useLiveSuspenseQuery.d.ts +22 -19
  42. package/dist/esm/useLiveSuspenseQuery.js +19 -5
  43. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  44. package/package.json +2 -2
  45. package/skills/react-db/SKILL.md +107 -84
  46. package/src/DbProvider.tsx +32 -0
  47. package/src/HydrationBoundary.tsx +25 -0
  48. package/src/index.ts +8 -1
  49. package/src/live-query-internals.ts +36 -0
  50. package/src/useLiveInfiniteQuery.ts +119 -65
  51. package/src/useLiveQuery.ts +441 -92
  52. package/src/useLiveSuspenseQuery.ts +56 -29
@@ -1,12 +1,15 @@
1
+ "use client";
1
2
  "use strict";
2
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
4
  const react = require("react");
4
5
  const useLiveQuery = require("./useLiveQuery.cjs");
5
- function useLiveSuspenseQuery(configOrQueryOrCollection, deps = []) {
6
+ const liveQueryInternals = require("./live-query-internals.cjs");
7
+ function useLiveSuspenseQuery(configOrQueryOrCollection, deps) {
6
8
  const promiseRef = react.useRef(null);
7
9
  const collectionRef = react.useRef(null);
8
10
  const hasBeenReadyRef = react.useRef(false);
9
- const result = useLiveQuery.useLiveQuery(configOrQueryOrCollection, deps);
11
+ const result = deps === void 0 ? useLiveQuery.useLiveQuery(configOrQueryOrCollection) : useLiveQuery.useLiveQuery(configOrQueryOrCollection, deps);
12
+ const queryInfo = liveQueryInternals.getLiveQueryResultInfo(result);
10
13
  if (collectionRef.current !== result.collection) {
11
14
  promiseRef.current = null;
12
15
  collectionRef.current = result.collection;
@@ -18,17 +21,28 @@ function useLiveSuspenseQuery(configOrQueryOrCollection, deps = []) {
18
21
  );
19
22
  }
20
23
  const collectionStatus = result.collection.status;
21
- if (collectionStatus === `ready`) {
24
+ if (result.isReady) {
22
25
  hasBeenReadyRef.current = true;
23
26
  promiseRef.current = null;
24
27
  }
28
+ const observerError = queryInfo.observer.getError();
29
+ if (observerError !== void 0 && !hasBeenReadyRef.current) {
30
+ promiseRef.current = null;
31
+ throw observerError;
32
+ }
25
33
  if (collectionStatus === `error` && !hasBeenReadyRef.current) {
26
34
  promiseRef.current = null;
27
35
  throw new Error(`Collection "${result.collection.id}" failed to load`);
28
36
  }
29
- if (collectionStatus === `loading` || collectionStatus === `idle`) {
37
+ if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle)) {
38
+ if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {
39
+ const reason = queryInfo.identityError ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}` : `the query has no stable identity`;
40
+ throw new Error(
41
+ `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`
42
+ );
43
+ }
30
44
  if (!promiseRef.current) {
31
- promiseRef.current = result.collection.preload();
45
+ promiseRef.current = queryInfo.observer.preload();
32
46
  }
33
47
  throw promiseRef.current;
34
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\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 and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((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 * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n *\n * @remarks\n * **Important:** This hook does NOT support disabled queries (returning undefined/null).\n * Following TanStack Query's useSuspenseQuery design, the query callback must always\n * return a valid query, collection, or config object.\n *\n * ❌ **This will cause a type error:**\n * ```ts\n * useLiveSuspenseQuery(\n * (q) => userId ? q.from({ users }) : undefined // ❌ Error!\n * )\n * ```\n *\n * ✅ **Use conditional rendering instead:**\n * ```ts\n * function Profile({ userId }: { userId: string }) {\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ users }).where(({ users }) => eq(users.id, userId))\n * )\n * return <div>{data.name}</div>\n * }\n *\n * // In parent component:\n * {userId ? <Profile userId={userId} /> : <div>No user</div>}\n * ```\n *\n * ✅ **Or use useLiveQuery for conditional queries:**\n * ```ts\n * const { data, isEnabled } = useLiveQuery(\n * (q) => userId ? q.from({ users }) : undefined, // ✅ Supported!\n * [userId]\n * )\n * ```\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<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}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<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}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\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}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\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}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // SUSPENSE LOGIC: Throw promise or error based on collection status\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!result.isEnabled) {\n // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` +\n `The Suspense pattern requires data to always be defined (T, not T | undefined). ` +\n `Solutions: ` +\n `1) Use conditional rendering - don't render the component until the condition is met. ` +\n `2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`,\n )\n }\n\n // It’s not recommended to suspend a render based on a store value returned by useSyncExternalStore.\n // result.status is the snapshot from syncExternalStore. We read the fresh status from the collection reference instead.\n const collectionStatus = result.collection.status\n\n // Track when we reach ready state\n if (collectionStatus === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n }\n\n // Only throw errors during initial load (before first ready)\n // After success, errors surface as stale data (matches TanStack Query behavior)\n if (collectionStatus === `error` && !hasBeenReadyRef.current) {\n promiseRef.current = null\n // TODO: Once collections hold a reference to their last error object (#671),\n // we should rethrow that actual error instead of creating a generic message\n throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n }\n\n if (collectionStatus === `loading` || collectionStatus === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":["useRef","useLiveQuery"],"mappings":";;;;AA0JO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAaA,MAAAA,OAA6B,IAAI;AACpD,QAAM,gBAAgBA,MAAAA,OAAyC,IAAI;AACnE,QAAM,kBAAkBA,MAAAA,OAAO,KAAK;AAGpC,QAAM,SAASC,aAAAA,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAMJ;AAIA,QAAM,mBAAmB,OAAO,WAAW;AAG3C,MAAI,qBAAqB,SAAS;AAChC,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,qBAAqB,WAAW,CAAC,gBAAgB,SAAS;AAC5D,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,qBAAqB,aAAa,qBAAqB,QAAQ;AAEjE,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;;"}
1
+ {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport { getLiveQueryResultInfo } from './live-query-internals'\nimport type { UseLiveQueryConfig } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery({\n * query: (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 * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // Structured captured values are included in derived query identity and trigger re-suspension\n * const { data } = useLiveSuspenseQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n *\n * @remarks\n * **Important:** This hook does NOT support disabled queries (returning undefined/null).\n * Following TanStack Query's useSuspenseQuery design, the query callback must always\n * return a valid query, collection, or config object.\n *\n * ❌ **This will cause a type error:**\n * ```ts\n * useLiveSuspenseQuery(\n * (q) => userId ? q.from({ users }) : undefined // ❌ Error!\n * )\n * ```\n *\n * ✅ **Use conditional rendering instead:**\n * ```ts\n * function Profile({ userId }: { userId: string }) {\n * const { data } = useLiveSuspenseQuery({\n * query: (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)),\n * })\n * return <div>{data.name}</div>\n * }\n *\n * // In parent component:\n * {userId ? <Profile userId={userId} /> : <div>No user</div>}\n * ```\n *\n * ✅ **For optional inputs, conditionally render a component with complete query inputs:**\n * ```ts\n * {userId ? <Profile userId={userId} /> : <div>No user</div>}\n * ```\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<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}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept legacy config object\nexport function useLiveSuspenseQuery<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}\n\n// Overload 4: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\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}\n\n// Overload 5: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\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}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result =\n deps === undefined\n ? useLiveQuery(configOrQueryOrCollection)\n : useLiveQuery(configOrQueryOrCollection, deps)\n const queryInfo = getLiveQueryResultInfo(result)\n\n // Reset promise and ready state when query identity changes\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // SUSPENSE LOGIC: Throw promise or error based on collection status\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!result.isEnabled) {\n // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` +\n `The Suspense pattern requires data to always be defined (T, not T | undefined). ` +\n `Solutions: ` +\n `1) Use conditional rendering - don't render the component until the condition is met. ` +\n `2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`,\n )\n }\n\n const collectionStatus = result.collection.status\n\n // Track when we reach ready state\n if (result.isReady) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n }\n\n const observerError = queryInfo.observer.getError()\n if (observerError !== undefined && !hasBeenReadyRef.current) {\n promiseRef.current = null\n throw observerError\n }\n\n // Only throw errors during initial load (before first ready)\n // After success, errors surface as stale data (matches TanStack Query behavior)\n if (collectionStatus === `error` && !hasBeenReadyRef.current) {\n promiseRef.current = null\n // TODO: Once collections hold a reference to their last error object (#671),\n // we should rethrow that actual error instead of creating a generic message\n throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n }\n\n if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle)) {\n if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {\n const reason = queryInfo.identityError\n ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}`\n : `the query has no stable identity`\n throw new Error(\n `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`,\n )\n }\n\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = queryInfo.observer.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":[],"mappings":";;;;;;AAoKO;AAIL;AACA;AACA;AAGA;AAIA;AAGA;AACE;AACA;AACA;AAA0B;AAK5B;AAEE;AAAU;AACR;AAAA;AAQJ;AAGA;AACE;AACA;AAAqB;AAGvB;AACA;AACE;AACA;AAAM;AAKR;AACE;AAGA;AAAqE;AAGvE;AACE;AACE;AAGA;AAAU;AACkD;AAAA;AAK9D;AACE;AAAwC;AAK1C;AAAiB;AAKnB;AAAO;AACS;AACD;AACM;AAEvB;;"}
@@ -1,19 +1,21 @@
1
+ import { UseLiveQueryConfig } from './useLiveQuery.cjs';
1
2
  import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
2
3
  /**
3
4
  * Create a live query with React Suspense support
4
5
  * @param queryFn - Query function that defines what data to fetch
5
- * @param deps - Array of dependencies that trigger query re-execution when changed
6
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
6
7
  * @returns Object with reactive data and state - data is guaranteed to be defined
7
8
  * @throws Promise when data is loading (caught by Suspense boundary)
8
9
  * @throws Error when collection fails (caught by Error boundary)
9
10
  * @example
10
11
  * // Basic usage with Suspense
11
12
  * function TodoList() {
12
- * const { data } = useLiveSuspenseQuery((q) =>
13
- * q.from({ todos: todosCollection })
14
- * .where(({ todos }) => eq(todos.completed, false))
15
- * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
16
- * )
13
+ * const { data } = useLiveSuspenseQuery({
14
+ * query: (q) =>
15
+ * q.from({ todos: todosCollection })
16
+ * .where(({ todos }) => eq(todos.completed, false))
17
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
18
+ * })
17
19
  *
18
20
  * return (
19
21
  * <ul>
@@ -40,12 +42,11 @@ import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, L
40
42
  * // data is guaranteed to be the single item (or undefined if not found)
41
43
  *
42
44
  * @example
43
- * // With dependencies that trigger re-suspension
44
- * const { data } = useLiveSuspenseQuery(
45
- * (q) => q.from({ todos: todosCollection })
45
+ * // Structured captured values are included in derived query identity and trigger re-suspension
46
+ * const { data } = useLiveSuspenseQuery({
47
+ * query: (q) => q.from({ todos: todosCollection })
46
48
  * .where(({ todos }) => gt(todos.priority, minPriority)),
47
- * [minPriority] // Re-suspends when minPriority changes
48
- * )
49
+ * })
49
50
  *
50
51
  * @example
51
52
  * // With Error boundary
@@ -74,9 +75,9 @@ import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, L
74
75
  * ✅ **Use conditional rendering instead:**
75
76
  * ```ts
76
77
  * function Profile({ userId }: { userId: string }) {
77
- * const { data } = useLiveSuspenseQuery(
78
- * (q) => q.from({ users }).where(({ users }) => eq(users.id, userId))
79
- * )
78
+ * const { data } = useLiveSuspenseQuery({
79
+ * query: (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)),
80
+ * })
80
81
  * return <div>{data.name}</div>
81
82
  * }
82
83
  *
@@ -84,12 +85,9 @@ import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, L
84
85
  * {userId ? <Profile userId={userId} /> : <div>No user</div>}
85
86
  * ```
86
87
  *
87
- * ✅ **Or use useLiveQuery for conditional queries:**
88
+ * ✅ **For optional inputs, conditionally render a component with complete query inputs:**
88
89
  * ```ts
89
- * const { data, isEnabled } = useLiveQuery(
90
- * (q) => userId ? q.from({ users }) : undefined, // ✅ Supported!
91
- * [userId]
92
- * )
90
+ * {userId ? <Profile userId={userId} /> : <div>No user</div>}
93
91
  * ```
94
92
  */
95
93
  export declare function useLiveSuspenseQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<unknown>): {
@@ -97,6 +95,11 @@ export declare function useLiveSuspenseQuery<TContext extends Context>(queryFn:
97
95
  data: InferResultType<TContext>;
98
96
  collection: Collection<GetResult<TContext>, string | number, {}>;
99
97
  };
98
+ export declare function useLiveSuspenseQuery<TContext extends Context>(config: UseLiveQueryConfig<TContext>): {
99
+ state: Map<string | number, GetResult<TContext>>;
100
+ data: InferResultType<TContext>;
101
+ collection: Collection<GetResult<TContext>, string | number, {}>;
102
+ };
100
103
  export declare function useLiveSuspenseQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<unknown>): {
101
104
  state: Map<string | number, GetResult<TContext>>;
102
105
  data: InferResultType<TContext>;
@@ -0,0 +1,9 @@
1
+ import { DbClient } from '@tanstack/db';
2
+ import { ReactNode } from 'react';
3
+ export type DbProviderProps = {
4
+ client: DbClient;
5
+ children?: ReactNode;
6
+ };
7
+ export declare function DbProvider(props: DbProviderProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function useDbClient(): DbClient;
9
+ export declare function useOptionalDbClient(): DbClient | undefined;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const DbContext = createContext(void 0);
5
+ function DbProvider(props) {
6
+ return /* @__PURE__ */ jsx(DbContext.Provider, { value: props.client, children: props.children });
7
+ }
8
+ function useDbClient() {
9
+ const client = useContext(DbContext);
10
+ if (!client) {
11
+ throw new Error(`useDbClient must be used within a DbProvider.`);
12
+ }
13
+ return client;
14
+ }
15
+ function useOptionalDbClient() {
16
+ return useContext(DbContext);
17
+ }
18
+ export {
19
+ DbProvider,
20
+ useDbClient,
21
+ useOptionalDbClient
22
+ };
23
+ //# sourceMappingURL=DbProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DbProvider.js","sources":["../../src/DbProvider.tsx"],"sourcesContent":["'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { DbClient } from '@tanstack/db'\nimport type { ReactNode } from 'react'\n\nconst DbContext = createContext<DbClient | undefined>(undefined)\n\nexport type DbProviderProps = {\n client: DbClient\n children?: ReactNode\n}\n\nexport function DbProvider(props: DbProviderProps) {\n return (\n <DbContext.Provider value={props.client}>\n {props.children}\n </DbContext.Provider>\n )\n}\n\nexport function useDbClient(): DbClient {\n const client = useContext(DbContext)\n if (!client) {\n throw new Error(`useDbClient must be used within a DbProvider.`)\n }\n return client\n}\n\nexport function useOptionalDbClient(): DbClient | undefined {\n return useContext(DbContext)\n}\n"],"names":[],"mappings":";;;AAMA;AAOO;AACL;AAKF;AAEO;AACL;AACA;AACE;AAA+D;AAEjE;AACF;AAEO;AACL;AACF;;;;;;"}
@@ -0,0 +1,7 @@
1
+ import { DehydratedDbState } from '@tanstack/db';
2
+ import { ReactNode } from 'react';
3
+ export type HydrationBoundaryProps = {
4
+ state: DehydratedDbState;
5
+ children?: ReactNode;
6
+ };
7
+ export declare function HydrationBoundary({ state, children }: HydrationBoundaryProps): ReactNode;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { useRef } from "react";
3
+ import { useDbClient } from "./DbProvider.js";
4
+ function HydrationBoundary({ state, children }) {
5
+ const client = useDbClient();
6
+ const hydrated = useRef(void 0);
7
+ if (hydrated.current?.client !== client || hydrated.current.state !== state) {
8
+ client.hydrate(state);
9
+ hydrated.current = { client, state };
10
+ }
11
+ return children;
12
+ }
13
+ export {
14
+ HydrationBoundary
15
+ };
16
+ //# sourceMappingURL=HydrationBoundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HydrationBoundary.js","sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport { useDbClient } from './DbProvider'\nimport type { DehydratedDbState } from '@tanstack/db'\nimport type { ReactNode } from 'react'\n\nexport type HydrationBoundaryProps = {\n state: DehydratedDbState\n children?: ReactNode\n}\n\nexport function HydrationBoundary({ state, children }: HydrationBoundaryProps) {\n const client = useDbClient()\n const hydrated = useRef<\n { client: typeof client; state: DehydratedDbState } | undefined\n >(undefined)\n\n if (hydrated.current?.client !== client || hydrated.current.state !== state) {\n client.hydrate(state)\n hydrated.current = { client, state }\n }\n\n return children\n}\n"],"names":[],"mappings":";;;AAYO;AACL;AACA;AAIA;AACE;AACA;AAA6B;AAG/B;AACF;;;;"}
@@ -1,4 +1,7 @@
1
- export * from './useLiveQuery.js';
1
+ export { useLiveQuery } from './useLiveQuery.js';
2
+ export type { LiveQueryKey, UseLiveQueryConfig, UseLiveQueryStatus, } from './useLiveQuery.js';
3
+ export * from './DbProvider.js';
4
+ export * from './HydrationBoundary.js';
2
5
  export * from './useLiveSuspenseQuery.js';
3
6
  export * from './usePacedMutations.js';
4
7
  export * from './useLiveInfiniteQuery.js';
package/dist/esm/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { useLiveQuery } from "./useLiveQuery.js";
2
+ import { DbProvider, useDbClient, useOptionalDbClient } from "./DbProvider.js";
3
+ import { HydrationBoundary } from "./HydrationBoundary.js";
2
4
  import { useLiveSuspenseQuery } from "./useLiveSuspenseQuery.js";
3
5
  import { usePacedMutations } from "./usePacedMutations.js";
4
6
  import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
@@ -6,11 +8,15 @@ import { useLiveQueryEffect } from "./useLiveQueryEffect.js";
6
8
  export * from "@tanstack/db";
7
9
  import { createTransaction } from "@tanstack/db";
8
10
  export {
11
+ DbProvider,
12
+ HydrationBoundary,
9
13
  createTransaction,
14
+ useDbClient,
10
15
  useLiveInfiniteQuery,
11
16
  useLiveQuery,
12
17
  useLiveQueryEffect,
13
18
  useLiveSuspenseQuery,
19
+ useOptionalDbClient,
14
20
  usePacedMutations
15
21
  };
16
22
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,9 @@
1
+ import { DbClient, LiveQueryObserver, UnhashableQueryIRError } from '@tanstack/db';
2
+ export type LiveQueryResultInfo = {
3
+ client: DbClient | undefined;
4
+ queryHash: string | undefined;
5
+ identityError: UnhashableQueryIRError | undefined;
6
+ observer: LiveQueryObserver<object, string | number>;
7
+ };
8
+ export declare function setLiveQueryResultInfo(result: object, info: LiveQueryResultInfo): void;
9
+ export declare function getLiveQueryResultInfo(result: object): LiveQueryResultInfo;
@@ -0,0 +1,19 @@
1
+ const liveQueryResultInfo = /* @__PURE__ */ Symbol(`liveQueryResultInfo`);
2
+ function setLiveQueryResultInfo(result, info) {
3
+ Object.defineProperty(result, liveQueryResultInfo, {
4
+ configurable: true,
5
+ value: info
6
+ });
7
+ }
8
+ function getLiveQueryResultInfo(result) {
9
+ const info = result[liveQueryResultInfo];
10
+ if (!info) {
11
+ throw new Error(`Missing internal live query result information.`);
12
+ }
13
+ return info;
14
+ }
15
+ export {
16
+ getLiveQueryResultInfo,
17
+ setLiveQueryResultInfo
18
+ };
19
+ //# sourceMappingURL=live-query-internals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-query-internals.js","sources":["../../src/live-query-internals.ts"],"sourcesContent":["import type {\n DbClient,\n LiveQueryObserver,\n UnhashableQueryIRError,\n} from '@tanstack/db'\n\nconst liveQueryResultInfo = Symbol(`liveQueryResultInfo`)\n\nexport type LiveQueryResultInfo = {\n client: DbClient | undefined\n queryHash: string | undefined\n identityError: UnhashableQueryIRError | undefined\n observer: LiveQueryObserver<object, string | number>\n}\n\ntype ResultWithInfo = {\n [liveQueryResultInfo]?: LiveQueryResultInfo\n}\n\nexport function setLiveQueryResultInfo(\n result: object,\n info: LiveQueryResultInfo,\n): void {\n Object.defineProperty(result, liveQueryResultInfo, {\n configurable: true,\n value: info,\n })\n}\n\nexport function getLiveQueryResultInfo(result: object): LiveQueryResultInfo {\n const info = (result as ResultWithInfo)[liveQueryResultInfo]\n if (!info) {\n throw new Error(`Missing internal live query result information.`)\n }\n return info\n}\n"],"names":[],"mappings":"AAMA,MAAM,6CAA6B,qBAAqB;AAajD,SAAS,uBACd,QACA,MACM;AACN,SAAO,eAAe,QAAQ,qBAAqB;AAAA,IACjD,cAAc;AAAA,IACd,OAAO;AAAA,EAAA,CACR;AACH;AAEO,SAAS,uBAAuB,QAAqC;AAC1E,QAAM,OAAQ,OAA0B,mBAAmB;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO;AACT;"}
@@ -1,6 +1,14 @@
1
- import { useLiveQuery } from './useLiveQuery.js';
2
- import { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
1
+ import { LiveQueryKey, useLiveQuery } from './useLiveQuery.js';
2
+ import { Collection, Context, DbClient, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
3
3
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
+ /**
5
+ * Explicit identity for queries that contain opaque functional variants or
6
+ * are hot enough that deriving identity from structured IR is too expensive.
7
+ * Structured queries should omit this so DB can derive identity directly.
8
+ */
9
+ queryKey?: LiveQueryKey;
10
+ /** Override the nearest DbProvider for this query. */
11
+ client?: DbClient;
4
12
  pageSize?: number;
5
13
  initialPageParam?: number;
6
14
  /**
@@ -20,69 +28,15 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
20
28
  error: unknown;
21
29
  };
22
30
  /**
23
- * Create an infinite query using a query function with live updates
31
+ * Create an infinite query using a query function with live updates.
24
32
  *
25
33
  * Uses `utils.setWindow()` to dynamically adjust the limit/offset window
26
34
  * without recreating the live query collection on each page change.
27
35
  *
28
36
  * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
29
37
  * @param config - Configuration including pageSize and getNextPageParam
30
- * @param deps - Array of dependencies that trigger query re-execution when changed
38
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
31
39
  * @returns Object with pages, data, and pagination controls
32
- *
33
- * @example
34
- * // Basic infinite query
35
- * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
36
- * (q) => q
37
- * .from({ posts: postsCollection })
38
- * .orderBy(({ posts }) => posts.createdAt, 'desc')
39
- * .select(({ posts }) => ({
40
- * id: posts.id,
41
- * title: posts.title
42
- * })),
43
- * {
44
- * pageSize: 20,
45
- * getNextPageParam: (lastPage, allPages) =>
46
- * lastPage.length === 20 ? allPages.length : undefined
47
- * }
48
- * )
49
- *
50
- * @example
51
- * // With dependencies
52
- * const { pages, fetchNextPage } = useLiveInfiniteQuery(
53
- * (q) => q
54
- * .from({ posts: postsCollection })
55
- * .where(({ posts }) => eq(posts.category, category))
56
- * .orderBy(({ posts }) => posts.createdAt, 'desc'),
57
- * {
58
- * pageSize: 10,
59
- * getNextPageParam: (lastPage) =>
60
- * lastPage.length === 10 ? lastPage.length : undefined
61
- * },
62
- * [category]
63
- * )
64
- *
65
- * @example
66
- * // Router loader pattern with pre-created collection
67
- * // In loader:
68
- * const postsQuery = createLiveQueryCollection({
69
- * query: (q) => q
70
- * .from({ posts: postsCollection })
71
- * .orderBy(({ posts }) => posts.createdAt, 'desc')
72
- * .limit(20)
73
- * })
74
- * await postsQuery.preload()
75
- * return { postsQuery }
76
- *
77
- * // In component:
78
- * const { postsQuery } = useLoaderData()
79
- * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
80
- * postsQuery,
81
- * {
82
- * pageSize: 20,
83
- * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
84
- * }
85
- * )
86
40
  */
87
41
  export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult, config: UseLiveInfiniteQueryConfig<any>): UseLiveInfiniteQueryReturn<any>;
88
42
  export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -1,19 +1,68 @@
1
+ "use client";
1
2
  import { useRef, useCallback, useSyncExternalStore } from "react";
2
3
  import { normalizeLiveQueryWindowPageSize, getLiveQueryWindowInputKind, compareLiveQueryWindowDependencies, resolveLiveQueryWindowInput, createLiveQueryCollection, getLiveQueryWindowCollectionWarning, assertLiveQueryWindowManyResult, shouldPreserveLiveQueryWindowPageCount, createLiveQueryWindowController, fetchNextLiveQueryWindowPage } from "@tanstack/db";
4
+ import { useOptionalDbClient } from "./DbProvider.js";
5
+ import { warnDeprecatedDepsArray, prepareDerivedQuery, warnUnhashableDerivedIdentity, prepareQueryValue } from "./useLiveQuery.js";
3
6
  const DEFAULT_GC_TIME_MS = 1;
4
- function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
7
+ const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
8
+ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
5
9
  const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize);
6
10
  const initialPageParam = config.initialPageParam ?? 0;
11
+ const contextDbClient = useOptionalDbClient();
12
+ const dbClient = config.client ?? contextDbClient;
7
13
  const inputIsCollection = getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
8
14
  const committedRef = useRef(null);
9
15
  const committed = committedRef.current;
10
16
  const inputKind = inputIsCollection ? `collection` : `query`;
17
+ const derivedIdentityProfilerRef = useRef({
18
+ renderCount: 0,
19
+ totalMs: 0,
20
+ maxMs: 0,
21
+ warned: false
22
+ });
23
+ const legacyUnhashableIdentityRef = useRef([
24
+ `legacy-unhashable`
25
+ ]);
26
+ const deferredCollections = /* @__PURE__ */ new Set();
27
+ let preparedQueryValue = unpreparedQueryValue;
28
+ let identityDeps = [];
29
+ let identityMode = `collection`;
30
+ if (!inputIsCollection) {
31
+ if (config.queryKey !== void 0) {
32
+ identityMode = `queryKey`;
33
+ identityDeps = config.queryKey;
34
+ } else if (deps !== void 0) {
35
+ identityMode = `legacyDeps`;
36
+ identityDeps = deps;
37
+ warnDeprecatedDepsArray(`useLiveInfiniteQuery`);
38
+ } else if (committed?.identityMode === `derived` && committed.inputQuery === queryFnOrCollection && committed.client === dbClient) {
39
+ identityMode = `derived`;
40
+ identityDeps = committed.dependencies ?? [];
41
+ } else {
42
+ identityMode = `derived`;
43
+ const preparation = prepareDerivedQuery(
44
+ queryFnOrCollection,
45
+ dbClient,
46
+ derivedIdentityProfilerRef.current,
47
+ deferredCollections
48
+ );
49
+ preparedQueryValue = preparation.value;
50
+ if (preparation.status === `hashable`) {
51
+ identityDeps = preparation.identityDeps;
52
+ } else {
53
+ warnUnhashableDerivedIdentity(preparation.error);
54
+ identityDeps = legacyUnhashableIdentityRef.current;
55
+ }
56
+ }
57
+ }
58
+ const usesLegacyDeps = !inputIsCollection && config.queryKey === void 0 && deps !== void 0;
11
59
  const dependencyComparison = compareLiveQueryWindowDependencies(
12
60
  committed?.dependencies,
13
- deps
61
+ identityDeps
14
62
  );
15
- const dependenciesChanged = !inputIsCollection && dependencyComparison.changed;
16
- const dependenciesStructurallyEqual = !inputIsCollection && dependencyComparison.structurallyEqual;
63
+ const sameClient = committed?.client === dbClient;
64
+ const dependenciesChanged = !inputIsCollection && (!sameClient || (usesLegacyDeps ? dependencyComparison.changed : !dependencyComparison.structurallyEqual));
65
+ const dependenciesStructurallyEqual = usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual;
17
66
  const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
18
67
  const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
19
68
  const needsNewController = committed === null || needsNewCollection || pageShapeChanged;
@@ -22,7 +71,18 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
22
71
  let collection = committed?.collection;
23
72
  let warning = null;
24
73
  if (needsNewCollection) {
25
- const input = resolveLiveQueryWindowInput(queryFnOrCollection);
74
+ let inputValue = queryFnOrCollection;
75
+ if (!inputIsCollection) {
76
+ if (preparedQueryValue === unpreparedQueryValue) {
77
+ preparedQueryValue = prepareQueryValue(
78
+ queryFnOrCollection,
79
+ dbClient,
80
+ deferredCollections
81
+ );
82
+ }
83
+ inputValue = () => preparedQueryValue;
84
+ }
85
+ const input = resolveLiveQueryWindowInput(inputValue);
26
86
  if (input.kind === `collection`) {
27
87
  collection = input.collection;
28
88
  } else {
@@ -57,7 +117,10 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
57
117
  renderState = {
58
118
  inputKind,
59
119
  inputCollection: inputIsCollection ? collection : null,
60
- dependencies: inputIsCollection ? null : [...deps],
120
+ inputQuery: inputIsCollection ? null : queryFnOrCollection,
121
+ client: dbClient,
122
+ identityMode,
123
+ dependencies: inputIsCollection ? null : [...identityDeps],
61
124
  pageSize,
62
125
  initialPageParam,
63
126
  collection,
@@ -67,7 +130,8 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
67
130
  initialPageCount
68
131
  }),
69
132
  warning,
70
- warned: false
133
+ warned: false,
134
+ deferredCollections
71
135
  };
72
136
  }
73
137
  const currentRenderState = renderState;
@@ -80,12 +144,16 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
80
144
  currentRenderState.warned = true;
81
145
  console.warn(currentRenderState.warning);
82
146
  }
147
+ for (const collection of currentRenderState.deferredCollections) {
148
+ collection._resumeSyncStart();
149
+ }
150
+ currentRenderState.deferredCollections.clear();
83
151
  return unsubscribe;
84
152
  },
85
153
  [controller, currentRenderState]
86
154
  );
87
155
  const getSnapshot = useCallback(() => controller.getSnapshot(), [controller]);
88
- const snapshot = useSyncExternalStore(subscribe, getSnapshot);
156
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
89
157
  const fetchNextPage = useCallback(
90
158
  () => fetchNextLiveQueryWindowPage(controller),
91
159
  [controller]
@@ -1 +1 @@
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":";;AA0BA,MAAM,qBAAqB;AAmIpB,SAAS,qBACd,qBACA,QACA,OAAuB,CAAA,GACe;AACtC,QAAM,WAAW,iCAAiC,OAAO,QAAQ;AACjE,QAAM,mBAAmB,OAAO,oBAAoB;AAEpD,QAAM,oBACJ,4BAA4B,mBAAmB,MAAM;AAEvD,QAAM,eAAe,OAAwC,IAAI;AACjE,QAAM,YAAY,aAAa;AAC/B,QAAM,YAAY,oBAAoB,eAAe;AAErD,QAAM,uBAAuB;AAAA,IAC3B,WAAW;AAAA,IACX;AAAA,EAAA;AAEF,QAAM,sBAAsB,CAAC,qBAAqB,qBAAqB;AACvE,QAAM,gCACJ,CAAC,qBAAqB,qBAAqB;AAC7C,QAAM,qBACJ,cAAc,QACd,UAAU,cAAc,aACvB,qBAAqB,UAAU,oBAAoB,uBACpD;AACF,QAAM,mBACJ,cAAc,QACd,UAAU,aAAa,YACvB,UAAU,qBAAqB;AACjC,QAAM,qBACJ,cAAc,QAAQ,sBAAsB;AAE9C,MAAI,cAAc;AAClB,MAAI,oBAAoB;AACtB,QAAI,aAAa,WAAW;AAC5B,QAAI,UAAyB;AAE7B,QAAI,oBAAoB;AACtB,YAAM,QAAQ,4BAAsC,mBAAmB;AACvE,UAAI,MAAM,SAAS,cAAc;AAC/B,qBAAa,MAAM;AAAA,MACrB,OAAO;AAGL,qBAAa,0BAA0B;AAAA,UACrC,OAAO,MAAM,MAAM,MAAM,WAAW,CAAC,EAAE,OAAO,CAAC;AAAA;AAAA;AAAA,UAG/C,WAAW;AAAA,UACX,QAAQ;AAAA,QAAA,CACT;AAAA,MACH;AAAA,IACF;AAEA,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AAEA,QAAI,mBAAmB;AACrB,gBACE,oCAAoC,YAAY,WAAW,CAAC,KAAK;AAAA,IACrE,OAAO;AACL,sCAAgC,UAAU;AAAA,IAC5C;AAEA,UAAM,uBAAuB,uCAAuC;AAAA,MAClE,uBAAuB,cAAc;AAAA,MACrC,mBAAmB,WAAW;AAAA,MAC9B;AAAA,MACA,gBACE,qBAAqB,WAAW,oBAAoB;AAAA,MACtD;AAAA,MACA;AAAA,MACA;AAAA,IAAA,CACD;AACD,UAAM,oBAAoB,YACtB,KAAK,IAAI,GAAG,UAAU,WAAW,YAAA,EAAc,MAAM,MAAM,IAC3D;AACJ,UAAM,mBAAmB,uBAAuB,oBAAoB;AACpE,kBAAc;AAAA,MACZ;AAAA,MACA,iBAAiB,oBAAoB,aAAa;AAAA,MAClD,cAAc,oBAAoB,OAAO,CAAC,GAAG,IAAI;AAAA,MACjD;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,gCAAgC,YAAY;AAAA,QACtD;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACD;AAAA,MACD;AAAA,MACA,QAAQ;AAAA,IAAA;AAAA,EAEZ;AACA,QAAM,qBAAqB;AAC3B,QAAM,aAAa,mBAAmB;AAEtC,QAAM,YAAY;AAAA,IAChB,CAAC,kBAA8B;AAC7B,YAAM,cAAc,WAAW,UAAU,aAAa;AACtD,mBAAa,UAAU;AACvB,UAAI,mBAAmB,WAAW,CAAC,mBAAmB,QAAQ;AAC5D,2BAAmB,SAAS;AAC5B,gBAAQ,KAAK,mBAAmB,OAAO;AAAA,MACzC;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC,YAAY,kBAAkB;AAAA,EAAA;AAEjC,QAAM,cAAc,YAAY,MAAM,WAAW,eAAe,CAAC,UAAU,CAAC;AAC5E,QAAM,WAAW,qBAAqB,WAAW,WAAW;AAE5D,QAAM,gBAAgB;AAAA,IACpB,MAAM,6BAA6B,UAAU;AAAA,IAC7C,CAAC,UAAU;AAAA,EAAA;AAGb,SAAO;AAAA,IACL,MAAM,SAAS;AAAA,IACf,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS;AAAA,IACjB,WAAW,SAAS;AAAA,IACpB,SAAS,SAAS;AAAA,IAClB,QAAQ,SAAS;AAAA,IACjB,SAAS,SAAS;AAAA,IAClB,aAAa,SAAS;AAAA,IACtB,YACE,SAAS;AAAA,IACX,WACE,SAAS;AAAA,IACX,OAAO,SAAS;AAAA,IAChB,YAAY,SAAS;AAAA,IACrB;AAAA,IACA,aAAa,SAAS;AAAA,IACtB,oBAAoB,SAAS;AAAA,IAC7B,OAAO,SAAS;AAAA,EAAA;AAEpB;"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { 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'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n Context,\n DbClient,\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\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\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 inputQuery: unknown\n client: DbClient | undefined\n identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`\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 deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\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 - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\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 const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\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 const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown> = []\n let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`\n\n if (!inputIsCollection) {\n if (config.queryKey !== undefined) {\n identityMode = `queryKey`\n identityDeps = config.queryKey\n } else if (deps !== undefined) {\n identityMode = `legacyDeps`\n identityDeps = deps\n warnDeprecatedDepsArray(`useLiveInfiniteQuery`)\n } else if (\n committed?.identityMode === `derived` &&\n committed.inputQuery === queryFnOrCollection &&\n committed.client === dbClient\n ) {\n identityMode = `derived`\n identityDeps = committed.dependencies ?? []\n } else {\n identityMode = `derived`\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n dbClient,\n derivedIdentityProfilerRef.current,\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = legacyUnhashableIdentityRef.current\n }\n }\n }\n\n const usesLegacyDeps =\n !inputIsCollection && config.queryKey === undefined && deps !== undefined\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n identityDeps,\n )\n const sameClient = committed?.client === dbClient\n const dependenciesChanged =\n !inputIsCollection &&\n (!sameClient ||\n (usesLegacyDeps\n ? dependencyComparison.changed\n : !dependencyComparison.structurallyEqual))\n const dependenciesStructurallyEqual =\n usesLegacyDeps && sameClient && 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 let inputValue = queryFnOrCollection\n if (!inputIsCollection) {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n inputValue = () => preparedQueryValue\n }\n const input = resolveLiveQueryWindowInput<TContext>(inputValue)\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 inputQuery: inputIsCollection ? null : queryFnOrCollection,\n client: dbClient,\n identityMode,\n dependencies: inputIsCollection ? null : [...identityDeps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n deferredCollections,\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 for (const collection of currentRenderState.deferredCollections) {\n collection._resumeSyncStart()\n }\n currentRenderState.deferredCollections.clear()\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, 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":";;;;;AAwCA;AACA;AA2FO;AAKL;AACA;AACA;AACA;AAEA;AAGA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoB;AAClB;AACA;AAC2B;AAC3B;AAEF;AACA;AACE;AAA2B;AAE3B;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6B;AAChB;AACX;AAEF;AACA;AAMA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AAEA;AACE;AACA;AACE;AACE;AAAqB;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AACE;AAAmB;AAInB;AAAuC;AACU;AAAA;AAAA;AAGpC;AACH;AACT;AACH;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnE;AAA0C;AAG5C;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AACA;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkB;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzC;AACE;AAAW;AAEb;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsB;AACyB;AAClC;AAGb;AAAO;AACU;AACC;AACC;AACG;AACF;AACD;AACC;AACI;AAEX;AAEA;AACK;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;;;"}