Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/data/setup.tsx
2500 views
1
/**
2
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { get, set, del } from "idb-keyval";
8
import {
9
PersistedClient,
10
Persister,
11
PersistQueryClientProvider,
12
PersistQueryClientProviderProps,
13
} from "@tanstack/react-query-persist-client";
14
import { QueryCache, QueryClient, QueryKey } from "@tanstack/react-query";
15
import { Message } from "@bufbuild/protobuf";
16
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
17
import { FunctionComponent } from "react";
18
import debounce from "lodash/debounce";
19
// Need to import all the protobuf classes we want to support for hydration
20
import * as OrganizationClasses from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
21
import * as WorkspaceClasses from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
22
import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
23
import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
24
import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
25
import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
26
import * as PrebuildClasses from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
27
import * as VerificationClasses from "@gitpod/public-api/lib/gitpod/v1/verification_pb";
28
import * as InstallationClasses from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
29
import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
30
import * as SSHClasses from "@gitpod/public-api/lib/gitpod/v1/ssh_pb";
31
import * as UserClasses from "@gitpod/public-api/lib/gitpod/v1/user_pb";
32
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
33
34
// This is used to version the cache
35
// If data we cache changes in a non-backwards compatible way, increment this version
36
// That will bust any previous cache versions a client may have stored
37
const CACHE_VERSION = "22";
38
39
export function noPersistence(queryKey: QueryKey): QueryKey {
40
return [...queryKey, "no-persistence"];
41
}
42
export function isNoPersistence(queryKey: QueryKey): boolean {
43
return queryKey.some((e) => e === "no-persistence");
44
}
45
46
const defaultRetryTimes = 3;
47
48
export const setupQueryClientProvider = () => {
49
const client = new QueryClient({
50
defaultOptions: {
51
queries: {
52
// Default stale time to help avoid re-fetching data too frequently
53
staleTime: 1000 * 5, // 5 seconds
54
refetchOnWindowFocus: false,
55
retry: (failureCount, error) => {
56
if (failureCount > defaultRetryTimes) {
57
return false;
58
}
59
// Don't retry if the error is a permission denied error
60
if (error && (error as any).code === ErrorCodes.PERMISSION_DENIED) {
61
return false;
62
}
63
return true;
64
},
65
},
66
},
67
queryCache: new QueryCache({
68
// log any errors our queries throw
69
onError: (error) => {
70
console.error(error);
71
},
72
}),
73
});
74
const queryClientPersister = createIDBPersister();
75
76
const persistOptions: PersistQueryClientProviderProps["persistOptions"] = {
77
persister: queryClientPersister,
78
// This allows the client to persist up to 24 hours
79
// Individual queries may expire prior to this though
80
maxAge: 1000 * 60 * 60 * 24, // 24 hours
81
buster: CACHE_VERSION,
82
dehydrateOptions: {
83
shouldDehydrateQuery: (query) => {
84
return !isNoPersistence(query.queryKey) && query.state.status === "success";
85
},
86
},
87
};
88
89
// Return a wrapper around PersistQueryClientProvider w/ the query client options we setp
90
const GitpodQueryClientProvider: FunctionComponent = ({ children }) => {
91
return (
92
<PersistQueryClientProvider client={client} persistOptions={persistOptions}>
93
{children}
94
<ReactQueryDevtools initialIsOpen={false} />
95
</PersistQueryClientProvider>
96
);
97
};
98
99
return GitpodQueryClientProvider;
100
};
101
102
// Persister that uses IndexedDB
103
export function createIDBPersister(idbValidKey: IDBValidKey = "gitpodQueryClient"): Persister {
104
// Track a flag that indicates if we're attempting to persist the client
105
// Some browsers/versions don't support using indexed-db w/ certain settings or in private mode
106
// If we get an error performing an operation, we'll disable persistance and assume it's not supported
107
let persistanceActive = true;
108
109
// Ensure we don't persist the client too frequently
110
// Important to debounce (not throttle) this so we aren't queuing up a bunch of writes
111
// but instead, only persist the latest state
112
const debouncedSet = debounce(
113
async (client: PersistedClient) => {
114
await set(idbValidKey, dehydrate(client));
115
},
116
500,
117
{
118
leading: true,
119
// important so we always persist the latest state when debouncing calls
120
trailing: true,
121
// ensure
122
maxWait: 1000,
123
},
124
);
125
126
return {
127
persistClient: async (client: PersistedClient) => {
128
if (!persistanceActive) {
129
return;
130
}
131
132
try {
133
await debouncedSet(client);
134
} catch (e) {
135
console.error("unable to persist query client");
136
persistanceActive = false;
137
}
138
},
139
restoreClient: async () => {
140
try {
141
const client = await get<PersistedClient>(idbValidKey);
142
hydrate(client);
143
return client;
144
} catch (e) {
145
console.error("unable to load query client from cache");
146
persistanceActive = false;
147
}
148
},
149
removeClient: async () => {
150
try {
151
await del(idbValidKey);
152
} catch (e) {
153
console.error("unable to remove query client");
154
persistanceActive = false;
155
}
156
},
157
};
158
}
159
160
const supportedMessages = new Map<string, typeof Message>();
161
162
function initializeMessages() {
163
const constr = [
164
...Object.values(OrganizationClasses),
165
...Object.values(WorkspaceClasses),
166
...Object.values(PaginationClasses),
167
...Object.values(ConfigurationClasses),
168
...Object.values(AuthProviderClasses),
169
...Object.values(EnvVarClasses),
170
...Object.values(PrebuildClasses),
171
...Object.values(VerificationClasses),
172
...Object.values(InstallationClasses),
173
...Object.values(SCMClasses),
174
...Object.values(SSHClasses),
175
...Object.values(UserClasses),
176
];
177
for (const c of constr) {
178
if ((c as any).prototype instanceof Message) {
179
supportedMessages.set((c as any).typeName, c as typeof Message);
180
}
181
}
182
}
183
initializeMessages();
184
185
export function dehydrate(message: any): any {
186
if (message instanceof Array) {
187
return message.map(dehydrate);
188
}
189
if (message instanceof Message) {
190
// store the constuctor index so we can deserialize it later
191
return "|" + (message.constructor as any).typeName + "|" + message.toJsonString();
192
}
193
if (message instanceof Object) {
194
const result: any = {};
195
for (const key in message) {
196
result[key] = dehydrate(message[key]);
197
}
198
return result;
199
}
200
return message;
201
}
202
203
// This is used to hydrate protobuf messages from the cache
204
// Serialized protobuf messages follow the format: |messageName|jsonstring
205
export function hydrate(value: any): any {
206
if (value instanceof Array) {
207
return value.map(hydrate);
208
}
209
if (typeof value === "string" && value.startsWith("|") && value.lastIndexOf("|") > 1) {
210
// Remove the leading |
211
const trimmedVal = value.substring(1);
212
// Find the first | after the leading | to get the message name
213
const separatorIdx = trimmedVal.indexOf("|");
214
const messageName = trimmedVal.substring(0, separatorIdx);
215
const json = trimmedVal.substring(separatorIdx + 1);
216
const constructor = supportedMessages.get(messageName);
217
if (!constructor) {
218
console.error("unsupported message type", messageName);
219
return value;
220
}
221
// Ensure an error w/ a single message doesn't prevent the entire cache from loading, as it will never get pruned
222
try {
223
return (constructor as any).fromJsonString(json);
224
} catch (e) {
225
console.error("unable to hydrate message", messageName, e, json);
226
return undefined;
227
}
228
}
229
if (value instanceof Object) {
230
for (const key in value) {
231
value[key] = hydrate(value[key]);
232
}
233
}
234
return value;
235
}
236
237