import { ref, watch, onUnmounted, Ref, isRef, computed } from '@vue/composition-api' import Vue from 'vue' import { DocumentNode } from 'graphql' import { OperationVariables, WatchQueryOptions, ObservableQuery, ApolloQueryResult, SubscribeToMoreOptions } from 'apollo-client' import { Subscription } from 'apollo-client/util/Observable' import { useApolloClient } from './useApolloClient' import { ReactiveFunction } from './util/ReactiveFunction' import { paramToRef } from './util/paramToRef' import { paramToReactive } from './util/paramToReactive' // import { trackQuery } from './util/loadingTracking' export interface UseQueryOptions< TResult = any, TVariables = OperationVariables > extends Omit, 'query' | 'variables'> { clientId?: string enabled?: boolean } interface SubscribeToMoreItem { options: any unsubscribeFns: Function[] } export function useQuery< TResult = any, TVariables = OperationVariables, TCacheShape = any > ( document: DocumentNode | Ref | ReactiveFunction, variables: TVariables | Ref | ReactiveFunction = null, options: UseQueryOptions | Ref> | ReactiveFunction> = {}, ) { if (variables == null) variables = ref() if (options == null) options = {} const documentRef = paramToRef(document) const variablesRef = paramToRef(variables) const optionsRef = paramToReactive(options) // Result /** * Result from the query */ const result = ref() const resultEvent = useEventHook>() const error = ref(null) const errorEvent = useEventHook() // Loading /** * Indicates if a network request is pending */ const loading = ref(false) // trackQuery(loading) const networkStatus = ref() // Apollo Client const { resolveClient } = useApolloClient() // Query const query: Ref> = ref() let observer: Subscription let started = false /** * Starts watching the query */ function start () { if (started || !isEnabled.value) return started = true loading.value = true const client = resolveClient(currentOptions.value.clientId) query.value = client.watchQuery({ query: currentDocument, variables: currentVariables, ...currentOptions.value, }) observer = query.value.subscribe({ next: onNextResult, error: onError, }) for (const item of subscribeToMoreItems) { addSubscribeToMore(item) } } function onNextResult (queryResult: ApolloQueryResult) { result.value = queryResult.data loading.value = queryResult.loading networkStatus.value = queryResult.networkStatus resultEvent.trigger(queryResult) } function onError (queryError: any) { error.value = queryError loading.value = false networkStatus.value = 8 errorEvent.trigger(queryError) } let onStopHandlers: (() => void)[] = [] /** * Stop watching the query */ function stop () { if (!started) return started = false loading.value = false onStopHandlers.forEach(handler => handler()) onStopHandlers = [] if (query.value) { query.value.stopPolling() query.value = null } if (observer) { observer.unsubscribe() observer = null } } // Restart let restarting = false /** * Queue a restart of the query (on next tick) if it is already active */ function restart () { if (!started || restarting) return restarting = true Vue.nextTick(() => { if (started) { stop() start() } restarting = false }) } // Applying document let currentDocument: DocumentNode watch(documentRef, value => { currentDocument = value restart() }) // Applying variables let currentVariables: TVariables watch(variablesRef, value => { currentVariables = value restart() }, { deep: true, }) // Applying options const currentOptions = ref>() watch(() => isRef(optionsRef) ? optionsRef.value : optionsRef, value => { currentOptions.value = value restart() }, { deep: true, }) // Fefetch function refetch (variables: TVariables = null) { if (query.value) { if (variables) { currentVariables = variables } return query.value.refetch(variables) } } // Subscribe to more const subscribeToMoreItems: SubscribeToMoreItem[] = [] function subscribeToMore< TSubscriptionVariables = OperationVariables, TSubscriptionData = TResult > ( options: SubscribeToMoreOptions | Ref> | ReactiveFunction> ) { const optionsRef = paramToRef(options) watch(optionsRef, (value, oldValue, onCleanup) => { const index = subscribeToMoreItems.findIndex(item => item.options === oldValue) if (index !== -1) { subscribeToMoreItems.splice(index, 1) } const item: SubscribeToMoreItem = { options: value, unsubscribeFns: [], } subscribeToMoreItems.push(item) addSubscribeToMore(item) onCleanup(() => { item.unsubscribeFns.forEach(fn => fn()) item.unsubscribeFns = [] }) }) } function addSubscribeToMore (item: SubscribeToMoreItem) { if (!started) return const unsubscribe = query.value.subscribeToMore(item.options) onStopHandlers.push(unsubscribe) item.unsubscribeFns.push(unsubscribe) } // Internal enabled returned to user // @TODO Doesn't fully work yet, need to initialize with option const enabled = ref() const enabledOption = computed(() => !currentOptions.value || currentOptions.value.enabled == null || currentOptions.value.enabled) const isEnabled = computed(() => !!((typeof enabled.value === 'boolean' && enabled.value) && enabledOption.value)) watch(enabled, value => { if (value == null) { enabled.value = enabledOption.value } }) // Auto start & stop watch(isEnabled, value => { if (value) { start() } else { stop() } }) // Teardown onUnmounted(() => { stop() subscribeToMoreItems.length = 0 }) return { result, loading, networkStatus, error, // @TODO doesn't fully work yet // enabled, start, stop, restart, document: documentRef, variables: variablesRef, options: optionsRef, query, refetch, subscribeToMore, onResult: resultEvent.on, onError: errorEvent.on, } }