refactor: move refresh logic back to TechDocsReaderPage

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-03-12 08:38:23 +01:00
parent a846c6fd2a
commit 048b875dbf
4 changed files with 46 additions and 72 deletions
+1 -63
View File
@@ -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<string> {
return await this.discoveryApi.getBaseUrl('techdocs');
}
@@ -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 <ErrorPanel error={error} />;
}