@tanstack/react-db 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,37 +4,67 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const react = require("react");
5
5
  const useLiveQuery = require("./useLiveQuery.cjs");
6
6
  const liveQueryInternals = require("./live-query-internals.cjs");
7
+ const initialRenderErrors = /* @__PURE__ */ new WeakMap();
8
+ function clearInitialRenderError(collection, client) {
9
+ const entry = initialRenderErrors.get(collection);
10
+ if (!entry) return;
11
+ if (client) entry.byClient.delete(client);
12
+ else delete entry.unscoped;
13
+ }
14
+ function rememberInitialRenderError(collection, client, error, clientQuery) {
15
+ if (collection.status === `cleaned-up`) return;
16
+ let entry = initialRenderErrors.get(collection);
17
+ if (!entry) {
18
+ entry = { byClient: /* @__PURE__ */ new WeakMap() };
19
+ collection.once(`status:cleaned-up`, () => {
20
+ initialRenderErrors.delete(collection);
21
+ });
22
+ initialRenderErrors.set(collection, entry);
23
+ }
24
+ if (client) entry.byClient.set(client, { error, clientQuery });
25
+ else entry.unscoped = { error };
26
+ }
7
27
  function useLiveSuspenseQuery(configOrQueryOrCollection, deps) {
8
28
  const promiseRef = react.useRef(null);
9
29
  const collectionRef = react.useRef(null);
10
30
  const hasBeenReadyRef = react.useRef(false);
11
- const result = deps === void 0 ? useLiveQuery.useLiveQuery(configOrQueryOrCollection) : useLiveQuery.useLiveQuery(configOrQueryOrCollection, deps);
31
+ const result = deps === void 0 ? useLiveQuery.useLiveQueryForSuspense(configOrQueryOrCollection, void 0) : useLiveQuery.useLiveQueryForSuspense(configOrQueryOrCollection, deps);
32
+ if (!result.isEnabled) {
33
+ throw new Error(
34
+ `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). The Suspense pattern requires data to always be defined (T, not T | undefined). Solutions: 1) Use conditional rendering - don't render the component until the condition is met. 2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`
35
+ );
36
+ }
12
37
  const queryInfo = liveQueryInternals.getLiveQueryResultInfo(result);
13
38
  if (collectionRef.current !== result.collection) {
14
39
  promiseRef.current = null;
15
40
  collectionRef.current = result.collection;
16
41
  hasBeenReadyRef.current = false;
17
42
  }
18
- if (!result.isEnabled) {
19
- throw new Error(
20
- `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). The Suspense pattern requires data to always be defined (T, not T | undefined). Solutions: 1) Use conditional rendering - don't render the component until the condition is met. 2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`
21
- );
22
- }
23
43
  const collectionStatus = result.collection.status;
24
- if (result.isReady) {
44
+ if (result.isReady || queryInfo.observer.isInitialRenderReady()) {
25
45
  hasBeenReadyRef.current = true;
26
46
  promiseRef.current = null;
47
+ clearInitialRenderError(result.collection, queryInfo.client);
27
48
  }
28
49
  const observerError = queryInfo.observer.getError();
29
- if (observerError !== void 0 && !hasBeenReadyRef.current) {
50
+ const clientQuery = queryInfo.client && queryInfo.queryHash ? queryInfo.client._getLiveQuery(queryInfo.queryHash) : void 0;
51
+ if (!hasBeenReadyRef.current && (result.persistedStatus === `unavailable` || result.persistedStatus === `error`) && (observerError !== void 0 || clientQuery?.status === `error`)) {
52
+ promiseRef.current = null;
53
+ throw observerError === void 0 ? clientQuery?.error : observerError;
54
+ }
55
+ const errorEntry = initialRenderErrors.get(result.collection);
56
+ const initialRenderError = queryInfo.client ? errorEntry?.byClient.get(queryInfo.client) : errorEntry?.unscoped;
57
+ if (initialRenderError?.clientQuery !== void 0 && initialRenderError.clientQuery !== clientQuery) {
58
+ clearInitialRenderError(result.collection, queryInfo.client);
59
+ } else if (initialRenderError && !hasBeenReadyRef.current) {
30
60
  promiseRef.current = null;
31
- throw observerError;
61
+ throw initialRenderError.error;
32
62
  }
33
- if (collectionStatus === `error` && !hasBeenReadyRef.current) {
63
+ if (collectionStatus === `error` && !hasBeenReadyRef.current && (result.persistedStatus === `unavailable` || result.persistedStatus === `error`)) {
34
64
  promiseRef.current = null;
35
65
  throw new Error(`Collection "${result.collection.id}" failed to load`);
36
66
  }
37
- if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle)) {
67
+ if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle || result.isError || collectionStatus === `error` && result.persistedStatus !== `unavailable`)) {
38
68
  if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {
39
69
  const reason = queryInfo.identityError ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}` : `the query has no stable identity`;
40
70
  throw new Error(
@@ -42,7 +72,32 @@ function useLiveSuspenseQuery(configOrQueryOrCollection, deps) {
42
72
  );
43
73
  }
44
74
  if (!promiseRef.current) {
45
- promiseRef.current = queryInfo.observer.preload();
75
+ const collection = result.collection;
76
+ const client = queryInfo.client;
77
+ const queryHash = queryInfo.queryHash;
78
+ let active = true;
79
+ const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
80
+ active = false;
81
+ });
82
+ let preload;
83
+ try {
84
+ preload = queryInfo.observer.preloadForInitialRender();
85
+ } catch (error) {
86
+ stopWatchingCleanup();
87
+ throw error;
88
+ }
89
+ const preloadClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
90
+ promiseRef.current = preload.catch((error) => {
91
+ const latestClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : void 0;
92
+ if (active && latestClientQuery === preloadClientQuery)
93
+ rememberInitialRenderError(
94
+ collection,
95
+ client,
96
+ error,
97
+ preloadClientQuery
98
+ );
99
+ throw error;
100
+ }).finally(stopWatchingCleanup);
46
101
  }
47
102
  throw promiseRef.current;
48
103
  }
@@ -1 +1 @@
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
+ {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport { useLiveQueryForSuspense } from './useLiveQuery'\nimport { getLiveQueryResultInfo } from './live-query-internals'\nimport type { UseLiveQueryConfig } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n// React can discard a render that suspends, including its refs. Keep the\n// initial-render failure across retries, scoped by client because streamed\n// preloads of the same collection can fail independently.\nconst initialRenderErrors = new WeakMap<\n Collection<any, any, any>,\n {\n byClient: WeakMap<DbClient, { error: unknown; clientQuery?: object }>\n unscoped?: { error: unknown; clientQuery?: object }\n }\n>()\n\nfunction clearInitialRenderError(\n collection: Collection<any, any, any>,\n client: DbClient | undefined,\n) {\n const entry = initialRenderErrors.get(collection)\n if (!entry) return\n if (client) entry.byClient.delete(client)\n else delete entry.unscoped\n}\n\nfunction rememberInitialRenderError(\n collection: Collection<any, any, any>,\n client: DbClient | undefined,\n error: unknown,\n clientQuery?: object,\n) {\n if (collection.status === `cleaned-up`) return\n let entry = initialRenderErrors.get(collection)\n if (!entry) {\n entry = { byClient: new WeakMap() }\n collection.once(`status:cleaned-up`, () => {\n initialRenderErrors.delete(collection)\n })\n initialRenderErrors.set(collection, entry)\n }\n if (client) entry.byClient.set(client, { error, clientQuery })\n else entry.unscoped = { error }\n}\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 ? useLiveQueryForSuspense(configOrQueryOrCollection, undefined)\n : useLiveQueryForSuspense(configOrQueryOrCollection, deps)\n\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 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\n const collectionStatus = result.collection.status\n\n // Track when we reach ready state\n if (result.isReady || queryInfo.observer.isInitialRenderReady()) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n clearInitialRenderError(result.collection, queryInfo.client)\n }\n\n const observerError = queryInfo.observer.getError()\n // A client request can reject with undefined, which getError() cannot\n // distinguish from no error. The request status preserves that distinction.\n const clientQuery =\n queryInfo.client && queryInfo.queryHash\n ? queryInfo.client._getLiveQuery(queryInfo.queryHash)\n : undefined\n // A configured persisted restore may still satisfy this render after the\n // client query fails. Let the observer classify that failure below.\n if (\n !hasBeenReadyRef.current &&\n (result.persistedStatus === `unavailable` ||\n result.persistedStatus === `error`) &&\n (observerError !== undefined || clientQuery?.status === `error`)\n ) {\n promiseRef.current = null\n throw observerError === undefined ? clientQuery?.error : observerError\n }\n\n const errorEntry = initialRenderErrors.get(result.collection)\n const initialRenderError = queryInfo.client\n ? errorEntry?.byClient.get(queryInfo.client)\n : errorEntry?.unscoped\n // A replacement client query must not inherit the prior request's error.\n if (\n initialRenderError?.clientQuery !== undefined &&\n initialRenderError.clientQuery !== clientQuery\n ) {\n clearInitialRenderError(result.collection, queryInfo.client)\n } else if (initialRenderError && !hasBeenReadyRef.current) {\n promiseRef.current = null\n throw initialRenderError.error\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 (\n collectionStatus === `error` &&\n !hasBeenReadyRef.current &&\n (result.persistedStatus === `unavailable` ||\n result.persistedStatus === `error`)\n ) {\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 (\n !hasBeenReadyRef.current &&\n (result.isLoading ||\n result.isIdle ||\n result.isError ||\n (collectionStatus === `error` &&\n result.persistedStatus !== `unavailable`))\n ) {\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 // Create or reuse promise for current collection\n if (!promiseRef.current) {\n const collection = result.collection\n const client = queryInfo.client\n const queryHash = queryInfo.queryHash\n let active = true\n const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {\n active = false\n })\n let preload: Promise<void>\n try {\n preload = queryInfo.observer.preloadForInitialRender()\n } catch (error) {\n stopWatchingCleanup()\n throw error\n }\n const preloadClientQuery =\n client && queryHash ? client._getLiveQuery(queryHash) : undefined\n promiseRef.current = preload\n .catch((error: unknown) => {\n const latestClientQuery =\n client && queryHash ? client._getLiveQuery(queryHash) : undefined\n if (active && latestClientQuery === preloadClientQuery)\n rememberInitialRenderError(\n collection,\n client,\n error,\n preloadClientQuery,\n )\n throw error\n })\n .finally(stopWatchingCleanup)\n }\n // React Suspense catches this promise and retries after preload settles.\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":";;;;;;AAsBA;AAQA;AAIE;AACA;AACA;AAAwC;AAE1C;AAEA;AAME;AACA;AACA;AACE;AACA;AACE;AAAqC;AAEvC;AAAyC;AAE3C;AAA6D;AAE/D;AAoJO;AAIL;AACA;AACA;AAGA;AAKA;AAEE;AAAU;AACR;AAAA;AAQJ;AAGA;AACE;AACA;AACA;AAA0B;AAK5B;AAGA;AACE;AACA;AACA;AAA2D;AAG7D;AAGA;AAMA;AAME;AACA;AAAyD;AAG3D;AACA;AAIA;AAIE;AAA2D;AAE3D;AACA;AAAyB;AAK3B;AAME;AAGA;AAAqE;AAGvE;AAQE;AACE;AAGA;AAAU;AACkD;AAAA;AAI9D;AACE;AACA;AACA;AACA;AACA;AACE;AAAS;AAEX;AACA;AACE;AAA6B;AAE7B;AACA;AAAM;AAER;AAEA;AAEI;AAEA;AACE;AAAA;AACE;AACA;AACA;AACA;AAEJ;AAAM;AAEoB;AAGhC;AAAiB;AAKnB;AAAO;AACS;AACD;AACM;AAEvB;;"}
@@ -1,5 +1,5 @@
1
1
  import { LiveQueryKey, useLiveQuery } from './useLiveQuery.js';
2
- import { Collection, Context, DbClient, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
2
+ import { Collection, CollectionStatus, Context, DbClient, InferResultType, InitialQueryBuilder, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder } from '@tanstack/db';
3
3
  export type UseLiveInfiniteQueryConfig<_TContext extends Context> = {
4
4
  /**
5
5
  * Explicit identity for queries that contain opaque functional variants or
@@ -22,6 +22,27 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
22
22
  isFetchingNextPage: boolean;
23
23
  error: unknown;
24
24
  };
25
+ export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>> = {
26
+ data: Array<TResult>;
27
+ state: Map<TKey, TResult>;
28
+ collection: Collection<TResult, TKey, TUtils> & NonSingleResult;
29
+ status: CollectionStatus;
30
+ isLoading: boolean;
31
+ isReady: boolean;
32
+ persistedStatus: LiveQueryPersistedStatus;
33
+ isPersistedReady: boolean;
34
+ persistedError: unknown | undefined;
35
+ isIdle: boolean;
36
+ isError: boolean;
37
+ isCleanedUp: boolean;
38
+ isEnabled: true;
39
+ pages: Array<Array<TResult>>;
40
+ pageParams: Array<number>;
41
+ fetchNextPage: () => Promise<void>;
42
+ hasNextPage: boolean;
43
+ isFetchingNextPage: boolean;
44
+ error: unknown;
45
+ };
25
46
  /**
26
47
  * Create an infinite query using a query function with live updates.
27
48
  *
@@ -33,5 +54,5 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
33
54
  * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
34
55
  * @returns Object with pages, data, and pagination controls
35
56
  */
36
- 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>;
57
+ 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>): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>;
37
58
  export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -169,6 +169,9 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
169
169
  status: snapshot.status,
170
170
  isLoading: snapshot.isLoading,
171
171
  isReady: snapshot.isReady,
172
+ persistedStatus: snapshot.persistedStatus,
173
+ isPersistedReady: snapshot.isPersistedReady,
174
+ persistedError: snapshot.persistedError,
172
175
  isIdle: snapshot.isIdle,
173
176
  isError: snapshot.isError,
174
177
  isCleanedUp: snapshot.isCleanedUp,
@@ -1 +1 @@
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\n// Keep the generic parameter for existing typed config wrappers.\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 /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\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 an optional initial page label\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 if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\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: snapshot.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;AAkFO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;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;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;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 CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryPersistedStatus,\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\n// Keep the generic parameter for existing typed config wrappers.\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 /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\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\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true\n pages: Array<Array<TResult>>\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 an optional initial page label\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): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\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 if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\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 persistedStatus: snapshot.persistedStatus,\n isPersistedReady: snapshot.isPersistedReady,\n persistedError: snapshot.persistedError,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.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":";;;;;AA0CA;AACA;AA4GO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;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;AACQ;AACC;AACF;AACR;AACC;AACI;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;;;"}
@@ -1,4 +1,4 @@
1
- import { UnhashableQueryIRError, Collection, CollectionImpl, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
1
+ import { UnhashableQueryIRError, Collection, CollectionImpl, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
2
2
  export type DerivedIdentityProfiler = {
3
3
  renderCount: number;
4
4
  totalMs: number;
@@ -111,6 +111,9 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
111
111
  status: CollectionStatus;
112
112
  isLoading: boolean;
113
113
  isReady: boolean;
114
+ persistedStatus: LiveQueryPersistedStatus;
115
+ isPersistedReady: boolean;
116
+ persistedError: unknown | undefined;
114
117
  isIdle: boolean;
115
118
  isError: boolean;
116
119
  isCleanedUp: boolean;
@@ -123,6 +126,9 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
123
126
  status: UseLiveQueryStatus;
124
127
  isLoading: boolean;
125
128
  isReady: boolean;
129
+ persistedStatus: LiveQueryPersistedStatus;
130
+ isPersistedReady: boolean;
131
+ persistedError: unknown | undefined;
126
132
  isIdle: boolean;
127
133
  isError: boolean;
128
134
  isCleanedUp: boolean;
@@ -135,6 +141,9 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
135
141
  status: UseLiveQueryStatus;
136
142
  isLoading: boolean;
137
143
  isReady: boolean;
144
+ persistedStatus: LiveQueryPersistedStatus;
145
+ isPersistedReady: boolean;
146
+ persistedError: unknown | undefined;
138
147
  isIdle: boolean;
139
148
  isError: boolean;
140
149
  isCleanedUp: boolean;
@@ -147,6 +156,9 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
147
156
  status: UseLiveQueryStatus;
148
157
  isLoading: boolean;
149
158
  isReady: boolean;
159
+ persistedStatus: LiveQueryPersistedStatus;
160
+ isPersistedReady: boolean;
161
+ persistedError: unknown | undefined;
150
162
  isIdle: boolean;
151
163
  isError: boolean;
152
164
  isCleanedUp: boolean;
@@ -159,6 +171,9 @@ export declare function useLiveQuery<TContext extends Context, TResult extends o
159
171
  status: UseLiveQueryStatus;
160
172
  isLoading: boolean;
161
173
  isReady: boolean;
174
+ persistedStatus: LiveQueryPersistedStatus;
175
+ isPersistedReady: boolean;
176
+ persistedError: unknown | undefined;
162
177
  isIdle: boolean;
163
178
  isError: boolean;
164
179
  isCleanedUp: boolean;
@@ -206,6 +221,9 @@ export declare function useLiveQuery<TContext extends Context>(config: UseLiveQu
206
221
  status: CollectionStatus;
207
222
  isLoading: boolean;
208
223
  isReady: boolean;
224
+ persistedStatus: LiveQueryPersistedStatus;
225
+ isPersistedReady: boolean;
226
+ persistedError: unknown | undefined;
209
227
  isIdle: boolean;
210
228
  isError: boolean;
211
229
  isCleanedUp: boolean;
@@ -218,6 +236,9 @@ export declare function useLiveQuery<TContext extends Context>(config: Condition
218
236
  status: UseLiveQueryStatus;
219
237
  isLoading: boolean;
220
238
  isReady: boolean;
239
+ persistedStatus: LiveQueryPersistedStatus;
240
+ isPersistedReady: boolean;
241
+ persistedError: unknown | undefined;
221
242
  isIdle: boolean;
222
243
  isError: boolean;
223
244
  isCleanedUp: boolean;
@@ -230,6 +251,9 @@ export declare function useLiveQuery<TContext extends Context>(config: LiveQuery
230
251
  status: CollectionStatus;
231
252
  isLoading: boolean;
232
253
  isReady: boolean;
254
+ persistedStatus: LiveQueryPersistedStatus;
255
+ isPersistedReady: boolean;
256
+ persistedError: unknown | undefined;
233
257
  isIdle: boolean;
234
258
  isError: boolean;
235
259
  isCleanedUp: boolean;
@@ -242,6 +266,9 @@ export declare function useLiveQuery<TContext extends Context>(config: Condition
242
266
  status: UseLiveQueryStatus;
243
267
  isLoading: boolean;
244
268
  isReady: boolean;
269
+ persistedStatus: LiveQueryPersistedStatus;
270
+ isPersistedReady: boolean;
271
+ persistedError: unknown | undefined;
245
272
  isIdle: boolean;
246
273
  isError: boolean;
247
274
  isCleanedUp: boolean;
@@ -283,6 +310,9 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
283
310
  status: CollectionStatus;
284
311
  isLoading: boolean;
285
312
  isReady: boolean;
313
+ persistedStatus: LiveQueryPersistedStatus;
314
+ isPersistedReady: boolean;
315
+ persistedError: unknown | undefined;
286
316
  isIdle: boolean;
287
317
  isError: boolean;
288
318
  isCleanedUp: boolean;
@@ -295,9 +325,14 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
295
325
  status: CollectionStatus;
296
326
  isLoading: boolean;
297
327
  isReady: boolean;
328
+ persistedStatus: LiveQueryPersistedStatus;
329
+ isPersistedReady: boolean;
330
+ persistedError: unknown | undefined;
298
331
  isIdle: boolean;
299
332
  isError: boolean;
300
333
  isCleanedUp: boolean;
301
334
  isEnabled: true;
302
335
  };
336
+ /** @internal Shared implementation for the Suspense wrapper. */
337
+ export declare function useLiveQueryForSuspense(configOrQueryOrCollection: any, deps: Array<unknown> | undefined): any;
303
338
  export {};
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { useRef, useSyncExternalStore } from "react";
3
- import { isCollection, getPreparedLiveQueryIdentity, UnhashableQueryIRError, getStableValueHash, deepEquals, createLiveQueryObserver, prepareLiveQueryValue, BaseQueryBuilder, createLiveQueryCollection } from "@tanstack/db";
3
+ import { isCollection, getPreparedLiveQueryIdentity, UnhashableQueryIRError, getStableValueHash, deepEquals, createLiveQueryObserver, prepareLiveQueryValue, BaseQueryBuilder, IR, createLiveQueryCollection } from "@tanstack/db";
4
4
  import { useOptionalDbClient } from "./DbProvider.js";
5
5
  import { setLiveQueryResultInfo } from "./live-query-internals.js";
6
6
  const DEFAULT_GC_TIME_MS = 1;
7
+ const DEFAULT_SUSPENSE_GC_TIME_MS = 5e3;
7
8
  const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16;
8
9
  const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10;
9
10
  const DERIVED_IDENTITY_TOTAL_WARN_MS = 50;
@@ -11,6 +12,41 @@ const warnedDepsCallsites = /* @__PURE__ */ new Set();
11
12
  const warnedDerivedIdentityCallsites = /* @__PURE__ */ new Set();
12
13
  const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
13
14
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
15
+ const unscopedSuspenseCollections = /* @__PURE__ */ new Map();
16
+ const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
17
+ const suspenseSourceIds = /* @__PURE__ */ new WeakMap();
18
+ let nextSuspenseSourceId = 0;
19
+ function getSuspenseSourceId(source) {
20
+ let id = suspenseSourceIds.get(source);
21
+ if (id === void 0) {
22
+ id = ++nextSuspenseSourceId;
23
+ suspenseSourceIds.set(source, id);
24
+ }
25
+ return id;
26
+ }
27
+ function getUnscopedSuspenseKey(preparedValue, queryHash) {
28
+ const query = preparedValue instanceof BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof BaseQueryBuilder ? preparedValue.query : void 0;
29
+ if (!query) return queryHash;
30
+ const sourceIds = IR.collectCollectionSources(query._getQuery()).map(
31
+ ({ collection }) => getSuspenseSourceId(collection)
32
+ );
33
+ return `${sourceIds.join(`,`)}:${queryHash}`;
34
+ }
35
+ function getSuspenseCollections(client) {
36
+ if (!client) return unscopedSuspenseCollections;
37
+ let collections = suspenseCollectionsByClient.get(client);
38
+ if (!collections) {
39
+ collections = /* @__PURE__ */ new Map();
40
+ suspenseCollectionsByClient.set(client, collections);
41
+ }
42
+ return collections;
43
+ }
44
+ function releaseSuspenseCollection(collections, queryHash, collection) {
45
+ const entry = collections.get(queryHash);
46
+ if (entry?.collection !== collection) return;
47
+ collections.delete(queryHash);
48
+ entry.removeStatusListeners();
49
+ }
14
50
  function warnDeprecatedDepsArray(hookName = `useLiveQuery`) {
15
51
  if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {
16
52
  return;
@@ -36,8 +72,12 @@ function getCurrentTime() {
36
72
  }
37
73
  function getWarningCallsite(stackIndex) {
38
74
  const stack = new Error().stack ?? `unknown`;
39
- return stack.split(`
40
- `)[stackIndex]?.trim() ?? stack;
75
+ const lines = stack.split(`
76
+ `);
77
+ const userFrame = lines.slice(1).find(
78
+ (line) => !line.includes(`useLiveQuery.ts`) && !line.includes(`useLiveSuspenseQuery.ts`) && !line.includes(`useLiveInfiniteQuery.ts`)
79
+ );
80
+ return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack;
41
81
  }
42
82
  function warnDerivedIdentityHotPath(profiler, durationMs) {
43
83
  if (profiler.warned || !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
@@ -110,7 +150,7 @@ function warnUnhashableDerivedIdentity(error) {
110
150
  `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. Unhashable queries without queryKey will throw in 1.0.`
111
151
  );
112
152
  }
113
- function createCollectionFromPreparedQuery(value) {
153
+ function createCollectionFromPreparedQuery(value, defaultGcTime = DEFAULT_GC_TIME_MS) {
114
154
  if (value === void 0 || value === null) {
115
155
  return null;
116
156
  }
@@ -122,13 +162,13 @@ function createCollectionFromPreparedQuery(value) {
122
162
  return createLiveQueryCollection({
123
163
  query: value,
124
164
  startSync: true,
125
- gcTime: DEFAULT_GC_TIME_MS
165
+ gcTime: defaultGcTime
126
166
  });
127
167
  }
128
168
  if (typeof value === `object`) {
129
169
  return createLiveQueryCollection({
130
170
  startSync: true,
131
- gcTime: DEFAULT_GC_TIME_MS,
171
+ gcTime: defaultGcTime,
132
172
  ...value
133
173
  });
134
174
  }
@@ -137,6 +177,12 @@ function createCollectionFromPreparedQuery(value) {
137
177
  );
138
178
  }
139
179
  function useLiveQuery(configOrQueryOrCollection, deps) {
180
+ return useLiveQueryImpl(configOrQueryOrCollection, deps);
181
+ }
182
+ function useLiveQueryForSuspense(configOrQueryOrCollection, deps) {
183
+ return useLiveQueryImpl(configOrQueryOrCollection, deps, true);
184
+ }
185
+ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false) {
140
186
  const contextDbClient = useOptionalDbClient();
141
187
  const inputIsCollection = isCollection(configOrQueryOrCollection);
142
188
  const dbClient = inputIsCollection ? contextDbClient : getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient;
@@ -163,6 +209,7 @@ function useLiveQuery(configOrQueryOrCollection, deps) {
163
209
  null
164
210
  );
165
211
  const queryHashRef = useRef(void 0);
212
+ const suspenseKeyRef = useRef(void 0);
166
213
  const identityErrorRef = useRef(void 0);
167
214
  const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
168
215
  let preparedQueryValue = unpreparedQueryValue;
@@ -226,6 +273,18 @@ function useLiveQuery(configOrQueryOrCollection, deps) {
226
273
  if (deps !== void 0) {
227
274
  warnDeprecatedDepsArray();
228
275
  }
276
+ const canReuseSuspenseKey = forSuspense && !inputIsCollection && queryHash !== void 0 && !dbClient && collectionRef.current !== null && clientRef.current === dbClient && queryHashRef.current === queryHash && suspenseKeyRef.current !== void 0;
277
+ if (forSuspense && !inputIsCollection && queryHash && !dbClient && !canReuseSuspenseKey && preparedQueryValue === unpreparedQueryValue) {
278
+ preparedQueryValue = prepareQueryValue(
279
+ configOrQueryOrCollection,
280
+ dbClient,
281
+ deferredCollectionsRef.current
282
+ );
283
+ }
284
+ const suspenseKey = queryHash && !dbClient ? canReuseSuspenseKey ? suspenseKeyRef.current : getUnscopedSuspenseKey(preparedQueryValue, queryHash) : queryHash;
285
+ const suspenseCollections = forSuspense && !inputIsCollection && suspenseKey ? getSuspenseCollections(dbClient) : void 0;
286
+ const suspenseEntry = suspenseKey ? suspenseCollections?.get(suspenseKey) : void 0;
287
+ const suspenseCollection = suspenseEntry?.collection;
229
288
  const identityChanged = depsRef.current === null || (deps !== void 0 ? depsRef.current.length !== identityDeps.length || depsRef.current.some((dep, index) => dep !== identityDeps[index]) : !deepEquals(depsRef.current, identityDeps));
230
289
  const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (clientRef.current !== dbClient || identityChanged);
231
290
  const resumeDeferredCollections = () => {
@@ -251,21 +310,56 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
251
310
  collectionRef.current = configOrQueryOrCollection;
252
311
  configRef.current = configOrQueryOrCollection;
253
312
  } else {
254
- if (preparedQueryValue === unpreparedQueryValue) {
255
- preparedQueryValue = prepareQueryValue(
256
- configOrQueryOrCollection,
257
- dbClient,
258
- deferredCollectionsRef.current
313
+ if (suspenseCollection) {
314
+ collectionRef.current = suspenseCollection;
315
+ } else {
316
+ if (preparedQueryValue === unpreparedQueryValue) {
317
+ preparedQueryValue = prepareQueryValue(
318
+ configOrQueryOrCollection,
319
+ dbClient,
320
+ deferredCollectionsRef.current
321
+ );
322
+ }
323
+ collectionRef.current = createCollectionFromPreparedQuery(
324
+ preparedQueryValue,
325
+ forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS
259
326
  );
327
+ if (suspenseCollections && suspenseKey && collectionRef.current) {
328
+ const collection = collectionRef.current;
329
+ const removeCleanupListener = collection.on(
330
+ `status:cleaned-up`,
331
+ () => releaseSuspenseCollection(
332
+ suspenseCollections,
333
+ suspenseKey,
334
+ collection
335
+ )
336
+ );
337
+ const removeErrorListener = collection.on(`status:error`, () => {
338
+ setTimeout(
339
+ () => releaseSuspenseCollection(
340
+ suspenseCollections,
341
+ suspenseKey,
342
+ collection
343
+ ),
344
+ 0
345
+ );
346
+ });
347
+ const entry = {
348
+ collection,
349
+ removeStatusListeners: () => {
350
+ removeCleanupListener();
351
+ removeErrorListener();
352
+ }
353
+ };
354
+ suspenseCollections.set(suspenseKey, entry);
355
+ }
260
356
  }
261
- collectionRef.current = createCollectionFromPreparedQuery(
262
- preparedQueryValue
263
- );
264
357
  configRef.current = configOrQueryOrCollection;
265
358
  depsRef.current = [...identityDeps];
266
359
  }
267
360
  clientRef.current = dbClient;
268
361
  queryHashRef.current = queryHash;
362
+ suspenseKeyRef.current = suspenseKey;
269
363
  identityErrorRef.current = identityError;
270
364
  }
271
365
  if (needsNewCollection) {
@@ -302,6 +396,7 @@ export {
302
396
  prepareDerivedQuery,
303
397
  prepareQueryValue,
304
398
  useLiveQuery,
399
+ useLiveQueryForSuspense,
305
400
  warnDeprecatedDepsArray,
306
401
  warnUnhashableDerivedIdentity
307
402
  };