diff --git a/packages/twenty-front/src/modules/auth/utils/ensureTokenRenewed.ts b/packages/twenty-front/src/modules/auth/utils/ensureTokenRenewed.ts new file mode 100644 index 0000000000..bb234cdb67 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/utils/ensureTokenRenewed.ts @@ -0,0 +1,60 @@ +import { renewToken } from '@/auth/services/AuthService'; +import { tokenPairState } from '@/auth/states/tokenPairState'; +import { type createStore } from 'jotai'; +import { isDefined } from 'twenty-shared/utils'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { retryWithBackoff } from '~/utils/retryWithBackoff'; + +const TOKEN_RENEWAL_MAX_RETRIES = 3; +const TOKEN_RENEWAL_RETRY_DELAY_MS = 1000; + +let renewalPromise: Promise | null = null; + +export const ensureTokenRenewed = ( + store: ReturnType, +): Promise => { + if (isDefined(renewalPromise)) { + return renewalPromise; + } + + const tokenPair = store.get(tokenPairState.atom); + + if (!isDefined(tokenPair)) { + return Promise.resolve(false); + } + + const refreshTokenSnapshot = tokenPair.refreshToken.token; + + renewalPromise = retryWithBackoff( + () => + renewToken( + `${REACT_APP_SERVER_BASE_URL}/metadata`, + store.get(tokenPairState.atom), + ), + { + maxRetries: TOKEN_RENEWAL_MAX_RETRIES, + baseDelayMs: TOKEN_RENEWAL_RETRY_DELAY_MS, + shouldRetry: () => isDefined(store.get(tokenPairState.atom)), + }, + ) + .then((tokens) => { + if (!isDefined(tokens)) return true; + + const currentPair = store.get(tokenPairState.atom); + + if ( + isDefined(currentPair) && + currentPair.refreshToken.token === refreshTokenSnapshot + ) { + store.set(tokenPairState.atom, tokens); + } + + return true; + }) + .catch(() => false) + .finally(() => { + renewalPromise = null; + }); + + return renewalPromise; +}; diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts index e06d40b028..a08acf2a92 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts @@ -1,17 +1,19 @@ import { tokenPairState } from '@/auth/states/tokenPairState'; +import { ensureTokenRenewed } from '@/auth/utils/ensureTokenRenewed'; import { SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS } from '@/sse-db-event/constants/SseConnectionRetryMaxWaitTimeInMs'; import { SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_FOR_DEV_MODE } from '@/sse-db-event/constants/SseConnectionRetryWaitTimeInMsForDevMode'; import { SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS } from '@/sse-db-event/constants/SseConnectionRetryWaitTimeInMsToAvoidRaceConditions'; import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestroyEventStreamState'; import { sseClientState } from '@/sse-db-event/states/sseClientState'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { getIsDevelopmentEnvironment } from '~/utils/getIsDevelopmentEnvironment'; import { sleep } from '~/utils/sleep'; -import { useStore } from 'jotai'; export const useHandleSseClientConnectionRetry = () => { const store = useStore(); + const handleSseClientConnectionRetry = useCallback( async (retryCount: number) => { const sseClient = store.get(sseClientState.atom); @@ -20,27 +22,38 @@ export const useHandleSseClientConnectionRetry = () => { await sleep( SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS, ); - return; } const tokenPair = store.get(tokenPairState.atom); - const currentAppToken = tokenPair?.accessOrWorkspaceAgnosticToken?.token; + const accessToken = tokenPair?.accessOrWorkspaceAgnosticToken; - const shouldResetSseClient = - !isDefined(currentAppToken) || retryCount > 10; - - if (shouldResetSseClient) { + if (!isDefined(accessToken) || retryCount > 10) { await sleep( SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS, ); - sseClient.dispose(); store.set(shouldDestroyEventStreamState.atom, true); store.set(sseClientState.atom, null); return; } + const isTokenExpired = new Date(accessToken.expiresAt) <= new Date(); + + if (isTokenExpired) { + const renewed = await ensureTokenRenewed(store); + + if (!renewed) { + await sleep( + SSE_CONNECTION_RETRY_WAIT_TIME_IN_MS_TO_AVOID_RACE_CONDITIONS, + ); + sseClient.dispose(); + store.set(shouldDestroyEventStreamState.atom, true); + store.set(sseClientState.atom, null); + return; + } + } + const randomWaitTimeInMsToSpaceAllClientsReconnection = Math.round( Math.random() * SSE_CONNECTION_RETRY_MAX_WAIT_TIME_IN_MS, );