@tanstack/vue-db 0.0.127 → 0.0.129

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/README.md CHANGED
@@ -1,3 +1,20 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source
4
+ media="(prefers-color-scheme: dark)"
5
+ srcset="https://tanstack.com/api/readme/db.png?framework=vue&theme=dark"
6
+ />
7
+ <source
8
+ media="(prefers-color-scheme: light)"
9
+ srcset="https://tanstack.com/api/readme/db.png?framework=vue"
10
+ />
11
+ <img
12
+ src="https://tanstack.com/api/readme/db.png?framework=vue"
13
+ alt="TanStack Vue DB"
14
+ width="900"
15
+ />
16
+ </picture>
17
+ </div>
1
18
  # @tanstack/vue-db
2
19
 
3
20
  Vue composables for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
@@ -76,71 +76,57 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
76
76
  internalData.length = 0;
77
77
  internalData.push(...Array.from(currentCollection.values()));
78
78
  };
79
- let currentUnsubscribe = null;
79
+ let currentObserver = null;
80
+ const syncFromObserver = (observer, currentCollection) => {
81
+ status.value = observer.getSnapshot().status;
82
+ syncDataFromCollection(currentCollection);
83
+ };
80
84
  vue.watchEffect((onInvalidate) => {
81
85
  const currentCollection = collection.value;
86
+ currentObserver?.dispose();
87
+ currentObserver = null;
82
88
  if (!currentCollection) {
83
89
  status.value = `disabled`;
84
90
  state.clear();
85
91
  internalData.length = 0;
86
- if (currentUnsubscribe) {
87
- currentUnsubscribe();
88
- currentUnsubscribe = null;
89
- }
90
92
  return;
91
93
  }
92
- status.value = currentCollection.status;
93
- if (currentUnsubscribe) {
94
- currentUnsubscribe();
95
- }
94
+ const observer = db.createLiveQueryObserver(currentCollection);
95
+ currentObserver = observer;
96
96
  state.clear();
97
- for (const [key, value] of currentCollection.entries()) {
98
- state.set(key, value);
99
- }
100
- syncDataFromCollection(currentCollection);
101
- currentCollection.onFirstReady(() => {
102
- vue.nextTick(() => {
103
- status.value = currentCollection.status;
104
- });
105
- });
106
- const subscription = currentCollection.subscribeChanges(
97
+ const unsubscribe = observer.subscribe(
107
98
  (changes) => {
108
- for (const change of changes) {
109
- switch (change.type) {
110
- case `insert`:
111
- case `update`:
112
- state.set(change.key, change.value);
113
- break;
114
- case `delete`:
115
- state.delete(change.key);
116
- break;
99
+ if (changes) {
100
+ for (const change of changes) {
101
+ switch (change.type) {
102
+ case `insert`:
103
+ case `update`:
104
+ state.set(change.key, change.value);
105
+ break;
106
+ case `delete`:
107
+ state.delete(change.key);
108
+ break;
109
+ }
110
+ }
111
+ } else {
112
+ state.clear();
113
+ for (const [key, value] of observer.getSnapshot().state ?? []) {
114
+ state.set(key, value);
117
115
  }
118
116
  }
119
- syncDataFromCollection(currentCollection);
120
- status.value = currentCollection.status;
121
- },
122
- {
123
- includeInitialState: true
117
+ syncFromObserver(observer, currentCollection);
124
118
  }
125
119
  );
126
- currentUnsubscribe = subscription.unsubscribe.bind(subscription);
127
- if (currentCollection.status === `idle`) {
128
- currentCollection.preload().catch(console.error);
129
- }
120
+ syncFromObserver(observer, currentCollection);
130
121
  onInvalidate(() => {
131
- if (currentUnsubscribe) {
132
- currentUnsubscribe();
133
- currentUnsubscribe = null;
134
- }
122
+ unsubscribe();
123
+ observer.dispose();
124
+ currentObserver = null;
135
125
  });
136
126
  });
137
127
  const instance = vue.getCurrentInstance();
138
128
  if (instance) {
139
- vue.onUnmounted(() => {
140
- if (currentUnsubscribe) {
141
- currentUnsubscribe();
142
- }
143
- });
129
+ vue.onUnmounted(() => currentObserver?.dispose());
144
130
  }
145
131
  return {
146
132
  state: vue.computed(() => state),
@@ -1 +1 @@
1
- {"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 {\n createLiveQueryCollection,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n ChangeMessage,\n Collection,\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\n const inputIsCollection = isCollection(unwrappedParam)\n\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 = (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 return isSingleResultCollection(currentCollection)\n ? internalData[0]\n : 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","isCollection","createLiveQueryCollection","reactive","isSingleResultCollection","ref","watchEffect","nextTick","getCurrentInstance","onUnmounted"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n computed,\n getCurrentInstance,\n onUnmounted,\n reactive,\n ref,\n toValue,\n watchEffect,\n} from 'vue'\nimport {\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n ChangeMessage,\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'\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\n const inputIsCollection = isCollection(unwrappedParam)\n\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 = (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 return isSingleResultCollection(currentCollection)\n ? internalData[0]\n : 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 // The shared observer owns subscription, the ready-race, and status; Vue\n // materializes into its own reactive map (granular) + ordered array.\n let currentObserver: LiveQueryObserver<any, any> | null = null\n\n const syncFromObserver = (\n observer: LiveQueryObserver<any, any>,\n currentCollection: Collection<any, any, any>,\n ) => {\n status.value = observer.getSnapshot().status as CollectionStatus\n syncDataFromCollection(currentCollection)\n }\n\n // Watch for collection changes and subscribe to updates\n watchEffect((onInvalidate) => {\n const currentCollection = collection.value\n\n // Tear down any previous observer.\n currentObserver?.dispose()\n currentObserver = null\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n status.value = `disabled` as const\n state.clear()\n internalData.length = 0\n return\n }\n\n const observer = createLiveQueryObserver(currentCollection)\n currentObserver = observer\n\n // Initial rows arrive as the observer's first delta (includeInitialState);\n // apply them and every subsequent delta granularly to the reactive map.\n state.clear()\n\n const unsubscribe = observer.subscribe(\n (changes: Array<ChangeMessage<any>> | undefined) => {\n if (changes) {\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 } else {\n // Cleanup and other status-only publications carry no row deltas.\n // Rebuild the keyed view so it cannot diverge from ordered data.\n state.clear()\n for (const [key, value] of observer.getSnapshot().state ?? []) {\n state.set(key, value)\n }\n }\n syncFromObserver(observer, currentCollection)\n },\n )\n syncFromObserver(observer, currentCollection)\n\n // Cleanup when effect is invalidated\n onInvalidate(() => {\n unsubscribe()\n observer.dispose()\n currentObserver = null\n })\n })\n\n // Cleanup on unmount (only if we're in a component context)\n const instance = getCurrentInstance()\n if (instance) {\n onUnmounted(() => currentObserver?.dispose())\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","isCollection","createLiveQueryCollection","reactive","isSingleResultCollection","ref","watchEffect","createLiveQueryObserver","getCurrentInstance","onUnmounted"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { computed, toValue, reactive, ref, watchEffect, nextTick, getCurrentInstance, onUnmounted } from "vue";
2
- import { isCollection, createLiveQueryCollection, isSingleResultCollection } from "@tanstack/db";
1
+ import { computed, toValue, reactive, ref, watchEffect, getCurrentInstance, onUnmounted } from "vue";
2
+ import { isCollection, createLiveQueryCollection, isSingleResultCollection, createLiveQueryObserver } from "@tanstack/db";
3
3
  function useLiveQuery(configOrQueryOrCollection, deps = []) {
4
4
  const collection = computed(() => {
5
5
  let unwrappedParam = configOrQueryOrCollection;
@@ -74,71 +74,57 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
74
74
  internalData.length = 0;
75
75
  internalData.push(...Array.from(currentCollection.values()));
76
76
  };
77
- let currentUnsubscribe = null;
77
+ let currentObserver = null;
78
+ const syncFromObserver = (observer, currentCollection) => {
79
+ status.value = observer.getSnapshot().status;
80
+ syncDataFromCollection(currentCollection);
81
+ };
78
82
  watchEffect((onInvalidate) => {
79
83
  const currentCollection = collection.value;
84
+ currentObserver?.dispose();
85
+ currentObserver = null;
80
86
  if (!currentCollection) {
81
87
  status.value = `disabled`;
82
88
  state.clear();
83
89
  internalData.length = 0;
84
- if (currentUnsubscribe) {
85
- currentUnsubscribe();
86
- currentUnsubscribe = null;
87
- }
88
90
  return;
89
91
  }
90
- status.value = currentCollection.status;
91
- if (currentUnsubscribe) {
92
- currentUnsubscribe();
93
- }
92
+ const observer = createLiveQueryObserver(currentCollection);
93
+ currentObserver = observer;
94
94
  state.clear();
95
- for (const [key, value] of currentCollection.entries()) {
96
- state.set(key, value);
97
- }
98
- syncDataFromCollection(currentCollection);
99
- currentCollection.onFirstReady(() => {
100
- nextTick(() => {
101
- status.value = currentCollection.status;
102
- });
103
- });
104
- const subscription = currentCollection.subscribeChanges(
95
+ const unsubscribe = observer.subscribe(
105
96
  (changes) => {
106
- for (const change of changes) {
107
- switch (change.type) {
108
- case `insert`:
109
- case `update`:
110
- state.set(change.key, change.value);
111
- break;
112
- case `delete`:
113
- state.delete(change.key);
114
- break;
97
+ if (changes) {
98
+ for (const change of changes) {
99
+ switch (change.type) {
100
+ case `insert`:
101
+ case `update`:
102
+ state.set(change.key, change.value);
103
+ break;
104
+ case `delete`:
105
+ state.delete(change.key);
106
+ break;
107
+ }
108
+ }
109
+ } else {
110
+ state.clear();
111
+ for (const [key, value] of observer.getSnapshot().state ?? []) {
112
+ state.set(key, value);
115
113
  }
116
114
  }
117
- syncDataFromCollection(currentCollection);
118
- status.value = currentCollection.status;
119
- },
120
- {
121
- includeInitialState: true
115
+ syncFromObserver(observer, currentCollection);
122
116
  }
123
117
  );
124
- currentUnsubscribe = subscription.unsubscribe.bind(subscription);
125
- if (currentCollection.status === `idle`) {
126
- currentCollection.preload().catch(console.error);
127
- }
118
+ syncFromObserver(observer, currentCollection);
128
119
  onInvalidate(() => {
129
- if (currentUnsubscribe) {
130
- currentUnsubscribe();
131
- currentUnsubscribe = null;
132
- }
120
+ unsubscribe();
121
+ observer.dispose();
122
+ currentObserver = null;
133
123
  });
134
124
  });
135
125
  const instance = getCurrentInstance();
136
126
  if (instance) {
137
- onUnmounted(() => {
138
- if (currentUnsubscribe) {
139
- currentUnsubscribe();
140
- }
141
- });
127
+ onUnmounted(() => currentObserver?.dispose());
142
128
  }
143
129
  return {
144
130
  state: computed(() => state),
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","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 {\n createLiveQueryCollection,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n ChangeMessage,\n Collection,\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\n const inputIsCollection = isCollection(unwrappedParam)\n\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 = (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 return isSingleResultCollection(currentCollection)\n ? internalData[0]\n : 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":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n computed,\n getCurrentInstance,\n onUnmounted,\n reactive,\n ref,\n toValue,\n watchEffect,\n} from 'vue'\nimport {\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport type {\n ChangeMessage,\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'\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\n const inputIsCollection = isCollection(unwrappedParam)\n\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 = (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 return isSingleResultCollection(currentCollection)\n ? internalData[0]\n : 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 // The shared observer owns subscription, the ready-race, and status; Vue\n // materializes into its own reactive map (granular) + ordered array.\n let currentObserver: LiveQueryObserver<any, any> | null = null\n\n const syncFromObserver = (\n observer: LiveQueryObserver<any, any>,\n currentCollection: Collection<any, any, any>,\n ) => {\n status.value = observer.getSnapshot().status as CollectionStatus\n syncDataFromCollection(currentCollection)\n }\n\n // Watch for collection changes and subscribe to updates\n watchEffect((onInvalidate) => {\n const currentCollection = collection.value\n\n // Tear down any previous observer.\n currentObserver?.dispose()\n currentObserver = null\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n status.value = `disabled` as const\n state.clear()\n internalData.length = 0\n return\n }\n\n const observer = createLiveQueryObserver(currentCollection)\n currentObserver = observer\n\n // Initial rows arrive as the observer's first delta (includeInitialState);\n // apply them and every subsequent delta granularly to the reactive map.\n state.clear()\n\n const unsubscribe = observer.subscribe(\n (changes: Array<ChangeMessage<any>> | undefined) => {\n if (changes) {\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 } else {\n // Cleanup and other status-only publications carry no row deltas.\n // Rebuild the keyed view so it cannot diverge from ordered data.\n state.clear()\n for (const [key, value] of observer.getSnapshot().state ?? []) {\n state.set(key, value)\n }\n }\n syncFromObserver(observer, currentCollection)\n },\n )\n syncFromObserver(observer, currentCollection)\n\n // Cleanup when effect is invalidated\n onInvalidate(() => {\n unsubscribe()\n observer.dispose()\n currentObserver = null\n })\n })\n\n // Cleanup on unmount (only if we're in a component context)\n const instance = getCurrentInstance()\n if (instance) {\n onUnmounted(() => currentObserver?.dispose())\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":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/vue-db",
3
- "version": "0.0.127",
3
+ "version": "0.0.129",
4
4
  "description": "Vue integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "skills"
41
41
  ],
42
42
  "dependencies": {
43
- "@tanstack/db": "0.6.16"
43
+ "@tanstack/db": "0.7.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "vue": ">=3.3.0"
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  computed,
3
3
  getCurrentInstance,
4
- nextTick,
5
4
  onUnmounted,
6
5
  reactive,
7
6
  ref,
@@ -10,6 +9,7 @@ import {
10
9
  } from 'vue'
11
10
  import {
12
11
  createLiveQueryCollection,
12
+ createLiveQueryObserver,
13
13
  isCollection,
14
14
  isSingleResultCollection,
15
15
  } from '@tanstack/db'
@@ -22,6 +22,7 @@ import type {
22
22
  InferResultType,
23
23
  InitialQueryBuilder,
24
24
  LiveQueryCollectionConfig,
25
+ LiveQueryObserver,
25
26
  NonSingleResult,
26
27
  QueryBuilder,
27
28
  SingleResult,
@@ -363,101 +364,80 @@ export function useLiveQuery(
363
364
  internalData.push(...Array.from(currentCollection.values()))
364
365
  }
365
366
 
366
- // Track current unsubscribe function
367
- let currentUnsubscribe: (() => void) | null = null
367
+ // The shared observer owns subscription, the ready-race, and status; Vue
368
+ // materializes into its own reactive map (granular) + ordered array.
369
+ let currentObserver: LiveQueryObserver<any, any> | null = null
370
+
371
+ const syncFromObserver = (
372
+ observer: LiveQueryObserver<any, any>,
373
+ currentCollection: Collection<any, any, any>,
374
+ ) => {
375
+ status.value = observer.getSnapshot().status as CollectionStatus
376
+ syncDataFromCollection(currentCollection)
377
+ }
368
378
 
369
379
  // Watch for collection changes and subscribe to updates
370
380
  watchEffect((onInvalidate) => {
371
381
  const currentCollection = collection.value
372
382
 
383
+ // Tear down any previous observer.
384
+ currentObserver?.dispose()
385
+ currentObserver = null
386
+
373
387
  // Handle null collection (disabled query)
374
388
  if (!currentCollection) {
375
389
  status.value = `disabled` as const
376
390
  state.clear()
377
391
  internalData.length = 0
378
- if (currentUnsubscribe) {
379
- currentUnsubscribe()
380
- currentUnsubscribe = null
381
- }
382
392
  return
383
393
  }
384
394
 
385
- // Update status ref whenever the effect runs
386
- status.value = currentCollection.status
387
-
388
- // Clean up previous subscription
389
- if (currentUnsubscribe) {
390
- currentUnsubscribe()
391
- }
395
+ const observer = createLiveQueryObserver(currentCollection)
396
+ currentObserver = observer
392
397
 
393
- // Initialize state with current collection data
398
+ // Initial rows arrive as the observer's first delta (includeInitialState);
399
+ // apply them and every subsequent delta granularly to the reactive map.
394
400
  state.clear()
395
- for (const [key, value] of currentCollection.entries()) {
396
- state.set(key, value)
397
- }
398
-
399
- // Initialize data array in correct order
400
- syncDataFromCollection(currentCollection)
401
401
 
402
- // Listen for the first ready event to catch status transitions
403
- // that might not trigger change events (fixes async status transition bug)
404
- currentCollection.onFirstReady(() => {
405
- // Use nextTick to ensure Vue reactivity updates properly
406
- nextTick(() => {
407
- status.value = currentCollection.status
408
- })
409
- })
410
-
411
- // Subscribe to collection changes with granular updates
412
- const subscription = currentCollection.subscribeChanges(
413
- (changes: Array<ChangeMessage<any>>) => {
414
- // Apply each change individually to the reactive state
415
- for (const change of changes) {
416
- switch (change.type) {
417
- case `insert`:
418
- case `update`:
419
- state.set(change.key, change.value)
420
- break
421
- case `delete`:
422
- state.delete(change.key)
423
- break
402
+ const unsubscribe = observer.subscribe(
403
+ (changes: Array<ChangeMessage<any>> | undefined) => {
404
+ if (changes) {
405
+ for (const change of changes) {
406
+ switch (change.type) {
407
+ case `insert`:
408
+ case `update`:
409
+ state.set(change.key, change.value)
410
+ break
411
+ case `delete`:
412
+ state.delete(change.key)
413
+ break
414
+ }
415
+ }
416
+ } else {
417
+ // Cleanup and other status-only publications carry no row deltas.
418
+ // Rebuild the keyed view so it cannot diverge from ordered data.
419
+ state.clear()
420
+ for (const [key, value] of observer.getSnapshot().state ?? []) {
421
+ state.set(key, value)
424
422
  }
425
423
  }
426
-
427
- // Update the data array to maintain sorted order
428
- syncDataFromCollection(currentCollection)
429
- // Update status ref on every change
430
- status.value = currentCollection.status
431
- },
432
- {
433
- includeInitialState: true,
424
+ syncFromObserver(observer, currentCollection)
434
425
  },
435
426
  )
436
-
437
- currentUnsubscribe = subscription.unsubscribe.bind(subscription)
438
-
439
- // Preload collection data if not already started
440
- if (currentCollection.status === `idle`) {
441
- currentCollection.preload().catch(console.error)
442
- }
427
+ syncFromObserver(observer, currentCollection)
443
428
 
444
429
  // Cleanup when effect is invalidated
445
430
  onInvalidate(() => {
446
- if (currentUnsubscribe) {
447
- currentUnsubscribe()
448
- currentUnsubscribe = null
449
- }
431
+ unsubscribe()
432
+ observer.dispose()
433
+ currentObserver = null
450
434
  })
451
435
  })
452
436
 
453
437
  // Cleanup on unmount (only if we're in a component context)
454
438
  const instance = getCurrentInstance()
455
439
  if (instance) {
456
- onUnmounted(() => {
457
- if (currentUnsubscribe) {
458
- currentUnsubscribe()
459
- }
460
- })
440
+ onUnmounted(() => currentObserver?.dispose())
461
441
  }
462
442
 
463
443
  return {