Skip to content

refactor: vue #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 100 additions & 123 deletions packages/vue-db/src/useLiveQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
computed,
getCurrentInstance,
onUnmounted,
reactive,
getCurrentScope,
onScopeDispose,
ref,
shallowRef,
toValue,
watchEffect,
} from "vue"
import { createLiveQueryCollection } from "@tanstack/db"
import { CollectionImpl, createLiveQueryCollection } from "@tanstack/db"
import type {
ChangeMessage,
Collection,
Expand All @@ -18,7 +18,9 @@ import type {
LiveQueryCollectionConfig,
QueryBuilder,
} from "@tanstack/db"
import type { ComputedRef, MaybeRefOrGetter } from "vue"
import type { MaybeRefOrGetter } from "vue"

const NOOP = () => {}

/**
* Return type for useLiveQuery hook
Expand All @@ -33,31 +35,31 @@ import type { ComputedRef, MaybeRefOrGetter } from "vue"
* @property isCleanedUp - True when query has been cleaned up
*/
export interface UseLiveQueryReturn<T extends object> {
state: ComputedRef<Map<string | number, T>>
data: ComputedRef<Array<T>>
collection: ComputedRef<Collection<T, string | number, {}>>
status: ComputedRef<CollectionStatus>
isLoading: ComputedRef<boolean>
isReady: ComputedRef<boolean>
isIdle: ComputedRef<boolean>
isError: ComputedRef<boolean>
isCleanedUp: ComputedRef<boolean>
state: Map<string | number, T>
data: Array<T>
collection: Collection<T, string | number, {}>
status: CollectionStatus
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
}

export interface UseLiveQueryReturnWithCollection<
T extends object,
TKey extends string | number,
TUtils extends Record<string, any>,
> {
state: ComputedRef<Map<TKey, T>>
data: ComputedRef<Array<T>>
collection: ComputedRef<Collection<T, TKey, TUtils>>
status: ComputedRef<CollectionStatus>
isLoading: ComputedRef<boolean>
isReady: ComputedRef<boolean>
isIdle: ComputedRef<boolean>
isError: ComputedRef<boolean>
isCleanedUp: ComputedRef<boolean>
state: Map<TKey, T>
data: Array<T>
collection: Collection<T, TKey, TUtils>
status: CollectionStatus
isLoading: boolean
isReady: boolean
isIdle: boolean
isError: boolean
isCleanedUp: boolean
}

/**
Expand Down Expand Up @@ -111,8 +113,7 @@ export interface UseLiveQueryReturnWithCollection<
*/
// Overload 1: Accept just the query function
export function useLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
deps?: Array<MaybeRefOrGetter<unknown>>
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
): UseLiveQueryReturn<GetResult<TContext>>

/**
Expand Down Expand Up @@ -149,8 +150,7 @@ export function useLiveQuery<TContext extends Context>(
*/
// Overload 2: Accept config object
export function useLiveQuery<TContext extends Context>(
config: LiveQueryCollectionConfig<TContext>,
deps?: Array<MaybeRefOrGetter<unknown>>
config: MaybeRefOrGetter<LiveQueryCollectionConfig<TContext>>
): UseLiveQueryReturn<GetResult<TContext>>

