{"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n  computed,\n  getCurrentInstance,\n  nextTick,\n  onUnmounted,\n  reactive,\n  ref,\n  toValue,\n  watchEffect,\n} from 'vue'\nimport { createLiveQueryCollection } from '@tanstack/db'\nimport type {\n  ChangeMessage,\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'\nimport type { ComputedRef, MaybeRefOrGetter } from 'vue'\n\n/**\n * Return type for useLiveQuery hook\n * @property state - Reactive Map of query results (key → item)\n * @property data - Reactive array of query results in order, or single result for findOne queries\n * @property collection - The underlying query collection instance\n * @property status - Current query status\n * @property isLoading - True while initial query data is loading\n * @property isReady - True when query has received first data and is ready\n * @property isIdle - True when query hasn't started yet\n * @property isError - True when query encountered an error\n * @property isCleanedUp - True when query has been cleaned up\n */\nexport interface UseLiveQueryReturn<TContext extends Context> {\n  state: ComputedRef<Map<string | number, GetResult<TContext>>>\n  data: ComputedRef<InferResultType<TContext>>\n  collection: ComputedRef<Collection<GetResult<TContext>, string | number, {}>>\n  status: ComputedRef<CollectionStatus>\n  isLoading: ComputedRef<boolean>\n  isReady: ComputedRef<boolean>\n  isIdle: ComputedRef<boolean>\n  isError: ComputedRef<boolean>\n  isCleanedUp: ComputedRef<boolean>\n}\n\nexport interface UseLiveQueryReturnWithCollection<\n  T extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n> {\n  state: ComputedRef<Map<TKey, T>>\n  data: ComputedRef<Array<T>>\n  collection: ComputedRef<Collection<T, TKey, TUtils>>\n  status: ComputedRef<CollectionStatus>\n  isLoading: ComputedRef<boolean>\n  isReady: ComputedRef<boolean>\n  isIdle: ComputedRef<boolean>\n  isError: ComputedRef<boolean>\n  isCleanedUp: ComputedRef<boolean>\n}\n\nexport interface UseLiveQueryReturnWithSingleResultCollection<\n  T extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n> {\n  state: ComputedRef<Map<TKey, T>>\n  data: ComputedRef<T | undefined>\n  collection: ComputedRef<Collection<T, TKey, TUtils> & SingleResult>\n  status: ComputedRef<CollectionStatus>\n  isLoading: ComputedRef<boolean>\n  isReady: ComputedRef<boolean>\n  isIdle: ComputedRef<boolean>\n  isError: ComputedRef<boolean>\n  isCleanedUp: ComputedRef<boolean>\n}\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 reactive dependencies that trigger query re-execution when changed\n * @returns Reactive object with query 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 * // With reactive dependencies\n * const minPriority = ref(5)\n * const { data, state } = useLiveQuery(\n *   (q) => q.from({ todos: todosCollection })\n *          .where(({ todos }) => gt(todos.priority, minPriority.value)),\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 in template\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n *   q.from({ todos: todoCollection })\n * )\n *\n * // In template:\n * // <div v-if=\"isLoading\">Loading...</div>\n * // <div v-else-if=\"isError\">Error: {{ status }}</div>\n * // <ul v-else>\n * //   <li v-for=\"todo in data\" :key=\"todo.id\">{{ todo.text }}</li>\n * // </ul>\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<MaybeRefOrGetter<unknown>>,\n): UseLiveQueryReturn<TContext>\n\n// Overload 1b: 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<MaybeRefOrGetter<unknown>>,\n): UseLiveQueryReturn<TContext>\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of reactive dependencies that trigger query re-execution when changed\n * @returns Reactive object with query 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 reactive dependencies\n * const filter = ref('active')\n * const { data, isReady } = useLiveQuery({\n *   query: (q) => q.from({ todos: todosCollection })\n *                  .where(({ todos }) => eq(todos.status, filter.value))\n * }, [filter])\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 * // In template:\n * // <div v-if=\"isLoading\">Loading...</div>\n * // <div v-else-if=\"isError\">Something went wrong</div>\n * // <div v-else-if=\"!isReady\">Preparing...</div>\n * // <div v-else>{{ data.length }} items loaded</div>\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n  config: LiveQueryCollectionConfig<TContext>,\n  deps?: Array<MaybeRefOrGetter<unknown>>,\n): UseLiveQueryReturn<TContext>\n\n/**\n * Subscribe to an existing query collection (can be reactive)\n * @param liveQueryCollection - Pre-created query collection to subscribe to (can be a ref)\n * @returns Reactive object with query data, state, and status information\n * @example\n * // Using pre-created 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 * // Reactive query collection reference\n * const selectedQuery = ref(todosQuery)\n * const { data, collection } = useLiveQuery(selectedQuery)\n *\n * // Switch queries reactively\n * selectedQuery.value = archiveQuery\n *\n * @example\n * // Access query collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingQuery)\n *\n * // Use underlying collection for mutations\n * const handleToggle = (id) => {\n *   collection.value.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedQuery)\n *\n * // In template:\n * // <div v-if=\"isLoading\">Loading...</div>\n * // <div v-else-if=\"isError\">Error loading data</div>\n * // <div v-else>\n * //   <Item v-for=\"item in data\" :key=\"item.id\" v-bind=\"item\" />\n * // </div>\n */\n// Overload 3: Accept pre-created live query collection (can be reactive) - non-single result\nexport function useLiveQuery<\n  TResult extends object,\n  TKey extends string | number,\n  TUtils extends Record<string, any>,\n>(\n  liveQueryCollection: MaybeRefOrGetter<\n    Collection<TResult, TKey, TUtils> & NonSingleResult\n  >,\n): UseLiveQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload 4: 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: MaybeRefOrGetter<\n    Collection<TResult, TKey, TUtils> & SingleResult\n  >,\n): UseLiveQueryReturnWithSingleResultCollection<TResult, TKey, TUtils>\n\n// Implementation\nexport function useLiveQuery(\n  configOrQueryOrCollection: any,\n  deps: Array<MaybeRefOrGetter<unknown>> = [],\n): UseLiveQueryReturn<any> | UseLiveQueryReturnWithCollection<any, any, any> {\n  const collection = computed(() => {\n    // First check if the original parameter might be a ref/getter\n    // by seeing if toValue returns something different than the original\n    // NOTE: Don't call toValue on functions - toValue treats functions as getters and calls them!\n    let unwrappedParam = configOrQueryOrCollection\n    if (typeof configOrQueryOrCollection !== `function`) {\n      try {\n        const potentiallyUnwrapped = toValue(configOrQueryOrCollection)\n        if (potentiallyUnwrapped !== configOrQueryOrCollection) {\n          unwrappedParam = potentiallyUnwrapped\n        }\n      } catch {\n        // If toValue fails, use original parameter\n        unwrappedParam = configOrQueryOrCollection\n      }\n    }\n\n    // Check if it's already a collection by checking for specific collection methods\n    const isCollection =\n      unwrappedParam &&\n      typeof unwrappedParam === `object` &&\n      typeof unwrappedParam.subscribeChanges === `function` &&\n      typeof unwrappedParam.startSyncImmediate === `function` &&\n      typeof unwrappedParam.id === `string`\n\n    if (isCollection) {\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 = (unwrappedParam 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 Vue hooks\n      // Only start sync if the collection is in idle state\n      if (unwrappedParam.status === `idle`) {\n        unwrappedParam.startSyncImmediate()\n      }\n      return unwrappedParam\n    }\n\n    // Reference deps to make computed reactive to them\n    deps.forEach((dep) => toValue(dep))\n\n    // Ensure we always start sync for Vue hooks\n    if (typeof unwrappedParam === `function`) {\n      // To avoid calling the query function twice, we wrap it to handle null/undefined returns\n      // The wrapper will be called once by createLiveQueryCollection\n      const wrappedQuery = (q: InitialQueryBuilder) => {\n        const result = unwrappedParam(q)\n        // If the query function returns null/undefined, throw a special error\n        // that we'll catch to return null collection\n        if (result === undefined || result === null) {\n          throw new Error(`__DISABLED_QUERY__`)\n        }\n        return result\n      }\n\n      try {\n        return createLiveQueryCollection({\n          query: wrappedQuery,\n          startSync: true,\n        })\n      } catch (error) {\n        // Check if this is our special disabled query marker\n        if (error instanceof Error && error.message === `__DISABLED_QUERY__`) {\n          return null\n        }\n        // Re-throw other errors\n        throw error\n      }\n    } else {\n      return createLiveQueryCollection({\n        ...unwrappedParam,\n        startSync: true,\n      })\n    }\n  })\n\n  // Reactive state that gets updated granularly through change events\n  const state = reactive(new Map<string | number, any>())\n\n  // Reactive data array that maintains sorted order\n  const internalData = reactive<Array<any>>([])\n\n  // Computed wrapper for the data to match expected return type\n  // Returns single item for singleResult collections, array otherwise\n  const data = computed(() => {\n    const currentCollection = collection.value\n    if (!currentCollection) {\n      return internalData\n    }\n    const config: CollectionConfigSingleRowOption<any, any, any> =\n      currentCollection.config\n    return config.singleResult ? internalData[0] : internalData\n  })\n\n  // Track collection status reactively\n  const status = ref(\n    collection.value ? collection.value.status : (`disabled` as const),\n  )\n\n  // Helper to sync data array from collection in correct order\n  const syncDataFromCollection = (\n    currentCollection: Collection<any, any, any>,\n  ) => {\n    internalData.length = 0\n    internalData.push(...Array.from(currentCollection.values()))\n  }\n\n  // Track current unsubscribe function\n  let currentUnsubscribe: (() => void) | null = null\n\n  // Watch for collection changes and subscribe to updates\n  watchEffect((onInvalidate) => {\n    const currentCollection = collection.value\n\n    // Handle null collection (disabled query)\n    if (!currentCollection) {\n      status.value = `disabled` as const\n      state.clear()\n      internalData.length = 0\n      if (currentUnsubscribe) {\n        currentUnsubscribe()\n        currentUnsubscribe = null\n      }\n      return\n    }\n\n    // Update status ref whenever the effect runs\n    status.value = currentCollection.status\n\n    // Clean up previous subscription\n    if (currentUnsubscribe) {\n      currentUnsubscribe()\n    }\n\n    // Initialize state with current collection data\n    state.clear()\n    for (const [key, value] of currentCollection.entries()) {\n      state.set(key, value)\n    }\n\n    // Initialize data array in correct order\n    syncDataFromCollection(currentCollection)\n\n    // Listen for the first ready event to catch status transitions\n    // that might not trigger change events (fixes async status transition bug)\n    currentCollection.onFirstReady(() => {\n      // Use nextTick to ensure Vue reactivity updates properly\n      nextTick(() => {\n        status.value = currentCollection.status\n      })\n    })\n\n    // Subscribe to collection changes with granular updates\n    const subscription = currentCollection.subscribeChanges(\n      (changes: Array<ChangeMessage<any>>) => {\n        // Apply each change individually to the reactive state\n        for (const change of changes) {\n          switch (change.type) {\n            case `insert`:\n            case `update`:\n              state.set(change.key, change.value)\n              break\n            case `delete`:\n              state.delete(change.key)\n              break\n          }\n        }\n\n        // Update the data array to maintain sorted order\n        syncDataFromCollection(currentCollection)\n        // Update status ref on every change\n        status.value = currentCollection.status\n      },\n      {\n        includeInitialState: true,\n      },\n    )\n\n    currentUnsubscribe = subscription.unsubscribe.bind(subscription)\n\n    // Preload collection data if not already started\n    if (currentCollection.status === `idle`) {\n      currentCollection.preload().catch(console.error)\n    }\n\n    // Cleanup when effect is invalidated\n    onInvalidate(() => {\n      if (currentUnsubscribe) {\n        currentUnsubscribe()\n        currentUnsubscribe = null\n      }\n    })\n  })\n\n  // Cleanup on unmount (only if we're in a component context)\n  const instance = getCurrentInstance()\n  if (instance) {\n    onUnmounted(() => {\n      if (currentUnsubscribe) {\n        currentUnsubscribe()\n      }\n    })\n  }\n\n  return {\n    state: computed(() => state),\n    data,\n    collection: computed(() => collection.value),\n    status: computed(() => status.value),\n    isLoading: computed(() => status.value === `loading`),\n    isReady: computed(\n      () => status.value === `ready` || status.value === `disabled`,\n    ),\n    isIdle: computed(() => status.value === `idle`),\n    isError: computed(() => status.value === `error`),\n    isCleanedUp: computed(() => status.value === `cleaned-up`),\n  }\n}\n"],"names":["computed","toValue","createLiveQueryCollection","reactive","ref","watchEffect","nextTick","getCurrentInstance","onUnmounted"],"mappings":"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"}