@tanstack/react-db 0.1.95 → 0.1.96

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.
@@ -1,8 +1,8 @@
1
- import { useState, useRef, useEffect, useMemo, useCallback } from "react";
2
- import { CollectionImpl } from "@tanstack/db";
3
- import { useLiveQuery } from "./useLiveQuery.js";
4
- function isLiveQueryCollectionUtils(utils) {
5
- return typeof utils.setWindow === `function`;
1
+ import { useRef, useCallback, useSyncExternalStore } from "react";
2
+ import { deepEquals, createLiveQueryCollection, createLiveQueryWindowController, CollectionImpl } from "@tanstack/db";
3
+ const DEFAULT_GC_TIME_MS = 1;
4
+ function hasSetWindow(collection) {
5
+ return typeof collection.utils?.setWindow === `function`;
6
6
  }
7
7
  function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
8
8
  const pageSize = config.pageSize || 20;
@@ -13,123 +13,95 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
13
13
  `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) or a query function. Received: ${typeof queryFnOrCollection}`
14
14
  );
15
15
  }
16
- const [loadedPageCount, setLoadedPageCount] = useState(1);
17
- const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
18
- const collectionRef = useRef(isCollection ? queryFnOrCollection : null);
19
- const hasValidatedCollectionRef = useRef(false);
20
- let depsKey;
21
- try {
22
- depsKey = JSON.stringify(deps);
23
- } catch {
24
- throw new Error(
25
- `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). Ensure all dependency values are JSON-serializable.`
26
- );
27
- }
28
- const prevDepsKeyRef = useRef(depsKey);
29
- useEffect(() => {
30
- let shouldReset = false;
16
+ const collectionRef = useRef(null);
17
+ const controllerRef = useRef(null);
18
+ const configRef = useRef(null);
19
+ const depsRef = useRef(null);
20
+ const pageSizeRef = useRef(pageSize);
21
+ const initialPageParamRef = useRef(initialPageParam);
22
+ const validatedCollectionRef = useRef(null);
23
+ const inputKind = isCollection ? `collection` : `query`;
24
+ const inputKindRef = useRef(null);
25
+ const previousInputKind = inputKindRef.current;
26
+ const dependenciesChanged = !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, index) => dep !== deps[index]));
27
+ const dependenciesStructurallyEqual = !isCollection && depsRef.current !== null && deepEquals(depsRef.current, deps);
28
+ const needsNewCollection = !collectionRef.current || inputKindRef.current !== inputKind || isCollection && configRef.current !== queryFnOrCollection || dependenciesChanged;
29
+ const pageShapeChanged = pageSizeRef.current !== pageSize || initialPageParamRef.current !== initialPageParam;
30
+ const needsNewController = !controllerRef.current || needsNewCollection || pageShapeChanged;
31
+ if (needsNewCollection) {
32
+ inputKindRef.current = inputKind;
31
33
  if (isCollection) {
32
- if (collectionRef.current !== queryFnOrCollection) {
33
- collectionRef.current = queryFnOrCollection;
34
- hasValidatedCollectionRef.current = false;
35
- shouldReset = true;
36
- }
37
- } else {
38
- if (prevDepsKeyRef.current !== depsKey) {
39
- prevDepsKeyRef.current = depsKey;
40
- shouldReset = true;
41
- }
42
- }
43
- if (shouldReset) {
44
- setLoadedPageCount(1);
45
- }
46
- }, [isCollection, queryFnOrCollection, depsKey]);
47
- const queryResult = isCollection ? useLiveQuery(queryFnOrCollection) : useLiveQuery(
48
- (q) => queryFnOrCollection(q).limit(pageSize + 1).offset(0),
49
- deps
50
- );
51
- useEffect(() => {
52
- const utils = queryResult.collection.utils;
53
- const expectedOffset = 0;
54
- const expectedLimit = loadedPageCount * pageSize + 1;
55
- if (!isLiveQueryCollectionUtils(utils)) {
56
- if (isCollection) {
34
+ const collection = queryFnOrCollection;
35
+ if (!hasSetWindow(collection)) {
57
36
  throw new Error(
58
37
  `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
59
38
  );
60
39
  }
61
- return;
62
- }
63
- if (isCollection && !hasValidatedCollectionRef.current) {
64
- const currentWindow = utils.getWindow();
65
- if (currentWindow && (currentWindow.offset !== expectedOffset || currentWindow.limit !== expectedLimit)) {
66
- console.warn(
67
- `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
68
- );
40
+ if (validatedCollectionRef.current !== collection) {
41
+ validatedCollectionRef.current = collection;
42
+ const currentWindow = collection.utils.getWindow?.();
43
+ if (currentWindow && (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)) {
44
+ console.warn(
45
+ `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`
46
+ );
47
+ }
69
48
  }
70
- hasValidatedCollectionRef.current = true;
71
- }
72
- if (!isCollection && !queryResult.isReady) return;
73
- let cancelled = false;
74
- const result = utils.setWindow({
75
- offset: expectedOffset,
76
- limit: expectedLimit
77
- });
78
- if (result !== true) {
79
- setIsFetchingNextPage(true);
80
- result.catch((error) => {
81
- if (!cancelled)
82
- console.error(`useLiveInfiniteQuery: setWindow failed:`, error);
83
- }).finally(() => {
84
- if (!cancelled) setIsFetchingNextPage(false);
85
- });
49
+ collectionRef.current = collection;
50
+ configRef.current = queryFnOrCollection;
86
51
  } else {
87
- setIsFetchingNextPage(false);
88
- }
89
- return () => {
90
- cancelled = true;
91
- };
92
- }, [
93
- isCollection,
94
- queryResult.collection,
95
- queryResult.isReady,
96
- loadedPageCount,
97
- pageSize
98
- ]);
99
- const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
100
- const dataArray = Array.isArray(queryResult.data) ? queryResult.data : [];
101
- const totalItemsRequested = loadedPageCount * pageSize;
102
- const hasMore = dataArray.length > totalItemsRequested;
103
- const pagesResult = [];
104
- const pageParamsResult = [];
105
- for (let i = 0; i < loadedPageCount; i++) {
106
- const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize);
107
- pagesResult.push(pageData);
108
- pageParamsResult.push(initialPageParam + i);
52
+ collectionRef.current = createLiveQueryCollection({
53
+ query: (q) => queryFnOrCollection(q).limit(pageSize + 1).offset(0),
54
+ // Construction happens during render. Synchronization starts only when
55
+ // useSyncExternalStore commits the controller subscription.
56
+ startSync: false,
57
+ gcTime: DEFAULT_GC_TIME_MS
58
+ });
59
+ depsRef.current = [...deps];
109
60
  }
110
- const flatDataResult = dataArray.slice(
111
- 0,
112
- totalItemsRequested
61
+ }
62
+ if (needsNewController) {
63
+ const previousController = controllerRef.current;
64
+ const canPreservePageCount = previousController !== null && (!needsNewCollection || previousInputKind === `query` && dependenciesStructurallyEqual);
65
+ const initialPageCount = canPreservePageCount ? Math.max(1, previousController.getSnapshot().pages.length) : 1;
66
+ pageSizeRef.current = pageSize;
67
+ initialPageParamRef.current = initialPageParam;
68
+ controllerRef.current = createLiveQueryWindowController(
69
+ collectionRef.current,
70
+ {
71
+ pageSize,
72
+ initialPageParam,
73
+ initialPageCount
74
+ }
113
75
  );
114
- return {
115
- pages: pagesResult,
116
- pageParams: pageParamsResult,
117
- hasNextPage: hasMore,
118
- flatData: flatDataResult
119
- };
120
- }, [queryResult.data, loadedPageCount, pageSize, initialPageParam]);
76
+ }
77
+ const controller = controllerRef.current;
78
+ const subscribe = useCallback(
79
+ (onStoreChange) => controller.subscribe(onStoreChange),
80
+ [controller]
81
+ );
82
+ const getSnapshot = useCallback(() => controller.getSnapshot(), [controller]);
83
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot);
121
84
  const fetchNextPage = useCallback(() => {
122
- if (!hasNextPage || isFetchingNextPage) return;
123
- setLoadedPageCount((prev) => prev + 1);
124
- }, [hasNextPage, isFetchingNextPage]);
85
+ void controller.fetchNextPage().catch(() => {
86
+ });
87
+ }, [controller]);
125
88
  return {
126
- ...queryResult,
127
- data: flatData,
128
- pages,
129
- pageParams,
89
+ data: snapshot.data,
90
+ state: snapshot.state,
91
+ status: snapshot.status,
92
+ isLoading: snapshot.isLoading,
93
+ isReady: snapshot.isReady,
94
+ isIdle: snapshot.isIdle,
95
+ isError: snapshot.isError,
96
+ isCleanedUp: snapshot.isCleanedUp,
97
+ collection: snapshot.collection,
98
+ isEnabled: snapshot.isEnabled,
99
+ pages: snapshot.pages,
100
+ pageParams: snapshot.pageParams,
130
101
  fetchNextPage,
131
- hasNextPage,
132
- isFetchingNextPage
102
+ hasNextPage: snapshot.hasNextPage,
103
+ isFetchingNextPage: snapshot.isFetchingNextPage,
104
+ error: snapshot.error
133
105
  };
