@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.
- package/dist/cjs/useLiveInfiniteQuery.cjs +3 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +23 -2
- package/dist/cjs/useLiveQuery.cjs +108 -13
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +36 -1
- package/dist/cjs/useLiveSuspenseQuery.cjs +67 -12
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +23 -2
- package/dist/esm/useLiveInfiniteQuery.js +3 -0
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +36 -1
- package/dist/esm/useLiveQuery.js +109 -14
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.js +68 -13
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/package.json +3 -5
- package/src/useLiveInfiniteQuery.ts +32 -1
- package/src/useLiveQuery.ts +225 -13
- package/src/useLiveSuspenseQuery.ts +132 -23
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>):
|
|
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 {};
|
package/dist/esm/useLiveQuery.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
`)
|
|
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:
|
|
165
|
+
gcTime: defaultGcTime
|
|
126
166
|
});
|
|
127
167
|
}
|
|
128
168
|
if (typeof value === `object`) {
|
|
129
169
|
return createLiveQueryCollection({
|
|
130
170
|
startSync: true,
|
|
131
|
-
gcTime:
|
|
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 (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
};
|