@tanstack/react-db 0.1.54 → 0.1.56

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 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.cjs","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 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 const depsKey = JSON.stringify(deps)\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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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 const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\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":["CollectionImpl","useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.cjs","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 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 const depsKey = JSON.stringify(deps)\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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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 const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\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":["CollectionImpl","useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":";;;;;AAgBA,SAAS,2BACP,OACmC;AACnC,SAAO,OAAQ,MAAc,cAAc;AAC7C;AA6GO,SAAS,qBACd,qBACA,QACA,OAAuB,CAAA,GACe;AACtC,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,mBAAmB,OAAO,oBAAoB;AAGpD,QAAM,eAAe,+BAA+BA,GAAAA;AAGpD,MAAI,CAAC,gBAAgB,OAAO,wBAAwB,YAAY;AAC9D,UAAM,IAAI;AAAA,MACR,2IACoC,OAAO,mBAAmB;AAAA,IAAA;AAAA,EAElE;AAGA,QAAM,CAAC,iBAAiB,kBAAkB,IAAIC,MAAAA,SAAS,CAAC;AACxD,QAAM,CAAC,oBAAoB,qBAAqB,IAAIA,MAAAA,SAAS,KAAK;AAGlE,QAAM,gBAAgBC,MAAAA,OAAO,eAAe,sBAAsB,IAAI;AACtE,QAAM,4BAA4BA,MAAAA,OAAO,KAAK;AAG9C,QAAM,UAAU,KAAK,UAAU,IAAI;AACnC,QAAM,iBAAiBA,MAAAA,OAAO,OAAO;AAGrCC,QAAAA,UAAU,MAAM;AACd,QAAI,cAAc;AAElB,QAAI,cAAc;AAEhB,UAAI,cAAc,YAAY,qBAAqB;AACjD,sBAAc,UAAU;AACxB,kCAA0B,UAAU;AACpC,sBAAc;AAAA,MAChB;AAAA,IACF,OAAO;AAEL,UAAI,eAAe,YAAY,SAAS;AACtC,uBAAe,UAAU;AACzB,sBAAc;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,aAAa;AACf,yBAAmB,CAAC;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,cAAc,qBAAqB,OAAO,CAAC;AAI/C,QAAM,cAAc,eAChBC,0BAAa,mBAAmB,IAChCA,aAAAA;AAAAA,IACE,CAAC,MAAM,oBAAoB,CAAC,EAAE,MAAM,QAAQ,EAAE,OAAO,CAAC;AAAA,IACtD;AAAA,EAAA;AAIND,QAAAA,UAAU,MAAM;AACd,UAAM,QAAQ,YAAY,WAAW;AACrC,UAAM,iBAAiB;AACvB,UAAM,gBAAgB,kBAAkB,WAAW;AAGnD,QAAI,CAAC,2BAA2B,KAAK,GAAG;AAEtC,UAAI,cAAc;AAChB,cAAM,IAAI;AAAA,UACR;AAAA,QAAA;AAAA,MAGJ;AACA;AAAA,IACF;AAGA,QAAI,gBAAgB,CAAC,0BAA0B,SAAS;AACtD,YAAM,gBAAgB,MAAM,UAAA;AAC5B,UACE,kBACC,cAAc,WAAW,kBACxB,cAAc,UAAU,gBAC1B;AACA,gBAAQ;AAAA,UACN,oEAAoE,cAAc,MAAM,YAAY,cAAc,KAAK,+BACxF,cAAc,YAAY,aAAa;AAAA,QAAA;AAAA,MAE1E;AACA,gCAA0B,UAAU;AAAA,IACtC;AAGA,QAAI,CAAC,gBAAgB,CAAC,YAAY,QAAS;AAG3C,UAAM,SAAS,MAAM,UAAU;AAAA,MAC7B,QAAQ;AAAA,MACR,OAAO;AAAA,IAAA,CACR;AAED,QAAI,WAAW,MAAM;AACnB,4BAAsB,IAAI;AAC1B,aAAO,KAAK,MAAM;AAChB,8BAAsB,KAAK;AAAA,MAC7B,CAAC;AAAA,IACH,OAAO;AACL,4BAAsB,KAAK;AAAA,IAC7B;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,EAAA,CACD;AAGD,QAAM,EAAE,OAAO,YAAY,aAAa,SAAA,IAAaE,MAAAA,QAAQ,MAAM;AACjE,UAAM,YACJ,MAAM,QAAQ,YAAY,IAAI,IAAI,YAAY,OAAO,CAAA;AAEvD,UAAM,sBAAsB,kBAAkB;AAG9C,UAAM,UAAU,UAAU,SAAS;AAGnC,UAAM,cAA+D,CAAA;AACrE,UAAM,mBAAkC,CAAA;AAExC,aAAS,IAAI,GAAG,IAAI,iBAAiB,KAAK;AACxC,YAAM,WAAW,UAAU,MAAM,IAAI,WAAW,IAAI,KAAK,QAAQ;AACjE,kBAAY,KAAK,QAAQ;AACzB,uBAAiB,KAAK,mBAAmB,CAAC;AAAA,IAC5C;AAGA,UAAM,iBAAiB,UAAU;AAAA,MAC/B;AAAA,MACA;AAAA,IAAA;AAGF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,IAAA;AAAA,EAEd,GAAG,CAAC,YAAY,MAAM,iBAAiB,UAAU,gBAAgB,CAAC;AAGlE,QAAM,gBAAgBC,MAAAA,YAAY,MAAM;AACtC,QAAI,CAAC,eAAe,mBAAoB;AAExC,uBAAmB,CAAC,SAAS,OAAO,CAAC;AAAA,EACvC,GAAG,CAAC,aAAa,kBAAkB,CAAC;AAEpC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;"}
@@ -107,7 +107,7 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
107
107
  collection: void 0,