/**
Expand Down Expand Up @@ -203,156 +203,133 @@ export function useLiveQuery<

// Implementation
export function useLiveQuery(
configOrQueryOrCollection: any,
deps: Array<MaybeRefOrGetter<unknown>> = []
configOrQueryOrCollection: any
): UseLiveQueryReturn<any> | UseLiveQueryReturnWithCollection<any, any, any> {
const collection = computed(() => {
// First check if the original parameter might be a ref/getter
// by seeing if toValue returns something different than the original
let unwrappedParam = configOrQueryOrCollection
try {
const potentiallyUnwrapped = toValue(configOrQueryOrCollection)
if (potentiallyUnwrapped !== configOrQueryOrCollection) {
unwrappedParam = potentiallyUnwrapped
}
} catch {
// If toValue fails, use original parameter
unwrappedParam = configOrQueryOrCollection
if (
typeof configOrQueryOrCollection === `function` &&
configOrQueryOrCollection.length === 1
) {
return createLiveQueryCollection({
query: configOrQueryOrCollection,
startSync: true,
})
}

// Check if it's already a collection by checking for specific collection methods
const isCollection =
unwrappedParam &&
typeof unwrappedParam === `object` &&
typeof unwrappedParam.subscribeChanges === `function` &&
typeof unwrappedParam.startSyncImmediate === `function` &&
typeof unwrappedParam.id === `string`
const configOrQueryOrCollectionVal = toValue(configOrQueryOrCollection)

if (isCollection) {
// It's already a collection, ensure sync is started for Vue hooks
unwrappedParam.startSyncImmediate()
return unwrappedParam
if (configOrQueryOrCollectionVal instanceof CollectionImpl) {
configOrQueryOrCollectionVal.startSyncImmediate()
return configOrQueryOrCollectionVal
}

// Reference deps to make computed reactive to them
deps.forEach((dep) => toValue(dep))

// Ensure we always start sync for Vue hooks
if (typeof unwrappedParam === `function`) {
return createLiveQueryCollection({
query: unwrappedParam,
startSync: true,
})
} else {
return createLiveQueryCollection({
...unwrappedParam,
startSync: true,
})
}
return createLiveQueryCollection({
...configOrQueryOrCollectionVal,
startSync: true,
})
})

// Reactive state that gets updated granularly through change events
const state = reactive(new Map<string | number, any>())
const state = ref(new Map<string | number, any>())

// Reactive data array that maintains sorted order
const internalData = reactive<Array<any>>([])

// Computed wrapper for the data to match expected return type
const data = computed(() => internalData)
const internalData = shallowRef<Array<any>>([])

// Track collection status reactively
const status = ref(collection.value.status)
const status = shallowRef(collection.value.status)

// Helper to sync data array from collection in correct order
const syncDataFromCollection = (
currentCollection: Collection<any, any, any>
) => {
internalData.length = 0
internalData.push(...Array.from(currentCollection.values()))
internalData.value = Array.from(currentCollection.values())
}

// Track current unsubscribe function
let currentUnsubscribe: (() => void) | null = null
let cleanup: () => void = NOOP
watchEffect(() => {
cleanup()

// Watch for collection changes and subscribe to updates
watchEffect((onInvalidate) => {
const currentCollection = collection.value
const collectionVal = collection.value

// Update status ref whenever the effect runs
status.value = currentCollection.status

// Clean up previous subscription
if (currentUnsubscribe) {
currentUnsubscribe()
}
status.value = collectionVal.status

// Initialize state with current collection data
state.clear()
for (const [key, value] of currentCollection.entries()) {
state.set(key, value)
}
state.value = new Map(collectionVal.entries())

// Initialize data array in correct order
syncDataFromCollection(currentCollection)
syncDataFromCollection(collectionVal)

// Subscribe to collection changes with granular updates
currentUnsubscribe = currentCollection.subscribeChanges(
cleanup = collectionVal.subscribeChanges(
(changes: Array<ChangeMessage<any>>) => {
// Apply each change individually to the reactive state
for (const change of changes) {
switch (change.type) {
case `insert`:
case `update`:
state.set(change.key, change.value)
state.value.set(change.key, change.value)
break
case `delete`:
state.delete(change.key)
state.value.delete(change.key)
break
}
}

// Update the data array to maintain sorted order
syncDataFromCollection(currentCollection)
syncDataFromCollection(collectionVal)
// Update status ref on every change
status.value = currentCollection.status
status.value = collectionVal.status
}
)

// Preload collection data if not already started
if (currentCollection.status === `idle`) {
currentCollection.preload().catch(console.error)
if (collectionVal.status === `idle`) {
collectionVal.preload().catch(console.error)
}

// Cleanup when effect is invalidated
onInvalidate(() => {
if (currentUnsubscribe) {
currentUnsubscribe()
currentUnsubscribe = null
}
})
})

// Cleanup on unmount (only if we're in a component context)
const instance = getCurrentInstance()
if (instance) {
onUnmounted(() => {
if (currentUnsubscribe) {
currentUnsubscribe()
}
})
// Cleanup
if (getCurrentScope()) {
onScopeDispose(cleanup)
}

const isLoading = computed(
() => status.value === `loading` || status.value === `initialCommit`
)
const isReady = computed(() => status.value === `ready`)
const isIdle = computed(() => status.value === `idle`)
const isError = computed(() => status.value === `error`)
const isCleanedUp = computed(() => status.value === `cleaned-up`)

return {
state: computed(() => state),
data,
collection: computed(() => collection.value),
status: computed(() => status.value),
isLoading: computed(
() => status.value === `loading` || status.value === `initialCommit`
),
isReady: computed(() => status.value === `ready`),
isIdle: computed(() => status.value === `idle`),
isError: computed(() => status.value === `error`),
isCleanedUp: computed(() => status.value === `cleaned-up`),
get state() {
return state.value
},
get data() {
return internalData.value
},
get collection() {
return collection.value
},
get status() {
return status.value
},
get isLoading() {
return isLoading.value
},
get isReady() {
return isReady.value
},
get isIdle() {
return isIdle.value
},
get isError() {
return isError.value
},
get isCleanedUp() {
return isCleanedUp.value
},
}
}
Loading