From 048b875dbfb187e65b598b0d81378206a436c754 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 12 Mar 2024 08:38:23 +0100 Subject: [PATCH] refactor: move refresh logic back to TechDocsReaderPage Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- plugins/techdocs-react/api-report.md | 8 +-- plugins/techdocs-react/src/api.ts | 2 +- plugins/techdocs/src/client.ts | 64 +------------------ .../TechDocsReaderPage/TechDocsReaderPage.tsx | 44 +++++++++++-- 4 files changed, 46 insertions(+), 72 deletions(-) diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index 28404a88fc..4feb3b7609 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -65,15 +65,15 @@ export interface TechDocsApi { // (undocumented) getApiOrigin(): Promise; // (undocumented) + getCookie(): Promise<{ + expiresAt: string; + }>; + // (undocumented) getEntityMetadata( entityId: CompoundEntityRef, ): Promise; // (undocumented) getTechDocsMetadata(entityId: CompoundEntityRef): Promise; - // (undocumented) - issueUserCookie(): Promise<{ - expiresAt: string; - }>; } // @public diff --git a/plugins/techdocs-react/src/api.ts b/plugins/techdocs-react/src/api.ts index a4bb6ccf34..c53481fe31 100644 --- a/plugins/techdocs-react/src/api.ts +++ b/plugins/techdocs-react/src/api.ts @@ -24,7 +24,7 @@ import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; * @public */ export interface TechDocsApi { - issueUserCookie(): Promise<{ expiresAt: string }>; + getCookie(): Promise<{ expiresAt: string }>; getApiOrigin(): Promise; getTechDocsMetadata(entityId: CompoundEntityRef): Promise; getEntityMetadata( diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 4b960454c2..ae81ecdaaa 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -31,8 +31,6 @@ import { } from '@backstage/plugin-techdocs-react'; import { EventSourcePolyfill } from 'event-source-polyfill'; -const EXPIRES_AT_STORAGE_KEY = 'backstage-techdocs-cookie-expires-at'; - /** * API to talk to `techdocs-backend`. * @@ -42,10 +40,6 @@ export class TechDocsClient implements TechDocsApi { public configApi: Config; public discoveryApi: DiscoveryApi; private fetchApi: FetchApi; - private locked = false; - private readonly channel = new BroadcastChannel( - 'backstage-techdocs-cookie-refresh', - ); constructor(options: { configApi: Config; @@ -55,23 +49,9 @@ export class TechDocsClient implements TechDocsApi { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; this.fetchApi = options.fetchApi; - this.channel.onmessage = event => { - const { action, payload } = event.data; - if (action === 'REFRESH_COOKIE') { - this.locked = payload.locked; - } - }; } - set expiresAt(value: string) { - localStorage.setItem(EXPIRES_AT_STORAGE_KEY, value); - } - - get expiresAt(): string | null { - return localStorage.getItem(EXPIRES_AT_STORAGE_KEY); - } - - private async getCookie(): Promise<{ expiresAt: string }> { + public async getCookie(): Promise<{ expiresAt: string }> { const apiOrigin = await this.getApiOrigin(); const requestUrl = `${apiOrigin}/cookie`; const response = await this.fetchApi.fetch(`${requestUrl}`, { @@ -83,48 +63,6 @@ export class TechDocsClient implements TechDocsApi { return await response.json(); } - private refreshUserCookie(options: { expiresAt: string }) { - const { expiresAt } = options; - const tenMinutesInMilliseconds = 10 * 60000; - const intervalId = setInterval(() => { - if (this.locked) return; - if (Date.parse(expiresAt) - Date.now() - tenMinutesInMilliseconds > 0) - return; - - this.channel.postMessage({ - action: 'REFRESH_COOKIE', - payload: { locked: true }, - }); - - this.issueUserCookie({ force: true }) - .then(() => clearInterval(intervalId)) - .finally(() => { - this.locked = false; - this.channel.postMessage({ - action: 'REFRESH_COOKIE', - payload: { locked: false }, - }); - }); - }, tenMinutesInMilliseconds); - - return { expiresAt }; - } - - public async issueUserCookie( - options: { - force?: boolean; - } = {}, - ): Promise<{ expiresAt: string }> { - const { force } = options; - if (force || !this.expiresAt || Date.parse(this.expiresAt) < Date.now()) { - return this.getCookie().then(({ expiresAt }) => { - this.expiresAt = expiresAt; - return this.refreshUserCookie({ expiresAt }); - }); - } - return this.refreshUserCookie({ expiresAt: this.expiresAt }); - } - async getApiOrigin(): Promise { return await this.discoveryApi.getBaseUrl('techdocs'); } diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 5b8c94072f..197bf15de7 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import React, { ReactNode, Children, ReactElement } from 'react'; +import React, { + ReactNode, + Children, + ReactElement, + useEffect, + useState, + useCallback, +} from 'react'; import { useOutlet } from 'react-router-dom'; import { ErrorPanel, Page } from '@backstage/core-components'; @@ -38,7 +45,7 @@ import { useApp, useRouteRefParams, } from '@backstage/core-plugin-api'; -import { useAsync } from 'react-use'; +import { useAsyncRetry } from 'react-use'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -151,12 +158,41 @@ function TechDocsAuthProvider({ children }: { children: ReactNode }) { const app = useApp(); const { Progress } = app.getComponents(); + const [channel] = useState(new BroadcastChannel('techdocs-cookie-refresh')); + const techdocsApi = useApi(techdocsApiRef); - const { loading, error } = useAsync(async () => { - return await techdocsApi.issueUserCookie(); + const { loading, error, value, retry } = useAsyncRetry(async () => { + return await techdocsApi.getCookie(); }, [techdocsApi]); + const startCookieRefresh = useCallback( + (expiresAt: string) => { + // Randomize the refreshing margin to avoid all tabs refreshing at the same time + const refreshingMargin = (1 + 3 * Math.random()) * 60_000; + const delay = Date.parse(expiresAt) - Date.now() - refreshingMargin; + const timeout = setTimeout(retry, delay); + return () => clearTimeout(timeout); + }, + [retry], + ); + + useEffect(() => { + if (!value) return () => {}; + channel.postMessage({ action: 'COOKIE_REFRESHED', payload: value }); + let stopCookieRefresh = startCookieRefresh(value.expiresAt); + channel.onmessage = event => { + const { action, payload } = event.data; + if (action === 'COOKIE_REFRESHED') { + stopCookieRefresh(); + stopCookieRefresh = startCookieRefresh(payload.expiresAt); + } + }; + return () => { + stopCookieRefresh(); + }; + }, [value, channel, startCookieRefresh]); + if (error) { return ; }