From a7753e6a5c5a627c78f5b9a420a710032b1977d2 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Tue, 27 Jul 2021 19:55:17 +0200
Subject: [PATCH] Refactor refreshing token

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/vue-apollo.ts | 107 +++++++++++++++++++------------------------
 1 file changed, 46 insertions(+), 61 deletions(-)

diff --git a/js/src/vue-apollo.ts b/js/src/vue-apollo.ts
index 6ae0ee0a6..9b1a7714c 100644
--- a/js/src/vue-apollo.ts
+++ b/js/src/vue-apollo.ts
@@ -6,9 +6,9 @@ import {
   ApolloClient,
   ApolloLink,
   defaultDataIdFromObject,
+  fromPromise,
   InMemoryCache,
   NormalizedCacheObject,
-  Observable,
   split,
 } from "@apollo/client/core";
 import buildCurrentUserResolver from "@/apollo/user";
@@ -31,8 +31,8 @@ import { GraphQLError } from "graphql";
 // Install the vue plugin
 Vue.use(VueApollo);
 
-let refreshingTokenPromise: Promise<boolean> | undefined;
-let alreadyRefreshedToken = false;
+let isRefreshing = false;
+let pendingRequests: any[] = [];
 
 // Endpoints
 const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
@@ -92,32 +92,55 @@ const link = split(
   uploadLink
 );
 
+const resolvePendingRequests = () => {
+  pendingRequests.map((callback) => callback());
+  pendingRequests = [];
+};
+
 const errorLink = onError(
   ({ graphQLErrors, networkError, forward, operation }) => {
-    if (
-      isServerError(networkError) &&
-      networkError?.statusCode === 401 &&
-      !alreadyRefreshedToken
-    ) {
-      if (!refreshingTokenPromise)
-        refreshingTokenPromise = refreshAccessToken(apolloClient);
+    if (isServerError(networkError) && networkError?.statusCode === 401) {
+      let forwardOperation;
 
-      return promiseToObservable(refreshingTokenPromise).flatMap(() => {
-        refreshingTokenPromise = undefined;
-        alreadyRefreshedToken = true;
+      if (!isRefreshing) {
+        isRefreshing = true;
 
-        const context = operation.getContext();
-        const oldHeaders = context.headers;
+        forwardOperation = fromPromise(
+          refreshAccessToken(apolloClient)
+            .then(() => {
+              resolvePendingRequests();
 
-        operation.setContext({
-          headers: {
-            ...oldHeaders,
-            authorization: generateTokenHeader(),
-          },
-        });
+              const context = operation.getContext();
+              const oldHeaders = context.headers;
 
-        return forward(operation);
-      });
+              operation.setContext({
+                headers: {
+                  ...oldHeaders,
+                  authorization: generateTokenHeader(),
+                },
+              });
+              return true;
+            })
+            .catch(() => {
+              pendingRequests = [];
+              logout(apolloClient);
+              return;
+            })
+            .finally(() => {
+              isRefreshing = false;
+            })
+        ).filter((value) => Boolean(value));
+      } else {
+        forwardOperation = fromPromise(
+          new Promise((resolve) => {
+            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            // @ts-ignore
+            pendingRequests.push(() => resolve());
+          })
+        );
+      }
+
+      return forwardOperation.flatMap(() => forward(operation));
     }
 
     if (graphQLErrors) {
@@ -171,41 +194,3 @@ export default new VueApollo({
     console.error(error);
   },
 });
-
-// Thanks: https://github.com/apollographql/apollo-link/issues/747#issuecomment-502676676
-const promiseToObservable = <T>(promise: Promise<T>) =>
-  new Observable<T>((subscriber) => {
-    promise.then(
-      (value) => {
-        if (subscriber.closed) {
-          return;
-        }
-        subscriber.next(value);
-        subscriber.complete();
-      },
-      (err) => {
-        console.error("Cannot refresh token.", err);
-
-        subscriber.error(err);
-        logout(apolloClient);
-      }
-    );
-  });
-
-// Manually call this when user log in
-// export function onLogin(apolloClient) {
-// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
-// }
-
-// Manually call this when user log out
-// export async function onLogout() {
-// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
-// We don't reset store because we rely on currentUser & currentActor
-// which are in the cache (even null). Maybe try to rerun cache init after resetStore?
-// try {
-//   await apolloClient.resetStore();
-// } catch (e) {
-//   // eslint-disable-next-line no-console
-//   console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
-// }
-// }