diff --git a/components/account.js b/components/account.js index dd9d3248dd..56434d97a0 100644 --- a/components/account.js +++ b/components/account.js @@ -7,10 +7,12 @@ import useCookie from '@/components/use-cookie' import Link from 'next/link' import AddIcon from '@/svgs/add-fill.svg' import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth' +import { abortPendingRequests } from '@/lib/apollo' const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8') export const nextAccount = async () => { + abortPendingRequests() // Cancel any pending requests during account switch const { status } = await fetch('/api/next-account', { credentials: 'include' }) // if status is 302, this means the server was able to switch us to the next available account return status === 302 @@ -69,7 +71,7 @@ const AccountListRow = ({ account, selected, ...props }) => { const onClick = async (e) => { // prevent navigation e.preventDefault() - + abortPendingRequests() // Cancel pending requests during account switch // update pointer cookie const options = cookieOptions({ httpOnly: false }) const anon = account.id === USER_ID.anon diff --git a/components/nav/common.js b/components/nav/common.js index 3c9f619705..d8422a7846 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -22,6 +22,7 @@ import { useWalletIndicator } from '@/wallets/client/hooks' import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account' import { useShowModal } from '@/components/modal' import { numWithUnits } from '@/lib/format' +import { abortPendingRequests } from '@/lib/apollo' export function Brand ({ className }) { return ( @@ -305,7 +306,8 @@ function LogoutObstacle ({ onClose }) { router.reload() return } - + // Abort all pending GraphQL requests to prevent race condition + abortPendingRequests() // order is important because we need to be logged in to delete push subscription on server const pushSubscription = await swRegistration?.pushManager.getSubscription() if (pushSubscription) { diff --git a/lib/apollo.js b/lib/apollo.js index b05d1a9440..b3f589edbe 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -1,4 +1,4 @@ -import { ApolloClient, InMemoryCache, HttpLink, makeVar, split, from } from '@apollo/client' +import { ApolloClient, InMemoryCache, HttpLink, makeVar, split, from, ApolloLink } from '@apollo/client' import { BatchHttpLink } from '@apollo/client/link/batch-http' import { decodeCursor, LIMIT } from './cursor' import { COMMENTS_LIMIT, SSR } from './constants' @@ -29,6 +29,36 @@ export default function getApolloClient () { } export const meAnonSats = {} +let globalAbortController = null +export function getGlobalAbortController () { + if (!globalAbortController || globalAbortController.signal.aborted) { + globalAbortController = new AbortController() + } + return globalAbortController +} + +export function abortPendingRequests () { + if (globalAbortController && !globalAbortController.signal.aborted) { + globalAbortController.abort('Logout in progress') + console.log('Aborted pending GraphQL requests during logout') + } +} + +const createAbortLink = () => new ApolloLink((operation, forward) => { + if (SSR) { + return forward(operation) + } + + const abortController = getGlobalAbortController() + operation.setContext((context) => ({ + ...context, + fetchOptions: { + ...context.fetchOptions, + signal: abortController.signal + } + })) + return forward(operation) +}) const retryLink = new RetryLink({ delay: { @@ -46,8 +76,11 @@ const retryLink = new RetryLink({ }) function getClient (uri) { + const abortLink = createAbortLink() + const link = from([ retryLink, + abortLink, split( // batch zaps if wallet is enabled so they can be executed serially in a single request operation => operation.operationName === 'act' && operation.variables.act === 'TIP' && operation.getContext().batch,