Skip to content

Commit

Permalink
feat: handle gql auth, errors, and retry with links (#292)
Browse files Browse the repository at this point in the history
* feat: handle auth, errors, and retry with links

* address comments

* move apollo client logic to separate function
  • Loading branch information
NikhilNarayana authored Jun 5, 2022
1 parent c578fe2 commit efe5760
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 35 deletions.
34 changes: 31 additions & 3 deletions src/dolphin/install/fetchLatestVersion.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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" : ""}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ export const StartBroadcastDialog: React.FC<StartBroadcastDialogProps> = ({ open
);

const fetchUser = debounce(async () => {
console.log("start debounced code");
await userQuery.refetch();
console.log("end debounced code");
}, 200);

const handleChange = React.useCallback(
Expand Down
80 changes: 50 additions & 30 deletions src/renderer/services/slippi/slippi.service.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) => {
Expand All @@ -25,45 +29,57 @@ const handleErrors = (errors: readonly GraphQLError[] | undefined) => {
};

class SlippiBackendClient implements SlippiBackendService {
private httpLink: HttpLink;
private client: ApolloClient<NormalizedCacheObject>;

constructor(
private readonly authService: AuthService,
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<AuthUser> {
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 }> {
Expand All @@ -89,7 +105,10 @@ class SlippiBackendClient implements SlippiBackendService {
}

public async fetchPlayKey(): Promise<PlayKey | null> {
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,
Expand Down Expand Up @@ -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,
Expand All @@ -149,8 +171,6 @@ class SlippiBackendClient implements SlippiBackendService {
}

public async initializeNetplay(codeStart: string): Promise<void> {
await this._refreshAuthToken();

const res = await this.client.mutate({ mutation: MUTATION_INIT_NETPLAY, variables: { codeStart } });
handleErrors(res.errors);
}
Expand Down

0 comments on commit efe5760

Please sign in to comment.