134
106
  }
135
107
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n utils: unknown,\n): utils is LiveQueryCollectionUtils {\n return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n /**\n * @deprecated This callback is not used by the current implementation.\n * Pagination is determined internally via a peek-ahead strategy.\n * Provided for API compatibility with TanStack Query conventions.\n */\n getNextPageParam?: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Track collection instance and whether we've validated it (only for pre-created collections)\n const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n const hasValidatedCollectionRef = useRef(false)\n\n // Track deps for query functions (stringify for comparison)\n let depsKey: string\n try {\n depsKey = JSON.stringify(deps)\n } catch {\n throw new Error(\n `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). ` +\n `Ensure all dependency values are JSON-serializable.`,\n )\n }\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset pagination when inputs change\n useEffect(() => {\n let shouldReset = false\n\n if (isCollection) {\n // Reset if collection instance changed\n if (collectionRef.current !== queryFnOrCollection) {\n collectionRef.current = queryFnOrCollection\n hasValidatedCollectionRef.current = false\n shouldReset = true\n }\n } else {\n // Reset if deps changed (for query functions)\n if (prevDepsKeyRef.current !== depsKey) {\n prevDepsKeyRef.current = depsKey\n shouldReset = true\n }\n }\n\n if (shouldReset) {\n setLoadedPageCount(1)\n }\n }, [isCollection, queryFnOrCollection, depsKey])\n\n // Create a live query with initial limit and offset\n // Either pass collection directly or wrap query function\n // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n deps,\n )\n\n // Adjust window when pagination changes\n useEffect(() => {\n const utils = queryResult.collection.utils\n const expectedOffset = 0\n const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n // Check if collection has orderBy (required for setWindow)\n if (!isLiveQueryCollectionUtils(utils)) {\n // For pre-created collections, throw an error if no orderBy\n if (isCollection) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n return\n }\n\n // For pre-created collections, validate window on first check\n if (isCollection && !hasValidatedCollectionRef.current) {\n const currentWindow = utils.getWindow()\n if (\n currentWindow &&\n (currentWindow.offset !== expectedOffset ||\n currentWindow.limit !== expectedLimit)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n )\n }\n hasValidatedCollectionRef.current = true\n }\n\n // For query functions, wait until collection is ready\n if (!isCollection && !queryResult.isReady) return\n\n // Adjust the window\n let cancelled = false\n const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result\n .catch((error: unknown) => {\n if (!cancelled)\n console.error(`useLiveInfiniteQuery: setWindow failed:`, error)\n })\n .finally(() => {\n if (!cancelled) setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n\n return () => {\n cancelled = true\n }\n }, [\n isCollection,\n queryResult.collection,\n queryResult.isReady,\n loadedPageCount,\n pageSize,\n ])\n\n // Split the data array into pages and determine if there's a next page\n const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n const dataArray = (\n Array.isArray(queryResult.data) ? queryResult.data : []\n ) as InferResultType<TContext>\n const totalItemsRequested = loadedPageCount * pageSize\n\n // Check if we have more data than requested (the peek ahead item)\n const hasMore = dataArray.length > totalItemsRequested\n\n // Build pages array (without the peek ahead item)\n const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n const pageParamsResult: Array<number> = []\n\n for (let i = 0; i < loadedPageCount; i++) {\n const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n pagesResult.push(pageData)\n pageParamsResult.push(initialPageParam + i)\n }\n\n // Flatten the pages for the data return (without peek ahead item)\n const flatDataResult = dataArray.slice(\n 0,\n totalItemsRequested,\n ) as InferResultType<TContext>\n\n return {\n pages: pagesResult,\n pageParams: pageParamsResult,\n hasNextPage: hasMore,\n flatData: flatDataResult,\n }\n }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n // Fetch next page\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return\n\n setLoadedPageCount((prev) => prev + 1)\n }, [hasNextPage, isFetchingNextPage])\n\n return {\n ...queryResult,\n data: flatData,\n pages,\n pageParams,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n CollectionImpl,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n deepEquals,\n} from '@tanstack/db'\n// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.\nimport type { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\n\ntype WindowedCollection = Collection<any, any, any> & {\n utils: {\n setWindow: (options: {\n offset: number\n limit: number\n }) => true | Promise<void>\n }\n}\n\n/** Type guard: does this collection expose `setWindow` (i.e. has an orderBy)? */\nfunction hasSetWindow(\n collection: Collection<any, any, any>,\n): collection is WindowedCollection {\n return typeof collection.utils?.setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n /**\n * @deprecated This callback is not used by the current implementation.\n * Pagination is determined internally via a peek-ahead strategy.\n * Provided for API compatibility with TanStack Query conventions.\n */\n getNextPageParam?: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const controllerRef = useRef<LiveQueryWindowController<any, any> | null>(null)\n const configRef = useRef<unknown>(null)\n const depsRef = useRef<Array<unknown> | null>(null)\n const pageSizeRef = useRef(pageSize)\n const initialPageParamRef = useRef(initialPageParam)\n const validatedCollectionRef = useRef<unknown>(null)\n const inputKind = isCollection ? `collection` : `query`\n const inputKindRef = useRef<typeof inputKind | null>(null)\n const previousInputKind = inputKindRef.current\n\n const dependenciesChanged =\n !isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, index) => dep !== deps[index]))\n const dependenciesStructurallyEqual =\n !isCollection &&\n depsRef.current !== null &&\n deepEquals(depsRef.current, deps)\n const needsNewCollection =\n !collectionRef.current ||\n inputKindRef.current !== inputKind ||\n (isCollection && configRef.current !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n pageSizeRef.current !== pageSize ||\n initialPageParamRef.current !== initialPageParam\n const needsNewController =\n !controllerRef.current || needsNewCollection || pageShapeChanged\n\n if (needsNewCollection) {\n inputKindRef.current = inputKind\n if (isCollection) {\n const collection = queryFnOrCollection as Collection<any, any, any>\n if (!hasSetWindow(collection)) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n // Warn once per collection instance if its current window doesn't match\n // the first page the hook is about to enforce.\n if (validatedCollectionRef.current !== collection) {\n validatedCollectionRef.current = collection\n const currentWindow = collection.utils.getWindow?.()\n if (\n currentWindow &&\n (currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`,\n )\n }\n }\n collectionRef.current = collection\n configRef.current = queryFnOrCollection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collectionRef.current = createLiveQueryCollection({\n query: (q: InitialQueryBuilder) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n depsRef.current = [...deps]\n }\n }\n\n if (needsNewController) {\n const previousController = controllerRef.current\n const canPreservePageCount =\n previousController !== null &&\n (!needsNewCollection ||\n (previousInputKind === `query` && dependenciesStructurallyEqual))\n const initialPageCount = canPreservePageCount\n ? Math.max(1, previousController.getSnapshot().pages.length)\n : 1\n pageSizeRef.current = pageSize\n initialPageParamRef.current = initialPageParam\n controllerRef.current = createLiveQueryWindowController(\n collectionRef.current,\n {\n pageSize,\n initialPageParam,\n initialPageCount,\n },\n )\n }\n const controller = controllerRef.current!\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => controller.subscribe(onStoreChange),\n [controller],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n\n const fetchNextPage = useCallback(() => {\n void controller.fetchNextPage().catch(() => {\n // Pagination errors are exposed through the controller snapshot. The\n // hook's void callback has no promise error channel, so consume it here.\n })\n }, [controller])\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled:\n snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
- import { isCollection, BaseQueryBuilder, createLiveQueryCollection, isSingleResultCollection, getLiveQueryStatusFlags } from "@tanstack/db";
2
+ import { isCollection, BaseQueryBuilder, createLiveQueryCollection, createLiveQueryObserver } from "@tanstack/db";
3
3
  const DEFAULT_GC_TIME_MS = 1;
4
4
  function useLiveQuery(configOrQueryOrCollection, deps = []) {
5
5
  const inputIsCollection = isCollection(configOrQueryOrCollection);
@@ -8,8 +8,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
8
8
  );
9
9
  const depsRef = useRef(null);
10
10
  const configRef = useRef(null);
11
- const versionRef = useRef(0);
12
- const snapshotRef = useRef(null);
11
+ const observerRef = useRef(
12
+ null
13
+ );
13
14
  const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
14
15
  if (needsNewCollection) {
15
16
  if (inputIsCollection) {
@@ -65,96 +66,19 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
65
66
  }
66
67
  }
67
68
  if (needsNewCollection) {
68
- versionRef.current = 0;
69
- snapshotRef.current = null;
69
+ observerRef.current = createLiveQueryObserver(collectionRef.current, {
70
+ mode: `wholesale`
71
+ });
70
72
  }
73
+ const observer = observerRef.current;
71
74
  const subscribeRef = useRef(null);
72
75
  if (!subscribeRef.current || needsNewCollection) {
73
- subscribeRef.current = (onStoreChange) => {
74
- if (!collectionRef.current) {
75
- return () => {
76
- };
77
- }
78
- let unsubscribed = false;
79
- const subscription = collectionRef.current.subscribeChanges(() => {
80
- if (unsubscribed) return;
81
- versionRef.current += 1;
82
- onStoreChange();
83
- });
84
- if (collectionRef.current.status === `ready`) {
85
- queueMicrotask(() => {
86
- if (unsubscribed) return;
87
- versionRef.current += 1;
88
- onStoreChange();
89
- });
90
- }
91
- return () => {
92
- unsubscribed = true;
93
- subscription.unsubscribe();
94
- };
95
- };
76
+ subscribeRef.current = (onStoreChange) => observer.subscribe(() => onStoreChange());
96
77
  }
97
- const getSnapshotRef = useRef(null);
98
- if (!getSnapshotRef.current || needsNewCollection) {
99
- getSnapshotRef.current = () => {
100
- const currentVersion = versionRef.current;
101
- const currentCollection = collectionRef.current;
102
- if (!snapshotRef.current || snapshotRef.current.version !== currentVersion || snapshotRef.current.collection !== currentCollection) {
103
- snapshotRef.current = {
104
- collection: currentCollection,
105
- version: currentVersion
106
- };
107
- }
108
- return snapshotRef.current;
109
- };
110
- }
111
- const snapshot = useSyncExternalStore(
78
+ return useSyncExternalStore(
112
79
  subscribeRef.current,
113
- getSnapshotRef.current
80
+ () => observer.getSnapshot()
114
81
  );
115
- const returnedSnapshotRef = useRef(null);
116
- const returnedRef = useRef(null);
117
- if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
118
- if (!snapshot.collection) {
119
- returnedRef.current = {
120
- state: void 0,
121
- data: void 0,
122
- collection: void 0,
123
- status: `disabled`,
124
- isLoading: false,
125
- isReady: true,
126
- isIdle: false,
127
- isError: false,
128
- isCleanedUp: false,
129
- isEnabled: false
130
- };
131
- } else {
132
- const entries = Array.from(snapshot.collection.entries());
133
- const singleResult = isSingleResultCollection(snapshot.collection);
134
- let stateCache = null;
135
- let dataCache = null;
136
- returnedRef.current = {
137
- get state() {
138
- if (!stateCache) {
139
- stateCache = new Map(entries);
140
- }
141
- return stateCache;
142
- },
143
- get data() {
144
- if (!dataCache) {
145
- dataCache = entries.map(([, value]) => value);
146
- }
147
- return singleResult ? dataCache[0] : dataCache;
148
- },
149
- collection: snapshot.collection,
150
- status: snapshot.collection.status,
151
- ...getLiveQueryStatusFlags(snapshot.collection.status),
152
- isEnabled: true
153
- };
154
- }
155
- returnedSnapshotRef.current = snapshot;
156
- }
157
- return returnedRef.current;
158
82
  }
