From e23f9343fa5530b9e4175a8639909071cde860ba Mon Sep 17 00:00:00 2001 From: Niek Bosch Date: Wed, 29 Jul 2020 22:47:01 +0200 Subject: [PATCH] refactor: add typescript support (#767) --- .babelrc | 1 + .eslintrc | 18 +- .gitignore | 3 + docs/src/manifests/manifest.json | 5 + docs/src/pages/docs/typescript.md | 24 + package.json | 13 +- rollup.config.js | 32 +- src/core/config.js | 61 -- src/core/config.ts | 58 ++ src/core/index.js | 11 - src/core/index.ts | 9 + src/core/query.js | 493 ------------- src/core/query.ts | 681 ++++++++++++++++++ src/core/queryCache.js | 254 ------- src/core/queryCache.ts | 412 +++++++++++ src/core/queryInstance.js | 84 --- src/core/queryInstance.ts | 106 +++ ...{setFocusHandler.js => setFocusHandler.ts} | 13 +- ...queryCache.test.js => queryCache.test.tsx} | 42 +- src/core/tests/utils.js | 5 - .../tests/{utils.test.js => utils.test.tsx} | 2 + src/core/tests/utils.tsx | 5 + src/core/types.ts | 215 ++++++ src/core/utils.js | 154 ---- src/core/utils.ts | 200 +++++ src/index.ts | 1 + ...rovider.js => ReactQueryCacheProvider.tsx} | 17 +- ...ovider.js => ReactQueryConfigProvider.tsx} | 25 +- src/react/{index.js => index.ts} | 11 +- ...st.js => ReactQueryCacheProvider.test.tsx} | 5 +- ...t.js => ReactQueryConfigProvider.test.tsx} | 14 +- src/react/tests/{ssr.test.js => ssr.test.tsx} | 18 +- .../{suspense.test.js => suspense.test.tsx} | 20 +- ...uery.test.js => useInfiniteQuery.test.tsx} | 110 +-- ...etching.test.js => useIsFetching.test.tsx} | 0 ...eMutation.test.js => useMutation.test.tsx} | 30 +- ...ery.test.js => usePaginatedQuery.test.tsx} | 43 +- .../{useQuery.test.js => useQuery.test.tsx} | 270 ++++--- src/react/tests/utils.js | 5 - src/react/tests/utils.tsx | 24 + src/react/useBaseQuery.js | 49 -- src/react/useBaseQuery.ts | 70 ++ src/react/useInfiniteQuery.js | 16 - src/react/useInfiniteQuery.ts | 95 +++ .../{useIsFetching.js => useIsFetching.ts} | 2 +- src/react/useMutation.js | 143 ---- src/react/useMutation.ts | 200 +++++ src/react/usePaginatedQuery.js | 68 -- src/react/usePaginatedQuery.ts | 134 ++++ src/react/useQuery.js | 10 - src/react/useQuery.ts | 75 ++ src/react/useQueryArgs.js | 17 - src/react/useQueryArgs.ts | 22 + src/react/utils.js | 78 -- src/react/utils.ts | 61 ++ tsconfig.json | 18 + tsconfig.types.json | 11 + yarn.lock | 280 ++++++- 58 files changed, 3119 insertions(+), 1724 deletions(-) create mode 100644 docs/src/pages/docs/typescript.md delete mode 100644 src/core/config.js create mode 100644 src/core/config.ts delete mode 100644 src/core/index.js create mode 100644 src/core/index.ts delete mode 100644 src/core/query.js create mode 100644 src/core/query.ts delete mode 100644 src/core/queryCache.js create mode 100644 src/core/queryCache.ts delete mode 100644 src/core/queryInstance.js create mode 100644 src/core/queryInstance.ts rename src/core/{setFocusHandler.js => setFocusHandler.ts} (81%) rename src/core/tests/{queryCache.test.js => queryCache.test.tsx} (86%) delete mode 100644 src/core/tests/utils.js rename src/core/tests/{utils.test.js => utils.test.tsx} (98%) create mode 100644 src/core/tests/utils.tsx create mode 100644 src/core/types.ts delete mode 100644 src/core/utils.js create mode 100644 src/core/utils.ts create mode 100644 src/index.ts rename src/react/{ReactQueryCacheProvider.js => ReactQueryCacheProvider.tsx} (72%) rename src/react/{ReactQueryConfigProvider.js => ReactQueryConfigProvider.tsx} (67%) rename src/react/{index.js => index.ts} (52%) rename src/react/tests/{ReactQueryCacheProvider.test.js => ReactQueryCacheProvider.test.tsx} (96%) rename src/react/tests/{ReactQueryConfigProvider.test.js => ReactQueryConfigProvider.test.tsx} (94%) rename src/react/tests/{ssr.test.js => ssr.test.tsx} (93%) rename src/react/tests/{suspense.test.js => suspense.test.tsx} (89%) rename src/react/tests/{useInfiniteQuery.test.js => useInfiniteQuery.test.tsx} (74%) rename src/react/tests/{useIsFetching.test.js => useIsFetching.test.tsx} (100%) rename src/react/tests/{useMutation.test.js => useMutation.test.tsx} (85%) rename src/react/tests/{usePaginatedQuery.test.js => usePaginatedQuery.test.tsx} (87%) rename src/react/tests/{useQuery.test.js => useQuery.test.tsx} (76%) delete mode 100644 src/react/tests/utils.js create mode 100644 src/react/tests/utils.tsx delete mode 100644 src/react/useBaseQuery.js create mode 100644 src/react/useBaseQuery.ts delete mode 100644 src/react/useInfiniteQuery.js create mode 100644 src/react/useInfiniteQuery.ts rename src/react/{useIsFetching.js => useIsFetching.ts} (92%) delete mode 100644 src/react/useMutation.js create mode 100644 src/react/useMutation.ts delete mode 100644 src/react/usePaginatedQuery.js create mode 100644 src/react/usePaginatedQuery.ts delete mode 100644 src/react/useQuery.js create mode 100644 src/react/useQuery.ts delete mode 100644 src/react/useQueryArgs.js create mode 100644 src/react/useQueryArgs.ts delete mode 100644 src/react/utils.js create mode 100644 src/react/utils.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.types.json diff --git a/.babelrc b/.babelrc index c57c091a14..0ff1ff4eeb 100644 --- a/.babelrc +++ b/.babelrc @@ -8,6 +8,7 @@ "exclude": ["@babel/plugin-transform-regenerator"] } ], + "@babel/preset-typescript", "@babel/react" ], "plugins": ["babel-plugin-transform-async-to-promises"], diff --git a/.eslintrc b/.eslintrc index 05544ddde0..71ebf1b4df 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,24 @@ { - "parser": "babel-eslint", - "extends": ["react-app", "prettier"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "react-app", + "prettier" + ], "env": { "es6": true }, "parserOptions": { "sourceType": "module" + }, + "rules": { + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off" } } diff --git a/.gitignore b/.gitignore index 233bcb077e..ab17e5b79c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ yarn-error.log* size-plugin.json stats.html .vscode/settings.json + +!/types/index.d.ts +types/**/*.ts diff --git a/docs/src/manifests/manifest.json b/docs/src/manifests/manifest.json index eb522afffb..4cbe6e9ecb 100644 --- a/docs/src/manifests/manifest.json +++ b/docs/src/manifests/manifest.json @@ -37,6 +37,11 @@ "title": "Comparison", "path": "/docs/comparison", "editUrl": "/docs/comparison.md" + }, + { + "title": "TypeScript", + "path": "/docs/typescript", + "editUrl": "/docs/typescript.md" } ] }, diff --git a/docs/src/pages/docs/typescript.md b/docs/src/pages/docs/typescript.md new file mode 100644 index 0000000000..7eacaa03c7 --- /dev/null +++ b/docs/src/pages/docs/typescript.md @@ -0,0 +1,24 @@ +--- +id: typescript +title: TypeScript +--- + +React Query is now written in **TypeScript** to make sure the library and your projects are type-safe! + +## Migration + +React Query is currently typed with an external type definition file, which unfortunately often gets out of sync with the actual code. + +This is one of the reasons why the library has been migrated to TypeScript. + +But before exposing the new types, we first want to get your feedback on it! + +Install the `tsnext` tag to get the latest React Query with the new types: + +```sh +npm install react-query@tsnext --save +``` + +## Changes + +- The query results are no longer discriminated unions, which means you have to check the actual `data` and `error` properties. diff --git a/package.json b/package.json index c95552ebbe..dbb7efff01 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,12 @@ "sideEffects": false, "scripts": { "test": "is-ci \"test:ci\" \"test:dev\"", - "test:dev": "jest --watch", - "test:ci": "jest && yarn dtslint", + "test:dev": "npm run test:types && npm run test:eslint && jest --watch", + "test:ci": "npm run test:types && npm run test:eslint && jest && yarn dtslint", "test:coverage": "yarn test:ci; open coverage/lcov-report/index.html", + "test:types": "tsc", + "test:eslint": "eslint --ext .ts,.tsx ./src", + "gen:types": "tsc --project ./tsconfig.types.json", "build": "NODE_ENV=production rollup -c", "now-build": "yarn && cd www && yarn && yarn build", "start": "rollup -c -w", @@ -53,10 +56,14 @@ "@babel/core": "^7.10.2", "@babel/preset-env": "^7.10.2", "@babel/preset-react": "^7.10.1", + "@babel/preset-typescript": "^7.10.4", "@rollup/plugin-replace": "^2.3.3", "@svgr/rollup": "^5.4.0", - "@testing-library/react": "^10.2.1", + "@testing-library/react": "^10.4.7", + "@types/jest": "^26.0.4", "@types/react": "^16.9.41", + "@typescript-eslint/eslint-plugin": "^3.6.1", + "@typescript-eslint/parser": "^3.6.1", "babel-eslint": "^10.1.0", "babel-jest": "^26.0.1", "babel-plugin-transform-async-to-promises": "^0.8.15", diff --git a/rollup.config.js b/rollup.config.js index 5641c3d355..f54fa83c7e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -13,7 +13,11 @@ const globals = { react: 'React', } -const inputSrc = 'src/react/index.js' +const inputSrc = 'src/index.ts' + +const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'] +const babelConfig = { extensions } +const resolveConfig = { extensions } export default [ { @@ -24,7 +28,12 @@ export default [ sourcemap: true, }, external, - plugins: [resolve(), babel(), commonJS(), externalDeps()], + plugins: [ + resolve(resolveConfig), + babel(babelConfig), + commonJS(), + externalDeps(), + ], }, { input: inputSrc, @@ -34,7 +43,13 @@ export default [ sourcemap: true, }, external, - plugins: [resolve(), babel(), commonJS(), externalDeps(), terser()], + plugins: [ + resolve(resolveConfig), + babel(babelConfig), + commonJS(), + externalDeps(), + terser(), + ], }, { input: inputSrc, @@ -46,7 +61,12 @@ export default [ globals, }, external, - plugins: [resolve(), babel(), commonJS(), externalDeps()], + plugins: [ + resolve(resolveConfig), + babel(babelConfig), + commonJS(), + externalDeps(), + ], }, { input: inputSrc, @@ -60,8 +80,8 @@ export default [ external, plugins: [ replace({ 'process.env.NODE_ENV': `"production"`, delimiters: ['', ''] }), - resolve(), - babel(), + resolve(resolveConfig), + babel(babelConfig), commonJS(), externalDeps(), terser(), diff --git a/src/core/config.js b/src/core/config.js deleted file mode 100644 index 9222c5c808..0000000000 --- a/src/core/config.js +++ /dev/null @@ -1,61 +0,0 @@ -import { noop, stableStringify, identity, deepEqual } from './utils' - -export const DEFAULT_CONFIG = { - shared: { - suspense: false, - }, - queries: { - queryKeySerializerFn: defaultQueryKeySerializerFn, - queryFn: undefined, - initialStale: undefined, - enabled: true, - retry: 3, - retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), - staleTime: 0, - cacheTime: 5 * 60 * 1000, - refetchOnWindowFocus: true, - refetchInterval: false, - queryFnParamsFilter: identity, - refetchOnMount: true, - isDataEqual: deepEqual, - onError: noop, - onSuccess: noop, - onSettled: noop, - useErrorBoundary: false, - }, - mutations: { - throwOnError: false, - onMutate: noop, - onError: noop, - onSuccess: noop, - onSettled: noop, - useErrorBoundary: false, - }, -} - -export const defaultConfigRef = { - current: DEFAULT_CONFIG, -} - -export function defaultQueryKeySerializerFn(queryKey) { - if (!queryKey) { - return [] - } - - if (!Array.isArray(queryKey)) { - queryKey = [queryKey] - } - - if (queryKey.some(d => typeof d === 'function')) { - throw new Error('A valid query key is required!') - } - - const queryHash = stableStringify(queryKey) - queryKey = JSON.parse(queryHash) - - if (!queryHash) { - return [] - } - - return [queryHash, queryKey] -} diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 0000000000..cb37fd94d4 --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,58 @@ +import { stableStringify, identity, deepEqual } from './utils' +import { + ArrayQueryKey, + QueryKey, + QueryKeySerializerFunction, + ReactQueryConfig, +} from './types' + +// TYPES + +export interface ReactQueryConfigRef { + current: ReactQueryConfig +} + +// CONFIG + +export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = ( + queryKey: QueryKey +): [string, ArrayQueryKey] => { + try { + let arrayQueryKey: ArrayQueryKey = Array.isArray(queryKey) + ? queryKey + : [queryKey] + const queryHash = stableStringify(arrayQueryKey) + arrayQueryKey = JSON.parse(queryHash) + return [queryHash, arrayQueryKey] + } catch { + throw new Error('A valid query key is required!') + } +} + +export const DEFAULT_CONFIG: ReactQueryConfig = { + shared: { + suspense: false, + }, + queries: { + queryKeySerializerFn: defaultQueryKeySerializerFn, + enabled: true, + retry: 3, + retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), + staleTime: 0, + cacheTime: 5 * 60 * 1000, + refetchOnWindowFocus: true, + refetchInterval: false, + queryFnParamsFilter: identity, + refetchOnMount: true, + isDataEqual: deepEqual, + useErrorBoundary: false, + }, + mutations: { + throwOnError: false, + useErrorBoundary: false, + }, +} + +export const defaultConfigRef: ReactQueryConfigRef = { + current: DEFAULT_CONFIG, +} diff --git a/src/core/index.js b/src/core/index.js deleted file mode 100644 index 83075984da..0000000000 --- a/src/core/index.js +++ /dev/null @@ -1,11 +0,0 @@ -export { queryCache, queryCaches, makeQueryCache } from './queryCache' -export { setFocusHandler } from './setFocusHandler' -export { - statusIdle, - statusLoading, - statusSuccess, - statusError, - stableStringify, - setConsole, - deepIncludes, -} from './utils' diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000000..a87a2ebc20 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,9 @@ +export { queryCache, queryCaches, makeQueryCache } from './queryCache' +export { setFocusHandler } from './setFocusHandler' +export { stableStringify, setConsole, deepIncludes } from './utils' + +// Types +export * from './types' +export type { Query } from './query' +export type { QueryCache } from './queryCache' +export type { ConsoleObject } from './utils' diff --git a/src/core/query.js b/src/core/query.js deleted file mode 100644 index c7121b1abc..0000000000 --- a/src/core/query.js +++ /dev/null @@ -1,493 +0,0 @@ -import { - isServer, - functionalUpdate, - cancelledError, - isDocumentVisible, - statusLoading, - statusSuccess, - statusError, - noop, - statusIdle, - Console, - getStatusBools, - shallowEqual, -} from './utils' -import { makeQueryInstance } from './queryInstance' - -const actionInit = 'Init' -const actionFailed = 'Failed' -const actionMarkStale = 'MarkStale' -const actionMarkGC = 'MarkGC' -const actionFetch = 'Fetch' -const actionSuccess = 'Success' -const actionError = 'Error' -const actionSetState = 'SetState' - -export function makeQuery({ - queryCache, - queryKey, - queryHash, - config, - notifyGlobalListeners, -}) { - const initialData = - typeof config.initialData === 'function' - ? config.initialData() - : config.initialData - - const hasInitialData = typeof initialData !== 'undefined' - - const isStale = - !config.enabled || - (typeof config.initialStale === 'function' - ? config.initialStale() - : config.initialStale ?? !hasInitialData) - - const initialStatus = hasInitialData - ? statusSuccess - : config.enabled - ? statusLoading - : statusIdle - - let query = { - queryKey, - queryHash, - config, - instances: [], - state: queryReducer(undefined, { - type: actionInit, - initialStatus, - initialData, - hasInitialData, - isStale, - }), - } - - query.dispatch = action => { - const newState = queryReducer(query.state, action) - - // Only update state if something has changed - if (!shallowEqual(query.state, newState)) { - query.state = newState - query.instances.forEach(d => d.onStateUpdate(query.state)) - notifyGlobalListeners(query) - } - } - - query.scheduleStaleTimeout = () => { - if (isServer) return - clearTimeout(query.staleTimeout) - - if (query.config.staleTime === Infinity) { - return - } - - query.staleTimeout = setTimeout(() => { - if (queryCache.getQuery(query.queryKey)) { - query.invalidate() - } - }, query.config.staleTime) - } - - query.invalidate = () => { - clearTimeout(query.staleTimeout) - query.dispatch({ type: actionMarkStale }) - } - - query.scheduleGarbageCollection = () => { - if (!queryCache.queries[query.queryHash]) return - if (query.config.cacheTime === Infinity) { - return - } - query.dispatch({ type: actionMarkGC }) - query.cacheTimeout = setTimeout( - () => { - queryCache.removeQueries( - d => - d.state.markedForGarbageCollection && - d.queryHash === query.queryHash - ) - }, - typeof query.state.data === 'undefined' && query.state.status !== 'error' - ? 0 - : query.config.cacheTime - ) - } - - query.refetch = async () => { - try { - await query.fetch() - } catch (error) { - Console.error(error) - } - } - - query.heal = () => { - // Stop the query from being garbage collected - clearTimeout(query.cacheTimeout) - - // Mark the query as not cancelled - query.cancelled = null - } - - query.cancel = () => { - query.cancelled = cancelledError - - if (query.cancelPromises) { - query.cancelPromises() - } - - delete query.promise - } - - query.clearIntervals = () => { - query.instances.forEach(instance => { - instance.clearInterval() - }) - } - - query.setState = updater => query.dispatch({ type: actionSetState, updater }) - - query.setData = updater => { - const isStale = query.config.staleTime === 0 - - // Set data and mark it as cached - query.dispatch({ - type: actionSuccess, - updater, - isStale, - }) - - if (!isStale) { - // Schedule a fresh invalidation! - query.scheduleStaleTimeout() - } - } - - query.clear = () => { - clearTimeout(query.staleTimeout) - clearTimeout(query.cacheTimeout) - clearTimeout(query.retryTimeout) - query.clearIntervals() - query.cancel() - query.dispatch = noop - delete queryCache.queries[query.queryHash] - notifyGlobalListeners(query) - } - - query.subscribe = (onStateUpdate = noop) => { - const instance = makeQueryInstance(query, onStateUpdate) - query.instances.push(instance) - query.heal() - return instance - } - - // Set up the core fetcher function - const tryFetchData = async (fn, ...args) => { - try { - // Perform the query - const promiseOrValue = fn(...query.config.queryFnParamsFilter(args)) - - query.cancelPromises = () => promiseOrValue?.cancel?.() - - const data = await promiseOrValue - delete query.shouldContinueRetryOnFocus - - delete query.cancelPromises - if (query.cancelled) throw query.cancelled - - return data - } catch (error) { - delete query.cancelPromises - if (query.cancelled) throw query.cancelled - - // Do we need to retry the request? - if ( - query.config.retry === true || - query.state.failureCount < query.config.retry || - (typeof query.config.retry === 'function' && - query.config.retry(query.state.failureCount, error)) - ) { - // If we retry, increase the failureCount - query.dispatch({ type: actionFailed }) - - // Only retry if the document is visible - if (!isDocumentVisible()) { - // set this flag to continue retries on focus - query.shouldContinueRetryOnFocus = true - // Resolve a - return new Promise(noop) - } - - delete query.shouldContinueRetryOnFocus - - // Determine the retryDelay - const delay = functionalUpdate( - query.config.retryDelay, - query.state.failureCount - ) - - // Return a new promise with the retry - return await new Promise((resolve, reject) => { - // Keep track of the retry timeout - query.retryTimeout = setTimeout(async () => { - if (query.cancelled) return reject(query.cancelled) - - try { - const data = await tryFetchData(fn, ...args) - if (query.cancelled) return reject(query.cancelled) - resolve(data) - } catch (error) { - if (query.cancelled) return reject(query.cancelled) - reject(error) - } - }, delay) - }) - } - - throw error - } - } - - query.fetch = async ({ fetchMore } = {}) => { - let queryFn = query.config.queryFn - - if (!queryFn) { - return - } - - if (query.config.infinite) { - const originalQueryFn = queryFn - - queryFn = async () => { - const data = [] - const pageVariables = [...query.pageVariables] - const rebuiltPageVariables = [] - - do { - const args = pageVariables.shift() - - if (!data.length) { - // the first page query doesn't need to be rebuilt - data.push(await originalQueryFn(...args)) - rebuiltPageVariables.push(args) - } else { - // get an up-to-date cursor based on the previous data set - - const nextCursor = query.config.getFetchMore( - data[data.length - 1], - data - ) - - // break early if there's no next cursor - // otherwise we'll start from the beginning - // which will cause unwanted duplication - if (!nextCursor) { - break - } - - const pageArgs = [ - // remove the last argument (the previously saved cursor) - ...args.slice(0, -1), - nextCursor, - ] - - data.push(await originalQueryFn(...pageArgs)) - rebuiltPageVariables.push(pageArgs) - } - } while (pageVariables.length) - - query.state.canFetchMore = query.config.getFetchMore( - data[data.length - 1], - data - ) - query.pageVariables = rebuiltPageVariables - - return data - } - - if (fetchMore) { - queryFn = async (...args) => { - const { fetchMoreInfo, previous } = fetchMore - try { - query.setState(old => ({ - ...old, - isFetchingMore: previous ? 'previous' : 'next', - })) - - const newArgs = [...args, fetchMoreInfo] - - query.pageVariables[previous ? 'unshift' : 'push'](newArgs) - - const newData = await originalQueryFn(...newArgs) - - const data = previous - ? [newData, ...query.state.data] - : [...query.state.data, newData] - - query.state.canFetchMore = query.config.getFetchMore(newData, data) - - return data - } finally { - query.setState(old => ({ - ...old, - isFetchingMore: false, - })) - } - } - } - } - - // Create a new promise for the query cache if necessary - if (!query.promise) { - query.promise = (async () => { - // If there are any retries pending for this query, kill them - query.cancelled = null - - const getCallbackInstances = () => { - const callbackInstances = [...query.instances] - - if (query.wasSuspended) { - callbackInstances.unshift(query.fallbackInstance) - } - return callbackInstances - } - - try { - // Set up the query refreshing state - query.dispatch({ type: actionFetch }) - - // Try to get the data - let data = await tryFetchData(queryFn, ...query.queryKey) - - query.setData(old => - query.config.isDataEqual(old, data) ? old : data - ) - - getCallbackInstances().forEach( - instance => - instance.config.onSuccess && - instance.config.onSuccess(query.state.data) - ) - - getCallbackInstances().forEach( - instance => - instance.config.onSettled && - instance.config.onSettled(query.state.data, null) - ) - - delete query.promise - - return data - } catch (error) { - query.dispatch({ - type: actionError, - cancelled: error === query.cancelled, - error, - }) - - delete query.promise - - if (error !== query.cancelled) { - getCallbackInstances().forEach( - instance => - instance.config.onError && instance.config.onError(error) - ) - - getCallbackInstances().forEach( - instance => - instance.config.onSettled && - instance.config.onSettled(undefined, error) - ) - - throw error - } - } - })() - } - - return query.promise - } - - if (query.config.infinite) { - query.fetchMore = ( - fetchMoreInfo = query.state.canFetchMore, - { previous = false } = {} - ) => query.fetch({ fetchMore: { fetchMoreInfo, previous } }) - } - - return query -} - -export function queryReducer(state, action) { - const newState = switchActions(state, action) - - return Object.assign(newState, getStatusBools(newState.status)) -} - -function switchActions(state, action) { - switch (action.type) { - case actionInit: - return { - status: action.initialStatus, - error: null, - isFetching: action.initialStatus === 'loading', - failureCount: 0, - isStale: action.isStale, - markedForGarbageCollection: false, - data: action.initialData, - updatedAt: action.hasInitialData ? Date.now() : 0, - } - case actionFailed: - return { - ...state, - failureCount: state.failureCount + 1, - } - case actionMarkStale: - return { - ...state, - isStale: true, - } - case actionMarkGC: { - return { - ...state, - markedForGarbageCollection: true, - } - } - case actionFetch: - return { - ...state, - status: - typeof state.data !== 'undefined' ? statusSuccess : statusLoading, - isFetching: true, - failureCount: 0, - } - case actionSuccess: - return { - ...state, - status: statusSuccess, - data: functionalUpdate(action.updater, state.data), - error: null, - isStale: action.isStale, - isFetching: false, - updatedAt: Date.now(), - failureCount: 0, - } - case actionError: - return { - ...state, - failureCount: state.failureCount + 1, - isFetching: false, - isStale: true, - ...(!action.cancelled && { - status: statusError, - error: action.error, - throwInErrorBoundary: true, - }), - } - case actionSetState: - return functionalUpdate(action.updater, state) - default: - throw new Error() - } -} diff --git a/src/core/query.ts b/src/core/query.ts new file mode 100644 index 0000000000..e9fe36011a --- /dev/null +++ b/src/core/query.ts @@ -0,0 +1,681 @@ +import { + isServer, + functionalUpdate, + cancelledError, + isDocumentVisible, + noop, + Console, + getStatusProps, + shallowEqual, + Updater, +} from './utils' +import { QueryInstance, OnStateUpdateFunction } from './queryInstance' +import { + ArrayQueryKey, + InfiniteQueryConfig, + InitialDataFunction, + IsFetchingMoreValue, + QueryConfig, + QueryFunction, + QueryStatus, +} from './types' +import { QueryCache } from './queryCache' + +// TYPES + +interface QueryInitConfig { + queryCache: QueryCache + queryKey: ArrayQueryKey + queryHash: string + config: QueryConfig + notifyGlobalListeners: (query: Query) => void +} + +export interface QueryState { + canFetchMore?: boolean + data?: TResult + error: TError | null + failureCount: number + isError: boolean + isFetching: boolean + isFetchingMore?: IsFetchingMoreValue + isIdle: boolean + isLoading: boolean + isStale: boolean + isSuccess: boolean + markedForGarbageCollection: boolean + status: QueryStatus + throwInErrorBoundary?: boolean + updatedAt: number +} + +interface FetchOptions { + fetchMore?: FetchMoreOptions +} + +export interface FetchMoreOptions { + fetchMoreVariable?: unknown + previous: boolean +} + +enum ActionType { + Failed = 'Failed', + MarkStale = 'MarkStale', + MarkGC = 'MarkGC', + Fetch = 'Fetch', + Success = 'Success', + Error = 'Error', + SetState = 'SetState', +} + +interface FailedAction { + type: ActionType.Failed +} + +interface MarkStaleAction { + type: ActionType.MarkStale +} + +interface MarkGCAction { + type: ActionType.MarkGC +} + +interface FetchAction { + type: ActionType.Fetch +} + +interface SuccessAction { + type: ActionType.Success + updater: Updater + isStale: boolean +} + +interface ErrorAction { + type: ActionType.Error + cancelled: boolean + error: TError +} + +interface SetStateAction { + type: ActionType.SetState + updater: Updater, QueryState> +} + +type Action = + | ErrorAction + | FailedAction + | FetchAction + | MarkGCAction + | MarkStaleAction + | SetStateAction + | SuccessAction + +// CLASS + +export class Query { + queryCache: QueryCache + queryKey: ArrayQueryKey + queryHash: string + config: QueryConfig + instances: QueryInstance[] + state: QueryState + fallbackInstance?: QueryInstance + wasSuspended?: boolean + shouldContinueRetryOnFocus?: boolean + promise?: Promise + + private fetchMoreVariable?: unknown + private pageVariables?: ArrayQueryKey[] + private cacheTimeout?: number + private retryTimeout?: number + private staleTimeout?: number + private cancelPromises?: () => void + private cancelled?: typeof cancelledError | null + private notifyGlobalListeners: (query: Query) => void + + constructor(init: QueryInitConfig) { + this.config = init.config + this.queryCache = init.queryCache + this.queryKey = init.queryKey + this.queryHash = init.queryHash + this.notifyGlobalListeners = init.notifyGlobalListeners + this.instances = [] + this.state = getDefaultState(init.config) + + if (init.config.infinite) { + const infiniteConfig = init.config as InfiniteQueryConfig + const infiniteData = (this.state.data as unknown) as TResult[] | undefined + + if ( + typeof infiniteData !== 'undefined' && + typeof this.state.canFetchMore === 'undefined' + ) { + this.fetchMoreVariable = infiniteConfig.getFetchMore( + infiniteData[infiniteData.length - 1], + infiniteData + ) + this.state.canFetchMore = this.fetchMoreVariable !== false + } + + // Here we seed the pageVariabes for the query + if (!this.pageVariables) { + this.pageVariables = [[...this.queryKey]] + } + } + } + + private dispatch(action: Action): void { + const newState = queryReducer(this.state, action) + + // Only update state if something has changed + if (!shallowEqual(this.state, newState)) { + this.state = newState + this.instances.forEach(d => d.onStateUpdate?.(this.state)) + this.notifyGlobalListeners(this) + } + } + + scheduleStaleTimeout(): void { + if (isServer) { + return + } + + this.clearStaleTimeout() + + if (this.state.isStale) { + return + } + + if (this.config.staleTime === Infinity) { + return + } + + this.staleTimeout = setTimeout(() => { + this.invalidate() + }, this.config.staleTime) + } + + invalidate(): void { + this.clearStaleTimeout() + + if (!this.queryCache.queries[this.queryHash]) { + return + } + + if (this.state.isStale) { + return + } + + this.dispatch({ type: ActionType.MarkStale }) + } + + scheduleGarbageCollection(): void { + this.clearCacheTimeout() + + if (!this.queryCache.queries[this.queryHash]) { + return + } + + if (this.config.cacheTime === Infinity) { + return + } + + this.dispatch({ type: ActionType.MarkGC }) + + this.cacheTimeout = setTimeout( + () => { + this.queryCache.removeQueries( + d => + d.state.markedForGarbageCollection && d.queryHash === this.queryHash + ) + }, + typeof this.state.data === 'undefined' && + this.state.status !== QueryStatus.Error + ? 0 + : this.config.cacheTime + ) + } + + async refetch(): Promise { + try { + await this.fetch() + } catch (error) { + Console.error(error) + } + } + + heal(): void { + // Stop the query from being garbage collected + this.clearCacheTimeout() + + // Mark the query as not cancelled + this.cancelled = null + } + + cancel(): void { + this.cancelled = cancelledError + + if (this.cancelPromises) { + this.cancelPromises() + } + + delete this.promise + } + + clearIntervals(): void { + this.instances.forEach(instance => { + instance.clearInterval() + }) + } + + private clearStaleTimeout() { + if (this.staleTimeout) { + clearTimeout(this.staleTimeout) + this.staleTimeout = undefined + } + } + + private clearCacheTimeout() { + if (this.cacheTimeout) { + clearTimeout(this.cacheTimeout) + this.cacheTimeout = undefined + } + } + + private clearRetryTimeout() { + if (this.retryTimeout) { + clearTimeout(this.retryTimeout) + this.retryTimeout = undefined + } + } + + private setState( + updater: Updater, QueryState> + ): void { + this.dispatch({ type: ActionType.SetState, updater }) + } + + setData(updater: Updater): void { + const isStale = this.config.staleTime === 0 + // Set data and mark it as cached + this.dispatch({ + type: ActionType.Success, + updater, + isStale, + }) + + if (!isStale) { + // Schedule a fresh invalidation! + this.scheduleStaleTimeout() + } + } + + clear(): void { + this.clearStaleTimeout() + this.clearCacheTimeout() + this.clearRetryTimeout() + this.clearIntervals() + this.cancel() + delete this.queryCache.queries[this.queryHash] + this.notifyGlobalListeners(this) + } + + subscribe( + onStateUpdate?: OnStateUpdateFunction + ): QueryInstance { + const instance = new QueryInstance(this, onStateUpdate) + this.instances.push(instance) + this.heal() + return instance + } + + // Set up the core fetcher function + private async tryFetchData( + fn: QueryFunction, + args: ArrayQueryKey + ): Promise { + try { + // Perform the query + const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args)) + + this.cancelPromises = () => (promiseOrValue as any)?.cancel?.() + + const data = await promiseOrValue + delete this.shouldContinueRetryOnFocus + + delete this.cancelPromises + if (this.cancelled) throw this.cancelled + + return data + } catch (error) { + delete this.cancelPromises + if (this.cancelled) throw this.cancelled + + // Do we need to retry the request? + if ( + this.config.retry === true || + this.state.failureCount < this.config.retry! || + (typeof this.config.retry === 'function' && + this.config.retry(this.state.failureCount, error)) + ) { + // If we retry, increase the failureCount + this.dispatch({ type: ActionType.Failed }) + + // Only retry if the document is visible + if (!isDocumentVisible()) { + // set this flag to continue retries on focus + this.shouldContinueRetryOnFocus = true + // Resolve a + return new Promise(noop) + } + + delete this.shouldContinueRetryOnFocus + + // Determine the retryDelay + const delay = functionalUpdate( + this.config.retryDelay, + this.state.failureCount + ) + + // Return a new promise with the retry + return await new Promise((resolve, reject) => { + // Keep track of the retry timeout + this.retryTimeout = setTimeout(async () => { + if (this.cancelled) return reject(this.cancelled) + + try { + const data = await this.tryFetchData(fn, args) + if (this.cancelled) return reject(this.cancelled) + resolve(data) + } catch (error) { + if (this.cancelled) return reject(this.cancelled) + reject(error) + } + }, delay) + }) + } + + throw error + } + } + + async fetch(options?: FetchOptions): Promise { + let queryFn = this.config.queryFn + + if (!queryFn) { + return + } + + // If we are already fetching, return current promise + if (this.promise) { + return this.promise + } + + if (this.config.infinite) { + const infiniteConfig = this.config as InfiniteQueryConfig + const infiniteData = (this.state.data as unknown) as TResult[] | undefined + const fetchMore = options?.fetchMore + + const originalQueryFn = queryFn + + queryFn = async () => { + const data: TResult[] = [] + const pageVariables = this.pageVariables ? [...this.pageVariables] : [] + const rebuiltPageVariables: ArrayQueryKey[] = [] + + do { + const args = pageVariables.shift()! + + if (!data.length) { + // the first page query doesn't need to be rebuilt + data.push(await originalQueryFn(...args)) + rebuiltPageVariables.push(args) + } else { + // get an up-to-date cursor based on the previous data set + + const nextCursor = infiniteConfig.getFetchMore( + data[data.length - 1], + data + ) + + // break early if there's no next cursor + // otherwise we'll start from the beginning + // which will cause unwanted duplication + if (!nextCursor) { + break + } + + const pageArgs = [ + // remove the last argument (the previously saved cursor) + ...args.slice(0, -1), + nextCursor, + ] as ArrayQueryKey + + data.push(await originalQueryFn(...pageArgs)) + rebuiltPageVariables.push(pageArgs) + } + } while (pageVariables.length) + + this.fetchMoreVariable = infiniteConfig.getFetchMore( + data[data.length - 1], + data + ) + this.state.canFetchMore = this.fetchMoreVariable !== false + this.pageVariables = rebuiltPageVariables + + return (data as unknown) as TResult + } + + if (fetchMore) { + queryFn = async (...args: ArrayQueryKey) => { + try { + const { fetchMoreVariable, previous } = fetchMore + + this.setState(old => ({ + ...old, + isFetchingMore: previous ? 'previous' : 'next', + })) + + const newArgs = [...args, fetchMoreVariable] as ArrayQueryKey + + if (this.pageVariables) { + this.pageVariables[previous ? 'unshift' : 'push'](newArgs) + } else { + this.pageVariables = [newArgs] + } + + const newData = await originalQueryFn(...newArgs) + + let data + + if (!infiniteData) { + data = [newData] + } else if (previous) { + data = [newData, ...infiniteData] + } else { + data = [...infiniteData, newData] + } + + this.fetchMoreVariable = infiniteConfig.getFetchMore(newData, data) + this.state.canFetchMore = this.fetchMoreVariable !== false + + return (data as unknown) as TResult + } finally { + this.setState(old => ({ + ...old, + isFetchingMore: false, + })) + } + } + } + } + + this.promise = (async () => { + // If there are any retries pending for this query, kill them + this.cancelled = null + + const getCallbackInstances = () => { + const callbackInstances = [...this.instances] + + if (this.wasSuspended && this.fallbackInstance) { + callbackInstances.unshift(this.fallbackInstance) + } + return callbackInstances + } + + try { + // Set up the query refreshing state + this.dispatch({ type: ActionType.Fetch }) + + // Try to get the data + const data = await this.tryFetchData(queryFn!, this.queryKey) + + this.setData(old => (this.config.isDataEqual!(old, data) ? old! : data)) + + getCallbackInstances().forEach(instance => { + instance.config.onSuccess?.(this.state.data!) + }) + + getCallbackInstances().forEach(instance => + instance.config.onSettled?.(this.state.data, null) + ) + + delete this.promise + + return data + } catch (error) { + this.dispatch({ + type: ActionType.Error, + cancelled: error === this.cancelled, + error, + }) + + delete this.promise + + if (error !== this.cancelled) { + getCallbackInstances().forEach(instance => + instance.config.onError?.(error) + ) + + getCallbackInstances().forEach(instance => + instance.config.onSettled?.(undefined, error) + ) + + throw error + } + + return + } + })() + + return this.promise + } + + fetchMore( + fetchMoreVariable?: unknown, + options?: FetchMoreOptions + ): Promise { + return this.fetch({ + fetchMore: { + fetchMoreVariable: fetchMoreVariable ?? this.fetchMoreVariable, + previous: options?.previous || false, + }, + }) + } +} + +function getDefaultState( + config: QueryConfig +): QueryState { + const initialData = + typeof config.initialData === 'function' + ? (config.initialData as InitialDataFunction)() + : config.initialData + + const hasInitialData = typeof initialData !== 'undefined' + + const isStale = + !config.enabled || + (typeof config.initialStale === 'function' + ? config.initialStale() + : config.initialStale ?? !hasInitialData) + + const initialStatus = hasInitialData + ? QueryStatus.Success + : config.enabled + ? QueryStatus.Loading + : QueryStatus.Idle + + return { + ...getStatusProps(initialStatus), + error: null, + isFetching: initialStatus === QueryStatus.Loading, + failureCount: 0, + isStale, + markedForGarbageCollection: false, + data: initialData, + updatedAt: hasInitialData ? Date.now() : 0, + } +} + +export function queryReducer( + state: QueryState, + action: Action +): QueryState { + switch (action.type) { + case ActionType.Failed: + return { + ...state, + failureCount: state.failureCount + 1, + } + case ActionType.MarkStale: + return { + ...state, + isStale: true, + } + case ActionType.MarkGC: { + return { + ...state, + markedForGarbageCollection: true, + } + } + case ActionType.Fetch: + const status = + typeof state.data !== 'undefined' + ? QueryStatus.Success + : QueryStatus.Loading + return { + ...state, + ...getStatusProps(status), + isFetching: true, + failureCount: 0, + } + case ActionType.Success: + return { + ...state, + ...getStatusProps(QueryStatus.Success), + data: functionalUpdate(action.updater, state.data), + error: null, + isStale: action.isStale, + isFetching: false, + updatedAt: Date.now(), + failureCount: 0, + } + case ActionType.Error: + return { + ...state, + failureCount: state.failureCount + 1, + isFetching: false, + isStale: true, + ...(!action.cancelled && { + ...getStatusProps(QueryStatus.Error), + error: action.error, + throwInErrorBoundary: true, + }), + } + case ActionType.SetState: + return functionalUpdate(action.updater, state) + default: + return state + } +} diff --git a/src/core/queryCache.js b/src/core/queryCache.js deleted file mode 100644 index 546ecd3c45..0000000000 --- a/src/core/queryCache.js +++ /dev/null @@ -1,254 +0,0 @@ -import { - isServer, - getQueryArgs, - deepIncludes, - Console, - isObject, -} from './utils' -import { defaultConfigRef } from './config' -import { makeQuery } from './query' - -export const queryCache = makeQueryCache({ frozen: isServer }) - -export const queryCaches = [queryCache] - -export function makeQueryCache({ frozen = false, defaultConfig } = {}) { - // A frozen cache does not add new queries to the cache - const globalListeners = [] - - const configRef = defaultConfig - ? { - current: { - shared: { - ...defaultConfigRef.current.shared, - ...defaultConfig.shared, - }, - queries: { - ...defaultConfigRef.current.queries, - ...defaultConfig.queries, - }, - mutations: { - ...defaultConfigRef.current.mutations, - ...defaultConfig.mutations, - }, - }, - } - : defaultConfigRef - - const queryCache = { - queries: {}, - isFetching: 0, - } - - const notifyGlobalListeners = query => { - queryCache.isFetching = Object.values(queryCache.queries).reduce( - (acc, query) => (query.state.isFetching ? acc + 1 : acc), - 0 - ) - - globalListeners.forEach(d => d(queryCache, query)) - } - - queryCache.subscribe = cb => { - globalListeners.push(cb) - return () => { - globalListeners.splice(globalListeners.indexOf(cb), 1) - } - } - - queryCache.clear = ({ notify = true } = {}) => { - Object.values(queryCache.queries).forEach(query => query.clear()) - queryCache.queries = {} - if (notify) { - notifyGlobalListeners() - } - } - - queryCache.getQueries = (predicate, { exact } = {}) => { - if (predicate === true) { - return Object.values(queryCache.queries) - } - - if (typeof predicate !== 'function') { - const [ - queryHash, - queryKey, - ] = configRef.current.queries.queryKeySerializerFn(predicate) - - predicate = d => - exact ? d.queryHash === queryHash : deepIncludes(d.queryKey, queryKey) - } - - return Object.values(queryCache.queries).filter(predicate) - } - - queryCache.getQuery = queryKey => - queryCache.getQueries(queryKey, { exact: true })[0] - - queryCache.getQueryData = queryKey => - queryCache.getQuery(queryKey)?.state.data - - queryCache.removeQueries = (...args) => { - queryCache.getQueries(...args).forEach(query => query.clear()) - } - - queryCache.cancelQueries = (...args) => { - queryCache.getQueries(...args).forEach(query => query.cancel()) - } - - queryCache.invalidateQueries = async ( - predicate, - { refetchActive = true, refetchInactive = false, exact, throwOnError } = {} - ) => { - try { - return await Promise.all( - queryCache.getQueries(predicate, { exact }).map(query => { - if (query.instances.length) { - if ( - refetchActive && - query.instances.some(instance => instance.config.enabled) - ) { - return query.fetch() - } - } else { - if (refetchInactive) { - return query.fetch() - } - } - - return query.invalidate() - }) - ) - } catch (err) { - if (throwOnError) { - throw err - } - } - } - - queryCache.resetErrorBoundaries = () => { - queryCache.getQueries(true).forEach(query => { - query.state.throwInErrorBoundary = false - }) - } - - queryCache.buildQuery = (userQueryKey, config = {}) => { - config = { - ...configRef.current.shared, - ...configRef.current.queries, - ...config, - } - - let [queryHash, queryKey] = config.queryKeySerializerFn(userQueryKey) - - let query = queryCache.queries[queryHash] - - if (query) { - Object.assign(query, { config }) - } else { - query = makeQuery({ - queryCache, - queryKey, - queryHash, - config, - notifyGlobalListeners, - }) - - if (config.infinite) { - if ( - typeof query.state.canFetchMore === 'undefined' && - typeof query.state.data !== 'undefined' - ) { - query.state.canFetchMore = config.getFetchMore( - query.state.data[query.state.data.length - 1], - query.state.data - ) - } - - // Here we seed the pageVariabes for the query - if (!query.pageVariables) { - query.pageVariables = [[...query.queryKey]] - } - } - - // If the query started with data, schedule - // a stale timeout - if (!isServer && query.state.data) { - query.scheduleStaleTimeout() - - // Simulate a query healing process - query.heal() - // Schedule for garbage collection in case - // nothing subscribes to this query - query.scheduleGarbageCollection() - } - - if (!frozen) { - queryCache.queries[queryHash] = query - - if (isServer) { - notifyGlobalListeners() - } else { - // Here, we setTimeout so as to not trigger - // any setState's in parent components in the - // middle of the render phase. - setTimeout(() => { - notifyGlobalListeners() - }) - } - } - } - - query.fallbackInstance = { - config: { - onSuccess: query.config.onSuccess, - onError: query.config.onError, - onSettled: query.config.onSettled, - }, - } - - return query - } - - queryCache.prefetchQuery = async (...args) => { - if ( - isObject(args[1]) && - (args[1].hasOwnProperty('throwOnError') || - args[1].hasOwnProperty('force')) - ) { - args[3] = args[1] - args[1] = undefined - args[2] = undefined - } - - let [queryKey, config, { force, throwOnError } = {}] = getQueryArgs(args) - - // https://github.com/tannerlinsley/react-query/issues/652 - config = { retry: false, ...config } - - try { - const query = queryCache.buildQuery(queryKey, config) - if (force || query.state.isStale) { - await query.fetch() - } - return query.state.data - } catch (err) { - if (throwOnError) { - throw err - } - Console.error(err) - } - } - - queryCache.setQueryData = (queryKey, updater, config = {}) => { - let query = queryCache.getQuery(queryKey) - - if (!query) { - query = queryCache.buildQuery(queryKey, config) - } - - query.setData(updater) - } - - return queryCache -} diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts new file mode 100644 index 0000000000..08f34f591e --- /dev/null +++ b/src/core/queryCache.ts @@ -0,0 +1,412 @@ +import { + isServer, + getQueryArgs, + deepIncludes, + Console, + isObject, + Updater, +} from './utils' +import { defaultConfigRef, ReactQueryConfigRef } from './config' +import { Query } from './query' +import { + QueryConfig, + QueryKey, + QueryKeyWithoutObject, + ReactQueryConfig, + QueryKeyWithoutArray, + QueryKeyWithoutObjectAndArray, + TupleQueryFunction, + TupleQueryKey, +} from './types' +import { QueryInstance } from './queryInstance' + +// TYPES + +interface QueryCacheConfig { + frozen?: boolean + defaultConfig?: ReactQueryConfig +} + +interface ClearOptions { + notify?: boolean +} + +interface PrefetchQueryOptions { + force?: boolean + throwOnError?: boolean +} + +interface InvalidateQueriesOptions extends QueryPredicateOptions { + refetchActive?: boolean + refetchInactive?: boolean + throwOnError?: boolean +} + +interface QueryPredicateOptions { + exact?: boolean +} + +type QueryPredicate = QueryKey | QueryPredicateFn | true + +type QueryPredicateFn = (query: Query) => boolean + +export interface PrefetchQueryObjectConfig< + TResult, + TError, + TKey extends TupleQueryKey +> { + queryKey: QueryKey + queryFn?: TupleQueryFunction + config?: QueryConfig + options?: PrefetchQueryOptions +} + +interface QueryHashMap { + [hash: string]: Query +} + +type QueryCacheListener = ( + cache: QueryCache, + query?: Query +) => void + +// CLASS + +export class QueryCache { + queries: QueryHashMap + isFetching: number + + private config: QueryCacheConfig + private configRef: ReactQueryConfigRef + private globalListeners: QueryCacheListener[] + + constructor(config?: QueryCacheConfig) { + this.config = config || {} + + // A frozen cache does not add new queries to the cache + this.globalListeners = [] + + this.configRef = this.config.defaultConfig + ? { + current: { + shared: { + ...defaultConfigRef.current.shared, + ...this.config.defaultConfig.shared, + }, + queries: { + ...defaultConfigRef.current.queries, + ...this.config.defaultConfig.queries, + }, + mutations: { + ...defaultConfigRef.current.mutations, + ...this.config.defaultConfig.mutations, + }, + }, + } + : defaultConfigRef + + this.queries = {} + this.isFetching = 0 + } + + private notifyGlobalListeners(query?: Query) { + this.isFetching = Object.values(this.queries).reduce( + (acc, query) => (query.state.isFetching ? acc + 1 : acc), + 0 + ) + + this.globalListeners.forEach(d => d(queryCache, query)) + } + + subscribe(listener: QueryCacheListener): () => void { + this.globalListeners.push(listener) + return () => { + this.globalListeners.splice(this.globalListeners.indexOf(listener), 1) + } + } + + clear(options?: ClearOptions): void { + Object.values(this.queries).forEach(query => query.clear()) + this.queries = {} + if (options?.notify) { + this.notifyGlobalListeners() + } + } + + getQueries( + predicate: QueryPredicate, + options?: QueryPredicateOptions + ): Query[] { + if (predicate === true) { + return Object.values(this.queries) + } + + let predicateFn: QueryPredicateFn + + if (typeof predicate === 'function') { + predicateFn = predicate as QueryPredicateFn + } else { + const [queryHash, queryKey] = this.configRef.current.queries! + .queryKeySerializerFn!(predicate) + + predicateFn = d => + options?.exact + ? d.queryHash === queryHash + : deepIncludes(d.queryKey, queryKey) + } + + return Object.values(this.queries).filter(predicateFn) + } + + getQuery( + predicate: QueryPredicate + ): Query | undefined { + return this.getQueries(predicate, { exact: true })[0] + } + + getQueryData(predicate: QueryPredicate): TResult | undefined { + return this.getQuery(predicate)?.state.data + } + + removeQueries( + predicate: QueryPredicate, + options?: QueryPredicateOptions + ): void { + this.getQueries(predicate, options).forEach(query => query.clear()) + } + + cancelQueries( + predicate: QueryPredicate, + options?: QueryPredicateOptions + ): void { + this.getQueries(predicate, options).forEach(query => query.cancel()) + } + + async invalidateQueries( + predicate: QueryPredicate, + options?: InvalidateQueriesOptions + ): Promise { + const { refetchActive = true, refetchInactive = false, throwOnError } = + options || {} + + try { + await Promise.all( + this.getQueries(predicate, options).map(query => { + if (query.instances.length) { + if ( + refetchActive && + query.instances.some(instance => instance.config.enabled) + ) { + return query.fetch() + } + } else { + if (refetchInactive) { + return query.fetch() + } + } + + return query.invalidate() + }) + ) + } catch (err) { + if (throwOnError) { + throw err + } + } + } + + resetErrorBoundaries(): void { + this.getQueries(true).forEach(query => { + query.state.throwInErrorBoundary = false + }) + } + + buildQuery( + userQueryKey: QueryKey, + queryConfig: QueryConfig = {} + ): Query { + const config = { + ...this.configRef.current.shared!, + ...this.configRef.current.queries!, + ...queryConfig, + } as QueryConfig + + const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey) + + let query + + if (this.queries[queryHash]) { + query = this.queries[queryHash] as Query + query.config = config + } + + if (!query) { + query = new Query({ + queryCache, + queryKey, + queryHash, + config, + notifyGlobalListeners: query => { + this.notifyGlobalListeners(query) + }, + }) + + // If the query started with data, schedule + // a stale timeout + if (!isServer && query.state.data) { + query.scheduleStaleTimeout() + + // Simulate a query healing process + query.heal() + // Schedule for garbage collection in case + // nothing subscribes to this query + query.scheduleGarbageCollection() + } + + if (!this.config.frozen) { + this.queries[queryHash] = query + + if (isServer) { + this.notifyGlobalListeners() + } else { + // Here, we setTimeout so as to not trigger + // any setState's in parent components in the + // middle of the render phase. + setTimeout(() => { + this.notifyGlobalListeners() + }) + } + } + } + + query.fallbackInstance = { + config: { + onSuccess: query.config.onSuccess, + onError: query.config.onError, + onSettled: query.config.onSettled, + }, + } as QueryInstance + + return query + } + + // Parameter syntax with optional prefetch options + async prefetchQuery( + queryKey: TKey, + options?: PrefetchQueryOptions + ): Promise + + // Parameter syntax with config and optional prefetch options + async prefetchQuery( + queryKey: TKey, + config: QueryConfig, + options?: PrefetchQueryOptions + ): Promise + + // Parameter syntax with query function and optional prefetch options + async prefetchQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObjectAndArray + >( + queryKey: TKey, + queryFn: TupleQueryFunction, + options?: PrefetchQueryOptions + ): Promise + + async prefetchQuery( + queryKey: TKey, + queryFn: TupleQueryFunction, + options?: PrefetchQueryOptions + ): Promise + + // Parameter syntax with query function, config and optional prefetch options + async prefetchQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObjectAndArray + >( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig: QueryConfig, + options?: PrefetchQueryOptions + ): Promise + + async prefetchQuery( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig: QueryConfig, + options?: PrefetchQueryOptions + ): Promise + + // Object syntax + async prefetchQuery( + config: PrefetchQueryObjectConfig + ): Promise + + async prefetchQuery( + config: PrefetchQueryObjectConfig + ): Promise + + // Implementation + async prefetchQuery( + ...args: any[] + ): Promise { + if ( + isObject(args[1]) && + (args[1].hasOwnProperty('throwOnError') || + args[1].hasOwnProperty('force')) + ) { + args[3] = args[1] + args[1] = undefined + args[2] = undefined + } + + const [queryKey, config, options] = getQueryArgs< + TResult, + TError, + PrefetchQueryOptions | undefined + >(args) + // https://github.com/tannerlinsley/react-query/issues/652 + const configWithoutRetry = { retry: false, ...config } + + try { + const query = this.buildQuery( + queryKey, + configWithoutRetry + ) + if (options?.force || query.state.isStale) { + await query.fetch() + } + return query.state.data + } catch (err) { + if (options?.throwOnError) { + throw err + } + Console.error(err) + return + } + } + + setQueryData( + queryKey: QueryKey, + updater: Updater, + config: QueryConfig = {} + ) { + let query = this.getQuery(queryKey) + + if (!query) { + query = this.buildQuery(queryKey, config) + } + + query.setData(updater) + } +} + +export const queryCache = makeQueryCache({ frozen: isServer }) + +export const queryCaches = [queryCache] + +export function makeQueryCache(config?: QueryCacheConfig) { + return new QueryCache(config) +} diff --git a/src/core/queryInstance.js b/src/core/queryInstance.js deleted file mode 100644 index 6e3d925295..0000000000 --- a/src/core/queryInstance.js +++ /dev/null @@ -1,84 +0,0 @@ -import { uid, isServer, isDocumentVisible, Console } from './utils' - -export function makeQueryInstance(query, onStateUpdate) { - const instance = { - id: uid(), - onStateUpdate, - } - - instance.clearInterval = () => { - clearInterval(instance.refetchIntervalId) - delete instance.refetchIntervalId - } - - instance.updateConfig = config => { - const oldConfig = instance.config - - // Update the config - instance.config = config - - if (!isServer) { - if (oldConfig?.refetchInterval === config.refetchInterval) { - return - } - - query.clearIntervals() - - const minInterval = Math.min( - ...query.instances.map(d => d.config.refetchInterval || Infinity) - ) - - if ( - !instance.refetchIntervalId && - minInterval > 0 && - minInterval < Infinity - ) { - instance.refetchIntervalId = setInterval(() => { - if ( - query.instances.some(instance => instance.config.enabled) && - (isDocumentVisible() || - query.instances.some( - instance => instance.config.refetchIntervalInBackground - )) - ) { - query.fetch() - } - }, minInterval) - } - } - } - - instance.run = async () => { - try { - // Perform the refetch for this query if necessary - if ( - query.config.enabled && // Don't auto refetch if disabled - !query.wasSuspended && // Don't double refetch for suspense - query.state.isStale && // Only refetch if stale - (query.config.refetchOnMount || query.instances.length === 1) - ) { - await query.fetch() - } - - query.wasSuspended = false - } catch (error) { - Console.error(error) - } - } - - instance.unsubscribe = () => { - query.instances = query.instances.filter(d => d.id !== instance.id) - - if (!query.instances.length) { - instance.clearInterval() - query.cancel() - - if (!isServer) { - // Schedule garbage collection - query.scheduleGarbageCollection() - } - } - } - - return instance -} diff --git a/src/core/queryInstance.ts b/src/core/queryInstance.ts new file mode 100644 index 0000000000..b737a726c9 --- /dev/null +++ b/src/core/queryInstance.ts @@ -0,0 +1,106 @@ +import { uid, isServer, isDocumentVisible, Console } from './utils' +import { Query, QueryState } from './query' +import { BaseQueryConfig } from './types' + +// TYPES + +export type OnStateUpdateFunction = ( + state: QueryState +) => void + +// CLASS + +export class QueryInstance { + id: number + config: BaseQueryConfig + onStateUpdate?: OnStateUpdateFunction + + private query: Query + private refetchIntervalId?: number + + constructor( + query: Query, + onStateUpdate?: OnStateUpdateFunction + ) { + this.id = uid() + this.onStateUpdate = onStateUpdate + this.query = query + this.config = {} + } + + clearInterval(): void { + if (this.refetchIntervalId) { + clearInterval(this.refetchIntervalId) + this.refetchIntervalId = undefined + } + } + + updateConfig(config: BaseQueryConfig): void { + const oldConfig = this.config + + // Update the config + this.config = config + + if (!isServer) { + if (oldConfig?.refetchInterval === config.refetchInterval) { + return + } + + this.query.clearIntervals() + + const minInterval = Math.min( + ...this.query.instances.map(d => d.config.refetchInterval || Infinity) + ) + + if ( + !this.refetchIntervalId && + minInterval > 0 && + minInterval < Infinity + ) { + this.refetchIntervalId = setInterval(() => { + if ( + this.query.instances.some(_ => this.config.enabled) && + (isDocumentVisible() || + this.query.instances.some( + _ => this.config.refetchIntervalInBackground + )) + ) { + this.query.fetch() + } + }, minInterval) + } + } + } + + async run(): Promise { + try { + // Perform the refetch for this query if necessary + if ( + this.query.config.enabled && // Don't auto refetch if disabled + !this.query.wasSuspended && // Don't double refetch for suspense + this.query.state.isStale && // Only refetch if stale + (this.query.config.refetchOnMount || this.query.instances.length === 1) + ) { + await this.query.fetch() + } + + this.query.wasSuspended = false + } catch (error) { + Console.error(error) + } + } + + unsubscribe(): void { + this.query.instances = this.query.instances.filter(d => d.id !== this.id) + + if (!this.query.instances.length) { + this.clearInterval() + this.query.cancel() + + if (!isServer) { + // Schedule garbage collection + this.query.scheduleGarbageCollection() + } + } + } +} diff --git a/src/core/setFocusHandler.js b/src/core/setFocusHandler.ts similarity index 81% rename from src/core/setFocusHandler.js rename to src/core/setFocusHandler.ts index 3c86530baf..e30702ee10 100644 --- a/src/core/setFocusHandler.js +++ b/src/core/setFocusHandler.ts @@ -1,10 +1,12 @@ import { isOnline, isDocumentVisible, Console, isServer } from './utils' import { queryCaches } from './queryCache' +type FocusHandler = () => void + const visibilityChangeEvent = 'visibilitychange' const focusEvent = 'focus' -const onWindowFocus = () => { +const onWindowFocus: FocusHandler = () => { if (isDocumentVisible() && isOnline()) { queryCaches.forEach(queryCache => queryCache @@ -26,16 +28,16 @@ const onWindowFocus = () => { delete query.promise } - return query.config.refetchOnWindowFocus + return !!query.config.refetchOnWindowFocus }) .catch(Console.error) ) } } -let removePreviousHandler +let removePreviousHandler: (() => void) | void -export function setFocusHandler(callback) { +export function setFocusHandler(callback: (callback: FocusHandler) => void) { // Unsub the old watcher if (removePreviousHandler) { removePreviousHandler() @@ -44,7 +46,7 @@ export function setFocusHandler(callback) { removePreviousHandler = callback(onWindowFocus) } -setFocusHandler(handleFocus => { +setFocusHandler((handleFocus: FocusHandler) => { // Listen to visibillitychange and focus if (!isServer && window?.addEventListener) { window.addEventListener(visibilityChangeEvent, handleFocus, false) @@ -56,4 +58,5 @@ setFocusHandler(handleFocus => { window.removeEventListener(focusEvent, handleFocus) } } + return }) diff --git a/src/core/tests/queryCache.test.js b/src/core/tests/queryCache.test.tsx similarity index 86% rename from src/core/tests/queryCache.test.js rename to src/core/tests/queryCache.test.tsx index fd5cb3c960..45028ce3fb 100644 --- a/src/core/tests/queryCache.test.js +++ b/src/core/tests/queryCache.test.tsx @@ -8,9 +8,10 @@ describe('queryCache', () => { }) test('setQueryData does not crash if query could not be found', () => { + const user = { userId: 1 } expect(() => - queryCache.setQueryData(['USER', { userId: 1 }], prevUser => ({ - ...prevUser, + queryCache.setQueryData(['USER', user], (prevUser?: typeof user) => ({ + ...prevUser!, name: 'Edvin', })) ).not.toThrow() @@ -107,15 +108,15 @@ describe('queryCache', () => { queryCache.subscribe(callback) - queryCache.prefetchQuery('test', () => {}, { initialData: 'initial' }) + queryCache.prefetchQuery('test', () => 'data', { initialData: 'initial' }) await sleep(100) expect(callback).toHaveBeenCalled() }) - test('setQueryData creates a new query if query was not found, using exact', () => { - queryCache.setQueryData('foo', 'bar', { exact: true }) + test('setQueryData creates a new query if query was not found', () => { + queryCache.setQueryData('foo', 'bar') expect(queryCache.getQueryData('foo')).toBe('bar') }) @@ -142,28 +143,31 @@ describe('queryCache', () => { test('setQueryData should schedule stale timeout, if staleTime is set', async () => { queryCache.setQueryData('key', 'test data', { staleTime: 10 }) - expect(queryCache.getQuery('key').staleTimeout).not.toBeUndefined() + // @ts-expect-error + expect(queryCache.getQuery('key')!.staleTimeout).not.toBeUndefined() }) test('setQueryData should not schedule stale timeout by default', async () => { queryCache.setQueryData('key', 'test data') - expect(queryCache.getQuery('key').staleTimeout).toBeUndefined() + // @ts-expect-error + expect(queryCache.getQuery('key')!.staleTimeout).toBeUndefined() }) test('setQueryData should not schedule stale timeout, if staleTime is set to `Infinity`', async () => { queryCache.setQueryData('key', 'test data', { staleTime: Infinity }) - expect(queryCache.getQuery('key').staleTimeout).toBeUndefined() + // @ts-expect-error + expect(queryCache.getQuery('key')!.staleTimeout).toBeUndefined() }) test('setQueryData schedules stale timeouts appropriately', async () => { queryCache.setQueryData('key', 'test data', { staleTime: 100 }) - expect(queryCache.getQuery('key').state.data).toEqual('test data') - expect(queryCache.getQuery('key').state.isStale).toEqual(false) + expect(queryCache.getQuery('key')!.state.data).toEqual('test data') + expect(queryCache.getQuery('key')!.state.isStale).toEqual(false) await new Promise(resolve => setTimeout(resolve, 100)) - expect(queryCache.getQuery('key').state.isStale).toEqual(true) + expect(queryCache.getQuery('key')!.state.isStale).toEqual(true) }) test('setQueryData updater function works as expected', () => { @@ -173,7 +177,7 @@ describe('queryCache', () => { queryCache.setQueryData('updater', updater) expect(updater).toHaveBeenCalled() - expect(queryCache.getQuery('updater').state.data).toEqual( + expect(queryCache.getQuery('updater')!.state.data).toEqual( 'new data + test data' ) }) @@ -195,10 +199,10 @@ describe('queryCache', () => { const fetchData = () => Promise.resolve('data') await queryCache.prefetchQuery(queryKey, fetchData, { staleTime: 100 }) const query = queryCache.getQuery(queryKey) - expect(query.state.isStale).toBe(false) + expect(query!.state.isStale).toBe(false) queryCache.removeQueries(queryKey) await sleep(50) - expect(query.state.isStale).toBe(false) + expect(query!.state.isStale).toBe(false) }) test('query interval is cleared when unsubscribed to a refetchInterval query', async () => { @@ -208,11 +212,13 @@ describe('queryCache', () => { cacheTime: 0, refetchInterval: 1, }) - const query = queryCache.getQuery(queryKey) + const query = queryCache.getQuery(queryKey)! const instance = query.subscribe() instance.updateConfig(query.config) + // @ts-expect-error expect(instance.refetchIntervalId).not.toBeUndefined() instance.unsubscribe() + // @ts-expect-error expect(instance.refetchIntervalId).toBeUndefined() await sleep(10) expect(queryCache.getQuery(queryKey)).toBeUndefined() @@ -222,7 +228,7 @@ describe('queryCache', () => { const queryKey = 'key' const fetchData = () => Promise.resolve('data') await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 }) - const query = queryCache.getQuery(queryKey) + const query = queryCache.getQuery(queryKey)! expect(query.state.markedForGarbageCollection).toBe(false) const instance = query.subscribe() instance.unsubscribe() @@ -235,7 +241,7 @@ describe('queryCache', () => { const queryKey = 'key' const fetchData = () => Promise.resolve(undefined) await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 }) - const query = queryCache.getQuery(queryKey) + const query = queryCache.getQuery(queryKey)! expect(query.state.markedForGarbageCollection).toBe(false) const instance = query.subscribe() instance.unsubscribe() @@ -243,7 +249,7 @@ describe('queryCache', () => { queryCache.clear({ notify: false }) queryCache.setQueryData(queryKey, 'data') await sleep(10) - const newQuery = queryCache.getQuery(queryKey) + const newQuery = queryCache.getQuery(queryKey)! expect(newQuery.state.markedForGarbageCollection).toBe(false) expect(newQuery.state.data).toBe('data') }) diff --git a/src/core/tests/utils.js b/src/core/tests/utils.js deleted file mode 100644 index c3ebacf537..0000000000 --- a/src/core/tests/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function sleep(timeout) { - return new Promise((resolve, reject) => { - setTimeout(resolve, timeout) - }) -} diff --git a/src/core/tests/utils.test.js b/src/core/tests/utils.test.tsx similarity index 98% rename from src/core/tests/utils.test.js rename to src/core/tests/utils.test.tsx index ecbac79972..cca2356271 100644 --- a/src/core/tests/utils.test.js +++ b/src/core/tests/utils.test.tsx @@ -9,6 +9,8 @@ describe('core/utils', () => { it('setConsole should override Console object', async () => { const mockConsole = { error: jest.fn(), + log: jest.fn(), + warn: jest.fn(), } setConsole(mockConsole) diff --git a/src/core/tests/utils.tsx b/src/core/tests/utils.tsx new file mode 100644 index 0000000000..1a3a619a22 --- /dev/null +++ b/src/core/tests/utils.tsx @@ -0,0 +1,5 @@ +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000000..d470cccf75 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,215 @@ +import { Query, FetchMoreOptions } from './query' + +export type QueryKeyObject = + | object + | { [key: string]: QueryKey } + | { [key: number]: QueryKey } + +export type QueryKeyPrimitive = string | boolean | number | null | undefined + +export type QueryKeyWithoutObjectAndArray = QueryKeyPrimitive + +export type QueryKeyWithoutObject = + | QueryKeyWithoutObjectAndArray + | readonly QueryKey[] + +export type QueryKeyWithoutArray = + | QueryKeyWithoutObjectAndArray + | QueryKeyObject + +export type QueryKey = QueryKeyWithoutObject | QueryKeyObject + +export type ArrayQueryKey = QueryKey[] + +export type QueryFunction = ( + ...args: any[] +) => TResult | Promise + +// The tuple variants are only to infer types in the public API +export type TupleQueryKey = readonly [QueryKey, ...QueryKey[]] + +export type TupleQueryFunction = ( + ...args: TKey +) => TResult | Promise + +export type InitialDataFunction = () => TResult | undefined + +export type InitialStaleFunction = () => boolean + +export type QueryKeySerializerFunction = ( + queryKey: QueryKey +) => [string, QueryKey[]] + +export interface BaseQueryConfig { + /** + * Set this to `false` to disable automatic refetching when the query mounts or changes query keys. + * To refetch the query, use the `refetch` method returned from the `useQuery` instance. + */ + enabled?: boolean | unknown + /** + * If `false`, failed queries will not retry by default. + * If `true`, failed queries will retry infinitely., failureCount: num + * If set to an integer number, e.g. 3, failed queries will retry until the failed query count meets that number. + * If set to a function `(failureCount, error) => boolean` failed queries will retry until the function returns false. + */ + retry?: boolean | number | ((failureCount: number, error: TError) => boolean) + retryDelay?: number | ((retryAttempt: number) => number) + staleTime?: number + cacheTime?: number + refetchInterval?: false | number + refetchIntervalInBackground?: boolean + refetchOnWindowFocus?: boolean + refetchOnMount?: boolean + onSuccess?: (data: TResult) => void + onError?: (err: TError) => void + onSettled?: (data: TResult | undefined, error: TError | null) => void + isDataEqual?: (oldData: unknown, newData: unknown) => boolean + useErrorBoundary?: boolean + queryFn?: QueryFunction + queryKeySerializerFn?: QueryKeySerializerFunction + queryFnParamsFilter?: (args: ArrayQueryKey) => ArrayQueryKey + suspense?: boolean + initialData?: TResult | InitialDataFunction + initialStale?: boolean | InitialStaleFunction + infinite?: true +} + +export interface QueryConfig + extends BaseQueryConfig {} + +export interface PaginatedQueryConfig + extends BaseQueryConfig {} + +export interface InfiniteQueryConfig + extends BaseQueryConfig { + getFetchMore: (lastPage: TResult, allPages: TResult[]) => unknown +} + +export type IsFetchingMoreValue = 'previous' | 'next' | false + +export enum QueryStatus { + Idle = 'idle', + Loading = 'loading', + Error = 'error', + Success = 'success', +} + +export interface QueryResultBase { + status: QueryStatus + error: TError | null + isLoading: boolean + isSuccess: boolean + isError: boolean + isIdle: boolean + isFetching: boolean + isStale: boolean + failureCount: number + query: Query + updatedAt: number + refetch: () => Promise + clear: () => void +} + +export interface QueryResult + extends QueryResultBase { + data: TResult | undefined +} + +export interface PaginatedQueryResult + extends QueryResultBase { + resolvedData: TResult | undefined + latestData: TResult | undefined +} + +export interface InfiniteQueryResult + extends QueryResultBase { + data: TResult[] | undefined + isFetchingMore?: IsFetchingMoreValue + canFetchMore: boolean | undefined + fetchMore: ( + fetchMoreVariable?: unknown, + options?: FetchMoreOptions + ) => Promise | undefined +} + +export interface MutateConfig< + TResult, + TError = unknown, + TVariables = unknown, + TSnapshot = unknown +> { + onSuccess?: (data: TResult, variables: TVariables) => Promise | void + onError?: ( + error: TError, + variables: TVariables, + snapshotValue: TSnapshot + ) => Promise | void + onSettled?: ( + data: undefined | TResult, + error: TError | null, + variables: TVariables, + snapshotValue?: TSnapshot + ) => Promise | void + throwOnError?: boolean +} + +export interface MutationConfig< + TResult, + TError = unknown, + TVariables = unknown, + TSnapshot = unknown +> extends MutateConfig { + onMutate?: (variables: TVariables) => Promise | TSnapshot + useErrorBoundary?: boolean + suspense?: boolean +} + +export type MutationFunction = ( + variables: TVariables +) => Promise + +export type MutateFunction< + TResult, + TError = unknown, + TVariables = unknown, + TSnapshot = unknown +> = ( + variables?: TVariables, + config?: MutateConfig +) => Promise + +export type MutationResultPair = [ + MutateFunction, + MutationResult +] + +export interface MutationResult { + status: QueryStatus + data: TResult | undefined + error: TError | null + isIdle: boolean + isLoading: boolean + isSuccess: boolean + isError: boolean + reset: () => void +} + +export interface ReactQueryConfig { + queries?: ReactQueryQueriesConfig + shared?: ReactQuerySharedConfig + mutations?: ReactQueryMutationsConfig +} + +export interface ReactQuerySharedConfig { + suspense?: boolean +} + +export interface ReactQueryQueriesConfig + extends BaseQueryConfig {} + +export interface ReactQueryMutationsConfig< + TResult, + TError = unknown, + TVariables = unknown, + TSnapshot = unknown +> extends MutationConfig {} diff --git a/src/core/utils.js b/src/core/utils.js deleted file mode 100644 index 03557e017a..0000000000 --- a/src/core/utils.js +++ /dev/null @@ -1,154 +0,0 @@ -export const statusIdle = 'idle' -export const statusLoading = 'loading' -export const statusError = 'error' -export const statusSuccess = 'success' - -let _uid = 0 -export const uid = () => _uid++ -export const cancelledError = {} -export let globalStateListeners = [] -export const isServer = typeof window === 'undefined' -export function noop() { - return void 0 -} -export function identity(d) { - return d -} -export let Console = console || { error: noop, warn: noop, log: noop } - -export function setConsole(c) { - Console = c -} - -export function functionalUpdate(updater, old) { - return typeof updater === 'function' ? updater(old) : updater -} - -export function stableStringifyReplacer(_, value) { - return isObject(value) - ? Object.assign( - {}, - ...Object.keys(value) - .sort() - .map(key => ({ - [key]: value[key], - })) - ) - : value -} - -export function stableStringify(obj) { - return JSON.stringify(obj, stableStringifyReplacer) -} - -export function isObject(a) { - return a && typeof a === 'object' && !Array.isArray(a) -} - -export function deepIncludes(a, b) { - if (a === b) { - return true - } - - if (typeof a !== typeof b) { - return false - } - - if (typeof a === 'object') { - return !Object.keys(b).some(key => !deepIncludes(a[key], b[key])) - } - - return false -} - -export function isDocumentVisible() { - return ( - typeof document === 'undefined' || - document.visibilityState === undefined || - document.visibilityState === 'visible' || - document.visibilityState === 'prerender' - ) -} - -export function isOnline() { - return navigator.onLine === undefined || navigator.onLine -} - -export function getQueryArgs(args) { - if (isObject(args[0])) { - const { queryKey, queryFn, config } = args[0] - args = [queryKey, queryFn, config, ...args.slice(1)] - } else if (isObject(args[1])) { - const [queryKey, config, ...rest] = args - args = [queryKey, undefined, config, ...rest] - } - - let [queryKey, queryFn, config = {}, ...rest] = args - - queryFn = queryFn || config.queryFn - - return [queryKey, queryFn ? { ...config, queryFn } : config, ...rest] -} - -export function deepEqual(a, b) { - return equal(a, b, true) -} - -export function shallowEqual(a, b) { - return equal(a, b, false) -} - -// This deep-equal is directly based on https://github.com/epoberezkin/fast-deep-equal. -// The parts for comparing any non-JSON-supported values has been removed -function equal(a, b, deep, depth = 0) { - if (a === b) return true - - if ( - (deep || !depth) && - a && - b && - typeof a == 'object' && - typeof b == 'object' - ) { - var length, i, keys - if (Array.isArray(a)) { - length = a.length - // eslint-disable-next-line eqeqeq - if (length != b.length) return false - for (i = length; i-- !== 0; ) - if (!equal(a[i], b[i], deep, depth + 1)) return false - return true - } - - if (a.valueOf !== Object.prototype.valueOf) - return a.valueOf() === b.valueOf() - - keys = Object.keys(a) - length = keys.length - if (length !== Object.keys(b).length) return false - - for (i = length; i-- !== 0; ) - if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false - - for (i = length; i-- !== 0; ) { - var key = keys[i] - - if (!equal(a[key], b[key], deep, depth + 1)) return false - } - - return true - } - - // true if both NaN, false otherwise - // eslint-disable-next-line no-self-compare - return a !== a && b !== b -} - -export function getStatusBools(status) { - return { - isLoading: status === statusLoading, - isSuccess: status === statusSuccess, - isError: status === statusError, - isIdle: status === statusIdle, - } -} diff --git a/src/core/utils.ts b/src/core/utils.ts new file mode 100644 index 0000000000..02a8114e5e --- /dev/null +++ b/src/core/utils.ts @@ -0,0 +1,200 @@ +import { QueryConfig, QueryStatus, QueryKey, QueryFunction } from './types' + +// TYPES + +export type DataUpdateFunction = (input: TInput) => TOutput + +export type Updater = + | TOutput + | DataUpdateFunction + +type ConsoleFunction = (...args: any[]) => void + +export interface ConsoleObject { + log: ConsoleFunction + warn: ConsoleFunction + error: ConsoleFunction +} + +// UTILS + +let _uid = 0 +export const uid = () => _uid++ +export const cancelledError = {} +export const globalStateListeners = [] +export const isServer = typeof window === 'undefined' +export function noop(): void { + return void 0 +} +export function identity(d: T): T { + return d +} +export let Console: ConsoleObject = console || { + error: noop, + warn: noop, + log: noop, +} + +export function setConsole(c: ConsoleObject) { + Console = c +} + +export function functionalUpdate( + updater: Updater, + input: TInput +): TOutput { + return typeof updater === 'function' + ? (updater as DataUpdateFunction)(input) + : updater +} + +function stableStringifyReplacer(_key: string, value: any): unknown { + if (typeof value === 'function') { + throw new Error('Cannot stringify non JSON value') + } + + if (isObject(value)) { + return Object.keys(value) + .sort() + .reduce((result, key) => { + result[key] = value[key] + return result + }, {} as any) + } + + return value +} + +export function stableStringify(value: any): string { + return JSON.stringify(value, stableStringifyReplacer) +} + +export function isObject(a: unknown): boolean { + return a && typeof a === 'object' && !Array.isArray(a) +} + +export function deepIncludes(a: any, b: any): boolean { + if (a === b) { + return true + } + + if (typeof a !== typeof b) { + return false + } + + if (typeof a === 'object') { + return !Object.keys(b).some(key => !deepIncludes(a[key], b[key])) + } + + return false +} + +export function isDocumentVisible(): boolean { + const visibilityState = document?.visibilityState as any + return ( + visibilityState === undefined || + visibilityState === 'visible' || + visibilityState === 'prerender' + ) +} + +export function isOnline(): boolean { + return navigator.onLine === undefined || navigator.onLine +} + +export function getQueryArgs( + args: any[] +): [QueryKey, QueryConfig, TOptions] { + let queryKey: QueryKey + let queryFn: QueryFunction | undefined + let config: QueryConfig | undefined + let options: TOptions + + if (isObject(args[0])) { + queryKey = args[0].queryKey + queryFn = args[0].queryFn + config = args[0].config + options = args[1] + } else if (isObject(args[1])) { + queryKey = args[0] + config = args[1] + options = args[2] + } else { + queryKey = args[0] + queryFn = args[1] + config = args[2] + options = args[3] + } + + config = config || {} + + if (queryFn) { + config = { ...config, queryFn } + } + + return [queryKey, config, options] +} + +export function deepEqual(a: any, b: any): boolean { + return equal(a, b, true) +} + +export function shallowEqual(a: any, b: any): boolean { + return equal(a, b, false) +} + +// This deep-equal is directly based on https://github.com/epoberezkin/fast-deep-equal. +// The parts for comparing any non-JSON-supported values has been removed +function equal(a: any, b: any, deep: boolean, depth = 0): boolean { + if (a === b) return true + + if ( + (deep || !depth) && + a && + b && + typeof a == 'object' && + typeof b == 'object' + ) { + let length, i + if (Array.isArray(a)) { + length = a.length + // eslint-disable-next-line eqeqeq + if (length != b.length) return false + for (i = length; i-- !== 0; ) + if (!equal(a[i], b[i], deep, depth + 1)) return false + return true + } + + if (a.valueOf !== Object.prototype.valueOf) + return a.valueOf() === b.valueOf() + + const keys = Object.keys(a) + length = keys.length + if (length !== Object.keys(b).length) return false + + for (i = length; i-- !== 0; ) + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false + + for (i = length; i-- !== 0; ) { + const key = keys[i] + + if (!equal(a[key], b[key], deep, depth + 1)) return false + } + + return true + } + + // true if both NaN, false otherwise + // eslint-disable-next-line no-self-compare + return a !== a && b !== b +} + +export function getStatusProps(status: T) { + return { + status, + isLoading: status === QueryStatus.Loading, + isSuccess: status === QueryStatus.Success, + isError: status === QueryStatus.Error, + isIdle: status === QueryStatus.Idle, + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..46f63b7a13 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './react/index' diff --git a/src/react/ReactQueryCacheProvider.js b/src/react/ReactQueryCacheProvider.tsx similarity index 72% rename from src/react/ReactQueryCacheProvider.js rename to src/react/ReactQueryCacheProvider.tsx index 52f5ab94d2..99265538ce 100644 --- a/src/react/ReactQueryCacheProvider.js +++ b/src/react/ReactQueryCacheProvider.tsx @@ -1,11 +1,24 @@ import React from 'react' -import { queryCache as defaultQueryCache, queryCaches, makeQueryCache } from '../core' + +import { + queryCache as defaultQueryCache, + queryCaches, + makeQueryCache, +} from '../core' +import { QueryCache } from '../core/queryCache' export const queryCacheContext = React.createContext(defaultQueryCache) export const useQueryCache = () => React.useContext(queryCacheContext) -export function ReactQueryCacheProvider({ queryCache, children }) { +export interface ReactQueryCacheProviderProps { + queryCache?: QueryCache +} + +export const ReactQueryCacheProvider: React.FC = ({ + queryCache, + children, +}) => { const resolvedQueryCache = React.useMemo( () => queryCache || makeQueryCache(), [queryCache] diff --git a/src/react/ReactQueryConfigProvider.js b/src/react/ReactQueryConfigProvider.tsx similarity index 67% rename from src/react/ReactQueryConfigProvider.js rename to src/react/ReactQueryConfigProvider.tsx index 5a10d5c47d..47f40fd53b 100644 --- a/src/react/ReactQueryConfigProvider.js +++ b/src/react/ReactQueryConfigProvider.tsx @@ -1,19 +1,30 @@ import React from 'react' -import { DEFAULT_CONFIG, defaultConfigRef } from '../core/config' -// +import { DEFAULT_CONFIG, defaultConfigRef } from '../core/config' +import { ReactQueryConfig } from '../core/types' -const configContext = React.createContext() +const configContext = React.createContext( + undefined +) export function useConfigContext() { return React.useContext(configContext) || defaultConfigRef.current } -export function ReactQueryConfigProvider({ config, children }) { - let configContextValueOrDefault = useConfigContext() - let configContextValue = React.useContext(configContext) +export interface ReactQueryProviderConfig extends ReactQueryConfig {} + +export interface ReactQueryConfigProviderProps { + config: ReactQueryProviderConfig +} + +export const ReactQueryConfigProvider: React.FC = ({ + config, + children, +}) => { + const configContextValueOrDefault = useConfigContext() + const configContextValue = React.useContext(configContext) - const newConfig = React.useMemo(() => { + const newConfig = React.useMemo(() => { const { shared = {}, queries = {}, mutations = {} } = config const { shared: contextShared = {}, diff --git a/src/react/index.js b/src/react/index.ts similarity index 52% rename from src/react/index.js rename to src/react/index.ts index 5c56ba070c..88a62f58b5 100644 --- a/src/react/index.js +++ b/src/react/index.ts @@ -5,10 +5,19 @@ export { ReactQueryCacheProvider, useQueryCache, } from './ReactQueryCacheProvider' - export { ReactQueryConfigProvider } from './ReactQueryConfigProvider' export { useIsFetching } from './useIsFetching' export { useMutation } from './useMutation' export { useQuery } from './useQuery' export { usePaginatedQuery } from './usePaginatedQuery' export { useInfiniteQuery } from './useInfiniteQuery' + +// Types +export type { UseQueryObjectConfig } from './useQuery' +export type { UseInfiniteQueryObjectConfig } from './useInfiniteQuery' +export type { UsePaginatedQueryObjectConfig } from './usePaginatedQuery' +export type { ReactQueryCacheProviderProps } from './ReactQueryCacheProvider' +export type { + ReactQueryConfigProviderProps, + ReactQueryProviderConfig, +} from './ReactQueryConfigProvider' diff --git a/src/react/tests/ReactQueryCacheProvider.test.js b/src/react/tests/ReactQueryCacheProvider.test.tsx similarity index 96% rename from src/react/tests/ReactQueryCacheProvider.test.js rename to src/react/tests/ReactQueryCacheProvider.test.tsx index e176065786..8334fa38eb 100644 --- a/src/react/tests/ReactQueryCacheProvider.test.js +++ b/src/react/tests/ReactQueryCacheProvider.test.tsx @@ -9,6 +9,7 @@ import { queryCaches, } from '../index' import { sleep } from './utils' +import { QueryCache } from '../../core/queryCache' describe('ReactQueryCacheProvider', () => { afterEach(() => { @@ -143,7 +144,7 @@ describe('ReactQueryCacheProvider', () => { }) test('when cache changes, previous cache is cleaned', () => { - let caches = [] + const caches: QueryCache[] = [] const customCache = makeQueryCache() function Page() { @@ -164,7 +165,7 @@ describe('ReactQueryCacheProvider', () => { ) } - function App({ cache }) { + function App({ cache }: { cache?: QueryCache }) { return ( diff --git a/src/react/tests/ReactQueryConfigProvider.test.js b/src/react/tests/ReactQueryConfigProvider.test.tsx similarity index 94% rename from src/react/tests/ReactQueryConfigProvider.test.js rename to src/react/tests/ReactQueryConfigProvider.test.tsx index 9148822a69..612598fd12 100644 --- a/src/react/tests/ReactQueryConfigProvider.test.js +++ b/src/react/tests/ReactQueryConfigProvider.test.tsx @@ -79,7 +79,7 @@ describe('ReactQueryConfigProvider', () => { const rendered = render() - await waitFor(() => rendered.findByText('Placeholder')) + await waitFor(() => rendered.getByText('Placeholder')) await queryCache.prefetchQuery('test') @@ -136,11 +136,13 @@ describe('ReactQueryConfigProvider', () => { const rendered = render() - await waitFor(() => rendered.findByText('Data: none')) + await waitFor(() => rendered.getByText('Data: none')) - await act(() => queryCache.prefetchQuery('test', queryFn)) + act(() => { + queryCache.prefetchQuery('test', queryFn) + }) - await waitFor(() => rendered.findByText('Data: data')) + await waitFor(() => rendered.getByText('Data: data')) // tear down and unmount // so we are NOT passing the config above (refetchOnMount should be `true` by default) @@ -152,7 +154,7 @@ describe('ReactQueryConfigProvider', () => { onSuccess.mockClear() }) - await waitFor(() => rendered.findByText('Data: data')) + await waitFor(() => rendered.getByText('Data: data')) }) it('should reset to previous config when nested provider is unmounted', async () => { @@ -210,7 +212,7 @@ describe('ReactQueryConfigProvider', () => { ) } - const rendered = render( + render( diff --git a/src/react/tests/ssr.test.js b/src/react/tests/ssr.test.tsx similarity index 93% rename from src/react/tests/ssr.test.js rename to src/react/tests/ssr.test.tsx index 99aa31e6cb..777f2f201a 100644 --- a/src/react/tests/ssr.test.js +++ b/src/react/tests/ssr.test.tsx @@ -3,6 +3,7 @@ */ import React from 'react' +// @ts-ignore import { renderToString } from 'react-dom/server' import { usePaginatedQuery, @@ -77,10 +78,10 @@ describe('Server Side Rendering', () => { const [page, setPage] = React.useState(1) const { resolvedData } = usePaginatedQuery( ['data', page], - async (queryName, page) => { + async (_queryName: string, page: number) => { return page }, - { initialData: '1' } + { initialData: 1 } ) return ( @@ -139,7 +140,7 @@ describe('Server Side Rendering', () => { const data = await queryCache.prefetchQuery('key', fetchFn) expect(data).toBe('data') - expect(queryCache.getQuery('key').state.data).toBe('data') + expect(queryCache.getQuery('key')?.state.data).toBe('data') }) it('should return existing data from the cache', async () => { @@ -176,10 +177,10 @@ describe('Server Side Rendering', () => { const [page, setPage] = React.useState(1) const { resolvedData } = usePaginatedQuery( ['data', page], - async (queryName, page) => { + async (_queryName: string, page: number) => { return page }, - { initialData: '1' } + { initialData: 1 } ) return ( @@ -200,7 +201,8 @@ describe('Server Side Rendering', () => { }) it('should not call setTimeout', async () => { - jest.spyOn(global, 'setTimeout') + // @ts-ignore + const setTimeoutMock = jest.spyOn(global, 'setTimeout') const queryCache = makeQueryCache({ frozen: false }) const queryFn = jest.fn(() => Promise.resolve()) @@ -227,9 +229,9 @@ describe('Server Side Rendering', () => { expect(markup).toContain('status success') expect(queryFn).toHaveBeenCalledTimes(1) - expect(global.setTimeout).toHaveBeenCalledTimes(0) + expect(setTimeoutMock).toHaveBeenCalledTimes(0) - global.setTimeout.mockRestore() + setTimeoutMock.mockRestore() }) }) }) diff --git a/src/react/tests/suspense.test.js b/src/react/tests/suspense.test.tsx similarity index 89% rename from src/react/tests/suspense.test.js rename to src/react/tests/suspense.test.tsx index 67f6121c0c..7f7d0ebca8 100644 --- a/src/react/tests/suspense.test.js +++ b/src/react/tests/suspense.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, fireEvent, act } from '@testing-library/react' +import { render, waitFor, fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import * as React from 'react' @@ -17,7 +17,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery(['test'], queryFn, { suspense: true }) - return 'rendered' + return <>rendered } const rendered = render( @@ -37,7 +37,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery([QUERY_KEY], () => sleep(10), { suspense: true }) - return 'rendered' + return <>rendered } function App() { @@ -59,12 +59,12 @@ describe("useQuery's in Suspense mode", () => { fireEvent.click(rendered.getByLabelText('toggle')) await waitFor(() => rendered.getByText('rendered')) - expect(queryCache.getQuery(QUERY_KEY).instances.length).toBe(1) + expect(queryCache.getQuery(QUERY_KEY)?.instances.length).toBe(1) fireEvent.click(rendered.getByLabelText('toggle')) expect(rendered.queryByText('rendered')).toBeNull() - expect(queryCache.getQuery(QUERY_KEY).instances.length).toBe(0) + expect(queryCache.getQuery(QUERY_KEY)?.instances.length).toBe(0) }) it('should call onSuccess on the first successful call', async () => { @@ -76,7 +76,7 @@ describe("useQuery's in Suspense mode", () => { onSuccess: successFn, }) - return 'rendered' + return <>rendered } const rendered = render( @@ -93,8 +93,8 @@ describe("useQuery's in Suspense mode", () => { // https://github.com/tannerlinsley/react-query/issues/468 it('should reset error state if new component instances are mounted', async () => { let succeed = false - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) function Page() { useQuery( @@ -119,7 +119,7 @@ describe("useQuery's in Suspense mode", () => { const rendered = render( queryCache.resetErrorBoundaries()} fallbackRender={({ resetErrorBoundary }) => (
error boundary
@@ -150,7 +150,7 @@ describe("useQuery's in Suspense mode", () => { await waitFor(() => rendered.getByText('rendered')) - console.error.mockRestore() + consoleMock.mockRestore() }) it('should not call the queryFn when not enabled', async () => { diff --git a/src/react/tests/useInfiniteQuery.test.js b/src/react/tests/useInfiniteQuery.test.tsx similarity index 74% rename from src/react/tests/useInfiniteQuery.test.js rename to src/react/tests/useInfiniteQuery.test.tsx index 1bb75c8b41..43327b2dee 100644 --- a/src/react/tests/useInfiniteQuery.test.js +++ b/src/react/tests/useInfiniteQuery.test.tsx @@ -3,10 +3,17 @@ import * as React from 'react' import { useInfiniteQuery, useQueryCache, queryCaches } from '../index' import { sleep } from './utils' +import { InfiniteQueryResult } from '../../core/types' + +interface Result { + items: number[] + nextId: number + ts: number +} const pageSize = 10 -const initialItems = page => { +const initialItems = (page: number): Result => { return { items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d), nextId: page + 1, @@ -14,7 +21,7 @@ const initialItems = page => { } } -const fetchItems = async (page, ts) => { +const fetchItems = async (page: number, ts: number): Promise => { await sleep(10) return { items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d), @@ -30,14 +37,14 @@ describe('useInfiniteQuery', () => { it('should return the correct states for a successful query', async () => { let count = 0 - const states = [] + const states: InfiniteQueryResult[] = [] function Page() { const state = useInfiniteQuery( 'items', - (key, nextId = 0) => fetchItems(nextId, count++), + (_key: string, nextId = 0) => fetchItems(nextId, count++), { - getFetchMore: (lastGroup, allGroups) => Boolean(lastGroup.nextId), + getFetchMore: (lastGroup, _allGroups) => Boolean(lastGroup.nextId), } ) @@ -54,7 +61,7 @@ describe('useInfiniteQuery', () => { await waitFor(() => rendered.getByText('Status: success')) - expect(states[0]).toMatchObject({ + expect(states[0]).toEqual({ clear: expect.any(Function), data: undefined, error: null, @@ -62,15 +69,18 @@ describe('useInfiniteQuery', () => { fetchMore: expect.any(Function), isError: false, isFetching: true, + isFetchingMore: undefined, isIdle: false, isLoading: true, isStale: true, isSuccess: false, + query: expect.any(Object), refetch: expect.any(Function), status: 'loading', + updatedAt: expect.any(Number), }) - expect(states[1]).toMatchObject({ + expect(states[1]).toEqual({ clear: expect.any(Function), canFetchMore: true, data: [ @@ -83,14 +93,17 @@ describe('useInfiniteQuery', () => { error: null, failureCount: 0, fetchMore: expect.any(Function), + isFetchingMore: undefined, isError: false, isFetching: false, isIdle: false, isLoading: false, isStale: true, isSuccess: true, + query: expect.any(Object), refetch: expect.any(Function), status: 'success', + updatedAt: expect.any(Number), }) }) @@ -106,11 +119,11 @@ describe('useInfiniteQuery', () => { fetchMore, canFetchMore, refetch, - } = useInfiniteQuery( + } = useInfiniteQuery( 'items', - (key, nextId = 0) => fetchItems(nextId, fetchCountRef.current++), + (_key, nextId = 0) => fetchItems(nextId, fetchCountRef.current++), { - getFetchMore: (lastGroup, allGroups) => lastGroup.nextId, + getFetchMore: (lastGroup, _allGroups) => lastGroup.nextId, } ) @@ -120,11 +133,11 @@ describe('useInfiniteQuery', () => { {status === 'loading' ? ( 'Loading...' ) : status === 'error' ? ( - Error: {error.message} + Error: {error?.message} ) : ( <>
Data:
- {data.map((page, i) => ( + {data?.map((page, i) => (
Page {i}: {page.ts} @@ -139,7 +152,7 @@ describe('useInfiniteQuery', () => {
- + +
) } @@ -41,11 +41,11 @@ describe('useMutation', () => { }) it('should be able to reset `error`', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) function Page() { - const [mutate, mutationResult] = useMutation( + const [mutate, mutationResult] = useMutation( () => { const error = new Error('Expected mock error. All is well!') error.stack = '' @@ -61,8 +61,8 @@ describe('useMutation', () => { {mutationResult.error && (

{mutationResult.error.message}

)} - - + +
) } @@ -83,7 +83,7 @@ describe('useMutation', () => { expect(queryByTestId('error')).toBeNull() - console.error.mockRestore() + consoleMock.mockRestore() }) it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => { @@ -93,7 +93,7 @@ describe('useMutation', () => { function Page() { const [mutate] = useMutation( - async ({ count }) => Promise.resolve(count), + async ({ count }: { count: number }) => Promise.resolve(count), { onSuccess: data => onSuccessMock(data), onSettled: data => onSettledMock(data), @@ -132,24 +132,22 @@ describe('useMutation', () => { }) it('should be able to call `onError` and `onSettled` after each failed mutate', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) const onErrorMock = jest.fn() const onSettledMock = jest.fn() let count = 0 function Page() { const [mutate] = useMutation( - ({ count }) => { + ({ count }: { count: number }) => { const error = new Error(`Expected mock error. All is well! ${count}`) error.stack = '' return Promise.reject(error) }, { - onError: error => onErrorMock(error.message), - onSettled: (_data, error) => onSettledMock(error.message), - }, - { + onError: (error: Error) => onErrorMock(error.message), + onSettled: (_data, error) => onSettledMock(error?.message), throwOnError: false, } ) diff --git a/src/react/tests/usePaginatedQuery.test.js b/src/react/tests/usePaginatedQuery.test.tsx similarity index 87% rename from src/react/tests/usePaginatedQuery.test.js rename to src/react/tests/usePaginatedQuery.test.tsx index a355b5260f..243f652bac 100644 --- a/src/react/tests/usePaginatedQuery.test.js +++ b/src/react/tests/usePaginatedQuery.test.tsx @@ -3,6 +3,7 @@ import * as React from 'react' import { sleep } from './utils' import { usePaginatedQuery, queryCaches } from '../index' +import { PaginatedQueryResult } from '../../core/types' describe('usePaginatedQuery', () => { afterEach(() => { @@ -10,13 +11,16 @@ describe('usePaginatedQuery', () => { }) it('should return the correct states for a successful query', async () => { - const states = [] + const states: PaginatedQueryResult[] = [] function Page() { - const state = usePaginatedQuery(['data', 1], async (queryName, page) => { - await sleep(10) - return page - }) + const state = usePaginatedQuery( + ['data', 1], + async (_queryName, page: number) => { + await sleep(10) + return page + } + ) states.push(state) @@ -31,9 +35,8 @@ describe('usePaginatedQuery', () => { await waitFor(() => rendered.getByText('Status: success')) - expect(states[0]).toMatchObject({ + expect(states[0]).toEqual({ clear: expect.any(Function), - data: undefined, error: null, failureCount: 0, isError: false, @@ -42,15 +45,16 @@ describe('usePaginatedQuery', () => { isLoading: true, isStale: true, isSuccess: false, + query: expect.any(Object), latestData: undefined, resolvedData: undefined, refetch: expect.any(Function), status: 'loading', + updatedAt: expect.any(Number), }) - expect(states[1]).toMatchObject({ + expect(states[1]).toEqual({ clear: expect.any(Function), - data: 1, error: null, failureCount: 0, isError: false, @@ -59,10 +63,12 @@ describe('usePaginatedQuery', () => { isLoading: false, isStale: true, isSuccess: true, + query: expect.any(Object), latestData: 1, resolvedData: 1, refetch: expect.any(Function), status: 'success', + updatedAt: expect.any(Number), }) }) @@ -71,7 +77,7 @@ describe('usePaginatedQuery', () => { const [page, setPage] = React.useState(1) const { resolvedData = 'undefined' } = usePaginatedQuery( ['data', page], - async (queryName, page) => { + async (_queryName: string, page: number) => { await sleep(10) return page } @@ -102,10 +108,11 @@ describe('usePaginatedQuery', () => { it('should use initialData only on the first page, then use previous page data while fetching the next page', async () => { function Page() { const [page, setPage] = React.useState(1) + const params = { page } const { resolvedData } = usePaginatedQuery( - ['data', { page }], - async (queryName, { page }) => { + ['data', params], + async (_queryName: string, { page }: typeof params) => { await sleep(10) return page }, @@ -141,10 +148,11 @@ describe('usePaginatedQuery', () => { it('should not trigger unnecessary loading state', async () => { function Page() { const [page, setPage] = React.useState(1) + const params = { page } const { resolvedData, status } = usePaginatedQuery( - ['data', { page }], - async (queryName, { page }) => { + ['data', params], + async (_queryName: string, { page }: typeof params) => { await sleep(10) return page }, @@ -179,7 +187,7 @@ describe('usePaginatedQuery', () => { const [page, setPage] = React.useState(1) const { resolvedData = 'undefined' } = usePaginatedQuery( ['data', searchTerm, page], - async (queryName, searchTerm, page) => { + async (_queryName: string, searchTerm: string, page: number) => { await sleep(10) return `${searchTerm} ${page}` }, @@ -239,10 +247,11 @@ describe('usePaginatedQuery', () => { it('should not suspend while fetching the next page', async () => { function Page() { const [page, setPage] = React.useState(1) + const params = { page } const { resolvedData } = usePaginatedQuery( - ['data', { page }], - async (queryName, { page }) => { + ['data', params], + async (_queryName: string, { page }: typeof params) => { await sleep(10) return page }, diff --git a/src/react/tests/useQuery.test.js b/src/react/tests/useQuery.test.tsx similarity index 76% rename from src/react/tests/useQuery.test.js rename to src/react/tests/useQuery.test.tsx index 9e35196cd7..4759865dda 100644 --- a/src/react/tests/useQuery.test.js +++ b/src/react/tests/useQuery.test.tsx @@ -2,13 +2,54 @@ import { render, act, waitFor, fireEvent } from '@testing-library/react' import * as React from 'react' import { useQuery, queryCache, queryCaches } from '../index' -import { sleep } from './utils' +import { sleep, expectType } from './utils' +import { QueryResult } from '../../core/types' describe('useQuery', () => { afterEach(() => { queryCaches.forEach(cache => cache.clear({ notify: false })) }) + it('should return the correct types', () => { + // @ts-ignore + // eslint-disable-next-line + function Page() { + // unspecified query function should default to unknown + const noQueryFn = useQuery('test') + expectType(noQueryFn.data) + expectType(noQueryFn.error) + + // it should infer the result type from the query function + const fromQueryFn = useQuery('test', () => 'test') + expectType(fromQueryFn.data) + expectType(fromQueryFn.error) + + // it should be possible to specify the error type + const withError = useQuery('test', () => 'test') + expectType(withError.data) + expectType(withError.error) + + // unspecified error type should default to unknown + const withoutError = useQuery('test', () => 1) + expectType(withoutError.data) + expectType(withoutError.error) + + // it should provide the result type in the configuration + useQuery(['key'], async () => true, { + onSuccess: data => expectType(data), + onSettled: data => expectType(data), + }) + + // should error when the query function result does not match with the specified type + // @ts-expect-error + useQuery('test', () => 'test') + + // should error when a non configuration object is given as first argument + // @ts-expect-error + useQuery({ a: 'a' }, () => 'test') + } + }) + // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const queryKey = Math.random() @@ -35,7 +76,7 @@ describe('useQuery', () => { it('should return the correct states for a successful query', async () => { const queryKey = Math.random() - const states = [] + const states: QueryResult[] = [] function Page() { const state = useQuery(queryKey, () => 'test') @@ -53,7 +94,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Status: success')) - expect(states[0]).toMatchObject({ + expect(states[0]).toEqual({ clear: expect.any(Function), data: undefined, error: null, @@ -64,11 +105,13 @@ describe('useQuery', () => { isLoading: true, isStale: true, isSuccess: false, + query: expect.any(Object), refetch: expect.any(Function), status: 'loading', + updatedAt: expect.any(Number), }) - expect(states[1]).toMatchObject({ + expect(states[1]).toEqual({ clear: expect.any(Function), data: 'test', error: null, @@ -79,23 +122,29 @@ describe('useQuery', () => { isLoading: false, isStale: true, isSuccess: true, + query: expect.any(Object), refetch: expect.any(Function), status: 'success', + updatedAt: expect.any(Number), }) }) it('should return the correct states for an unsuccessful query', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) const queryKey = Math.random() - const states = [] + const states: QueryResult[] = [] function Page() { - const state = useQuery(queryKey, () => Promise.reject('rejected'), { - retry: 1, - retryDelay: 1, - }) + const state = useQuery( + queryKey, + () => Promise.reject('rejected'), + { + retry: 1, + retryDelay: 1, + } + ) states.push(state) @@ -110,7 +159,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Status: error')) - expect(states[0]).toMatchObject({ + expect(states[0]).toEqual({ clear: expect.any(Function), data: undefined, error: null, @@ -121,11 +170,13 @@ describe('useQuery', () => { isLoading: true, isStale: true, isSuccess: false, + query: expect.any(Object), refetch: expect.any(Function), status: 'loading', + updatedAt: expect.any(Number), }) - expect(states[1]).toMatchObject({ + expect(states[1]).toEqual({ clear: expect.any(Function), data: undefined, error: null, @@ -136,11 +187,13 @@ describe('useQuery', () => { isLoading: true, isStale: true, isSuccess: false, + query: expect.any(Object), refetch: expect.any(Function), status: 'loading', + updatedAt: expect.any(Number), }) - expect(states[2]).toMatchObject({ + expect(states[2]).toEqual({ clear: expect.any(Function), data: undefined, error: 'rejected', @@ -151,8 +204,10 @@ describe('useQuery', () => { isLoading: false, isStale: true, isSuccess: false, + query: expect.any(Object), refetch: expect.any(Function), status: 'error', + updatedAt: expect.any(Number), }) }) @@ -274,11 +329,11 @@ describe('useQuery', () => { }) it('should set status to error if queryFn throws', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( 'test', () => { return Promise.reject('Error test jaylen') @@ -299,12 +354,12 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('error')) await waitFor(() => rendered.getByText('Error test jaylen')) - console.error.mockRestore() + consoleMock.mockRestore() }) it('should retry specified number of times', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) const queryFn = jest.fn() queryFn.mockImplementation(() => { @@ -334,12 +389,12 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('Failed 2 times')) expect(queryFn).toHaveBeenCalledTimes(2) - console.error.mockRestore() + consoleMock.mockRestore() }) it('should not retry if retry function `false`', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) const queryFn = jest.fn() @@ -352,9 +407,13 @@ describe('useQuery', () => { }) function Page() { - const { status, failureCount, error } = useQuery('test', queryFn, { + const { status, failureCount, error } = useQuery< + undefined, + string, + string + >('test', queryFn, { retryDelay: 1, - retry: (failureCount, error) => error !== 'NoRetry', + retry: (_failureCount, error) => error !== 'NoRetry', }) return ( @@ -375,7 +434,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('NoRetry')) expect(queryFn).toHaveBeenCalledTimes(2) - console.error.mockRestore() + consoleMock.mockRestore() }) it('should garbage collect queries without data immediately', async () => { @@ -383,7 +442,7 @@ describe('useQuery', () => { const [filter, setFilter] = React.useState('') const { data } = useQuery( ['todos', { filter }], - async (key, { filter } = {}) => { + async (_key, { filter }) => { await sleep(10) return `todo ${filter}` } @@ -417,7 +476,7 @@ describe('useQuery', () => { it('should continue retry after focus regain', async () => { const originalVisibilityState = document.visibilityState - function mockVisibilityState(value) { + function mockVisibilityState(value: string) { Object.defineProperty(document, 'visibilityState', { value, configurable: true, @@ -523,8 +582,8 @@ describe('useQuery', () => { // See https://github.com/tannerlinsley/react-query/issues/190 it('should reset failureCount on successful fetch', async () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) function Page() { let counter = 0 @@ -554,7 +613,7 @@ describe('useQuery', () => { await waitFor(() => rendered.getByText('failureCount 2')) await waitFor(() => rendered.getByText('failureCount 0')) - console.error.mockRestore() + consoleMock.mockRestore() }) // See https://github.com/tannerlinsley/react-query/issues/199 @@ -563,7 +622,7 @@ describe('useQuery', () => { const [enabled, setEnabled] = React.useState(false) const [isPrefetched, setPrefetched] = React.useState(false) - const query = useQuery('key', () => {}, { + const query = useQuery('key', () => undefined, { enabled, }) @@ -595,10 +654,10 @@ describe('useQuery', () => { it('should support dependent queries via the enable config option', async () => { function Page() { - const [shouldFetch, setShouldFetch] = React.useState() + const [shouldFetch, setShouldFetch] = React.useState(false) const query = useQuery('key', () => 'data', { - enabled: shouldFetch?.on, + enabled: shouldFetch, }) return ( @@ -606,7 +665,7 @@ describe('useQuery', () => {
Status: {query.status}

Data: {query.data || 'no data'}

{query.isStale ? ( - + ) : null}
) @@ -628,7 +687,7 @@ describe('useQuery', () => { it('should not mark query as fetching, when using initialData', async () => { function Page() { - const query = useQuery('key', () => {}, { initialData: 'data' }) + const query = useQuery('key', () => 'serverData', { initialData: 'data' }) return (
@@ -646,7 +705,7 @@ describe('useQuery', () => { it('should initialize state properly, when initialData is falsy', async () => { function Page() { - const query = useQuery('key', () => {}, { initialData: 0 }) + const query = useQuery('key', () => 1, { initialData: 0 }) return (
@@ -722,25 +781,10 @@ describe('useQuery', () => { rendered.getByText('status: idle') }) - it('it should throw when using a bad query syntax', async () => { - // mock console.error to avoid the wall of red text, - // you could also do this on beforeEach/afterEach - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) - - function Page() { - useQuery(() => {}) - return null - } - expect(() => render()).toThrowError(/query key/) - - console.error.mockRestore() - }) - // See https://github.com/tannerlinsley/react-query/issues/360 test('should init to status:idle when enabled is falsey', async () => { function Page() { - const query = useQuery('key', () => {}, { + const query = useQuery('key', () => undefined, { enabled: false, }) @@ -771,7 +815,8 @@ describe('useQuery', () => { rendered.unmount() const query = queryCache.getQuery('test') - expect(query.cacheTimeout).toBe(undefined) + // @ts-expect-error + expect(query!.cacheTimeout).toBe(undefined) }) it('should not cause memo churn when data does not change', async () => { @@ -779,7 +824,7 @@ describe('useQuery', () => { const memoFn = jest.fn() function Page() { - const queryInfo = useQuery('test', async () => { + const result = useQuery('test', async () => { await sleep(10) return ( queryFn() || { @@ -792,14 +837,14 @@ describe('useQuery', () => { React.useMemo(() => { memoFn() - return queryInfo.data - }, [queryInfo.data]) + return result.data + }, [result.data]) return (
-
status {queryInfo.status}
-
isFetching {queryInfo.isFetching ? 'true' : 'false'}
- +
status {result.status}
+
isFetching {result.isFetching ? 'true' : 'false'}
+
) } @@ -842,95 +887,86 @@ describe('useQuery', () => { }) it('should error when using functions as query keys', () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + const consoleMock = jest.spyOn(console, 'error') + consoleMock.mockImplementation(() => undefined) function Page() { - const { data } = useQuery( - () => {}, + useQuery( + // @ts-expect-error + () => undefined, () => 'data' ) - return data + return null } - try { - render() - } catch {} - - expect(console.error).toHaveBeenCalledTimes(2) + expect(() => render()).toThrowError(/query key/) - console.error.mockRestore() + consoleMock.mockRestore() }) - it('should error when using an empty query key', () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) - + it('should accept undefined as query key', async () => { function Page() { - const { data } = useQuery('', () => 'data') - return data + const result = useQuery(undefined, (key: undefined) => key) + return <>{JSON.stringify(result.data)} } - try { - render() - } catch {} - - expect(console.error).toHaveBeenCalledTimes(2) + const rendered = render() - console.error.mockRestore() + await waitFor(() => rendered.getByText('null')) }) - it('should error when using an undefined query key', () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) - + it('should accept a boolean as query key', async () => { function Page() { - const { data } = useQuery(undefined, () => 'data') - return data + const result = useQuery(false, (key: boolean) => key) + return <>{JSON.stringify(result.data)} } - try { - render() - } catch {} - - expect(console.error).toHaveBeenCalledTimes(2) + const rendered = render() - console.error.mockRestore() + await waitFor(() => rendered.getByText('false')) }) - it('should error when using a falsy query key', () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) - + it('should accept null as query key', async () => { function Page() { - const { data } = useQuery(false, () => 'data') - return data + const result = useQuery(null, key => key) + return <>{JSON.stringify(result.data)} } - try { - render() - } catch {} - - expect(console.error).toHaveBeenCalledTimes(2) + const rendered = render() - console.error.mockRestore() + await waitFor(() => rendered.getByText('null')) }) - it('should error when using a null query key', () => { - jest.spyOn(console, 'error') - console.error.mockImplementation(() => {}) + it('should accept a number as query key', async () => { + function Page() { + const result = useQuery(1, (key: number) => key) + return <>{JSON.stringify(result.data)} + } + + const rendered = render() + await waitFor(() => rendered.getByText('1')) + }) + + it('should accept an empty string as query key', async () => { function Page() { - const { data } = useQuery(false, () => 'data') - return data + const result = useQuery('', (key: string) => key) + return <>{JSON.stringify(result.data)} } - try { - render() - } catch {} + const rendered = render() + + await waitFor(() => rendered.getByText('')) + }) + + it('should accept an object as query key', async () => { + function Page() { + const result = useQuery([{ a: 'a' }], (key: { a: string }) => key) + return <>{JSON.stringify(result.data)} + } - expect(console.error).toHaveBeenCalledTimes(2) + const rendered = render() - console.error.mockRestore() + await waitFor(() => rendered.getByText('{"a":"a"}')) }) }) diff --git a/src/react/tests/utils.js b/src/react/tests/utils.js deleted file mode 100644 index c3ebacf537..0000000000 --- a/src/react/tests/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function sleep(timeout) { - return new Promise((resolve, reject) => { - setTimeout(resolve, timeout) - }) -} diff --git a/src/react/tests/utils.tsx b/src/react/tests/utils.tsx new file mode 100644 index 0000000000..7aa1674315 --- /dev/null +++ b/src/react/tests/utils.tsx @@ -0,0 +1,24 @@ +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +} + +/** + * Checks that `T` is of type `U`. + */ +export type TypeOf = Exclude extends never ? true : false + +/** + * Checks that `T` is equal to `U`. + */ +export type TypeEqual = Exclude extends never + ? Exclude extends never + ? true + : false + : false + +/** + * Assert the parameter is of a specific type. + */ +export const expectType = (_: T): void => undefined diff --git a/src/react/useBaseQuery.js b/src/react/useBaseQuery.js deleted file mode 100644 index b4fc955e71..0000000000 --- a/src/react/useBaseQuery.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' - -import { useQueryCache } from './ReactQueryCacheProvider' -import { useRerenderer } from './utils' - -export function useBaseQuery(queryKey, config = {}) { - // Make a rerender function - const rerender = useRerenderer() - - // Get the query cache - const queryCache = useQueryCache() - - // Build the query for use - const query = queryCache.buildQuery(queryKey, config) - - // Create a query instance ref - const instanceRef = React.useRef() - - // Subscribe to the query when the subscribe function changes - React.useEffect(() => { - instanceRef.current = query.subscribe(() => { - rerender() - }) - - // Unsubscribe when things change - return instanceRef.current.unsubscribe - }, [query, rerender]) - - // Always update the config - React.useEffect(() => { - instanceRef.current.updateConfig(config) - }) - - const enabledBool = Boolean(config.enabled) - - // Run the instance when the query or enabled change - React.useEffect(() => { - if (enabledBool && query) { - // Just for change detection - } - instanceRef.current.run() - }, [enabledBool, query]) - - return { - ...query, - ...query.state, - query, - } -} diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts new file mode 100644 index 0000000000..8280db6ba0 --- /dev/null +++ b/src/react/useBaseQuery.ts @@ -0,0 +1,70 @@ +import React from 'react' + +import { useQueryCache } from './ReactQueryCacheProvider' +import { useRerenderer } from './utils' +import { QueryInstance } from '../core/queryInstance' +import { QueryConfig, QueryKey, QueryResultBase } from '../core/types' + +export function useBaseQuery( + queryKey: QueryKey, + config: QueryConfig = {} +): QueryResultBase { + // Make a rerender function + const rerender = useRerenderer() + + // Get the query cache + const queryCache = useQueryCache() + + // Build the query for use + const query = queryCache.buildQuery(queryKey, config) + const state = query.state + + // Create a query instance ref + const instanceRef = React.useRef>() + + // Subscribe to the query when the subscribe function changes + React.useEffect(() => { + const instance = query.subscribe(() => { + rerender() + }) + + instanceRef.current = instance + + // Unsubscribe when things change + return () => instance.unsubscribe() + }, [query, rerender]) + + // Always update the config + React.useEffect(() => { + instanceRef.current?.updateConfig(config) + }) + + const enabledBool = !!config.enabled + + // Run the instance when the query or enabled change + React.useEffect(() => { + if (enabledBool && query) { + // Just for change detection + } + instanceRef.current?.run() + }, [enabledBool, query]) + + const clear = React.useMemo(() => query.clear.bind(query), [query]) + const refetch = React.useMemo(() => query.refetch.bind(query), [query]) + + return { + clear, + error: state.error, + failureCount: state.failureCount, + isError: state.isError, + isFetching: state.isFetching, + isIdle: state.isIdle, + isLoading: state.isLoading, + isStale: state.isStale, + isSuccess: state.isSuccess, + query, + refetch, + status: state.status, + updatedAt: state.updatedAt, + } +} diff --git a/src/react/useInfiniteQuery.js b/src/react/useInfiniteQuery.js deleted file mode 100644 index 20101e0fd1..0000000000 --- a/src/react/useInfiniteQuery.js +++ /dev/null @@ -1,16 +0,0 @@ -// - -import { useBaseQuery } from './useBaseQuery' -import { useQueryArgs, handleSuspense } from './utils' - -export function useInfiniteQuery(...args) { - let [queryKey, config] = useQueryArgs(args) - - config.infinite = true - - const queryInfo = useBaseQuery(queryKey, config) - - handleSuspense(queryInfo) - - return queryInfo -} diff --git a/src/react/useInfiniteQuery.ts b/src/react/useInfiniteQuery.ts new file mode 100644 index 0000000000..cdaa000d09 --- /dev/null +++ b/src/react/useInfiniteQuery.ts @@ -0,0 +1,95 @@ +import React from 'react' + +import { useBaseQuery } from './useBaseQuery' +import { handleSuspense } from './utils' +import { + InfiniteQueryConfig, + InfiniteQueryResult, + QueryKey, + QueryKeyWithoutArray, + QueryKeyWithoutObject, + QueryKeyWithoutObjectAndArray, + TupleQueryFunction, + TupleQueryKey, +} from '../core/types' +import { useQueryArgs } from './useQueryArgs' + +// TYPES + +export interface UseInfiniteQueryObjectConfig< + TResult, + TError, + TKey extends TupleQueryKey +> { + queryKey: QueryKey + queryFn?: TupleQueryFunction + config?: InfiniteQueryConfig +} + +// HOOK + +// Parameter syntax with optional config +export function useInfiniteQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObject +>( + queryKey: TKey, + queryConfig?: InfiniteQueryConfig +): InfiniteQueryResult + +// Parameter syntax with query function and optional config +export function useInfiniteQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObjectAndArray +>( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: InfiniteQueryConfig +): InfiniteQueryResult + +export function useInfiniteQuery( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: InfiniteQueryConfig +): InfiniteQueryResult + +// Object syntax +export function useInfiniteQuery< + TResult, + TError, + TKey extends QueryKeyWithoutArray +>( + config: UseInfiniteQueryObjectConfig +): InfiniteQueryResult + +export function useInfiniteQuery( + config: UseInfiniteQueryObjectConfig +): InfiniteQueryResult + +// Implementation +export function useInfiniteQuery( + ...args: any[] +): InfiniteQueryResult { + const [queryKey, config] = useQueryArgs(args) + + config.infinite = true + + const result = useBaseQuery(queryKey, config) + const query = result.query + const state = result.query.state + + handleSuspense(result) + + const fetchMore = React.useMemo(() => query.fetchMore.bind(query), [query]) + + return { + ...result, + data: state.data, + canFetchMore: state.canFetchMore, + fetchMore, + isFetching: state.isFetching, + isFetchingMore: state.isFetchingMore, + } +} diff --git a/src/react/useIsFetching.js b/src/react/useIsFetching.ts similarity index 92% rename from src/react/useIsFetching.js rename to src/react/useIsFetching.ts index 33230e704f..58f04a6be1 100644 --- a/src/react/useIsFetching.js +++ b/src/react/useIsFetching.ts @@ -3,7 +3,7 @@ import React from 'react' import { useRerenderer, useGetLatest } from './utils' import { useQueryCache } from './ReactQueryCacheProvider' -export function useIsFetching() { +export function useIsFetching(): number { const queryCache = useQueryCache() const rerender = useRerenderer() const isFetching = queryCache.isFetching diff --git a/src/react/useMutation.js b/src/react/useMutation.js deleted file mode 100644 index 8db2c3922e..0000000000 --- a/src/react/useMutation.js +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react' - -// - -import { useConfigContext } from './ReactQueryConfigProvider' -import { useGetLatest, useMountedCallback } from './utils' -import { - statusIdle, - statusLoading, - statusSuccess, - statusError, - Console, - uid, - noop, -} from '../core/utils' - -const getDefaultState = () => ({ - status: statusIdle, - data: undefined, - error: null, -}) - -const actionReset = {} -const actionLoading = {} -const actionResolve = {} -const actionReject = {} - -function mutationReducer(state, action) { - if (action.type === actionReset) { - return getDefaultState() - } - if (action.type === actionLoading) { - return { - status: statusLoading, - } - } - if (action.type === actionResolve) { - return { - status: statusSuccess, - data: action.data, - } - } - if (action.type === actionReject) { - return { - status: statusError, - error: action.error, - } - } - throw new Error() -} - -export function useMutation(mutationFn, config = {}) { - const [state, unsafeDispatch] = React.useReducer( - mutationReducer, - null, - getDefaultState - ) - - const dispatch = useMountedCallback(unsafeDispatch) - - const getMutationFn = useGetLatest(mutationFn) - - const getConfig = useGetLatest({ - ...useConfigContext().shared, - ...useConfigContext().mutations, - ...config, - }) - - const latestMutationRef = React.useRef() - - const mutate = React.useCallback( - async ( - variables, - { onSuccess = noop, onError = noop, onSettled = noop, throwOnError } = {} - ) => { - const config = getConfig() - - const mutationId = uid() - latestMutationRef.current = mutationId - - const isLatest = () => latestMutationRef.current === mutationId - - let snapshotValue - - try { - dispatch({ type: actionLoading }) - snapshotValue = await config.onMutate(variables) - - let data = await getMutationFn()(variables) - - if (isLatest()) { - dispatch({ type: actionResolve, data }) - } - - await config.onSuccess(data, variables) - await onSuccess(data, variables) - await config.onSettled(data, null, variables) - await onSettled(data, null, variables) - - return data - } catch (error) { - Console.error(error) - await config.onError(error, variables, snapshotValue) - await onError(error, variables, snapshotValue) - await config.onSettled(undefined, error, variables, snapshotValue) - await onSettled(undefined, error, variables, snapshotValue) - - if (isLatest()) { - dispatch({ type: actionReject, error }) - } - - if (throwOnError ?? config.throwOnError) { - throw error - } - } - }, - [dispatch, getConfig, getMutationFn] - ) - - const reset = React.useCallback(() => dispatch({ type: actionReset }), [ - dispatch, - ]) - - React.useEffect(() => { - const { suspense, useErrorBoundary } = getConfig() - - if ((useErrorBoundary ?? suspense) && state.error) { - throw state.error - } - }, [getConfig, state.error]) - - return [ - mutate, - { - ...state, - reset, - isIdle: state.status === statusIdle, - isLoading: state.status === statusLoading, - isSuccess: state.status === statusSuccess, - isError: state.status === statusError, - }, - ] -} diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts new file mode 100644 index 0000000000..b0606ae045 --- /dev/null +++ b/src/react/useMutation.ts @@ -0,0 +1,200 @@ +import React from 'react' + +import { useConfigContext } from './ReactQueryConfigProvider' +import { useGetLatest, useMountedCallback } from './utils' +import { Console, uid, getStatusProps } from '../core/utils' +import { + QueryStatus, + MutationResultPair, + MutationFunction, + MutationConfig, + MutateConfig, +} from '../core/types' + +// TYPES + +type Reducer = (prevState: S, action: A) => S + +interface State { + status: QueryStatus + data: TResult | undefined + error: TError | null + isIdle: boolean + isLoading: boolean + isSuccess: boolean + isError: boolean +} + +enum ActionType { + Reset = 'Reset', + Loading = 'Loading', + Resolve = 'Resolve', + Reject = 'Reject', +} + +interface ResetAction { + type: ActionType.Reset +} + +interface LoadingAction { + type: ActionType.Loading +} + +interface ResolveAction { + type: ActionType.Resolve + data: TResult +} + +interface RejectAction { + type: ActionType.Reject + error: TError +} + +type Action = + | ResetAction + | LoadingAction + | ResolveAction + | RejectAction + +// HOOK + +const getDefaultState = (): State => ({ + ...getStatusProps(QueryStatus.Idle), + data: undefined, + error: null, +}) + +function mutationReducer( + state: State, + action: Action +): State { + switch (action.type) { + case ActionType.Reset: + return getDefaultState() + case ActionType.Loading: + return { + ...getStatusProps(QueryStatus.Loading), + data: undefined, + error: null, + } + case ActionType.Resolve: + return { + ...getStatusProps(QueryStatus.Success), + data: action.data, + error: null, + } + case ActionType.Reject: + return { + ...getStatusProps(QueryStatus.Error), + data: undefined, + error: action.error, + } + default: + return state + } +} + +export function useMutation< + TResult, + TError = unknown, + TVariables = undefined, + TSnapshot = unknown +>( + mutationFn: MutationFunction, + config: MutationConfig = {} +): MutationResultPair { + const [state, unsafeDispatch] = React.useReducer( + mutationReducer as Reducer, Action>, + null, + getDefaultState + ) + + const dispatch = useMountedCallback(unsafeDispatch) + + const getMutationFn = useGetLatest(mutationFn) + + const contextConfig = useConfigContext() + + const getConfig = useGetLatest({ + ...contextConfig.shared, + ...contextConfig.mutations, + ...config, + }) + + const latestMutationRef = React.useRef() + + const mutate = React.useCallback( + async ( + variables?: TVariables, + mutateConfig: MutateConfig = {} + ): Promise => { + const config = getConfig() + + const mutationId = uid() + latestMutationRef.current = mutationId + + const isLatest = () => latestMutationRef.current === mutationId + + let snapshotValue: TSnapshot | undefined + + try { + dispatch({ type: ActionType.Loading }) + snapshotValue = (await config.onMutate?.(variables!)) as TSnapshot + + const data = await getMutationFn()(variables!) + + if (isLatest()) { + dispatch({ type: ActionType.Resolve, data }) + } + + await config.onSuccess?.(data, variables!) + await mutateConfig.onSuccess?.(data, variables!) + await config.onSettled?.(data, null, variables!) + await mutateConfig.onSettled?.(data, null, variables!) + + return data + } catch (error) { + Console.error(error) + await config.onError?.(error, variables!, snapshotValue!) + await mutateConfig.onError?.(error, variables!, snapshotValue!) + await config.onSettled?.( + undefined, + error, + variables!, + snapshotValue as TSnapshot + ) + await mutateConfig.onSettled?.( + undefined, + error, + variables!, + snapshotValue + ) + + if (isLatest()) { + dispatch({ type: ActionType.Reject, error }) + } + + if (mutateConfig.throwOnError ?? config.throwOnError) { + throw error + } + + return + } + }, + [dispatch, getConfig, getMutationFn] + ) + + const reset = React.useCallback(() => { + dispatch({ type: ActionType.Reset }) + }, [dispatch]) + + React.useEffect(() => { + const { suspense, useErrorBoundary } = getConfig() + + if ((useErrorBoundary ?? suspense) && state.error) { + throw state.error + } + }, [getConfig, state.error]) + + return [mutate, { ...state, reset }] +} diff --git a/src/react/usePaginatedQuery.js b/src/react/usePaginatedQuery.js deleted file mode 100644 index 1fa3bc5f61..0000000000 --- a/src/react/usePaginatedQuery.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react' - -// - -import { useBaseQuery } from './useBaseQuery' -import { useQueryArgs, handleSuspense } from './utils' -import { getStatusBools } from '../core/utils' - -// A paginated query is more like a "lag" query, which means -// as the query key changes, we keep the results from the -// last query and use them as placeholder data in the next one -// We DON'T use it as initial data though. That's important -export function usePaginatedQuery(...args) { - let [queryKey, config = {}] = useQueryArgs(args) - - // Keep track of the latest data result - const lastDataRef = React.useRef() - - // If latestData is there, don't use initialData - if (typeof lastDataRef.current !== 'undefined') { - delete config.initialData - } - - // Make the query as normal - const queryInfo = useBaseQuery(queryKey, config) - - // If the query is disabled, get rid of the latest data - if (!queryInfo.query.config.enabled) { - lastDataRef.current = undefined - } - - // Get the real data and status from the query - let { data: latestData, status } = queryInfo - - // If the real query succeeds, and there is data in it, - // update the latest data - React.useEffect(() => { - if (status === 'success' && typeof latestData !== 'undefined') { - lastDataRef.current = latestData - } - }, [latestData, status]) - - // Resolved data should be either the real data we're waiting on - // or the latest placeholder data - let resolvedData = latestData - if (typeof resolvedData === 'undefined') { - resolvedData = lastDataRef.current - } - - // If we have any data at all from either, we - // need to make sure the status is success, even though - // the real query may still be loading - if (typeof resolvedData !== 'undefined') { - const overrides = { status: 'success', ...getStatusBools('success') } - Object.assign(queryInfo.query.state, overrides) - Object.assign(queryInfo, overrides) - } - - const paginatedQueryInfo = { - ...queryInfo, - resolvedData, - latestData, - } - - handleSuspense(paginatedQueryInfo) - - return paginatedQueryInfo -} diff --git a/src/react/usePaginatedQuery.ts b/src/react/usePaginatedQuery.ts new file mode 100644 index 0000000000..ba36c1d243 --- /dev/null +++ b/src/react/usePaginatedQuery.ts @@ -0,0 +1,134 @@ +import React from 'react' + +import { useBaseQuery } from './useBaseQuery' +import { handleSuspense } from './utils' +import { getStatusProps } from '../core/utils' +import { + PaginatedQueryConfig, + PaginatedQueryResult, + QueryKey, + QueryKeyWithoutArray, + QueryKeyWithoutObject, + QueryKeyWithoutObjectAndArray, + QueryStatus, + TupleQueryFunction, + TupleQueryKey, +} from '../core/types' +import { useQueryArgs } from './useQueryArgs' + +// A paginated query is more like a "lag" query, which means +// as the query key changes, we keep the results from the +// last query and use them as placeholder data in the next one +// We DON'T use it as initial data though. That's important + +// TYPES + +export interface UsePaginatedQueryObjectConfig< + TResult, + TError, + TKey extends TupleQueryKey +> { + queryKey: QueryKey + queryFn?: TupleQueryFunction + config?: PaginatedQueryConfig +} + +// HOOK + +// Parameter syntax with optional config +export function usePaginatedQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObject +>( + queryKey: TKey, + queryConfig?: PaginatedQueryConfig +): PaginatedQueryResult + +// Parameter syntax with query function and optional config +export function usePaginatedQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObjectAndArray +>( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: PaginatedQueryConfig +): PaginatedQueryResult + +export function usePaginatedQuery( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: PaginatedQueryConfig +): PaginatedQueryResult + +// Object syntax +export function usePaginatedQuery< + TResult, + TError, + TKey extends QueryKeyWithoutArray +>( + config: UsePaginatedQueryObjectConfig +): PaginatedQueryResult + +export function usePaginatedQuery( + config: UsePaginatedQueryObjectConfig +): PaginatedQueryResult + +// Implementation +export function usePaginatedQuery( + ...args: any[] +): PaginatedQueryResult { + const [queryKey, config] = useQueryArgs(args) + + // Keep track of the latest data result + const lastDataRef = React.useRef() + + // If latestData is there, don't use initialData + if (typeof lastDataRef.current !== 'undefined') { + delete config.initialData + } + + // Make the query as normal + const result = useBaseQuery(queryKey, config) + + // If the query is disabled, get rid of the latest data + if (!result.query.config.enabled) { + lastDataRef.current = undefined + } + + // Get the real data and status from the query + const { data: latestData, status } = result.query.state + + // If the real query succeeds, and there is data in it, + // update the latest data + React.useEffect(() => { + if (status === QueryStatus.Success && typeof latestData !== 'undefined') { + lastDataRef.current = latestData + } + }, [latestData, status]) + + // Resolved data should be either the real data we're waiting on + // or the latest placeholder data + let resolvedData = latestData + if (typeof resolvedData === 'undefined') { + resolvedData = lastDataRef.current + } + + // If we have any data at all from either, we + // need to make sure the status is success, even though + // the real query may still be loading + if (typeof resolvedData !== 'undefined') { + const overrides = getStatusProps(QueryStatus.Success) + Object.assign(result.query.state, overrides) + Object.assign(result, overrides) + } + + handleSuspense(result) + + return { + ...result, + resolvedData, + latestData, + } +} diff --git a/src/react/useQuery.js b/src/react/useQuery.js deleted file mode 100644 index 14bbbb429d..0000000000 --- a/src/react/useQuery.js +++ /dev/null @@ -1,10 +0,0 @@ -import { useBaseQuery } from './useBaseQuery' -import { useQueryArgs, handleSuspense } from './utils' - -export function useQuery(...args) { - const query = useBaseQuery(...useQueryArgs(args)) - - handleSuspense(query) - - return query -} diff --git a/src/react/useQuery.ts b/src/react/useQuery.ts new file mode 100644 index 0000000000..95e5044fc4 --- /dev/null +++ b/src/react/useQuery.ts @@ -0,0 +1,75 @@ +import { useBaseQuery } from './useBaseQuery' +import { handleSuspense } from './utils' +import { + QueryConfig, + QueryKey, + QueryKeyWithoutArray, + QueryKeyWithoutObject, + QueryKeyWithoutObjectAndArray, + QueryResult, + TupleQueryFunction, + TupleQueryKey, +} from '../core/types' +import { useQueryArgs } from './useQueryArgs' + +// TYPES + +export interface UseQueryObjectConfig< + TResult, + TError, + TKey extends TupleQueryKey +> { + queryKey: QueryKey + queryFn?: TupleQueryFunction + config?: QueryConfig +} + +// HOOK + +// Parameter syntax with optional config +export function useQuery( + queryKey: TKey, + queryConfig?: QueryConfig +): QueryResult + +// Parameter syntax with query function and optional config +export function useQuery< + TResult, + TError, + TKey extends QueryKeyWithoutObjectAndArray +>( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: QueryConfig +): QueryResult + +// Parameter syntax with query function and optional config +export function useQuery( + queryKey: TKey, + queryFn: TupleQueryFunction, + queryConfig?: QueryConfig +): QueryResult + +// Object syntax +export function useQuery( + config: UseQueryObjectConfig +): QueryResult + +export function useQuery( + config: UseQueryObjectConfig +): QueryResult + +// Implementation +export function useQuery( + ...args: any[] +): QueryResult { + const [queryKey, config] = useQueryArgs(args) + const result = useBaseQuery(queryKey, config) + + handleSuspense(result) + + return { + ...result, + data: result.query.state.data, + } +} diff --git a/src/react/useQueryArgs.js b/src/react/useQueryArgs.js deleted file mode 100644 index 2e0322fc11..0000000000 --- a/src/react/useQueryArgs.js +++ /dev/null @@ -1,17 +0,0 @@ -import { getQueryArgs } from '../core/utils' -import { useConfigContext } from './ReactQueryConfigProvider' - -export function useQueryArgs(args) { - const configContext = useConfigContext() - - let [queryKey, config, ...rest] = getQueryArgs(args) - - // Build the final config - config = { - ...configContext.shared, - ...configContext.queries, - ...config, - } - - return [queryKey, config, ...rest] -} diff --git a/src/react/useQueryArgs.ts b/src/react/useQueryArgs.ts new file mode 100644 index 0000000000..502bbd70b9 --- /dev/null +++ b/src/react/useQueryArgs.ts @@ -0,0 +1,22 @@ +import { getQueryArgs } from '../core/utils' +import { useConfigContext } from './ReactQueryConfigProvider' +import { QueryConfig, QueryKey } from '../core/types' + +export function useQueryArgs( + args: any[] +): [QueryKey, QueryConfig, TOptions] { + const configContext = useConfigContext() + + const [queryKey, config, options] = getQueryArgs( + args + ) + + // Build the final config + const configWithContext = { + ...configContext.shared, + ...configContext.queries, + ...config, + } as QueryConfig + + return [queryKey, configWithContext, options] +} diff --git a/src/react/utils.js b/src/react/utils.js deleted file mode 100644 index 25d9ae79db..0000000000 --- a/src/react/utils.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' - -import { useConfigContext } from './ReactQueryConfigProvider' - -import { - uid, - isServer, - statusError, - statusSuccess, - getQueryArgs, -} from '../core/utils' - -export function useUid() { - const ref = React.useRef(null) - - if (ref.current === null) { - ref.current = uid() - } - - return ref.current -} - -export function useGetLatest(obj) { - const ref = React.useRef() - ref.current = obj - - return React.useCallback(() => ref.current, []) -} - -export function useQueryArgs(args) { - const configContext = useConfigContext() - - let [queryKey, config, ...rest] = getQueryArgs(args) - - // Build the final config - config = { - ...configContext.shared, - ...configContext.queries, - ...config, - } - - return [queryKey, config, ...rest] -} - -export function useMountedCallback(callback) { - const mounted = React.useRef(false) - - React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { - mounted.current = true - return () => (mounted.current = false) - }, []) - - return React.useCallback( - (...args) => (mounted.current ? callback(...args) : void 0), - [callback] - ) -} - -export function useRerenderer() { - const rerender = useMountedCallback(React.useState()[1]) - return React.useCallback(() => rerender({}), [rerender]) -} - -export function handleSuspense(queryInfo) { - const { error, query } = queryInfo - const { config, state } = query - - if (config.suspense || config.useErrorBoundary) { - if (state.status === statusError && state.throwInErrorBoundary) { - throw error - } - - if (config.suspense && state.status !== statusSuccess && config.enabled) { - query.wasSuspended = true - throw query.fetch() - } - } -} diff --git a/src/react/utils.ts b/src/react/utils.ts new file mode 100644 index 0000000000..ac74197ab5 --- /dev/null +++ b/src/react/utils.ts @@ -0,0 +1,61 @@ +import React from 'react' + +import { uid, isServer } from '../core/utils' +import { QueryResultBase, QueryStatus } from '../core/types' + +export function useUid(): number { + const ref = React.useRef(0) + + if (ref.current === null) { + ref.current = uid() + } + + return ref.current +} + +export function useGetLatest(obj: T): () => T { + const ref = React.useRef(obj) + ref.current = obj + return React.useCallback(() => ref.current, []) +} + +export function useMountedCallback(callback: T): T { + const mounted = React.useRef(false) + + React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { + mounted.current = true + return () => { + mounted.current = false + } + }, []) + + return (React.useCallback( + (...args: any[]) => (mounted.current ? callback(...args) : void 0), + [callback] + ) as any) as T +} + +export function useRerenderer() { + const rerender = useMountedCallback(React.useState()[1]) + return React.useCallback(() => rerender({}), [rerender]) +} + +export function handleSuspense(result: QueryResultBase) { + const { error, query } = result + const { config, state } = query + + if (config.suspense || config.useErrorBoundary) { + if (state.status === QueryStatus.Error && state.throwInErrorBoundary) { + throw error + } + + if ( + config.suspense && + state.status !== QueryStatus.Success && + config.enabled + ) { + query.wasSuspended = true + throw query.fetch() + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..a6d78ae24c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "lib": ["ESNext", "dom"], + "target": "es5", + "noEmit": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "strict": true, + "types": ["jest"] + }, + "include": ["./src"] +} diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000000..2abb74449b --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "noEmit": false + }, + "files": ["./src/index.ts"], + "exclude": ["./src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 7874f6fbf8..85974efe32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,13 @@ dependencies: "@babel/highlight" "^7.10.1" +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -84,6 +91,15 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" + integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== + dependencies: + "@babel/types" "^7.10.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.6.3": version "7.6.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" @@ -159,6 +175,18 @@ "@babel/helper-replace-supers" "^7.10.1" "@babel/helper-split-export-declaration" "^7.10.1" +"@babel/helper-create-class-features-plugin@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.1", "@babel/helper-create-regexp-features-plugin@^7.8.3": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd" @@ -203,6 +231,15 @@ "@babel/template" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-function-name@^7.7.4": version "7.7.4" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e" @@ -226,6 +263,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-get-function-arity@^7.7.4": version "7.7.4" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0" @@ -247,6 +291,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" + integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== + dependencies: + "@babel/types" "^7.10.5" + "@babel/helper-module-imports@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" @@ -281,6 +332,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" @@ -291,6 +349,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127" integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA== +"@babel/helper-plugin-utils@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + "@babel/helper-plugin-utils@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" @@ -324,6 +387,16 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-simple-access@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" @@ -339,6 +412,13 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-split-export-declaration@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" + integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-split-export-declaration@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" @@ -363,6 +443,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helper-wrap-function@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9" @@ -409,6 +494,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.8.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.3.tgz#c633bb34adf07c5c13156692f5922c81ec53f28d" @@ -428,6 +522,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== +"@babel/parser@^7.10.4", "@babel/parser@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== + "@babel/parser@^7.6.0", "@babel/parser@^7.6.3": version "7.6.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" @@ -614,6 +713,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-syntax-typescript@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" + integrity sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-arrow-functions@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b" @@ -907,6 +1013,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.1" +"@babel/plugin-transform-typescript@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.5.tgz#edf353944e979f40d8ff9fe4e9975d0a465037c5" + integrity sha512-YCyYsFrrRMZ3qR7wRwtSSJovPG5vGyG4ZdcSAivGwTfoasMp3VOB/AKhohu3dFtmB4cCDcsndCSxGtrdliCsZQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.10.4" + "@babel/plugin-transform-unicode-escapes@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940" @@ -1016,6 +1131,14 @@ "@babel/plugin-transform-react-jsx-source" "^7.10.1" "@babel/plugin-transform-react-pure-annotations" "^7.10.1" +"@babel/preset-typescript@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.10.4.tgz#7d5d052e52a682480d6e2cc5aa31be61c8c25e36" + integrity sha512-SdYnvGPv+bLlwkF2VkJnaX/ni1sMNetcGI1+nThF1gyv6Ph8Qucc4ZZAjM5yZcE/AKRXIOTZz7eSRDWOEjPyRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-typescript" "^7.10.4" + "@babel/runtime-corejs3@^7.10.2": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" @@ -1078,6 +1201,15 @@ "@babel/parser" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" @@ -1117,6 +1249,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" + integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/types" "^7.10.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/traverse@^7.7.4": version "7.7.4" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" @@ -1150,6 +1297,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@babel/types@^7.7.4": version "7.7.4" resolved "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" @@ -1560,10 +1716,10 @@ dom-accessibility-api "^0.4.5" pretty-format "^25.5.0" -"@testing-library/react@^10.2.1": - version "10.4.3" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.3.tgz#c6f356688cffc51f6b35385583d664bb11a161f4" - integrity sha512-A/ydYXcwAcfY7vkPrfUkUTf9HQLL3/GtixTefcu3OyGQtAYQ7XBQj1S9FWbLEhfWa0BLwFwTBFS3Ao1O0tbMJg== +"@testing-library/react@^10.4.7": + version "10.4.7" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.7.tgz#fc14847fb70a5e93576b8f7f0d1490ead02a9061" + integrity sha512-hUYbum3X2f1ZKusKfPaooKNYqE/GtPiQ+D2HJaJ4pkxeNJQFVUEvAvEh9+3QuLdBeTWkDMNY5NSijc5+pGdM4Q== dependencies: "@babel/runtime" "^7.10.3" "@testing-library/dom" "^7.17.1" @@ -1606,6 +1762,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -1638,6 +1799,19 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@^26.0.4": + version "26.0.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.4.tgz#d2e513e85aca16992816f192582b5e67b0b15efb" + integrity sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + +"@types/json-schema@^7.0.3": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + "@types/node@*": version "14.0.13" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" @@ -1710,6 +1884,66 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.1.tgz#5ced8fd2087fbb83a76973dea4a0d39d9cb4a642" + integrity sha512-06lfjo76naNeOMDl+mWG9Fh/a0UHKLGhin+mGaIw72FUMbMGBkdi/FEJmgEDzh4eE73KIYzHWvOCYJ0ak7nrJQ== + dependencies: + "@typescript-eslint/experimental-utils" "3.6.1" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.6.1.tgz#b5a2738ebbceb3fa90c5b07d50bb1225403c4a54" + integrity sha512-oS+hihzQE5M84ewXrTlVx7eTgc52eu+sVmG7ayLfOhyZmJ8Unvf3osyFQNADHP26yoThFfbxcibbO0d2FjnYhg== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/types" "3.6.1" + "@typescript-eslint/typescript-estree" "3.6.1" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.6.1.tgz#216e8adf4ee9c629f77c985476a2ea07fb80e1dc" + integrity sha512-SLihQU8RMe77YJ/jGTqOt0lMq7k3hlPVfp7v/cxMnXA9T0bQYoMDfTsNgHXpwSJM1Iq2aAJ8WqekxUwGv5F67Q== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "3.6.1" + "@typescript-eslint/types" "3.6.1" + "@typescript-eslint/typescript-estree" "3.6.1" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/types@3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.6.1.tgz#87600fe79a1874235d3cc1cf5c7e1a12eea69eee" + integrity sha512-NPxd5yXG63gx57WDTW1rp0cF3XlNuuFFB5G+Kc48zZ+51ZnQn9yjDEsjTPQ+aWM+V+Z0I4kuTFKjKvgcT1F7xQ== + +"@typescript-eslint/typescript-estree@3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.6.1.tgz#a5c91fcc5497cce7922ff86bc37d5e5891dcdefa" + integrity sha512-G4XRe/ZbCZkL1fy09DPN3U0mR6SayIv1zSeBNquRFRk7CnVLgkC2ZPj8llEMJg5Y8dJ3T76SvTGtceytniaztQ== + dependencies: + "@typescript-eslint/types" "3.6.1" + "@typescript-eslint/visitor-keys" "3.6.1" + debug "^4.1.1" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/visitor-keys@3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.6.1.tgz#5c57a7772f4dd623cfeacc219303e7d46f963b37" + integrity sha512-qC8Olwz5ZyMTZrh4Wl3K4U6tfms0R/mzU4/5W3XeUZptVraGVmbptJbn6h2Ey6Rb3hOs3zWoAUebZk8t47KGiQ== + dependencies: + eslint-visitor-keys "^1.1.0" + abab@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" @@ -2686,6 +2920,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff-sequences@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6" @@ -3059,7 +3298,7 @@ eslint-plugin-standard@^4.0.1: resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== -eslint-scope@^5.1.0: +eslint-scope@^5.0.0, eslint-scope@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== @@ -3557,7 +3796,7 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" -glob@^7.1.1: +glob@^7.1.1, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -4178,6 +4417,16 @@ jest-config@^26.0.1: micromatch "^4.0.2" pretty-format "^26.0.1" +jest-diff@^25.2.1: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.0.1.tgz#c44ab3cdd5977d466de69c46929e0e57f89aa1de" @@ -4229,6 +4478,11 @@ jest-environment-node@^26.0.1: jest-mock "^26.0.1" jest-util "^26.0.1" +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-get-type@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" @@ -4773,6 +5027,11 @@ lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -5492,7 +5751,7 @@ pretty-bytes@^5.3.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== -pretty-format@^25.5.0: +pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== @@ -6811,6 +7070,13 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"