GQLClient.ts (hoppscotch-2.2.1) | : | GQLClient.ts (hoppscotch-3.0.0) | ||
---|---|---|---|---|
import { | // TODO: fix cache | |||
ref, | import { ref } from "vue" | |||
reactive, | ||||
Ref, | ||||
unref, | ||||
watchEffect, | ||||
watchSyncEffect, | ||||
WatchStopHandle, | ||||
set, | ||||
isRef, | ||||
} from "@nuxtjs/composition-api" | ||||
import { | import { | |||
createClient, | createClient, | |||
TypedDocumentNode, | TypedDocumentNode, | |||
OperationResult, | ||||
dedupExchange, | dedupExchange, | |||
OperationContext, | OperationContext, | |||
fetchExchange, | fetchExchange, | |||
makeOperation, | makeOperation, | |||
GraphQLRequest, | ||||
createRequest, | createRequest, | |||
subscriptionExchange, | subscriptionExchange, | |||
} from "@urql/core" | } from "@urql/core" | |||
import { authExchange } from "@urql/exchange-auth" | import { authExchange } from "@urql/exchange-auth" | |||
import { offlineExchange } from "@urql/exchange-graphcache" | // import { offlineExchange } from "@urql/exchange-graphcache" | |||
import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage" | // import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage | |||
" | ||||
import { devtoolsExchange } from "@urql/devtools" | import { devtoolsExchange } from "@urql/devtools" | |||
import { SubscriptionClient } from "subscriptions-transport-ws" | import { SubscriptionClient } from "subscriptions-transport-ws" | |||
import * as E from "fp-ts/Either" | import * as E from "fp-ts/Either" | |||
import * as TE from "fp-ts/TaskEither" | import * as TE from "fp-ts/TaskEither" | |||
import { pipe, constVoid } from "fp-ts/function" | import { pipe, constVoid, flow } from "fp-ts/function" | |||
import { Source, subscribe, pipe as wonkaPipe, onEnd } from "wonka" | import { subscribe, pipe as wonkaPipe } from "wonka" | |||
import { keyDefs } from "./caching/keys" | import { filter, map, Subject } from "rxjs" | |||
import { optimisticDefs } from "./caching/optimistic" | // import { keyDefs } from "./caching/keys" | |||
import { updatesDef } from "./caching/updates" | // import { optimisticDefs } from "./caching/optimistic" | |||
import { resolversDef } from "./caching/resolvers" | // import { updatesDef } from "./caching/updates" | |||
import schema from "./backend-schema.json" | // import { resolversDef } from "./caching/resolvers" | |||
// import schema from "./backend-schema.json" | ||||
import { | import { | |||
authIdToken$, | authIdToken$, | |||
getAuthIDToken, | getAuthIDToken, | |||
probableUser$, | probableUser$, | |||
waitProbableLoginToConfirm, | waitProbableLoginToConfirm, | |||
} from "~/helpers/fb/auth" | } from "~/helpers/fb/auth" | |||
const BACKEND_GQL_URL = | const BACKEND_GQL_URL = | |||
process.env.context === "production" | import.meta.env.VITE_BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql" | |||
? "https://api.hoppscotch.io/graphql" | const BACKEND_WS_URL = | |||
: "https://api.hoppscotch.io/graphql" | import.meta.env.VITE_BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql" | |||
const storage = makeDefaultStorage({ | // const storage = makeDefaultStorage({ | |||
idbName: "hoppcache-v1", | // idbName: "hoppcache-v1", | |||
maxAge: 7, | // maxAge: 7, | |||
// }) | ||||
const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, { | ||||
reconnect: true, | ||||
connectionParams: () => { | ||||
return { | ||||
authorization: `Bearer ${authIdToken$.value}`, | ||||
} | ||||
}, | ||||
}) | }) | |||
const subscriptionClient = new SubscriptionClient( | ||||
process.env.context === "production" | ||||
? "wss://api.hoppscotch.io/graphql" | ||||
: "wss://api.hoppscotch.io/graphql", | ||||
{ | ||||
reconnect: true, | ||||
connectionParams: () => { | ||||
return { | ||||
authorization: `Bearer ${authIdToken$.value}`, | ||||
} | ||||
}, | ||||
} | ||||
) | ||||
authIdToken$.subscribe(() => { | authIdToken$.subscribe(() => { | |||
subscriptionClient.client?.close() | subscriptionClient.client?.close() | |||
}) | }) | |||
const createHoppClient = () => | const createHoppClient = () => | |||
createClient({ | createClient({ | |||
url: BACKEND_GQL_URL, | url: BACKEND_GQL_URL, | |||
exchanges: [ | exchanges: [ | |||
devtoolsExchange, | devtoolsExchange, | |||
dedupExchange, | dedupExchange, | |||
offlineExchange({ | // offlineExchange({ | |||
schema: schema as any, | // schema: schema as any, | |||
keys: keyDefs, | // keys: keyDefs, | |||
optimistic: optimisticDefs, | // optimistic: optimisticDefs, | |||
updates: updatesDef, | // updates: updatesDef, | |||
resolvers: resolversDef, | // resolvers: resolversDef, | |||
storage, | // storage, | |||
}), | // }), | |||
authExchange({ | authExchange({ | |||
addAuthToOperation({ authState, operation }) { | addAuthToOperation({ authState, operation }) { | |||
if (!authState || !authState.authToken) { | if (!authState || !authState.authToken) { | |||
return operation | return operation | |||
} | } | |||
const fetchOptions = | const fetchOptions = | |||
typeof operation.context.fetchOptions === "function" | typeof operation.context.fetchOptions === "function" | |||
? operation.context.fetchOptions() | ? operation.context.fetchOptions() | |||
: operation.context.fetchOptions || {} | : operation.context.fetchOptions || {} | |||
skipping to change at line 125 | skipping to change at line 110 | |||
await waitProbableLoginToConfirm() | await waitProbableLoginToConfirm() | |||
return { | return { | |||
authToken: getAuthIDToken(), | authToken: getAuthIDToken(), | |||
} | } | |||
}, | }, | |||
}), | }), | |||
fetchExchange, | fetchExchange, | |||
subscriptionExchange({ | subscriptionExchange({ | |||
forwardSubscription: (operation) => | forwardSubscription: (operation) => | |||
// @ts-expect-error: An issue with the Urql typing | ||||
subscriptionClient.request(operation), | subscriptionClient.request(operation), | |||
}), | }), | |||
], | ], | |||
}) | }) | |||
export const client = ref(createHoppClient()) | export const client = ref(createHoppClient()) | |||
authIdToken$.subscribe(() => { | authIdToken$.subscribe(() => { | |||
client.value = createHoppClient() | client.value = createHoppClient() | |||
}) | }) | |||
type MaybeRef<X> = X | Ref<X> | type RunQueryOptions<T = any, V = object> = { | |||
type UseQueryOptions<T = any, V = object> = { | ||||
query: TypedDocumentNode<T, V> | query: TypedDocumentNode<T, V> | |||
variables?: MaybeRef<V> | variables?: V | |||
updateSubs?: MaybeRef<GraphQLRequest<any, object>[]> | ||||
defer?: boolean | ||||
pollDuration?: number | undefined | ||||
} | } | |||
/** | /** | |||
* A wrapper type for defining errors possible in a GQL operation | * A wrapper type for defining errors possible in a GQL operation | |||
*/ | */ | |||
export type GQLError<T extends string> = | export type GQLError<T extends string> = | |||
| { | | { | |||
type: "network_error" | type: "network_error" | |||
error: Error | error: Error | |||
} | } | |||
| { | | { | |||
type: "gql_error" | type: "gql_error" | |||
error: T | error: T | |||
} | } | |||
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>( | export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>( | |||
_args: UseQueryOptions<DocType, DocVarType> | args: RunQueryOptions<DocType, DocVarType> | |||
) => { | ): Promise<E.Either<GQLError<DocErrorType>, DocType>> => { | |||
const stops: WatchStopHandle[] = [] | const request = createRequest<DocType, DocVarType>(args.query, args.variables) | |||
const source = client.value.executeQuery(request, { | ||||
const args = reactive(_args) | requestPolicy: "network-only", | |||
}) | ||||
const loading: Ref<boolean> = ref(true) | ||||
const isStale: Ref<boolean> = ref(true) | ||||
const data: Ref<E.Either<GQLError<DocErrorType>, DocType>> = ref() as any | ||||
if (!args.updateSubs) set(args, "updateSubs", []) | ||||
const isPaused: Ref<boolean> = ref(args.defer ?? false) | ||||
const pollDuration: Ref<number | null> = ref(args.pollDuration ?? null) | ||||
const request: Ref<GraphQLRequest<DocType, DocVarType>> = ref( | ||||
createRequest<DocType, DocVarType>( | ||||
args.query, | ||||
unref<DocVarType>(args.variables as any) as any | ||||
) | ||||
) as any | ||||
const source: Ref<Source<OperationResult> | undefined> = ref() | ||||
// Toggles between true and false to cause the polling operation to tick | ||||
const pollerTick: Ref<boolean> = ref(true) | ||||
stops.push( | return new Promise((resolve) => { | |||
watchEffect((onInvalidate) => { | const sub = wonkaPipe( | |||
if (pollDuration.value !== null && !isPaused.value) { | source, | |||
const handle = setInterval(() => { | subscribe((res) => { | |||
pollerTick.value = !pollerTick.value | if (sub) { | |||
}, pollDuration.value) | sub.unsubscribe() | |||
} | ||||
onInvalidate(() => { | ||||
clearInterval(handle) | ||||
}) | ||||
} | ||||
}) | ||||
) | ||||
stops.push( | pipe( | |||
watchEffect( | // The target | |||
() => { | res.data as DocType | undefined, | |||
const newRequest = createRequest<DocType, DocVarType>( | // Define what happens if data does not exist (it is an error) | |||
args.query, | E.fromNullable( | |||
unref<DocVarType>(args.variables as any) as any | 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) => | ||||
<GQLError<DocErrorType>>{ | ||||
type: "gql_error", | ||||
error: parseGQLErrorString(gqlErr ?? "") as DocErrorType, | ||||
}, | ||||
// The right case (it was a GraphQL Error) | ||||
(networkErr) => | ||||
<GQLError<DocErrorType>>{ | ||||
type: "network_error", | ||||
error: networkErr, | ||||
} | ||||
) | ||||
) | ||||
), | ||||
resolve | ||||
) | ) | |||
}) | ||||
if (request.value.key !== newRequest.key) { | ||||
request.value = newRequest | ||||
} | ||||
}, | ||||
{ flush: "pre" } | ||||
) | ) | |||
) | }) | |||
} | ||||
stops.push( | // TODO: The subscription system seems to be firing multiple updates for certain | |||
watchEffect( | subscriptions. | |||
() => { | // Make sure to handle cases if the subscription fires with the same update mult | |||
// Just listen to the polling ticks | iple times | |||
// eslint-disable-next-line no-unused-expressions | export const runGQLSubscription = < | |||
pollerTick.value | DocType, | |||
DocVarType, | ||||
source.value = !isPaused.value | DocErrorType extends string | |||
? client.value.executeQuery<DocType, DocVarType>(request.value, { | >( | |||
requestPolicy: "cache-and-network", | args: RunQueryOptions<DocType, DocVarType> | |||
}) | ) => { | |||
: undefined | const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>() | |||
}, | ||||
{ flush: "pre" } | ||||
) | ||||
) | ||||
watchSyncEffect((onInvalidate) => { | const source = client.value.executeSubscription( | |||
if (source.value) { | createRequest(args.query, args.variables) | |||
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( | const sub = wonkaPipe( | |||
wonkaPipe( | source, | |||
source.value, | subscribe((res) => { | |||
onEnd(() => { | result$.next( | |||
loading.value = false | pipe( | |||
isStale.value = false | // The target | |||
}), | res.data as DocType | undefined, | |||
subscribe((res) => { | // Define what happens if data does not exist (it is an error) | |||
if (res.operation.key === request.value.key) { | E.fromNullable( | |||
data.value = pipe( | pipe( | |||
// The target | // Take the network error value | |||
res.data as DocType | undefined, | res.error?.networkError, | |||
// Define what happens if data does not exist (it is an error) | // If it null, set the left to the generic error name | |||
E.fromNullable( | E.fromNullable(res.error?.message), | |||
pipe( | E.match( | |||
// Take the network error value | // The left case (network error was null) | |||
res.error?.networkError, | (gqlErr) => | |||
// If it null, set the left to the generic error name | <GQLError<DocErrorType>>{ | |||
E.fromNullable(res.error?.message), | type: "gql_error", | |||
E.match( | error: parseGQLErrorString(gqlErr ?? "") as DocErrorType, | |||
// The left case (network error was null) | }, | |||
(gqlErr) => | // The right case (it was a GraphQL Error) | |||
<GQLError<DocErrorType>>{ | (networkErr) => | |||
type: "gql_error", | <GQLError<DocErrorType>>{ | |||
error: parseGQLErrorString( | type: "network_error", | |||
gqlErr ?? "" | error: networkErr, | |||
) as DocErrorType, | } | |||
}, | ||||
// The right case (it was a GraphQL Error) | ||||
(networkErr) => | ||||
<GQLError<DocErrorType>>{ | ||||
type: "network_error", | ||||
error: networkErr, | ||||
} | ||||
) | ||||
) | ||||
) | ||||
) | ) | |||
) | ||||
loading.value = false | ) | |||
} | ) | |||
}) | ||||
).unsubscribe | ||||
) | ) | |||
}) | ||||
) | ||||
onInvalidate(() => invalidateStops.forEach((unsub) => unsub())) | // Returns the stream and a subscription handle to unsub | |||
} | return [result$, sub] as const | |||
}) | } | |||
const execute = (updatedVars?: DocVarType) => { | /** | |||
if (updatedVars) { | * Same as `runGQLSubscription` but stops the subscription silently | |||
if (isRef(args.variables)) { | * if there is an authentication error because of logged out | |||
args.variables.value = updatedVars | */ | |||
} else { | export const runAuthOnlyGQLSubscription = flow( | |||
set(args, "variables", updatedVars) | runGQLSubscription, | |||
} | ([result$, sub]) => { | |||
} | const updatedResult$ = result$.pipe( | |||
map((res) => { | ||||
if ( | ||||
E.isLeft(res) && | ||||
res.left.type === "gql_error" && | ||||
res.left.error === "auth/fail" | ||||
) { | ||||
sub.unsubscribe() | ||||
return null | ||||
} else return res | ||||
}), | ||||
filter((res): res is Exclude<typeof res, null> => res !== null) | ||||
) | ||||
isPaused.value = false | return [updatedResult$, sub] as const | |||
} | } | |||
) | ||||
const response = reactive({ | export const parseGQLErrorString = (s: string) => | |||
loading, | ||||
data, | ||||
isStale, | ||||
execute, | ||||
}) | ||||
return response | ||||
} | ||||
const parseGQLErrorString = (s: string) => | ||||
s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s | s.startsWith("[GraphQL] ") ? s.split("[GraphQL] ")[1] : s | |||
export const runMutation = < | export const runMutation = < | |||
DocType, | DocType, | |||
DocVariables extends object | undefined, | DocVariables extends object | undefined, | |||
DocErrors extends string | DocErrors extends string | |||
>( | >( | |||
mutation: TypedDocumentNode<DocType, DocVariables>, | mutation: TypedDocumentNode<DocType, DocVariables>, | |||
variables?: DocVariables, | variables?: DocVariables, | |||
additionalConfig?: Partial<OperationContext> | additionalConfig?: Partial<OperationContext> | |||
End of changes. 26 change blocks. | ||||
211 lines changed or deleted | 156 lines changed or added |