108
108
  status: `disabled`,
109
109
  isLoading: false,
110
- isReady: false,
110
+ isReady: true,
111
111
  isIdle: false,
112
112
  isError: false,
113
113
  isCleanedUp: false,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionConfigSingleRowOption,\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 by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\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 (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\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 (isCollection) {\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 (result instanceof CollectionImpl) {\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 const subscription = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\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: false,\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 config: CollectionConfigSingleRowOption<any, any, any> =\n snapshot.collection.config\n const singleResult = config.singleResult\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 isLoading: snapshot.collection.status === `loading`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\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":["useRef","BaseQueryBuilder","CollectionImpl","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionConfigSingleRowOption,\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 by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\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 (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\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 (isCollection) {\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 (result instanceof CollectionImpl) {\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 const subscription = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\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 config: CollectionConfigSingleRowOption<any, any, any> =\n snapshot.collection.config\n const singleResult = config.singleResult\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 isLoading: snapshot.collection.status === `loading`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\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":["useRef","BaseQueryBuilder","CollectionImpl","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\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 - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`\n )\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 (result.status === `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 (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":["useRef","useLiveQuery"],"mappings":";;;;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAaA,MAAAA,OAA6B,IAAI;AACpD,QAAM,gBAAgBA,MAAAA,OAAyC,IAAI;AACnE,QAAM,kBAAkBA,MAAAA,OAAO,KAAK;AAGpC,QAAM,SAASC,aAAAA,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;;"}
1
+ {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\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 - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`,\n )\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 (result.status === `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 (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":["useRef","useLiveQuery"],"mappings":";;;;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAaA,MAAAA,OAA6B,IAAI;AACpD,QAAM,gBAAgBA,MAAAA,OAAyC,IAAI;AACnE,QAAM,kBAAkBA,MAAAA,OAAO,KAAK;AAGpC,QAAM,SAASC,aAAAA,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePacedMutations.cjs","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":["useRef","useCallback","useMemo","createPacedMutations"],"mappings":";;;;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAcA,MAAAA,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgBA,MAAAA,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiBC,kBAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmBA,kBAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAASC,MAAAA,QAAQ,MAAM;AAC3B,WAAOC,wBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAeF,MAAAA,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;;"}
1
+ {"version":3,"file":"usePacedMutations.cjs","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from 'react'\nimport { createPacedMutations } from '@tanstack/db'\nimport type { PacedMutationsConfig, Transaction } from '@tanstack/db'\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>,\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":["useRef","useCallback","useMemo","createPacedMutations"],"mappings":";;;;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAcA,MAAAA,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgBA,MAAAA,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiBC,kBAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmBA,kBAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAASC,MAAAA,QAAQ,MAAM;AAC3B,WAAOC,wBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAeF,MAAAA,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;;"}
@@ -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 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 const depsKey = JSON.stringify(deps)\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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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 const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\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, 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 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 const depsKey = JSON.stringify(deps)\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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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 const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\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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"}
@@ -105,7 +105,7 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
105
105
  collection: void 0,
106
106
  status: `disabled`,
107
107
  isLoading: false,
108
- isReady: false,
108
+ isReady: true,
109
109
  isIdle: false,
110
110
  isError: false,
111
111
  isCleanedUp: false,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionConfigSingleRowOption,\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 by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\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 (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\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 (isCollection) {\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 (result instanceof CollectionImpl) {\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 const subscription = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\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: false,\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 config: CollectionConfigSingleRowOption<any, any, any> =\n snapshot.collection.config\n const singleResult = config.singleResult\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 isLoading: snapshot.collection.status === `loading`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\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":";;AAoBA,MAAM,qBAAqB;AAuSpB,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,eACJ,6BACA,OAAO,8BAA8B,YACrC,OAAO,0BAA0B,qBAAqB,cACtD,OAAO,0BAA0B,uBAAuB,cACxD,OAAO,0BAA0B,OAAO;AAG1C,QAAM,gBAAgB;AAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAU,OAA8B,IAAI;AAClD,QAAM,YAAY,OAAgB,IAAI;AAGtC,QAAM,aAAa,OAAO,CAAC;AAC3B,QAAM,cAAc,OAGV,IAAI;AAGd,QAAM,qBACJ,CAAC,cAAc,WACd,gBAAgB,UAAU,YAAY,6BACtC,CAAC,iBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,cAAc;AAEhB,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,kBAAkB,gBAAgB;AAE3C,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;AAGA,MAAI,oBAAoB;AACtB,eAAW,UAAU;AACrB,gBAAY,UAAU;AAAA,EACxB;AAGA,QAAM,eAAe,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBAA8B;AAEpD,UAAI,CAAC,cAAc,SAAS;AAC1B,eAAO,MAAM;AAAA,QAAC;AAAA,MAChB;AAEA,YAAM,eAAe,cAAc,QAAQ,iBAAiB,MAAM;AAEhE,mBAAW,WAAW;AACtB,sBAAA;AAAA,MACF,CAAC;AAED,UAAI,cAAc,QAAQ,WAAW,SAAS;AAC5C,mBAAW,WAAW;AACtB,sBAAA;AAAA,MACF;AACA,aAAO,MAAM;AACX,qBAAa,YAAA;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiB,OAMrB,IAAI;AACN,MAAI,CAAC,eAAe,WAAW,oBAAoB;AACjD,mBAAe,UAAU,MAAM;AAC7B,YAAM,iBAAiB,WAAW;AAClC,YAAM,oBAAoB,cAAc;AAGxC,UACE,CAAC,YAAY,WACb,YAAY,QAAQ,YAAY,kBAChC,YAAY,QAAQ,eAAe,mBACnC;AACA,oBAAY,UAAU;AAAA,UACpB,YAAY;AAAA,UACZ,SAAS;AAAA,QAAA;AAAA,MAEb;AAEA,aAAO,YAAY;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AAAA,IACf,aAAa;AAAA,IACb,eAAe;AAAA,EAAA;AAIjB,QAAM,sBAAsB,OAGlB,IAAI;AAEd,QAAM,cAAc,OAAY,IAAI;AAGpC,MACE,CAAC,oBAAoB,WACrB,oBAAoB,QAAQ,YAAY,SAAS,WACjD,oBAAoB,QAAQ,eAAe,SAAS,YACpD;AAEA,QAAI,CAAC,SAAS,YAAY;AACxB,kBAAY,UAAU;AAAA,QACpB,OAAO;AAAA,QACP,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,aAAa;AAAA,QACb,WAAW;AAAA,MAAA;AAAA,IAEf,OAAO;AAEL,YAAM,UAAU,MAAM,KAAK,SAAS,WAAW,SAAS;AACxD,YAAM,SACJ,SAAS,WAAW;AACtB,YAAM,eAAe,OAAO;AAC5B,UAAI,aAAmD;AACvD,UAAI,YAAmC;AAEvC,kBAAY,UAAU;AAAA,QACpB,IAAI,QAAQ;AACV,cAAI,CAAC,YAAY;AACf,yBAAa,IAAI,IAAI,OAAO;AAAA,UAC9B;AACA,iBAAO;AAAA,QACT;AAAA,QACA,IAAI,OAAO;AACT,cAAI,CAAC,WAAW;AACd,wBAAY,QAAQ,IAAI,CAAC,CAAA,EAAG,KAAK,MAAM,KAAK;AAAA,UAC9C;AACA,iBAAO,eAAe,UAAU,CAAC,IAAI;AAAA,QACvC;AAAA,QACA,YAAY,SAAS;AAAA,QACrB,QAAQ,SAAS,WAAW;AAAA,QAC5B,WAAW,SAAS,WAAW,WAAW;AAAA,QAC1C,SAAS,SAAS,WAAW,WAAW;AAAA,QACxC,QAAQ,SAAS,WAAW,WAAW;AAAA,QACvC,SAAS,SAAS,WAAW,WAAW;AAAA,QACxC,aAAa,SAAS,WAAW,WAAW;AAAA,QAC5C,WAAW;AAAA,MAAA;AAAA,IAEf;AAGA,wBAAoB,UAAU;AAAA,EAChC;AAEA,SAAO,YAAY;AACrB;"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionConfigSingleRowOption,\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 by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\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 (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\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 (isCollection) {\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 (result instanceof CollectionImpl) {\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 const subscription = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\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 config: CollectionConfigSingleRowOption<any, any, any> =\n snapshot.collection.config\n const singleResult = config.singleResult\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 isLoading: snapshot.collection.status === `loading`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\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 +1 @@
1
- {"version":3,"file":"useLiveSuspenseQuery.js","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\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 - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`\n )\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 (result.status === `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 (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":[],"mappings":";;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAa,OAA6B,IAAI;AACpD,QAAM,gBAAgB,OAAyC,IAAI;AACnE,QAAM,kBAAkB,OAAO,KAAK;AAGpC,QAAM,SAAS,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;"}
1
+ {"version":3,"file":"useLiveSuspenseQuery.js","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from 'react'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = [],\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\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 - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`,\n )\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 (result.status === `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 (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":[],"mappings":";;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAa,OAA6B,IAAI;AACpD,QAAM,gBAAgB,OAAyC,IAAI;AACnE,QAAM,kBAAkB,OAAO,KAAK;AAGpC,QAAM,SAAS,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePacedMutations.js","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":[],"mappings":";;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgB,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiB,YAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmB,YAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAAS,QAAQ,MAAM;AAC3B,WAAO,qBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAe,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;"}
1
+ {"version":3,"file":"usePacedMutations.js","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from 'react'\nimport { createPacedMutations } from '@tanstack/db'\nimport type { PacedMutationsConfig, Transaction } from '@tanstack/db'\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>,\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":[],"mappings":";;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgB,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiB,YAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmB,YAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAAS,QAAQ,MAAM;AAC3B,WAAO,qBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAe,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
+ "version": "0.1.56",
3
4
  "description": "React integration for @tanstack/db",
4
- "version": "0.1.54",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/TanStack/db.git",
9
+ "url": "git+https://github.com/TanStack/db.git",
10
10
  "directory": "packages/react-db"
11
11
  },
12
12
  "homepage": "https://tanstack.com/db",
@@ -15,20 +15,10 @@
15
15
  "react",
16
16
  "typescript"
17
17
  ],
18
- "dependencies": {
19
- "use-sync-external-store": "^1.6.0",
20
- "@tanstack/db": "0.5.10"
21
- },
22
- "devDependencies": {
23
- "@electric-sql/client": "1.2.0",
24
- "@testing-library/react": "^16.3.0",
25
- "@types/react": "^19.2.7",
26
- "@types/react-dom": "^19.2.3",
27
- "@types/use-sync-external-store": "^1.5.0",
28
- "@vitest/coverage-istanbul": "^3.2.4",
29
- "react": "^19.2.0",
30
- "react-dom": "^19.2.0"
31
- },
18
+ "type": "module",
19
+ "main": "dist/cjs/index.cjs",
20
+ "module": "dist/esm/index.js",
21
+ "types": "dist/esm/index.d.ts",
32
22
  "exports": {
33
23
  ".": {
34
24
  "import": {
@@ -42,23 +32,33 @@
42
32
  },
43
33
  "./package.json": "./package.json"
44
34
  },
35
+ "sideEffects": false,
45
36
  "files": [
46
37
  "dist",
47
38
  "src"
48
39
  ],
49
- "main": "dist/cjs/index.cjs",
50
- "module": "dist/esm/index.js",
40
+ "dependencies": {
41
+ "use-sync-external-store": "^1.6.0",
42
+ "@tanstack/db": "0.5.12"
43
+ },
51
44
  "peerDependencies": {
52
45
  "react": ">=16.8.0"
53
46
  },
54
- "sideEffects": false,
55
- "type": "module",
56
- "types": "dist/esm/index.d.ts",
47
+ "devDependencies": {
48
+ "@electric-sql/client": "1.2.0",
49
+ "@testing-library/react": "^16.3.0",
50
+ "@types/react": "^19.2.7",
51
+ "@types/react-dom": "^19.2.3",
52
+ "@types/use-sync-external-store": "^1.5.0",
53
+ "@vitest/coverage-istanbul": "^3.2.4",
54
+ "react": "^19.2.1",
55
+ "react-dom": "^19.2.1"
56
+ },
57
57
  "scripts": {
58
58
  "build": "vite build",
59
59
  "build:minified": "vite build --minify",
60
60
  "dev": "vite build --watch",
61
- "test": "npx vitest --run",
61
+ "test": "vitest --run",
62
62
  "lint": "eslint . --fix"
63
63
  }
64
64
  }
package/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  // Re-export all public APIs
2
- export * from "./useLiveQuery"
3
- export * from "./useLiveSuspenseQuery"
4
- export * from "./usePacedMutations"
5
- export * from "./useLiveInfiniteQuery"
2
+ export * from './useLiveQuery'
3
+ export * from './useLiveSuspenseQuery'
4
+ export * from './usePacedMutations'
5
+ export * from './useLiveInfiniteQuery'
6
6
 
7
7
  // Re-export everything from @tanstack/db
8
- export * from "@tanstack/db"
8
+ export * from '@tanstack/db'
9
9
 
10
10
  // Re-export some stuff explicitly to ensure the type & value is exported
11
- export type { Collection } from "@tanstack/db"
12
- export { createTransaction } from "@tanstack/db"
11
+ export type { Collection } from '@tanstack/db'
12
+ export { createTransaction } from '@tanstack/db'
@@ -1,6 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
- import { CollectionImpl } from "@tanstack/db"
3
- import { useLiveQuery } from "./useLiveQuery"
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { CollectionImpl } from '@tanstack/db'
3
+ import { useLiveQuery } from './useLiveQuery'
4
4
  import type {
5
5
  Collection,
6
6
  Context,
@@ -9,13 +9,13 @@ import type {
9
9
  LiveQueryCollectionUtils,
10
10
  NonSingleResult,
11
11
  QueryBuilder,
12
- } from "@tanstack/db"
12
+ } from '@tanstack/db'
13
13
 
14
14
  /**
15
15
  * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)
16
16
  */
17
17
  function isLiveQueryCollectionUtils(
18
- utils: unknown
18
+ utils: unknown,
19
19
  ): utils is LiveQueryCollectionUtils {
20
20
  return typeof (utils as any).setWindow === `function`
21
21
  }
@@ -27,7 +27,7 @@ export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
27
27
  lastPage: Array<InferResultType<TContext>[number]>,
28
28
  allPages: Array<Array<InferResultType<TContext>[number]>>,
29
29
  lastPageParam: number,
30
- allPageParams: Array<number>
30
+ allPageParams: Array<number>,
31
31
  ) => number | undefined
32
32
  }
33
33
 
@@ -116,21 +116,21 @@ export function useLiveInfiniteQuery<
116
116
  TUtils extends Record<string, any>,
117
117
  >(
118
118
  liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
119
- config: UseLiveInfiniteQueryConfig<any>
119
+ config: UseLiveInfiniteQueryConfig<any>,
120
120
  ): UseLiveInfiniteQueryReturn<any>
121
121
 
122
122
  // Overload for query function
123
123
  export function useLiveInfiniteQuery<TContext extends Context>(
124
124
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
125
125
  config: UseLiveInfiniteQueryConfig<TContext>,
126
- deps?: Array<unknown>
126
+ deps?: Array<unknown>,
127
127
  ): UseLiveInfiniteQueryReturn<TContext>
128
128
 
129
129
  // Implementation
130
130
  export function useLiveInfiniteQuery<TContext extends Context>(
131
131
  queryFnOrCollection: any,
132
132
  config: UseLiveInfiniteQueryConfig<TContext>,
133
- deps: Array<unknown> = []
133
+ deps: Array<unknown> = [],
134
134
  ): UseLiveInfiniteQueryReturn<TContext> {
135
135
  const pageSize = config.pageSize || 20
136
136
  const initialPageParam = config.initialPageParam ?? 0
@@ -142,7 +142,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
142
142
  if (!isCollection && typeof queryFnOrCollection !== `function`) {
143
143
  throw new Error(
144
144
  `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +
145
- `or a query function. Received: ${typeof queryFnOrCollection}`
145
+ `or a query function. Received: ${typeof queryFnOrCollection}`,
146
146
  )
147
147
  }
148
148
 
@@ -188,7 +188,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
188
188
  ? useLiveQuery(queryFnOrCollection)
189
189
  : useLiveQuery(
190
190
  (q) => queryFnOrCollection(q).limit(pageSize).offset(0),
191
- deps
191
+ deps,
192
192
  )
193
193
 
194
194
  // Adjust window when pagination changes
@@ -203,7 +203,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
203
203
  if (isCollection) {
204
204
  throw new Error(
205
205
  `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
206
- `Please add .orderBy() to your createLiveQueryCollection query.`
206
+ `Please add .orderBy() to your createLiveQueryCollection query.`,
207
207
  )
208
208
  }
209
209
  return
@@ -219,7 +219,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
219
219
  ) {
220
220
  console.warn(
221
221
  `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +
222
- `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
222
+ `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,
223
223
  )
224
224
  }
225
225
  hasValidatedCollectionRef.current = true
@@ -273,7 +273,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
273
273
  // Flatten the pages for the data return (without peek ahead item)
274
274
  const flatDataResult = dataArray.slice(
275
275
  0,
276
- totalItemsRequested
276
+ totalItemsRequested,
277
277
  ) as InferResultType<TContext>
278
278
 
279
279
  return {
@@ -1,9 +1,9 @@
1
- import { useRef, useSyncExternalStore } from "react"
1
+ import { useRef, useSyncExternalStore } from 'react'
2
2
  import {
3
3
  BaseQueryBuilder,
4
4
  CollectionImpl,
5
5
  createLiveQueryCollection,
6
- } from "@tanstack/db"
6
+ } from '@tanstack/db'
7
7
  import type {
8
8
  Collection,
9
9
  CollectionConfigSingleRowOption,
@@ -16,7 +16,7 @@ import type {
16
16
  NonSingleResult,
17
17
  QueryBuilder,
18
18
  SingleResult,
19
- } from "@tanstack/db"
19
+ } from '@tanstack/db'
20
20
 
21
21
  const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
22
22
 
@@ -83,7 +83,7 @@ export type UseLiveQueryStatus = CollectionStatus | `disabled`
83
83
  // Overload 1: Accept query function that always returns QueryBuilder
84
84
  export function useLiveQuery<TContext extends Context>(
85
85
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
86
- deps?: Array<unknown>
86
+ deps?: Array<unknown>,
87
87
  ): {
88
88
  state: Map<string | number, GetResult<TContext>>
89
89
  data: InferResultType<TContext>
@@ -100,9 +100,9 @@ export function useLiveQuery<TContext extends Context>(
100
100
  // Overload 2: Accept query function that can return undefined/null
101
101
  export function useLiveQuery<TContext extends Context>(
102
102
  queryFn: (
103
- q: InitialQueryBuilder
103
+ q: InitialQueryBuilder,
104
104
  ) => QueryBuilder<TContext> | undefined | null,
105
- deps?: Array<unknown>
105
+ deps?: Array<unknown>,
106
106
  ): {
107
107
  state: Map<string | number, GetResult<TContext>> | undefined
108
108
  data: InferResultType<TContext> | undefined
@@ -119,9 +119,9 @@ export function useLiveQuery<TContext extends Context>(
119
119
  // Overload 3: Accept query function that can return LiveQueryCollectionConfig
120
120
  export function useLiveQuery<TContext extends Context>(
121
121
  queryFn: (
122
- q: InitialQueryBuilder
122
+ q: InitialQueryBuilder,
123
123
  ) => LiveQueryCollectionConfig<TContext> | undefined | null,
124
- deps?: Array<unknown>
124
+ deps?: Array<unknown>,
125
125
  ): {
126
126
  state: Map<string | number, GetResult<TContext>> | undefined
127
127
  data: InferResultType<TContext> | undefined
@@ -142,9 +142,9 @@ export function useLiveQuery<
142
142
  TUtils extends Record<string, any>,
143
143
  >(
144
144
  queryFn: (
145
- q: InitialQueryBuilder
145
+ q: InitialQueryBuilder,
146
146
  ) => Collection<TResult, TKey, TUtils> | undefined | null,
147
- deps?: Array<unknown>
147
+ deps?: Array<unknown>,
148
148
  ): {
149
149
  state: Map<TKey, TResult> | undefined
150
150
  data: Array<TResult> | undefined
@@ -166,14 +166,14 @@ export function useLiveQuery<
166
166
  TUtils extends Record<string, any>,
167
167
  >(
168
168
  queryFn: (
169
- q: InitialQueryBuilder
169
+ q: InitialQueryBuilder,
170
170
  ) =>
171
171
  | QueryBuilder<TContext>
172
172
  | LiveQueryCollectionConfig<TContext>
173
173
  | Collection<TResult, TKey, TUtils>
174
174
  | undefined
175
175
  | null,
176
- deps?: Array<unknown>
176
+ deps?: Array<unknown>,
177
177
  ): {
178
178
  state:
179
179
  | Map<string | number, GetResult<TContext>>
@@ -229,7 +229,7 @@ export function useLiveQuery<
229
229
  // Overload 6: Accept config object
230
230
  export function useLiveQuery<TContext extends Context>(
231
231
  config: LiveQueryCollectionConfig<TContext>,
232
- deps?: Array<unknown>
232
+ deps?: Array<unknown>,
233
233
  ): {
234
234
  state: Map<string | number, GetResult<TContext>>
235
235
  data: InferResultType<TContext>
@@ -278,7 +278,7 @@ export function useLiveQuery<
278
278
  TKey extends string | number,
279
279
  TUtils extends Record<string, any>,
280
280
  >(
281
- liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult
281
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
282
282
  ): {
283
283
  state: Map<TKey, TResult>
284
284
  data: Array<TResult>
@@ -298,7 +298,7 @@ export function useLiveQuery<
298
298
  TKey extends string | number,
299
299
  TUtils extends Record<string, any>,
300
300
  >(
301
- liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult
301
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,
302
302
  ): {
303
303
  state: Map<TKey, TResult>
304
304
  data: TResult | undefined
@@ -315,7 +315,7 @@ export function useLiveQuery<
315
315
  // Implementation - use function overloads to infer the actual collection type
316
316
  export function useLiveQuery(
317
317
  configOrQueryOrCollection: any,
318
- deps: Array<unknown> = []
318
+ deps: Array<unknown> = [],
319
319
  ) {
320
320
  // Check if it's already a collection by checking for specific collection methods
321
321
  const isCollection =
@@ -327,7 +327,7 @@ export function useLiveQuery(
327
327
 
328
328
  // Use refs to cache collection and track dependencies
329
329
  const collectionRef = useRef<Collection<object, string | number, {}> | null>(
330
- null
330
+ null,
331
331
  )
332
332
  const depsRef = useRef<Array<unknown> | null>(null)
333
333
  const configRef = useRef<unknown>(null)
@@ -386,7 +386,7 @@ export function useLiveQuery(
386
386
  } else {
387
387
  // Unexpected return type
388
388
  throw new Error(
389
- `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
389
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,
390
390
  )
391
391
  }
392
392
  depsRef.current = [...deps]
@@ -467,7 +467,7 @@ export function useLiveQuery(
467
467
  // Use useSyncExternalStore to subscribe to collection changes
468
468
  const snapshot = useSyncExternalStore(
469
469
  subscribeRef.current,
470
- getSnapshotRef.current
470
+ getSnapshotRef.current,
471
471
  )
472
472
 
473
473
  // Track last snapshot (from useSyncExternalStore) and the returned value separately
@@ -492,7 +492,7 @@ export function useLiveQuery(
492
492
  collection: undefined,
493
493
  status: `disabled`,
494
494
  isLoading: false,
495
- isReady: false,
495
+ isReady: true,
496
496
  isIdle: false,
497
497
  isError: false,
498
498
  isCleanedUp: false,
@@ -1,5 +1,5 @@
1
- import { useRef } from "react"
2
- import { useLiveQuery } from "./useLiveQuery"
1
+ import { useRef } from 'react'
2
+ import { useLiveQuery } from './useLiveQuery'
3
3
  import type {
4
4
  Collection,
5
5
  Context,
@@ -10,7 +10,7 @@ import type {
10
10
  NonSingleResult,
11
11
  QueryBuilder,
12
12
  SingleResult,
13
- } from "@tanstack/db"
13
+ } from '@tanstack/db'
14
14
 
15
15
  /**
16
16
  * Create a live query with React Suspense support
@@ -75,7 +75,7 @@ import type {
75
75
  // Overload 1: Accept query function that always returns QueryBuilder
76
76
  export function useLiveSuspenseQuery<TContext extends Context>(
77
77
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
78
- deps?: Array<unknown>
78
+ deps?: Array<unknown>,
79
79
  ): {
80
80
  state: Map<string | number, GetResult<TContext>>
81
81
  data: InferResultType<TContext>
@@ -85,7 +85,7 @@ export function useLiveSuspenseQuery<TContext extends Context>(
85
85
  // Overload 2: Accept config object
86
86
  export function useLiveSuspenseQuery<TContext extends Context>(
87
87
  config: LiveQueryCollectionConfig<TContext>,
88
- deps?: Array<unknown>
88
+ deps?: Array<unknown>,
89
89
  ): {
90
90
  state: Map<string | number, GetResult<TContext>>
91
91
  data: InferResultType<TContext>
@@ -98,7 +98,7 @@ export function useLiveSuspenseQuery<
98
98
  TKey extends string | number,
99
99
  TUtils extends Record<string, any>,
100
100
  >(
101
- liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult
101
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
102
102
  ): {
103
103
  state: Map<TKey, TResult>
104
104
  data: Array<TResult>
@@ -111,7 +111,7 @@ export function useLiveSuspenseQuery<
111
111
  TKey extends string | number,
112
112
  TUtils extends Record<string, any>,
113
113
  >(
114
- liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult
114
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,
115
115
  ): {
116
116
  state: Map<TKey, TResult>
117
117
  data: TResult | undefined
@@ -121,7 +121,7 @@ export function useLiveSuspenseQuery<
121
121
  // Implementation - uses useLiveQuery internally and adds Suspense logic
122
122
  export function useLiveSuspenseQuery(
123
123
  configOrQueryOrCollection: any,
124
- deps: Array<unknown> = []
124
+ deps: Array<unknown> = [],
125
125
  ) {
126
126
  const promiseRef = useRef<Promise<void> | null>(null)
127
127
  const collectionRef = useRef<Collection<any, any, any> | null>(null)
@@ -148,7 +148,7 @@ export function useLiveSuspenseQuery(
148
148
  if (!result.isEnabled) {
149
149
  // Suspense queries cannot be disabled - throw error
150
150
  throw new Error(
151
- `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
151
+ `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`,
152
152
  )
153
153
  }
154
154
 
@@ -1,6 +1,6 @@
1
- import { useCallback, useMemo, useRef } from "react"
2
- import { createPacedMutations } from "@tanstack/db"
3
- import type { PacedMutationsConfig, Transaction } from "@tanstack/db"
1
+ import { useCallback, useMemo, useRef } from 'react'
2
+ import { createPacedMutations } from '@tanstack/db'
3
+ import type { PacedMutationsConfig, Transaction } from '@tanstack/db'
4
4
 
5
5
  /**
6
6
  * React hook for managing paced mutations with timing strategies.
@@ -94,7 +94,7 @@ export function usePacedMutations<
94
94
  TVariables = unknown,
95
95
  T extends object = Record<string, unknown>,
96
96
  >(
97
- config: PacedMutationsConfig<TVariables, T>
97
+ config: PacedMutationsConfig<TVariables, T>,
98
98
  ): (variables: TVariables) => Transaction<T> {
99
99
  // Keep refs to the latest callbacks so we can call them without recreating the instance
100
100
  const onMutateRef = useRef(config.onMutate)