diff --git a/catalog/app/components/Preview/loaders/Html/Html.tsx b/catalog/app/components/Preview/loaders/Html/Html.tsx index cb7dc88174a..74eb486e4aa 100644 --- a/catalog/app/components/Preview/loaders/Html/Html.tsx +++ b/catalog/app/components/Preview/loaders/Html/Html.tsx @@ -31,8 +31,8 @@ export const detect = utils.extIn(['.htm', '.html']) export const FILE_TYPE = FileType.Html -const SESSION_TTL = 60 * 3 -const REFRESH_INTERVAL = SESSION_TTL * 0.2 * 1000 +const SESSION_TTL = 60 * 3 // 3 minutes +const REFRESH_INTERVAL = 60 * 1000 // 1 minute type SessionId = Model.GQLTypes.BrowsingSession['id'] type CreateData = GQL.DataForDoc['browsingSessionCreate'] @@ -172,8 +172,8 @@ function useSession(handle: FileHandle) { async function initSession() { setResult(AsyncResult.Pending()) try { - await ensureCookie() const session = await createSession(scope, SESSION_TTL) + await ensureCookie(SESSION_TTL, REFRESH_INTERVAL) if (ignore) return sessionId = session.id setResult(AsyncResult.Ok(sessionId)) @@ -181,9 +181,14 @@ function useSession(handle: FileHandle) { handleError(e) } - timer = setInterval(() => { + timer = setInterval(async () => { if (!sessionId) return - refreshSession(sessionId, SESSION_TTL).catch(handleError) + try { + await refreshSession(sessionId, SESSION_TTL) + await ensureCookie(SESSION_TTL, REFRESH_INTERVAL) + } catch (e) { + handleError(e) + } }, REFRESH_INTERVAL) } diff --git a/catalog/app/utils/PFSCookieManager.tsx b/catalog/app/utils/PFSCookieManager.tsx index 971cb6527a6..1d0754b822e 100644 --- a/catalog/app/utils/PFSCookieManager.tsx +++ b/catalog/app/utils/PFSCookieManager.tsx @@ -1,15 +1,15 @@ +import * as dateFns from 'date-fns' import invariant from 'invariant' import * as React from 'react' import * as redux from 'react-redux' import { createSelector } from 'reselect' -import * as Sentry from '@sentry/react' import cfg from 'constants/config' import { tokens as tokensSelector } from 'containers/Auth/selectors' import { useApi } from 'utils/APIConnector' import usePrevious from 'utils/usePrevious' -type Ensure = () => Promise +type Ensure = (ttl: number, histeresis: number) => Promise const Ctx = React.createContext(null) @@ -18,37 +18,77 @@ const selectToken = createSelector( (tokens) => tokens?.token as string | undefined, ) +interface State { + token: string | undefined + expires: Date + promise: Promise +} + +function needsRefresh( + state: State | undefined, + params: { + token: string | undefined + expires: Date + histeresis: number + }, +): boolean { + if (!state) return true + if (state.token !== params.token) return true + if (state.expires.getTime() + params.histeresis < params.expires.getTime()) return true + return false +} + export function PFSCookieManager({ children }: React.PropsWithChildren<{}>) { - const promiseRef = React.useRef>() + const stateRef = React.useRef() const req = useApi() + const setPFSCookie = React.useCallback( + (token: string | undefined, ttl: number): Promise => + token + ? req({ + auth: { tokens: { token }, handleInvalidToken: false }, + url: `${cfg.s3Proxy}/browse/set_browse_cookie`, + method: 'POST', + credentials: 'include', + data: { ttl }, + }).catch((e: any) => { + throw new Error(`Could not set PFS cookie: ${e.message}`) + }) + : Promise.resolve(), + + [req], + ) + const token = redux.useSelector(selectToken) - const setBrowseCookie = React.useCallback(async () => { - if (!token) throw new Error('Unable to set PFS cookie: not authenticated') - - try { - await req({ - auth: { tokens: { token }, handleInvalidToken: false }, - url: `${cfg.s3Proxy}/browse/set_browse_cookie`, - method: 'POST', - credentials: 'include', - }) - } catch (e: any) { - Sentry.captureException(e) - throw new Error(`Could not set PFS cookie: ${e.message}`) - } - }, [req, token]) - - const ensure = React.useCallback(() => { - if (!promiseRef.current) promiseRef.current = setBrowseCookie() - return promiseRef.current - }, [promiseRef, setBrowseCookie]) + const ensure = React.useCallback( + async (ttl: number, histeresis: number) => { + const expires = dateFns.addSeconds(new Date(), ttl) + if (needsRefresh(stateRef.current, { token, expires, histeresis })) { + const promise = setPFSCookie(token, ttl) + stateRef.current = { token, expires, promise } + } + while (true) { + // if a new request has been issued while waiting for the response, + // wait for it to complete + let promise = stateRef.current?.promise + if (promise) await promise + if (promise === stateRef.current?.promise) return + } + }, + [token, stateRef, setPFSCookie], + ) // refresh on token change usePrevious(token, (prev) => { - if (token !== prev && promiseRef.current) promiseRef.current = setBrowseCookie() + if (token === prev) return + if (!stateRef.current) return + const now = new Date() + const ttlLeft = Math.ceil((stateRef.current.expires.getTime() - now.getTime()) / 1000) + if (ttlLeft <= 0) return + stateRef.current.token = token + stateRef.current.promise = setPFSCookie(token, ttlLeft) }) return {children}