@tanstack/react-db 0.1.55 → 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.
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/usePacedMutations.cjs.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/dist/esm/usePacedMutations.js.map +1 -1
- package/package.json +22 -22
- package/src/index.ts +7 -7
- package/src/useLiveInfiniteQuery.ts +14 -14
- package/src/useLiveQuery.ts +19 -19
- package/src/useLiveSuspenseQuery.ts +9 -9
- package/src/usePacedMutations.ts +4 -4
|
@@ -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;;"}
|
|
@@ -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: 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
|
+
{"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":";;;;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,gBAAgBA,MAAAA;AAAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAUA,MAAAA,OAA8B,IAAI;AAClD,QAAM,YAAYA,MAAAA,OAAgB,IAAI;AAGtC,QAAM,aAAaA,MAAAA,OAAO,CAAC;AAC3B,QAAM,cAAcA,MAAAA,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,IAAIC,oBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkBC,mBAAgB;AAE3C,iBAAO,mBAAA;AACP,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkBD,qBAAkB;AAG7C,wBAAc,UAAUE,6BAA0B;AAAA,YAChD,OAAO;AAAA,YACP,WAAW;AAAA,YACX,QAAQ;AAAA,UAAA,CACT;AAAA,QACH,WAAW,UAAU,OAAO,WAAW,UAAU;AAE/C,wBAAc,UAAUA,6BAA0B;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,UAAUA,6BAA0B;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,eAAeH,MAAAA,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,iBAAiBA,MAAAA,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,WAAWI,MAAAA;AAAAA,IACf,aAAa;AAAA,IACb,eAAe;AAAA,EAAA;AAIjB,QAAM,sBAAsBJ,MAAAA,OAGlB,IAAI;AAEd,QAAM,cAAcA,MAAAA,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from
|
|
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
|
|
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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"}
|
|
@@ -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: 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
|
+
{"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
|
|
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
|
|
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.55",
|
|
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
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
"
|
|
50
|
-
|
|
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
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
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": "
|
|
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
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
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
|
|
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
|
|
12
|
-
export { createTransaction } from
|
|
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
|
|
2
|
-
import { CollectionImpl } from
|
|
3
|
-
import { useLiveQuery } from
|
|
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
|
|
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 {
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useRef, useSyncExternalStore } from
|
|
1
|
+
import { useRef, useSyncExternalStore } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
BaseQueryBuilder,
|
|
4
4
|
CollectionImpl,
|
|
5
5
|
createLiveQueryCollection,
|
|
6
|
-
} from
|
|
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
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useRef } from
|
|
2
|
-
import { useLiveQuery } from
|
|
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
|
|
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
|
|
package/src/usePacedMutations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useCallback, useMemo, useRef } from
|
|
2
|
-
import { createPacedMutations } from
|
|
3
|
-
import type { PacedMutationsConfig, Transaction } from
|
|
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)
|