@tanstack/vue-db 0.0.85 → 0.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/useLiveQuery.cjs +42 -12
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +1 -0
- package/dist/esm/useLiveQuery.d.ts +1 -0
- package/dist/esm/useLiveQuery.js +42 -12
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveQuery.ts +62 -14
|
@@ -5,13 +5,15 @@ const db = require("@tanstack/db");
|
|
|
5
5
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
6
6
|
const collection = vue.computed(() => {
|
|
7
7
|
let unwrappedParam = configOrQueryOrCollection;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
if (typeof configOrQueryOrCollection !== `function`) {
|
|
9
|
+
try {
|
|
10
|
+
const potentiallyUnwrapped = vue.toValue(configOrQueryOrCollection);
|
|
11
|
+
if (potentiallyUnwrapped !== configOrQueryOrCollection) {
|
|
12
|
+
unwrappedParam = potentiallyUnwrapped;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
unwrappedParam = configOrQueryOrCollection;
|
|
12
16
|
}
|
|
13
|
-
} catch {
|
|
14
|
-
unwrappedParam = configOrQueryOrCollection;
|
|
15
17
|
}
|
|
16
18
|
const isCollection = unwrappedParam && typeof unwrappedParam === `object` && typeof unwrappedParam.subscribeChanges === `function` && typeof unwrappedParam.startSyncImmediate === `function` && typeof unwrappedParam.id === `string`;
|
|
17
19
|
if (isCollection) {
|
|
@@ -22,10 +24,24 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
22
24
|
}
|
|
23
25
|
deps.forEach((dep) => vue.toValue(dep));
|
|
24
26
|
if (typeof unwrappedParam === `function`) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const wrappedQuery = (q) => {
|
|
28
|
+
const result = unwrappedParam(q);
|
|
29
|
+
if (result === void 0 || result === null) {
|
|
30
|
+
throw new Error(`__DISABLED_QUERY__`);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
return db.createLiveQueryCollection({
|
|
36
|
+
query: wrappedQuery,
|
|
37
|
+
startSync: true
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof Error && error.message === `__DISABLED_QUERY__`) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
29
45
|
} else {
|
|
30
46
|
return db.createLiveQueryCollection({
|
|
31
47
|
...unwrappedParam,
|
|
@@ -36,7 +52,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
36
52
|
const state = vue.reactive(/* @__PURE__ */ new Map());
|
|
37
53
|
const internalData = vue.reactive([]);
|
|
38
54
|
const data = vue.computed(() => internalData);
|
|
39
|
-
const status = vue.ref(
|
|
55
|
+
const status = vue.ref(
|
|
56
|
+
collection.value ? collection.value.status : `disabled`
|
|
57
|
+
);
|
|
40
58
|
const syncDataFromCollection = (currentCollection) => {
|
|
41
59
|
internalData.length = 0;
|
|
42
60
|
internalData.push(...Array.from(currentCollection.values()));
|
|
@@ -44,6 +62,16 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
44
62
|
let currentUnsubscribe = null;
|
|
45
63
|
vue.watchEffect((onInvalidate) => {
|
|
46
64
|
const currentCollection = collection.value;
|
|
65
|
+
if (!currentCollection) {
|
|
66
|
+
status.value = `disabled`;
|
|
67
|
+
state.clear();
|
|
68
|
+
internalData.length = 0;
|
|
69
|
+
if (currentUnsubscribe) {
|
|
70
|
+
currentUnsubscribe();
|
|
71
|
+
currentUnsubscribe = null;
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
47
75
|
status.value = currentCollection.status;
|
|
48
76
|
if (currentUnsubscribe) {
|
|
49
77
|
currentUnsubscribe();
|
|
@@ -103,7 +131,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
103
131
|
collection: vue.computed(() => collection.value),
|
|
104
132
|
status: vue.computed(() => status.value),
|
|
105
133
|
isLoading: vue.computed(() => status.value === `loading`),
|
|
106
|
-
isReady: vue.computed(
|
|
134
|
+
isReady: vue.computed(
|
|
135
|
+
() => status.value === `ready` || status.value === `disabled`
|
|
136
|
+
),
|
|
107
137
|
isIdle: vue.computed(() => status.value === `idle`),
|
|
108
138
|
isError: vue.computed(() => status.value === `error`),
|
|
109
139
|
isCleanedUp: vue.computed(() => status.value === `cleaned-up`)
|
|
@@ -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 { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\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\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<T extends object> {\n state: ComputedRef<Map<string | number, T>>\n data: ComputedRef<Array<T>>\n collection: ComputedRef<Collection<T, 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\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 just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<MaybeRefOrGetter<unknown>>\n): UseLiveQueryReturn<GetResult<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<GetResult<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)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: MaybeRefOrGetter<Collection<TResult, TKey, TUtils>>\n): UseLiveQueryReturnWithCollection<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 let unwrappedParam = configOrQueryOrCollection\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 // 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 // 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 return createLiveQueryCollection({\n query: unwrappedParam,\n startSync: true,\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 const data = computed(() => internalData)\n\n // Track collection status reactively\n const status = ref(collection.value.status)\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 // 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(() => status.value === `ready`),\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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"}
|
|
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 { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\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\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<T extends object> {\n state: ComputedRef<Map<string | number, T>>\n data: ComputedRef<Array<T>>\n collection: ComputedRef<Collection<T, 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\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<GetResult<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<GetResult<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<GetResult<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)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: MaybeRefOrGetter<Collection<TResult, TKey, TUtils>>\n): UseLiveQueryReturnWithCollection<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 // 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 const data = computed(() => internalData)\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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"}
|
|
@@ -84,6 +84,7 @@ export interface UseLiveQueryReturnWithCollection<T extends object, TKey extends
|
|
|
84
84
|
* // </ul>
|
|
85
85
|
*/
|
|
86
86
|
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<MaybeRefOrGetter<unknown>>): UseLiveQueryReturn<GetResult<TContext>>;
|
|
87
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<MaybeRefOrGetter<unknown>>): UseLiveQueryReturn<GetResult<TContext>>;
|
|
87
88
|
/**
|
|
88
89
|
* Create a live query using configuration object
|
|
89
90
|
* @param config - Configuration object with query and options
|
|
@@ -84,6 +84,7 @@ export interface UseLiveQueryReturnWithCollection<T extends object, TKey extends
|
|
|
84
84
|
* // </ul>
|
|
85
85
|
*/
|
|
86
86
|
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<MaybeRefOrGetter<unknown>>): UseLiveQueryReturn<GetResult<TContext>>;
|
|
87
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<MaybeRefOrGetter<unknown>>): UseLiveQueryReturn<GetResult<TContext>>;
|
|
87
88
|
/**
|
|
88
89
|
* Create a live query using configuration object
|
|
89
90
|
* @param config - Configuration object with query and options
|
package/dist/esm/useLiveQuery.js
CHANGED
|
@@ -3,13 +3,15 @@ import { createLiveQueryCollection } from "@tanstack/db";
|
|
|
3
3
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
4
4
|
const collection = computed(() => {
|
|
5
5
|
let unwrappedParam = configOrQueryOrCollection;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (typeof configOrQueryOrCollection !== `function`) {
|
|
7
|
+
try {
|
|
8
|
+
const potentiallyUnwrapped = toValue(configOrQueryOrCollection);
|
|
9
|
+
if (potentiallyUnwrapped !== configOrQueryOrCollection) {
|
|
10
|
+
unwrappedParam = potentiallyUnwrapped;
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
unwrappedParam = configOrQueryOrCollection;
|
|
10
14
|
}
|
|
11
|
-
} catch {
|
|
12
|
-
unwrappedParam = configOrQueryOrCollection;
|
|
13
15
|
}
|
|
14
16
|
const isCollection = unwrappedParam && typeof unwrappedParam === `object` && typeof unwrappedParam.subscribeChanges === `function` && typeof unwrappedParam.startSyncImmediate === `function` && typeof unwrappedParam.id === `string`;
|
|
15
17
|
if (isCollection) {
|
|
@@ -20,10 +22,24 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
20
22
|
}
|
|
21
23
|
deps.forEach((dep) => toValue(dep));
|
|
22
24
|
if (typeof unwrappedParam === `function`) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const wrappedQuery = (q) => {
|
|
26
|
+
const result = unwrappedParam(q);
|
|
27
|
+
if (result === void 0 || result === null) {
|
|
28
|
+
throw new Error(`__DISABLED_QUERY__`);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
return createLiveQueryCollection({
|
|
34
|
+
query: wrappedQuery,
|
|
35
|
+
startSync: true
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof Error && error.message === `__DISABLED_QUERY__`) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
27
43
|
} else {
|
|
28
44
|
return createLiveQueryCollection({
|
|
29
45
|
...unwrappedParam,
|
|
@@ -34,7 +50,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
34
50
|
const state = reactive(/* @__PURE__ */ new Map());
|
|
35
51
|
const internalData = reactive([]);
|
|
36
52
|
const data = computed(() => internalData);
|
|
37
|
-
const status = ref(
|
|
53
|
+
const status = ref(
|
|
54
|
+
collection.value ? collection.value.status : `disabled`
|
|
55
|
+
);
|
|
38
56
|
const syncDataFromCollection = (currentCollection) => {
|
|
39
57
|
internalData.length = 0;
|
|
40
58
|
internalData.push(...Array.from(currentCollection.values()));
|
|
@@ -42,6 +60,16 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
42
60
|
let currentUnsubscribe = null;
|
|
43
61
|
watchEffect((onInvalidate) => {
|
|
44
62
|
const currentCollection = collection.value;
|
|
63
|
+
if (!currentCollection) {
|
|
64
|
+
status.value = `disabled`;
|
|
65
|
+
state.clear();
|
|
66
|
+
internalData.length = 0;
|
|
67
|
+
if (currentUnsubscribe) {
|
|
68
|
+
currentUnsubscribe();
|
|
69
|
+
currentUnsubscribe = null;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
45
73
|
status.value = currentCollection.status;
|
|
46
74
|
if (currentUnsubscribe) {
|
|
47
75
|
currentUnsubscribe();
|
|
@@ -101,7 +129,9 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
101
129
|
collection: computed(() => collection.value),
|
|
102
130
|
status: computed(() => status.value),
|
|
103
131
|
isLoading: computed(() => status.value === `loading`),
|
|
104
|
-
isReady: computed(
|
|
132
|
+
isReady: computed(
|
|
133
|
+
() => status.value === `ready` || status.value === `disabled`
|
|
134
|
+
),
|
|
105
135
|
isIdle: computed(() => status.value === `idle`),
|
|
106
136
|
isError: computed(() => status.value === `error`),
|
|
107
137
|
isCleanedUp: computed(() => status.value === `cleaned-up`)
|
|
@@ -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 { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\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\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<T extends object> {\n state: ComputedRef<Map<string | number, T>>\n data: ComputedRef<Array<T>>\n collection: ComputedRef<Collection<T, 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\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 just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<MaybeRefOrGetter<unknown>>\n): UseLiveQueryReturn<GetResult<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<GetResult<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)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: MaybeRefOrGetter<Collection<TResult, TKey, TUtils>>\n): UseLiveQueryReturnWithCollection<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 let unwrappedParam = configOrQueryOrCollection\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 // 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 // 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 return createLiveQueryCollection({\n query: unwrappedParam,\n startSync: true,\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 const data = computed(() => internalData)\n\n // Track collection status reactively\n const status = ref(collection.value.status)\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 // 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(() => status.value === `ready`),\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 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 CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\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\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<T extends object> {\n state: ComputedRef<Map<string | number, T>>\n data: ComputedRef<Array<T>>\n collection: ComputedRef<Collection<T, 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\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<GetResult<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<GetResult<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<GetResult<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)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: MaybeRefOrGetter<Collection<TResult, TKey, TUtils>>\n): UseLiveQueryReturnWithCollection<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 // 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 const data = computed(() => internalData)\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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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/vue-db",
|
|
3
3
|
"description": "Vue integration for @tanstack/db",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.87",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"typescript"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@tanstack/db": "0.5.
|
|
19
|
+
"@tanstack/db": "0.5.11"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@electric-sql/client": "1.2.0",
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -110,12 +110,20 @@ export interface UseLiveQueryReturnWithCollection<
|
|
|
110
110
|
* // <li v-for="todo in data" :key="todo.id">{{ todo.text }}</li>
|
|
111
111
|
* // </ul>
|
|
112
112
|
*/
|
|
113
|
-
// Overload 1: Accept
|
|
113
|
+
// Overload 1: Accept query function that always returns QueryBuilder
|
|
114
114
|
export function useLiveQuery<TContext extends Context>(
|
|
115
115
|
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
116
116
|
deps?: Array<MaybeRefOrGetter<unknown>>
|
|
117
117
|
): UseLiveQueryReturn<GetResult<TContext>>
|
|
118
118
|
|
|
119
|
+
// Overload 1b: Accept query function that can return undefined/null
|
|
120
|
+
export function useLiveQuery<TContext extends Context>(
|
|
121
|
+
queryFn: (
|
|
122
|
+
q: InitialQueryBuilder
|
|
123
|
+
) => QueryBuilder<TContext> | undefined | null,
|
|
124
|
+
deps?: Array<MaybeRefOrGetter<unknown>>
|
|
125
|
+
): UseLiveQueryReturn<GetResult<TContext>>
|
|
126
|
+
|
|
119
127
|
/**
|
|
120
128
|
* Create a live query using configuration object
|
|
121
129
|
* @param config - Configuration object with query and options
|
|
@@ -210,15 +218,18 @@ export function useLiveQuery(
|
|
|
210
218
|
const collection = computed(() => {
|
|
211
219
|
// First check if the original parameter might be a ref/getter
|
|
212
220
|
// by seeing if toValue returns something different than the original
|
|
221
|
+
// NOTE: Don't call toValue on functions - toValue treats functions as getters and calls them!
|
|
213
222
|
let unwrappedParam = configOrQueryOrCollection
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
if (typeof configOrQueryOrCollection !== `function`) {
|
|
224
|
+
try {
|
|
225
|
+
const potentiallyUnwrapped = toValue(configOrQueryOrCollection)
|
|
226
|
+
if (potentiallyUnwrapped !== configOrQueryOrCollection) {
|
|
227
|
+
unwrappedParam = potentiallyUnwrapped
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// If toValue fails, use original parameter
|
|
231
|
+
unwrappedParam = configOrQueryOrCollection
|
|
218
232
|
}
|
|
219
|
-
} catch {
|
|
220
|
-
// If toValue fails, use original parameter
|
|
221
|
-
unwrappedParam = configOrQueryOrCollection
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
// Check if it's already a collection by checking for specific collection methods
|
|
@@ -243,10 +254,31 @@ export function useLiveQuery(
|
|
|
243
254
|
|
|
244
255
|
// Ensure we always start sync for Vue hooks
|
|
245
256
|
if (typeof unwrappedParam === `function`) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
// To avoid calling the query function twice, we wrap it to handle null/undefined returns
|
|
258
|
+
// The wrapper will be called once by createLiveQueryCollection
|
|
259
|
+
const wrappedQuery = (q: InitialQueryBuilder) => {
|
|
260
|
+
const result = unwrappedParam(q)
|
|
261
|
+
// If the query function returns null/undefined, throw a special error
|
|
262
|
+
// that we'll catch to return null collection
|
|
263
|
+
if (result === undefined || result === null) {
|
|
264
|
+
throw new Error(`__DISABLED_QUERY__`)
|
|
265
|
+
}
|
|
266
|
+
return result
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
return createLiveQueryCollection({
|
|
271
|
+
query: wrappedQuery,
|
|
272
|
+
startSync: true,
|
|
273
|
+
})
|
|
274
|
+
} catch (error) {
|
|
275
|
+
// Check if this is our special disabled query marker
|
|
276
|
+
if (error instanceof Error && error.message === `__DISABLED_QUERY__`) {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
// Re-throw other errors
|
|
280
|
+
throw error
|
|
281
|
+
}
|
|
250
282
|
} else {
|
|
251
283
|
return createLiveQueryCollection({
|
|
252
284
|
...unwrappedParam,
|
|
@@ -265,7 +297,9 @@ export function useLiveQuery(
|
|
|
265
297
|
const data = computed(() => internalData)
|
|
266
298
|
|
|
267
299
|
// Track collection status reactively
|
|
268
|
-
const status = ref(
|
|
300
|
+
const status = ref(
|
|
301
|
+
collection.value ? collection.value.status : (`disabled` as const)
|
|
302
|
+
)
|
|
269
303
|
|
|
270
304
|
// Helper to sync data array from collection in correct order
|
|
271
305
|
const syncDataFromCollection = (
|
|
@@ -282,6 +316,18 @@ export function useLiveQuery(
|
|
|
282
316
|
watchEffect((onInvalidate) => {
|
|
283
317
|
const currentCollection = collection.value
|
|
284
318
|
|
|
319
|
+
// Handle null collection (disabled query)
|
|
320
|
+
if (!currentCollection) {
|
|
321
|
+
status.value = `disabled` as const
|
|
322
|
+
state.clear()
|
|
323
|
+
internalData.length = 0
|
|
324
|
+
if (currentUnsubscribe) {
|
|
325
|
+
currentUnsubscribe()
|
|
326
|
+
currentUnsubscribe = null
|
|
327
|
+
}
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
285
331
|
// Update status ref whenever the effect runs
|
|
286
332
|
status.value = currentCollection.status
|
|
287
333
|
|
|
@@ -366,7 +412,9 @@ export function useLiveQuery(
|
|
|
366
412
|
collection: computed(() => collection.value),
|
|
367
413
|
status: computed(() => status.value),
|
|
368
414
|
isLoading: computed(() => status.value === `loading`),
|
|
369
|
-
isReady: computed(
|
|
415
|
+
isReady: computed(
|
|
416
|
+
() => status.value === `ready` || status.value === `disabled`
|
|
417
|
+
),
|
|
370
418
|
isIdle: computed(() => status.value === `idle`),
|
|
371
419
|
isError: computed(() => status.value === `error`),
|
|
372
420
|
isCleanedUp: computed(() => status.value === `cleaned-up`),
|