-
Notifications
You must be signed in to change notification settings - Fork 323
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Share tanstack QueryClient between dashboard and IDE (#10431)
* Share tanstack QueryClient between dashboard and IDE Part of #10400. * Lint * Review: Use enso-common * Remove outdated README * Naming * Fix * Lint * enso-common CODEOWNERS: GUI+Dashboard * Review: Prepare for GUI to be run from cloud entry point * Lint * Lint * Fix e2e tests * Fix e2e tests in CI? * Clean CI build * Revert "Clean CI build" This reverts commit 73f2fb7. * Fix redundant dependency * Work around a vue-query bug * Lint * fmt
- Loading branch information
Showing
13 changed files
with
367 additions
and
193 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
/** | ||
* @file | ||
* | ||
* Tanstack Query client for Enso IDE and dashboard. | ||
*/ | ||
|
||
import * as idbKeyval from 'idb-keyval' | ||
import * as persistClientCore from '@tanstack/query-persist-client-core' | ||
import * as queryCore from '@tanstack/query-core' | ||
import * as vueQuery from './vueQuery' | ||
|
||
declare module '@tanstack/query-core' { | ||
/** | ||
* Query client with additional methods. | ||
*/ | ||
interface QueryClient { | ||
/** | ||
* Clear the cache stored in Tanstack Query and the persister storage. | ||
* Please use this method with caution, as it will clear all cache data. | ||
* Usually you should use `queryClient.invalidateQueries` instead. | ||
*/ | ||
readonly clearWithPersister: () => Promise<void> | ||
/** | ||
* Clear the cache stored in the persister storage. | ||
*/ | ||
readonly nukePersister: () => Promise<void> | ||
} | ||
/** | ||
* Specifies the invalidation behavior of a mutation. | ||
*/ | ||
interface Register { | ||
readonly mutationMeta: { | ||
/** | ||
* List of query keys to invalidate when the mutation succeeds. | ||
*/ | ||
readonly invalidates?: queryCore.QueryKey[] | ||
/** | ||
* List of query keys to await invalidation before the mutation is considered successful. | ||
* | ||
* If `true`, all `invalidates` are awaited. | ||
* | ||
* If `false`, no invalidations are awaited. | ||
* | ||
* You can also provide an array of query keys to await. | ||
* | ||
* Queries that are not listed in invalidates will be ignored. | ||
* @default false | ||
*/ | ||
readonly awaitInvalidates?: queryCore.QueryKey[] | boolean | ||
} | ||
|
||
readonly queryMeta: { | ||
/** | ||
* Whether to persist the query cache in the storage. Defaults to `true`. | ||
* Use `false` to disable persistence for a specific query, for example for | ||
* a sensitive data or data that can't be persisted, e.g. class instances. | ||
* @default true | ||
*/ | ||
readonly persist?: boolean | ||
} | ||
} | ||
} | ||
|
||
/** Query Client type suitable for shared use in React and Vue. */ | ||
export type QueryClient = vueQuery.QueryClient | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
const DEFAULT_QUERY_STALE_TIME_MS = 2 * 60 * 1000 | ||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
const DEFAULT_QUERY_PERSIST_TIME_MS = 30 * 24 * 60 * 60 * 1000 // 30 days | ||
|
||
const DEFAULT_BUSTER = 'v1.1' | ||
|
||
/** | ||
* Create a new Tanstack Query client. | ||
*/ | ||
export function createQueryClient(): QueryClient { | ||
const store = idbKeyval.createStore('enso', 'query-persist-cache') | ||
queryCore.onlineManager.setOnline(navigator.onLine) | ||
|
||
const persister = persistClientCore.experimental_createPersister({ | ||
storage: { | ||
getItem: key => idbKeyval.get<persistClientCore.PersistedQuery>(key, store), | ||
setItem: (key, value) => idbKeyval.set(key, value, store), | ||
removeItem: key => idbKeyval.del(key, store), | ||
}, | ||
// Prefer online first and don't rely on the local cache if user is online | ||
// fallback to the local cache only if the user is offline | ||
maxAge: queryCore.onlineManager.isOnline() ? -1 : DEFAULT_QUERY_PERSIST_TIME_MS, | ||
buster: DEFAULT_BUSTER, | ||
filters: { predicate: query => query.meta?.persist !== false }, | ||
prefix: 'enso:query-persist:', | ||
serialize: persistedQuery => persistedQuery, | ||
deserialize: persistedQuery => persistedQuery, | ||
}) | ||
|
||
const queryClient: QueryClient = new vueQuery.QueryClient({ | ||
mutationCache: new queryCore.MutationCache({ | ||
onSuccess: (_data, _variables, _context, mutation) => { | ||
const shouldAwaitInvalidates = mutation.meta?.awaitInvalidates ?? false | ||
const invalidates = mutation.meta?.invalidates ?? [] | ||
const invalidatesToAwait = (() => { | ||
if (Array.isArray(shouldAwaitInvalidates)) { | ||
return shouldAwaitInvalidates | ||
} else { | ||
return shouldAwaitInvalidates ? invalidates : [] | ||
} | ||
})() | ||
const invalidatesToIgnore = invalidates.filter( | ||
queryKey => !invalidatesToAwait.includes(queryKey) | ||
) | ||
|
||
for (const queryKey of invalidatesToIgnore) { | ||
void queryClient.invalidateQueries({ | ||
predicate: query => queryCore.matchQuery({ queryKey }, query), | ||
}) | ||
} | ||
|
||
if (invalidatesToAwait.length > 0) { | ||
// eslint-disable-next-line no-restricted-syntax | ||
return Promise.all( | ||
invalidatesToAwait.map(queryKey => | ||
queryClient.invalidateQueries({ | ||
predicate: query => queryCore.matchQuery({ queryKey }, query), | ||
}) | ||
) | ||
) | ||
} | ||
}, | ||
}), | ||
defaultOptions: { | ||
queries: { | ||
persister, | ||
refetchOnReconnect: 'always', | ||
staleTime: DEFAULT_QUERY_STALE_TIME_MS, | ||
retry: (failureCount, error: unknown) => { | ||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
const statusesToIgnore = [401, 403, 404] | ||
const errorStatus = | ||
typeof error === 'object' && | ||
error != null && | ||
'status' in error && | ||
typeof error.status === 'number' | ||
? error.status | ||
: -1 | ||
|
||
if (statusesToIgnore.includes(errorStatus)) { | ||
return false | ||
} else { | ||
return failureCount < 3 | ||
} | ||
}, | ||
}, | ||
}, | ||
}) | ||
|
||
Object.defineProperty(queryClient, 'nukePersister', { | ||
value: () => idbKeyval.clear(store), | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}) | ||
|
||
Object.defineProperty(queryClient, 'clearWithPersister', { | ||
value: () => { | ||
queryClient.clear() | ||
return queryClient.nukePersister() | ||
}, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}) | ||
|
||
return queryClient | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/** @file QueryClient based on the '@tanstack/vue-query' implementation. */ | ||
|
||
import * as vueQuery from '@tanstack/vue-query' | ||
import * as queryCore from '@tanstack/query-core' | ||
import * as vue from 'vue' | ||
|
||
/** The QueryClient from vue-query, but with immediate query invalidation. */ | ||
export class QueryClient extends vueQuery.QueryClient { | ||
/** Like the `invalidateQueries` method of `vueQuery.QueryClient`, but invalidates queries immediately. */ | ||
// Workaround for https://github.com/TanStack/query/issues/7694 | ||
override invalidateQueries( | ||
filters: MaybeRefDeep<queryCore.InvalidateQueryFilters> = {}, | ||
options: MaybeRefDeep<queryCore.InvalidateOptions> = {} | ||
): Promise<void> { | ||
const filtersValue = cloneDeepUnref(filters) | ||
const optionsValue = cloneDeepUnref(options) | ||
queryCore.notifyManager.batch(() => { | ||
this.getQueryCache() | ||
.findAll(filtersValue) | ||
.forEach(query => { | ||
query.invalidate() | ||
}) | ||
}) | ||
if (filtersValue.refetchType === 'none') { | ||
return Promise.resolve() | ||
} else { | ||
const refetchType = filtersValue.refetchType | ||
return vue.nextTick(() => | ||
queryCore.notifyManager.batch(() => { | ||
const refetchFilters: queryCore.RefetchQueryFilters = { | ||
...filtersValue, | ||
type: refetchType ?? filtersValue.type ?? 'active', | ||
} | ||
return this.refetchQueries(refetchFilters, optionsValue) | ||
}) | ||
) | ||
} | ||
} | ||
} | ||
|
||
/* eslint-disable */ | ||
|
||
function isPlainObject(value: unknown): value is Object { | ||
if (Object.prototype.toString.call(value) !== '[object Object]') { | ||
return false | ||
} | ||
|
||
const prototype = Object.getPrototypeOf(value) | ||
return prototype === null || prototype === Object.prototype | ||
} | ||
|
||
function cloneDeep<T>( | ||
value: MaybeRefDeep<T>, | ||
customize?: (val: MaybeRefDeep<T>) => T | undefined | ||
): T { | ||
if (customize) { | ||
const result = customize(value) | ||
// If it's a ref of undefined, return undefined | ||
if (result === undefined && vue.isRef(value)) { | ||
return result as T | ||
} | ||
if (result !== undefined) { | ||
return result | ||
} | ||
} | ||
|
||
if (Array.isArray(value)) { | ||
return value.map(val => cloneDeep(val, customize)) as unknown as T | ||
} | ||
|
||
if (typeof value === 'object' && isPlainObject(value)) { | ||
const entries = Object.entries(value).map(([key, val]) => [key, cloneDeep(val, customize)]) | ||
return Object.fromEntries(entries) | ||
} | ||
|
||
return value as T | ||
} | ||
|
||
function cloneDeepUnref<T>(obj: MaybeRefDeep<T>): T { | ||
return cloneDeep(obj, val => { | ||
if (vue.isRef(val)) { | ||
return cloneDeepUnref(vue.unref(val)) | ||
} | ||
|
||
return undefined | ||
}) | ||
} | ||
|
||
type MaybeRefDeep<T> = vue.MaybeRef< | ||
T extends Function | ||
? T | ||
: T extends object | ||
? { | ||
[Property in keyof T]: MaybeRefDeep<T[Property]> | ||
} | ||
: T | ||
> | ||
|
||
/* eslint-enable */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.