{"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n  BaseQueryBuilder,\n  createLiveQueryCollection,\n  createLiveQueryObserver,\n  isCollection,\n} from '@tanstack/db'\nimport type {\n  Collection,\n  CollectionStatus,\n  Context,\n  GetResult,\n  InferResultType,\n  InitialQueryBuilder,\n  LiveQueryCollectionConfig,\n  LiveQueryObserver,\n  NonSingleResult,\n  QueryBuilder,\n  SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n *   q.from({ todos: todosCollection })\n *    .where(({ todos }) => eq(todos.completed, false))\n *    .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n *  @example\n * // Single result query\n * const { data } = useLiveQuery(\n *   (q) => q.from({ todos: todosCollection })\n *          .where(({ todos }) => eq(todos.id, 1))\n *          .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n *   (q) => q.from({ todos: todosCollection })\n *          .where(({ todos }) => gt(todos.priority, minPriority)),\n *   [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n *   q.from({ issues: issueCollection })\n *    .join({ persons: personCollection }, ({ issues, persons }) =>\n *      eq(issues.userId, persons.id)\n *    )\n *    .select(({ issues, persons }) => ({\n *      id: issues.id,\n *      title: issues.title,\n *      userName: persons.name\n *    }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n *   q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n *   <ul>\n *     {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n *   </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n  deps?: Array<unknown>,\n): {\n  state: Map<string | number, GetResult<TContext>>\n  data: InferResultType<TContext>\n  collection: Collection<GetResult<TContext>, string | number, {}>\n  status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n  queryFn: (\n    q: InitialQueryBuilder,\n  ) => QueryBuilder<TContext> | undefined | null,\n  deps?: Array<unknown>,\n): {\n  state: Map<string | number, GetResult<TContext>> | undefined\n  data: InferResultType<TContext> | undefined\n  collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n  status: UseLiveQueryStatus\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n  queryFn: (\n    q: InitialQueryBuilder,\n  ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n  deps?: Array<unknown>,\n): {\n  state: Map<string | number, GetResult<TContext>> | undefined\n  data: InferResultType<TContext> | undefined\n  collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n  status: UseLiveQueryStatus\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  queryFn: (\n    q: InitialQueryBuilder,\n  ) => Collection<TResult, TKey, TUtils> | undefined | null,\n  deps?: Array<unknown>,\n): {\n  state: Map<TKey, TResult> | undefined\n  data: Array<TResult> | undefined\n  collection: Collection<TResult, TKey, TUtils> | undefined\n  status: UseLiveQueryStatus\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n  TContext extends Context,\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  queryFn: (\n    q: InitialQueryBuilder,\n  ) =>\n    | QueryBuilder<TContext>\n    | LiveQueryCollectionConfig<TContext>\n    | Collection<TResult, TKey, TUtils>\n    | undefined\n    | null,\n  deps?: Array<unknown>,\n): {\n  state:\n    | Map<string | number, GetResult<TContext>>\n    | Map<TKey, TResult>\n    | undefined\n  data: InferResultType<TContext> | Array<TResult> | undefined\n  collection:\n    | Collection<GetResult<TContext>, string | number, {}>\n    | Collection<TResult, TKey, TUtils>\n    | undefined\n  status: UseLiveQueryStatus\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n *   query: (q) => q.from({ todos: todosCollection }),\n *   gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n *   .from({ persons: collection })\n *   .where(({ persons }) => gt(persons.age, 30))\n *   .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n *   query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n  config: LiveQueryCollectionConfig<TContext>,\n  deps?: Array<unknown>,\n): {\n  state: Map<string | number, GetResult<TContext>>\n  data: InferResultType<TContext>\n  collection: Collection<GetResult<TContext>, string | number, {}>\n  status: CollectionStatus // Can't be disabled for config objects\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n *   q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n *   collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n  state: Map<TKey, TResult>\n  data: Array<TResult>\n  collection: Collection<TResult, TKey, TUtils>\n  status: CollectionStatus // Can't be disabled for pre-created live query collections\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n  state: Map<TKey, TResult>\n  data: TResult | undefined\n  collection: Collection<TResult, TKey, TUtils> & SingleResult\n  status: CollectionStatus // Can't be disabled for pre-created live query collections\n  isLoading: boolean\n  isReady: boolean\n  isIdle: boolean\n  isError: boolean\n  isCleanedUp: boolean\n  isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n  configOrQueryOrCollection: any,\n  deps: Array<unknown> = [],\n) {\n  // Check if it's already a collection\n  const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n  // Use refs to cache collection and track dependencies\n  const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n    null,\n  )\n  const depsRef = useRef<Array<unknown> | null>(null)\n  const configRef = useRef<unknown>(null)\n\n  // The shared observer owns subscription, the ready-race, and the snapshot.\n  const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(\n    null,\n  )\n\n  // Check if we need to create/recreate the collection\n  const needsNewCollection =\n    !collectionRef.current ||\n    (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n    (!inputIsCollection &&\n      (depsRef.current === null ||\n        depsRef.current.length !== deps.length ||\n        depsRef.current.some((dep, i) => dep !== deps[i])))\n\n  if (needsNewCollection) {\n    if (inputIsCollection) {\n      // Warn when passing a collection directly with on-demand sync mode\n      // In on-demand mode, data is only loaded when queries with predicates request it\n      // Passing the collection directly doesn't provide any predicates, so no data loads\n      const syncMode = (\n        configOrQueryOrCollection as { config?: { syncMode?: string } }\n      ).config?.syncMode\n      if (syncMode === `on-demand`) {\n        console.warn(\n          `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n            `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n            `Instead, use a query builder function:\\n` +\n            `  const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\\n\\n` +\n            `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n        )\n      }\n      // It's already a collection, ensure sync is started for React hooks\n      configOrQueryOrCollection.startSyncImmediate()\n      collectionRef.current = configOrQueryOrCollection\n      configRef.current = configOrQueryOrCollection\n    } else {\n      // Handle different callback return types\n      if (typeof configOrQueryOrCollection === `function`) {\n        // Call the function with a query builder to see what it returns\n        const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n        const result = configOrQueryOrCollection(queryBuilder)\n\n        if (result === undefined || result === null) {\n          // Callback returned undefined/null - disabled query\n          collectionRef.current = null\n        } else if (isCollection(result)) {\n          // Callback returned a Collection instance - use it directly\n          result.startSyncImmediate()\n          collectionRef.current = result\n        } else if (result instanceof BaseQueryBuilder) {\n          // Callback returned QueryBuilder - create live query collection using the original callback\n          // (not the result, since the result might be from a different query builder instance)\n          collectionRef.current = createLiveQueryCollection({\n            query: configOrQueryOrCollection,\n            startSync: true,\n            gcTime: DEFAULT_GC_TIME_MS,\n          })\n        } else if (result && typeof result === `object`) {\n          // Assume it's a LiveQueryCollectionConfig\n          collectionRef.current = createLiveQueryCollection({\n            startSync: true,\n            gcTime: DEFAULT_GC_TIME_MS,\n            ...result,\n          })\n        } else {\n          // Unexpected return type\n          throw new Error(\n            `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n          )\n        }\n        depsRef.current = [...deps]\n      } else {\n        // Original logic for config objects\n        collectionRef.current = createLiveQueryCollection({\n          startSync: true,\n          gcTime: DEFAULT_GC_TIME_MS,\n          ...configOrQueryOrCollection,\n        })\n        depsRef.current = [...deps]\n      }\n    }\n  }\n\n  // Recreate the observer when the underlying collection changes. The observer\n  // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n  // unsubscribes it when the subscribe changes or the component unmounts, which\n  // detaches the collection subscription; the observer is then GC'd. (An unmount\n  // effect that disposed it would misfire under StrictMode/offscreen effect\n  // replay, leaving a disposed observer in the ref.)\n  if (needsNewCollection) {\n    // Defer the initial notify: useSyncExternalStore must not be notified\n    // synchronously during subscribe.\n    // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n    // hook's pre-observer loading policy, and — because wholesale delivers\n    // nothing synchronously during subscribe — never notifies\n    // useSyncExternalStore inside its own subscribe call.\n    observerRef.current = createLiveQueryObserver(collectionRef.current, {\n      mode: `wholesale`,\n    })\n  }\n  const observer = observerRef.current!\n\n  // Stable subscribe bound to the current observer; the observer owns the\n  // subscription, ready-race, and disposal.\n  const subscribeRef = useRef<\n    ((onStoreChange: () => void) => () => void) | null\n  >(null)\n  if (!subscribeRef.current || needsNewCollection) {\n    subscribeRef.current = (onStoreChange) =>\n      observer.subscribe(() => onStoreChange())\n  }\n\n  // The observer returns a stable snapshot per revision, which is the return\n  // shape this hook exposes. Keep the return loose to satisfy the overloads.\n  return useSyncExternalStore(subscribeRef.current, () =>\n    observer.getSnapshot(),\n  ) as any\n}\n"],"names":["isCollection","useRef","BaseQueryBuilder","createLiveQueryCollection","createLiveQueryObserver","useSyncExternalStore"],"mappings":";;;;AAqBA,MAAM,qBAAqB;AAuSpB,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,oBAAoBA,GAAAA,aAAa,yBAAyB;AAGhE,QAAM,gBAAgBC,MAAAA;AAAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAUA,MAAAA,OAA8B,IAAI;AAClD,QAAM,YAAYA,MAAAA,OAAgB,IAAI;AAGtC,QAAM,cAAcA,MAAAA;AAAAA,IAClB;AAAA,EAAA;AAIF,QAAM,qBACJ,CAAC,cAAc,WACd,qBAAqB,UAAU,YAAY,6BAC3C,CAAC,sBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,mBAAmB;AAIrB,YAAM,WACJ,0BACA,QAAQ;AACV,UAAI,aAAa,aAAa;AAC5B,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA;AAAA,MAMJ;AAEA,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAEL,UAAI,OAAO,8BAA8B,YAAY;AAEnD,cAAM,eAAe,IAAIC,oBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,wBAAc,UAAU;AAAA,QAC1B,WAAWF,gBAAa,MAAM,GAAG;AAE/B,iBAAO,mBAAA;AACP,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkBE,qBAAkB;AAG7C,wBAAc,UAAUC,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;AAQA,MAAI,oBAAoB;AAOtB,gBAAY,UAAUC,2BAAwB,cAAc,SAAS;AAAA,MACnE,MAAM;AAAA,IAAA,CACP;AAAA,EACH;AACA,QAAM,WAAW,YAAY;AAI7B,QAAM,eAAeH,MAAAA,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBACtB,SAAS,UAAU,MAAM,eAAe;AAAA,EAC5C;AAIA,SAAOI,MAAAA;AAAAA,IAAqB,aAAa;AAAA,IAAS,MAChD,SAAS,YAAA;AAAA,EAAY;AAEzB;;"}