From efe57600b47e6a5bf91a2658c1d20a3dab884b26 Mon Sep 17 00:00:00 2001 From: Nikhil Narayana Date: Sun, 5 Jun 2022 10:15:09 -0700 Subject: [PATCH] feat: handle gql auth, errors, and retry with links (#292) * feat: handle auth, errors, and retry with links * address comments * move apollo client logic to separate function --- src/dolphin/install/fetchLatestVersion.ts | 34 +++++++- .../StartBroadcastDialog.tsx | 2 - .../services/slippi/slippi.service.ts | 80 ++++++++++++------- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/dolphin/install/fetchLatestVersion.ts b/src/dolphin/install/fetchLatestVersion.ts index 681d25b86..3670cfe74 100644 --- a/src/dolphin/install/fetchLatestVersion.ts +++ b/src/dolphin/install/fetchLatestVersion.ts @@ -1,6 +1,9 @@ -import { ApolloClient, gql, HttpLink, InMemoryCache } from "@apollo/client"; +import { ApolloClient, ApolloLink, gql, HttpLink, InMemoryCache } from "@apollo/client"; +import { onError } from "@apollo/client/link/error"; +import { RetryLink } from "@apollo/client/link/retry"; import { appVersion } from "@common/constants"; import { fetch } from "cross-fetch"; +import electronLog from "electron-log"; import type { GraphQLError } from "graphql"; import type { DolphinLaunchType } from "../types"; @@ -14,11 +17,36 @@ export type DolphinVersionResponse = { }; }; -const httpLink = new HttpLink({ uri: process.env.SLIPPI_GRAPHQL_ENDPOINT, fetch }); +const log = electronLog.scope("dolphin/checkVersion"); const isDevelopment = process.env.NODE_ENV !== "production"; +const httpLink = new HttpLink({ uri: process.env.SLIPPI_GRAPHQL_ENDPOINT, fetch }); +const retryLink = new RetryLink({ + delay: { + initial: 300, + max: Infinity, + jitter: true, + }, + attempts: { + max: 3, + retryIf: (error) => Boolean(error), + }, +}); +const errorLink = onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.map(({ message, locations, path }) => + log.error(`Apollo GQL Error: Message: ${message}, Location: ${locations}, Path: ${path}`), + ); + } + if (networkError) { + log.error(`Apollo Network Error: ${networkError}`); + } +}); + +const apolloLink = ApolloLink.from([errorLink, retryLink, httpLink]); + const client = new ApolloClient({ - link: httpLink, + link: apolloLink, cache: new InMemoryCache(), name: "slippi-launcher", version: `${appVersion}${isDevelopment ? "-dev" : ""}`, diff --git a/src/renderer/containers/SpectatePage/ShareGameplayBlock/StartBroadcastDialog.tsx b/src/renderer/containers/SpectatePage/ShareGameplayBlock/StartBroadcastDialog.tsx index 8b8e0683c..7d4941d58 100644 --- a/src/renderer/containers/SpectatePage/ShareGameplayBlock/StartBroadcastDialog.tsx +++ b/src/renderer/containers/SpectatePage/ShareGameplayBlock/StartBroadcastDialog.tsx @@ -54,9 +54,7 @@ export const StartBroadcastDialog: React.FC = ({ open ); const fetchUser = debounce(async () => { - console.log("start debounced code"); await userQuery.refetch(); - console.log("end debounced code"); }, 200); const handleChange = React.useCallback( diff --git a/src/renderer/services/slippi/slippi.service.ts b/src/renderer/services/slippi/slippi.service.ts index ea88681c6..fe03a47ba 100644 --- a/src/renderer/services/slippi/slippi.service.ts +++ b/src/renderer/services/slippi/slippi.service.ts @@ -1,9 +1,12 @@ import type { NormalizedCacheObject } from "@apollo/client"; import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/client"; +import { setContext } from "@apollo/client/link/context"; +import { onError } from "@apollo/client/link/error"; +import { RetryLink } from "@apollo/client/link/retry"; import type { DolphinService, PlayKey } from "@dolphin/types"; import type { GraphQLError } from "graphql"; -import type { AuthService, AuthUser } from "../auth/types"; +import type { AuthService } from "../auth/types"; import { MUTATION_INIT_NETPLAY, MUTATION_RENAME_USER, @@ -12,6 +15,7 @@ import { } from "./graphqlEndpoints"; import type { SlippiBackendService } from "./types"; +const log = window.electron.log; const SLIPPI_BACKEND_URL = process.env.SLIPPI_GRAPHQL_ENDPOINT; const handleErrors = (errors: readonly GraphQLError[] | undefined) => { @@ -25,7 +29,6 @@ const handleErrors = (errors: readonly GraphQLError[] | undefined) => { }; class SlippiBackendClient implements SlippiBackendService { - private httpLink: HttpLink; private client: ApolloClient; constructor( @@ -33,37 +36,50 @@ class SlippiBackendClient implements SlippiBackendService { private readonly dolphinService: DolphinService, clientVersion?: string, ) { - this.httpLink = new HttpLink({ uri: SLIPPI_BACKEND_URL }); - this.client = new ApolloClient({ - link: this.httpLink, - cache: new InMemoryCache(), - name: "slippi-launcher", - version: clientVersion, - }); + this.client = this._createApolloClient(clientVersion); } - // The firebase ID token expires after 1 hour so we will refresh it for actions that require it. - private async _refreshAuthToken(): Promise { - const user = this.authService.getCurrentUser(); - if (!user) { - throw new Error("User is not logged in."); - } - const token = await this.authService.getUserToken(); + private _createApolloClient(clientVersion?: string) { + const httpLink = new HttpLink({ uri: SLIPPI_BACKEND_URL }); + const authLink = setContext(async () => { + // The firebase ID token expires after 1 hour so we will update the header on all actions + const token = await this.authService.getUserToken(); - const authLink = new ApolloLink((operation, forward) => { - // Use the setContext method to set the HTTP headers. - operation.setContext({ + return { headers: { - authorization: token ? `Bearer ${token}` : "", + authorization: token ? `Bearer ${token}` : undefined, }, - }); - - // Call the next link in the middleware chain. - return forward(operation); + }; + }); + const retryLink = new RetryLink({ + delay: { + initial: 300, + max: Infinity, + jitter: true, + }, + attempts: { + max: 3, + retryIf: (error) => Boolean(error), + }, + }); + const errorLink = onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.map(({ message, locations, path }) => + log.error(`Apollo GQL Error: Message: ${message}, Location: ${locations}, Path: ${path}`), + ); + } + if (networkError) { + log.error(`Apollo Network Error: ${networkError}`); + } }); - this.client.setLink(authLink.concat(this.httpLink)); - return user; + const apolloLink = ApolloLink.from([authLink, errorLink, retryLink, httpLink]); + return new ApolloClient({ + link: apolloLink, + cache: new InMemoryCache(), + name: "slippi-launcher", + version: clientVersion, + }); } public async validateUserId(userId: string): Promise<{ displayName: string; connectCode: string }> { @@ -89,7 +105,10 @@ class SlippiBackendClient implements SlippiBackendService { } public async fetchPlayKey(): Promise { - const user = await this._refreshAuthToken(); + const user = this.authService.getCurrentUser(); + if (!user) { + throw new Error("User is not logged in"); + } const res = await this.client.query({ query: QUERY_GET_USER_KEY, @@ -132,7 +151,10 @@ class SlippiBackendClient implements SlippiBackendService { } public async changeDisplayName(name: string) { - const user = await this._refreshAuthToken(); + const user = this.authService.getCurrentUser(); + if (!user) { + throw new Error("User is not logged in"); + } const res = await this.client.mutate({ mutation: MUTATION_RENAME_USER, @@ -149,8 +171,6 @@ class SlippiBackendClient implements SlippiBackendService { } public async initializeNetplay(codeStart: string): Promise { - await this._refreshAuthToken(); - const res = await this.client.mutate({ mutation: MUTATION_INIT_NETPLAY, variables: { codeStart } }); handleErrors(res.errors); }