@tanstack/react-db
Version:
React integration for @tanstack/db
1 lines • 12.4 kB
Source Map (JSON)
{"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n bindLiveQueryWindow,\n createLiveQuerySourceIdClaims,\n fetchNextLiveQueryWindowPage,\n getLiveQueryKey,\n isCollection,\n readLiveQueryWindowPageShape,\n rejectLiveQueryInput,\n releaseDeferredOnReclaim,\n resumeDeferredLiveQueryCollections,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n getIdentityScope,\n prepareDerivedQuery,\n prepareQueryValue,\n} from './useLiveQuery'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQuerySourceIdClaims,\n LiveQueryStatusFlags,\n LiveQueryWindowBinding,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\n// Keep the generic parameter for existing typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = LiveQueryStatusFlags & {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\n/** One render's window binding and what its commit still owes. */\ntype InfiniteQueryRender = {\n binding: LiveQueryWindowBinding\n /** The query callback whose identity this render derived, if any. */\n derivedQuery: unknown\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n /** Drops the reclaim release when this render commits. */\n cancelReclaimRelease: (() => void) | undefined\n warned: 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 an optional initial page label\n * @returns Object with pages, data, and pagination controls\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageShape = readLiveQueryWindowPageShape(config)\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n const inputIsCollection = isCollection(queryFnOrCollection)\n\n const committedRef = useRef<InfiniteQueryRender | null>(null)\n const committed = committedRef.current\n // Like useLiveQuery's instance memo, keep the latest render whose binding\n // built a window collection. A second render before commit, such as a\n // StrictMode double render, reuses that collection instead of starting\n // another. Each render still builds its own controller and page count from\n // the committed binding, which an uncommitted render never changes.\n const renderedRef = useRef<InfiniteQueryRender | null>(null)\n const sourceIdsRef = useRef<LiveQuerySourceIdClaims | null>(null)\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n const identityScope = () =>\n getIdentityScope(\n (sourceIdsRef.current ??= createLiveQuerySourceIdClaims()),\n dbClient,\n `useLiveInfiniteQuery`,\n )\n\n const queryKey = getLiveQueryKey(config)\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let dependencies: ReadonlyArray<unknown> | undefined\n let derivedQuery: unknown = undefined\n\n const rendered = renderedRef.current\n let binding: LiveQueryWindowBinding\n // The render boundary: a render that throws anywhere after preparation,\n // such as an unhashable, single-result, or uncompilable query, never\n // commits, so it releases the sync starts it deferred here.\n try {\n if (!inputIsCollection) {\n if (queryKey !== undefined) {\n dependencies = queryKey\n } else if (\n committed !== null &&\n committed.derivedQuery === queryFnOrCollection &&\n committed.binding.scope === dbClient\n ) {\n // The same callback in the same client scope keeps its identity.\n derivedQuery = queryFnOrCollection\n dependencies = committed.binding.dependencies\n } else {\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n derivedIdentityProfilerRef.current,\n identityScope(),\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n derivedQuery = queryFnOrCollection\n dependencies = preparation.identityDeps\n }\n }\n\n // The binding calls this only when it compiles a new window collection.\n const compileQuery = () => {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n return preparedQueryValue\n }\n\n binding = bindLiveQueryWindow(\n committed?.binding ?? null,\n {\n ...pageShape,\n input:\n typeof queryFnOrCollection === `function`\n ? compileQuery\n : queryFnOrCollection,\n dependencies,\n scope: dbClient,\n client: dbClient,\n queryKey,\n recompile: `identity`,\n },\n rendered?.binding,\n )\n } catch (error) {\n rejectLiveQueryInput(deferredCollections, error)\n }\n\n let currentRenderState = committed\n if (binding !== committed?.binding) {\n const newCollection = binding.collection !== committed?.binding.collection\n if (newCollection && binding.collection === rendered?.binding.collection) {\n // Sources the earlier render deferred still resume at this commit.\n for (const deferred of rendered.deferredCollections) {\n deferredCollections.add(deferred)\n }\n }\n const reusesRendered =\n newCollection && binding.collection === rendered?.binding.collection\n currentRenderState = {\n binding,\n derivedQuery,\n deferredCollections,\n // A render that never commits releases its deferred sync starts when\n // garbage collection reclaims the window collection it built. A reused\n // collection keeps the release its first render registered.\n cancelReclaimRelease: reusesRendered\n ? rendered.cancelReclaimRelease\n : newCollection && binding.ownsCollection\n ? releaseDeferredOnReclaim(binding.collection, deferredCollections)\n : undefined,\n warned: false,\n }\n // Keep a created collection for the next render only once it validated\n // and started, so a retry after a startup error builds a new one.\n if (binding.supplied) renderedRef.current = null\n else if (newCollection && binding.ownsCollection) {\n renderedRef.current = currentRenderState\n }\n }\n const renderState = currentRenderState!\n const controller = renderState.binding.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = renderState\n if (renderState.binding.warning && !renderState.warned) {\n renderState.warned = true\n console.warn(renderState.binding.warning)\n }\n // A commit releases the starts itself, so it drops the reclaim release.\n renderState.cancelReclaimRelease?.()\n resumeDeferredLiveQueryCollections(renderState.deferredCollections)\n return unsubscribe\n },\n [controller, renderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, () =>\n controller.getServerSnapshot(),\n )\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n get state() {\n return snapshot.state as EnabledLiveQueryReturn<TContext>[`state`]\n },\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isLoadingError: snapshot.isLoadingError,\n isCrashed: snapshot.isCrashed,\n isCleanedUp: snapshot.isCleanedUp,\n isSyncError: snapshot.isSyncError,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.isEnabled,\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n health: snapshot.health,\n }\n}\n"],"names":["getIdentityScope","prepareDerivedQuery","prepareQueryValue","bindLiveQueryWindow","rejectLiveQueryInput","useCallback","resumeDeferredLiveQueryCollections","useSyncExternalStore"],"mappings":";;;;;;;AAwCA;AA2FO;AAIL;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAGA;AACEA;AAC4B;AAC1B;AACA;AAGJ;AACA;AAEA;AACA;AAEA;AACA;AAIA;AACE;AACE;AACE;AAAe;AAOf;AACA;AAAiC;AAEjC;AAAoBC;AAClB;AAC2B;AAC3B;AACA;AAEF;AACA;AACA;AAA2B;AAC7B;AAIF;AACE;AACE;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAO;AAGT;AAAUC;AACc;AACtB;AACK;AAIG;AACN;AACO;AACC;AACR;AACW;AAAA;AAEH;AAAA;AAGZC;AAA+C;AAGjD;AACA;AACE;AACA;AAEE;AACE;AAAgC;AAClC;AAEF;AAEA;AAAqB;AACnB;AACA;AACA;AAAA;AAAA;AAAA;AAQM;AACE;AAIV;AAA4C;AAE1C;AAAsB;AACxB;AAEF;AACA;AAEA;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAwC;AAG1C;AACAC;AACA;AAAO;AACT;AACwB;AAE1B;AACA;AAAiBC;AAAqB;AAAW;AACpC;AAGb;AAAsBF;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACD;AACQ;AACL;AACE;AACA;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACZ;AAErB;;"}