import * as E from "fp-ts/Either" import { pipe } from "fp-ts/function" import { reactive, ref, Ref, unref, isRef, watchEffect, WatchStopHandle, watchSyncEffect, watch, nextTick, } from "vue" import { client, GQLError, parseGQLErrorString, } from "@helpers/backend/GQLClient" import { AnyVariables, createRequest, GraphQLRequest, OperationResult, TypedDocumentNode, } from "@urql/core" import { Source, pipe as wonkaPipe, onEnd, subscribe } from "wonka" type MaybeRef = X | Ref type UseQueryOptions = { query: TypedDocumentNode variables?: MaybeRef updateSubs?: MaybeRef[]> defer?: boolean pollDuration?: number | undefined } export const useGQLQuery = < DocType, DocVarType extends AnyVariables, DocErrorType extends string, >( _args: UseQueryOptions ) => { const stops: WatchStopHandle[] = [] const args = reactive(_args) const loading: Ref = ref(true) const isStale: Ref = ref(true) const data: Ref, DocType>> = ref() as any if (!args.updateSubs) args.updateSubs = [] const isPaused: Ref = ref(args.defer ?? false) const pollDuration: Ref = ref(args.pollDuration ?? null) const request: Ref> = ref( createRequest( args.query, unref(args.variables as any) as any ) ) as any const source: Ref | undefined> = ref() // Toggles between true and false to cause the polling operation to tick const pollerTick: Ref = ref(true) stops.push( watchEffect((onInvalidate) => { if (pollDuration.value !== null && !isPaused.value) { const handle = setInterval(() => { pollerTick.value = !pollerTick.value }, pollDuration.value) onInvalidate(() => { clearInterval(handle) }) } }) ) stops.push( watchEffect( () => { const newRequest = createRequest( args.query, unref(args.variables as any) as any ) if (request.value.key !== newRequest.key) { request.value = newRequest } }, { flush: "pre" } ) ) const rerunQuery = () => { source.value = !isPaused.value ? client.value?.executeQuery(request.value, { requestPolicy: "network-only", }) : undefined } stops.push( watch( pollerTick, () => { rerunQuery() }, { flush: "pre", } ) ) watchSyncEffect((onInvalidate) => { if (source.value) { loading.value = true isStale.value = false const invalidateStops = args.updateSubs!.map((sub) => { return wonkaPipe( client.value!.executeSubscription(sub), onEnd(() => { if (source.value) execute() }), subscribe(() => { return execute() }) ).unsubscribe }) invalidateStops.push( wonkaPipe( source.value, onEnd(() => { loading.value = false isStale.value = false }), subscribe((res) => { if (res.operation.key === request.value.key) { data.value = pipe( // The target res.data as DocType | undefined, // Define what happens if data does not exist (it is an error) E.fromNullable( pipe( // Take the network error value res.error?.networkError, // If it null, set the left to the generic error name E.fromNullable(res.error?.message), E.match( // The left case (network error was null) (gqlErr) => >{ type: "gql_error", error: parseGQLErrorString( gqlErr ?? "" ) as DocErrorType, }, // The right case (it was a GraphQL Error) (networkErr) => >{ type: "network_error", error: networkErr, } ) ) ) ) loading.value = false } }) ).unsubscribe ) onInvalidate(() => invalidateStops.forEach((unsub) => unsub())) } }) const execute = (updatedVars?: DocVarType) => { if (updatedVars) { if (isRef(args.variables)) { args.variables.value = updatedVars } else { args.variables = updatedVars } nextTick(rerunQuery) } else { rerunQuery() } isPaused.value = false } const pause = () => { isPaused.value = true } const unpause = () => { isPaused.value = false } const response = reactive({ loading, data, pause, unpause, isStale, execute, }) return response }