159
83
  export {
160
84
  useLiveQuery
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n getLiveQueryStatusFlags,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (syncMode === `on-demand`) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (isCollection(result)) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n let unsubscribed = false\n\n const subscription = collectionRef.current.subscribeChanges(() => {\n // Drop late notifies that race with unsubscribe.\n if (unsubscribed) return\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Already-ready collections won't emit an initial change. Notify React\n // ourselves, but defer to a microtask — calling onStoreChange synchronously\n // here lands during the render-to-commit window and trips React's\n // \"state update on a component that hasn't mounted yet\" warning.\n if (collectionRef.current.status === `ready`) {\n queueMicrotask(() => {\n if (unsubscribed) return\n versionRef.current += 1\n onStoreChange()\n })\n }\n return () => {\n unsubscribed = true\n subscription.unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current,\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: true,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n const singleResult = isSingleResultCollection(snapshot.collection)\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return singleResult ? dataCache[0] : dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n ...getLiveQueryStatusFlags(snapshot.collection.status),\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // The shared observer owns subscription, the ready-race, and the snapshot.\n const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(\n null,\n )\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (syncMode === `on-demand`) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (isCollection(result)) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n observerRef.current = createLiveQueryObserver(collectionRef.current, {\n mode: `wholesale`,\n })\n }\n const observer = observerRef.current!\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange) =>\n observer.subscribe(() => onStoreChange())\n }\n\n // The observer returns a stable snapshot per revision, which is the return\n // shape this hook exposes. Keep the return loose to satisfy the overloads.\n return useSyncExternalStore(subscribeRef.current, () =>\n observer.getSnapshot(),\n ) as any\n}\n"],"names":[],"mappings":";;AAqBA,MAAM,qBAAqB;AAuSpB,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,oBAAoB,aAAa,yBAAyB;AAGhE,QAAM,gBAAgB;AAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAU,OAA8B,IAAI;AAClD,QAAM,YAAY,OAAgB,IAAI;AAGtC,QAAM,cAAc;AAAA,IAClB;AAAA,EAAA;AAIF,QAAM,qBACJ,CAAC,cAAc,WACd,qBAAqB,UAAU,YAAY,6BAC3C,CAAC,sBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,mBAAmB;AAIrB,YAAM,WACJ,0BACA,QAAQ;AACV,UAAI,aAAa,aAAa;AAC5B,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA;AAAA,MAMJ;AAEA,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAEL,UAAI,OAAO,8BAA8B,YAAY;AAEnD,cAAM,eAAe,IAAI,iBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,wBAAc,UAAU;AAAA,QAC1B,WAAW,aAAa,MAAM,GAAG;AAE/B,iBAAO,mBAAA;AACP,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkB,kBAAkB;AAG7C,wBAAc,UAAU,0BAA0B;AAAA,YAChD,OAAO;AAAA,YACP,WAAW;AAAA,YACX,QAAQ;AAAA,UAAA,CACT;AAAA,QACH,WAAW,UAAU,OAAO,WAAW,UAAU;AAE/C,wBAAc,UAAU,0BAA0B;AAAA,YAChD,WAAW;AAAA,YACX,QAAQ;AAAA,YACR,GAAG;AAAA,UAAA,CACJ;AAAA,QACH,OAAO;AAEL,gBAAM,IAAI;AAAA,YACR,qHAAqH,OAAO,MAAM;AAAA,UAAA;AAAA,QAEtI;AACA,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B,OAAO;AAEL,sBAAc,UAAU,0BAA0B;AAAA,UAChD,WAAW;AAAA,UACX,QAAQ;AAAA,UACR,GAAG;AAAA,QAAA,CACJ;AACD,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAQA,MAAI,oBAAoB;AAOtB,gBAAY,UAAU,wBAAwB,cAAc,SAAS;AAAA,MACnE,MAAM;AAAA,IAAA,CACP;AAAA,EACH;AACA,QAAM,WAAW,YAAY;AAI7B,QAAM,eAAe,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBACtB,SAAS,UAAU,MAAM,eAAe;AAAA,EAC5C;AAIA,SAAO;AAAA,IAAqB,aAAa;AAAA,IAAS,MAChD,SAAS,YAAA;AAAA,EAAY;AAEzB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
- "version": "0.1.95",
3
+ "version": "0.1.96",
4
4
  "description": "React integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "use-sync-external-store": "^1.6.0",
44
- "@tanstack/db": "0.6.17"
44
+ "@tanstack/db": "0.7.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": ">=16.8.0"