Path: blob/main/components/dashboard/src/data/setup.tsx
2500 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { get, set, del } from "idb-keyval";7import {8PersistedClient,9Persister,10PersistQueryClientProvider,11PersistQueryClientProviderProps,12} from "@tanstack/react-query-persist-client";13import { QueryCache, QueryClient, QueryKey } from "@tanstack/react-query";14import { Message } from "@bufbuild/protobuf";15import { ReactQueryDevtools } from "@tanstack/react-query-devtools";16import { FunctionComponent } from "react";17import debounce from "lodash/debounce";18// Need to import all the protobuf classes we want to support for hydration19import * as OrganizationClasses from "@gitpod/public-api/lib/gitpod/v1/organization_pb";20import * as WorkspaceClasses from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";21import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";22import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";23import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";24import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";25import * as PrebuildClasses from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";26import * as VerificationClasses from "@gitpod/public-api/lib/gitpod/v1/verification_pb";27import * as InstallationClasses from "@gitpod/public-api/lib/gitpod/v1/installation_pb";28import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb";29import * as SSHClasses from "@gitpod/public-api/lib/gitpod/v1/ssh_pb";30import * as UserClasses from "@gitpod/public-api/lib/gitpod/v1/user_pb";31import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";3233// This is used to version the cache34// If data we cache changes in a non-backwards compatible way, increment this version35// That will bust any previous cache versions a client may have stored36const CACHE_VERSION = "22";3738export function noPersistence(queryKey: QueryKey): QueryKey {39return [...queryKey, "no-persistence"];40}41export function isNoPersistence(queryKey: QueryKey): boolean {42return queryKey.some((e) => e === "no-persistence");43}4445const defaultRetryTimes = 3;4647export const setupQueryClientProvider = () => {48const client = new QueryClient({49defaultOptions: {50queries: {51// Default stale time to help avoid re-fetching data too frequently52staleTime: 1000 * 5, // 5 seconds53refetchOnWindowFocus: false,54retry: (failureCount, error) => {55if (failureCount > defaultRetryTimes) {56return false;57}58// Don't retry if the error is a permission denied error59if (error && (error as any).code === ErrorCodes.PERMISSION_DENIED) {60return false;61}62return true;63},64},65},66queryCache: new QueryCache({67// log any errors our queries throw68onError: (error) => {69console.error(error);70},71}),72});73const queryClientPersister = createIDBPersister();7475const persistOptions: PersistQueryClientProviderProps["persistOptions"] = {76persister: queryClientPersister,77// This allows the client to persist up to 24 hours78// Individual queries may expire prior to this though79maxAge: 1000 * 60 * 60 * 24, // 24 hours80buster: CACHE_VERSION,81dehydrateOptions: {82shouldDehydrateQuery: (query) => {83return !isNoPersistence(query.queryKey) && query.state.status === "success";84},85},86};8788// Return a wrapper around PersistQueryClientProvider w/ the query client options we setp89const GitpodQueryClientProvider: FunctionComponent = ({ children }) => {90return (91<PersistQueryClientProvider client={client} persistOptions={persistOptions}>92{children}93<ReactQueryDevtools initialIsOpen={false} />94</PersistQueryClientProvider>95);96};9798return GitpodQueryClientProvider;99};100101// Persister that uses IndexedDB102export function createIDBPersister(idbValidKey: IDBValidKey = "gitpodQueryClient"): Persister {103// Track a flag that indicates if we're attempting to persist the client104// Some browsers/versions don't support using indexed-db w/ certain settings or in private mode105// If we get an error performing an operation, we'll disable persistance and assume it's not supported106let persistanceActive = true;107108// Ensure we don't persist the client too frequently109// Important to debounce (not throttle) this so we aren't queuing up a bunch of writes110// but instead, only persist the latest state111const debouncedSet = debounce(112async (client: PersistedClient) => {113await set(idbValidKey, dehydrate(client));114},115500,116{117leading: true,118// important so we always persist the latest state when debouncing calls119trailing: true,120// ensure121maxWait: 1000,122},123);124125return {126persistClient: async (client: PersistedClient) => {127if (!persistanceActive) {128return;129}130131try {132await debouncedSet(client);133} catch (e) {134console.error("unable to persist query client");135persistanceActive = false;136}137},138restoreClient: async () => {139try {140const client = await get<PersistedClient>(idbValidKey);141hydrate(client);142return client;143} catch (e) {144console.error("unable to load query client from cache");145persistanceActive = false;146}147},148removeClient: async () => {149try {150await del(idbValidKey);151} catch (e) {152console.error("unable to remove query client");153persistanceActive = false;154}155},156};157}158159const supportedMessages = new Map<string, typeof Message>();160161function initializeMessages() {162const constr = [163...Object.values(OrganizationClasses),164...Object.values(WorkspaceClasses),165...Object.values(PaginationClasses),166...Object.values(ConfigurationClasses),167...Object.values(AuthProviderClasses),168...Object.values(EnvVarClasses),169...Object.values(PrebuildClasses),170...Object.values(VerificationClasses),171...Object.values(InstallationClasses),172...Object.values(SCMClasses),173...Object.values(SSHClasses),174...Object.values(UserClasses),175];176for (const c of constr) {177if ((c as any).prototype instanceof Message) {178supportedMessages.set((c as any).typeName, c as typeof Message);179}180}181}182initializeMessages();183184export function dehydrate(message: any): any {185if (message instanceof Array) {186return message.map(dehydrate);187}188if (message instanceof Message) {189// store the constuctor index so we can deserialize it later190return "|" + (message.constructor as any).typeName + "|" + message.toJsonString();191}192if (message instanceof Object) {193const result: any = {};194for (const key in message) {195result[key] = dehydrate(message[key]);196}197return result;198}199return message;200}201202// This is used to hydrate protobuf messages from the cache203// Serialized protobuf messages follow the format: |messageName|jsonstring204export function hydrate(value: any): any {205if (value instanceof Array) {206return value.map(hydrate);207}208if (typeof value === "string" && value.startsWith("|") && value.lastIndexOf("|") > 1) {209// Remove the leading |210const trimmedVal = value.substring(1);211// Find the first | after the leading | to get the message name212const separatorIdx = trimmedVal.indexOf("|");213const messageName = trimmedVal.substring(0, separatorIdx);214const json = trimmedVal.substring(separatorIdx + 1);215const constructor = supportedMessages.get(messageName);216if (!constructor) {217console.error("unsupported message type", messageName);218return value;219}220// Ensure an error w/ a single message doesn't prevent the entire cache from loading, as it will never get pruned221try {222return (constructor as any).fromJsonString(json);223} catch (e) {224console.error("unable to hydrate message", messageName, e, json);225return undefined;226}227}228if (value instanceof Object) {229for (const key in value) {230value[key] = hydrate(value[key]);231}232}233return value;234}235236237