From 4c1624bbe254c077e8fdbde50a10e6957d7175de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 12:11:10 +0000 Subject: [PATCH 01/50] First test implementation of useOnyx hook --- lib/index.d.ts | 3 +- lib/index.js | 3 +- lib/useOnyx.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 lib/useOnyx.ts diff --git a/lib/index.d.ts b/lib/index.d.ts index 60f9ed28..31572960 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,6 +1,7 @@ import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx'; import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector} from './types'; import withOnyx from './withOnyx'; +import {useOnyx, useOnyxWithSyncExternalStore} from './useOnyx'; export default Onyx; -export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector}; +export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, useOnyx, useOnyxWithSyncExternalStore}; diff --git a/lib/index.js b/lib/index.js index bb2bba7e..f6b0a0ef 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,6 @@ import Onyx from './Onyx'; import withOnyx from './withOnyx'; +import {useOnyx, useOnyxWithSyncExternalStore} from './useOnyx'; export default Onyx; -export {withOnyx}; +export {withOnyx, useOnyx, useOnyxWithSyncExternalStore}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts new file mode 100644 index 00000000..ae892596 --- /dev/null +++ b/lib/useOnyx.ts @@ -0,0 +1,89 @@ +/* eslint-disable es/no-nullish-coalescing-operators */ +/* eslint-disable es/no-optional-chaining */ +import {useEffect, useRef, useState} from 'react'; +import Onyx from './Onyx'; +// eslint-disable-next-line rulesdir/prefer-import-module-contents +import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; + +type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; + +type UseOnyxOptions = { + /** + * Determines if this key in this subscription is safe to be evicted. + */ + canEvict?: boolean; + + /** + * If set to false, then no data will be prefilled into the component. + */ + initWithStoredValues?: boolean; + + /** + * TODO: Check if we still need this flag and associated logic. + */ + allowStaleData?: boolean; + + /** + * Sets an initial value to be returned by the hook during the first render. + */ + initialValue?: OnyxValue; +}; + +function useOnyx(keyParam: TKey, options?: UseOnyxOptions): OnyxValue { + const [value, setValue] = useState>(options?.initialValue ?? (null as OnyxValue)); + + /** + * Prevents key reassignment. + */ + const keyRef = useRef(keyParam); + const key = keyRef.current; + + const connectionIDRef = useRef(null); + + useEffect(() => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + connectionIDRef.current = Onyx.connect({ + key, + callback: (val) => { + setValue(val as OnyxValue); + }, + initWithStoredValues: options?.initWithStoredValues, + }); + + return () => { + if (!Onyx.isSafeEvictionKey(key)) { + throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); + } + + if (!connectionIDRef.current) { + return; + } + + Onyx.disconnect(connectionIDRef.current); + }; + }, []); + + /** + * Mimics withOnyx's checkEvictableKeys() behavior. + */ + useEffect(() => { + if (options?.canEvict === undefined || !connectionIDRef.current) { + return; + } + + if (options.canEvict) { + Onyx.removeFromEvictionBlockList(key, connectionIDRef.current); + } else { + Onyx.addToEvictionBlockList(key, connectionIDRef.current); + } + }, [options?.canEvict]); + + return value; +} + +function useOnyxWithSyncExternalStore(_key: TKey, _options?: UseOnyxOptions): OnyxValue { + // @ts-expect-error TODO + return null; +} + +export {useOnyx, useOnyxWithSyncExternalStore}; From 9c10b255213de7788e979067a4cfc58b216e1da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 12:14:50 +0000 Subject: [PATCH 02/50] Fix isSafeEvictionKey condition --- lib/useOnyx.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index ae892596..d0b814fd 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -51,10 +51,6 @@ function useOnyx(keyParam: TKey, options?: UseOnyxOptions< }); return () => { - if (!Onyx.isSafeEvictionKey(key)) { - throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); - } - if (!connectionIDRef.current) { return; } @@ -71,6 +67,10 @@ function useOnyx(keyParam: TKey, options?: UseOnyxOptions< return; } + if (!Onyx.isSafeEvictionKey(key)) { + throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); + } + if (options.canEvict) { Onyx.removeFromEvictionBlockList(key, connectionIDRef.current); } else { From be3204bbbaaa5c4f7d48dee55ed27c9a7a284242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 17:37:46 +0000 Subject: [PATCH 03/50] Fix some ESLint rules --- .eslintrc.js | 6 +++++- package-lock.json | 1 + package.json | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5c0cea65..1e17aaa9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { - extends: ['expensify', 'prettier'], + extends: ['expensify', 'prettier', 'plugin:react-hooks/recommended'], + plugins: ['react-hooks'], parser: '@typescript-eslint/parser', env: { jest: true, @@ -53,6 +54,9 @@ module.exports = { '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], '@typescript-eslint/consistent-type-definitions': 'off', 'rulesdir/no-multiple-onyx-in-file': 'off', + 'rulesdir/prefer-onyx-connect-in-libs': 'off', + 'es/no-nullish-coalescing-operators': 'off', + 'es/no-optional-chaining': 'off', }, }, ], diff --git a/package-lock.json b/package-lock.json index 8e5a0fba..abe71a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.31.10", + "eslint-plugin-react-hooks": "^4.6.0", "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", diff --git a/package.json b/package.json index bb004f4a..91d81f7c 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.31.10", + "eslint-plugin-react-hooks": "^4.6.0", "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", @@ -81,9 +82,9 @@ "idb-keyval": "^6.2.1", "react": ">=18.1.0", "react-dom": ">=18.1.0", + "react-native-device-info": "^10.3.0", "react-native-performance": "^5.1.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-device-info": "^10.3.0" + "react-native-quick-sqlite": "^8.0.0-beta.2" }, "peerDependenciesMeta": { "idb-keyval": { From f0a7c5635b6146bd4d425247171224196a559b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 17:38:18 +0000 Subject: [PATCH 04/50] Fix key logic to allow changes --- lib/useOnyx.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index d0b814fd..1459df76 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,5 +1,3 @@ -/* eslint-disable es/no-nullish-coalescing-operators */ -/* eslint-disable es/no-optional-chaining */ import {useEffect, useRef, useState} from 'react'; import Onyx from './Onyx'; // eslint-disable-next-line rulesdir/prefer-import-module-contents @@ -29,19 +27,12 @@ type UseOnyxOptions = { initialValue?: OnyxValue; }; -function useOnyx(keyParam: TKey, options?: UseOnyxOptions): OnyxValue { +function useOnyx(key: TKey, options?: UseOnyxOptions): OnyxValue { const [value, setValue] = useState>(options?.initialValue ?? (null as OnyxValue)); - /** - * Prevents key reassignment. - */ - const keyRef = useRef(keyParam); - const key = keyRef.current; - const connectionIDRef = useRef(null); useEffect(() => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs connectionIDRef.current = Onyx.connect({ key, callback: (val) => { @@ -57,7 +48,7 @@ function useOnyx(keyParam: TKey, options?: UseOnyxOptions< Onyx.disconnect(connectionIDRef.current); }; - }, []); + }, [key, options?.initWithStoredValues]); /** * Mimics withOnyx's checkEvictableKeys() behavior. @@ -76,7 +67,7 @@ function useOnyx(keyParam: TKey, options?: UseOnyxOptions< } else { Onyx.addToEvictionBlockList(key, connectionIDRef.current); } - }, [options?.canEvict]); + }, [key, options?.canEvict]); return value; } From 615e39ca985e5355262a478c52fb7934a1e97c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 18:54:05 +0000 Subject: [PATCH 05/50] Fix value returned by hook when it is a collection --- lib/Onyx.d.ts | 9 ++++++++- lib/Onyx.js | 2 +- lib/useOnyx.ts | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index b1c0cbe4..05bbc0bc 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -35,7 +35,7 @@ type BaseConnectOptions = { type ConnectOptions = BaseConnectOptions & ( | { - key: TKey extends CollectionKey ? TKey : never; + key: TKey extends CollectionKeyBase ? TKey : never; callback?: (value: OnyxCollection) => void; waitForCollectionCallback: true; } @@ -115,6 +115,12 @@ declare const METHOD: { */ declare function getAllKeys(): Promise>; +/** + * Checks to see if the a subscriber's supplied key + * is associated with a collection of keys. + */ +declare function isCollectionKey(key: OnyxKey): boolean; + /** * Checks to see if this key has been flagged as * safe for removal. @@ -309,6 +315,7 @@ declare const Onyx: { isSafeEvictionKey: typeof isSafeEvictionKey; METHOD: typeof METHOD; setMemoryOnlyKeys: typeof setMemoryOnlyKeys; + isCollectionKey: typeof isCollectionKey; }; export default Onyx; diff --git a/lib/Onyx.js b/lib/Onyx.js index 16ec3789..da33eaf3 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -196,7 +196,6 @@ function getAllKeys() { * Checks to see if the a subscriber's supplied key * is associated with a collection of keys. * - * @private * @param {String} key * @returns {Boolean} */ @@ -1619,6 +1618,7 @@ const Onyx = { setMemoryOnlyKeys, tryGetCachedValue, hasPendingMergeForKey, + isCollectionKey, }; /** diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 1459df76..e39ec30f 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -34,11 +34,12 @@ function useOnyx(key: TKey, options?: UseOnyxOptions useEffect(() => { connectionIDRef.current = Onyx.connect({ - key, - callback: (val) => { + key: key as CollectionKeyBase, + callback: (val: unknown) => { setValue(val as OnyxValue); }, initWithStoredValues: options?.initWithStoredValues, + waitForCollectionCallback: Onyx.isCollectionKey(key), }); return () => { From 1588c8a93b0951622bcdcb9bbaefde9242ec7e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 25 Jan 2024 19:20:48 +0000 Subject: [PATCH 06/50] Initial implementation of useSyncExternalStore hook version --- lib/useOnyx.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index e39ec30f..32505278 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; // eslint-disable-next-line rulesdir/prefer-import-module-contents import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; @@ -73,9 +73,60 @@ function useOnyx(key: TKey, options?: UseOnyxOptions return value; } -function useOnyxWithSyncExternalStore(_key: TKey, _options?: UseOnyxOptions): OnyxValue { - // @ts-expect-error TODO - return null; +const cache = new Map(); + +function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue { + const connectionIDRef = useRef(null); + + const getSnapshot = useCallback(() => { + return (cache.get(key) ?? null) as OnyxValue; + }, [key]); + + const subscribe = useCallback( + (onStoreChange: () => void) => { + connectionIDRef.current = Onyx.connect({ + key: key as CollectionKeyBase, + callback: (val: unknown) => { + cache.set(key, val); + onStoreChange(); + }, + initWithStoredValues: options?.initWithStoredValues, + waitForCollectionCallback: Onyx.isCollectionKey(key), + }); + + return () => { + if (!connectionIDRef.current) { + return; + } + + Onyx.disconnect(connectionIDRef.current); + }; + }, + [key, options?.initWithStoredValues], + ); + + /** + * Mimics withOnyx's checkEvictableKeys() behavior. + */ + useEffect(() => { + if (options?.canEvict === undefined || !connectionIDRef.current) { + return; + } + + if (!Onyx.isSafeEvictionKey(key)) { + throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); + } + + if (options.canEvict) { + Onyx.removeFromEvictionBlockList(key, connectionIDRef.current); + } else { + Onyx.addToEvictionBlockList(key, connectionIDRef.current); + } + }, [key, options?.canEvict]); + + const value = useSyncExternalStore>(subscribe, getSnapshot); + + return value; } export {useOnyx, useOnyxWithSyncExternalStore}; From 1436535331873b2345c99406487a4e9d7c5ac2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 7 Feb 2024 16:57:29 +0000 Subject: [PATCH 07/50] Improve getSnapshot logic to fix cache issue when changing the key --- lib/useOnyx.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 32505278..5134a5f4 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -3,6 +3,17 @@ import Onyx from './Onyx'; // eslint-disable-next-line rulesdir/prefer-import-module-contents import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; +// TODO: Move to a different file once issue with imports is resolved. +function usePrevious(value: T): T { + const ref = useRef(value); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} + type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; type UseOnyxOptions = { @@ -77,10 +88,23 @@ const cache = new Map(); function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue { const connectionIDRef = useRef(null); + const previousKey = usePrevious(key); + /** + * According to React docs, `getSnapshot` is a function that returns a snapshot of the data in the store that’s needed by the component. + * **While the store has not changed, repeated calls to getSnapshot must return the same value.** + * If the store changes and the returned value is different (as compared by Object.is), React re-renders the component. + * + * When the `key` is changed (e.g. to get a different record from a collection) and it's not yet in the cache, + * we return the value from the previous key to avoid briefly returning a `null` value to the component, thus avoiding a useless re-render. + */ const getSnapshot = useCallback(() => { + if (previousKey !== key && !cache.has(key)) { + return (cache.get(previousKey) ?? null) as OnyxValue; + } + return (cache.get(key) ?? null) as OnyxValue; - }, [key]); + }, [key, previousKey]); const subscribe = useCallback( (onStoreChange: () => void) => { From 4c0f445ef2744f4549ec98394c66116fff9fa5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 13 Feb 2024 09:57:00 +0000 Subject: [PATCH 08/50] Fix build:watch script to consider TS files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 061ced80..fe3a059c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "typecheck": "tsc --noEmit", "test": "jest", "build": "tsc -p tsconfig.build.json && cp ./lib/*.d.ts ./dist", - "build:watch": "nodemon --watch lib --exec \"npm run build && npm pack\"", + "build:watch": "nodemon --watch lib --ext js,json,ts,tsx --exec \"npm run build && npm pack\"", "build:docs": "node buildDocs.js", "lint-tests": "eslint tests/**", "prettier": "prettier --write ." From 572079f5ba80b057b1a1491f7fe40636ae713b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 13 Feb 2024 10:00:27 +0000 Subject: [PATCH 09/50] Fix ESlint and TSConfig --- .eslintrc.js | 16 ++++++++++++++-- tsconfig.json | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1e17aaa9..34164da1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,9 +54,21 @@ module.exports = { '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], '@typescript-eslint/consistent-type-definitions': 'off', 'rulesdir/no-multiple-onyx-in-file': 'off', - 'rulesdir/prefer-onyx-connect-in-libs': 'off', - 'es/no-nullish-coalescing-operators': 'off', + 'valid-jsdoc': 'off', + 'rulesdir/prefer-import-module-contents': 'off', 'es/no-optional-chaining': 'off', + 'es/no-nullish-coalescing-operators': 'off', + 'import/extensions': [ + 'error', + 'ignorePackages', + { + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], + 'rulesdir/prefer-onyx-connect-in-libs': 'off', }, }, ], diff --git a/tsconfig.json b/tsconfig.json index 151eefc8..f8b58a30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "target": "es2015", "module": "commonjs", "types": ["react-native", "react", "jest", "node"], - "lib": ["esnext"], + "lib": ["esnext", "dom"], "allowJs": true, "checkJs": false, "jsx": "react", From 6b2ae81b0d4cdd832f21258d0b8e09f63547c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 13 Feb 2024 10:00:39 +0000 Subject: [PATCH 10/50] Extract usePrevious to a separate file --- lib/useOnyx.ts | 13 +------------ lib/usePrevious.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 lib/usePrevious.ts diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 5134a5f4..8d2e1f63 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,18 +1,7 @@ import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; -// eslint-disable-next-line rulesdir/prefer-import-module-contents import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; - -// TODO: Move to a different file once issue with imports is resolved. -function usePrevious(value: T): T { - const ref = useRef(value); - - useEffect(() => { - ref.current = value; - }, [value]); - - return ref.current; -} +import usePrevious from './usePrevious'; type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; diff --git a/lib/usePrevious.ts b/lib/usePrevious.ts new file mode 100644 index 00000000..bc049f32 --- /dev/null +++ b/lib/usePrevious.ts @@ -0,0 +1,13 @@ +import {useEffect, useRef} from 'react'; + +function usePrevious(value: T): T { + const ref = useRef(value); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} + +export default usePrevious; From 8c8eb38b91901f6616f76d4c75c83e21531855a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 13 Feb 2024 10:38:17 +0000 Subject: [PATCH 11/50] Use cache from OnyxCache --- lib/useOnyx.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 8d2e1f63..c451e463 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,5 +1,6 @@ import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; +import cache from './OnyxCache'; import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; import usePrevious from './usePrevious'; @@ -73,8 +74,6 @@ function useOnyx(key: TKey, options?: UseOnyxOptions return value; } -const cache = new Map(); - function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); @@ -88,11 +87,11 @@ function useOnyxWithSyncExternalStore(key: TKey, options?: * we return the value from the previous key to avoid briefly returning a `null` value to the component, thus avoiding a useless re-render. */ const getSnapshot = useCallback(() => { - if (previousKey !== key && !cache.has(key)) { - return (cache.get(previousKey) ?? null) as OnyxValue; + if (previousKey !== key && !cache.hasCacheForKey(key)) { + return (cache.getValue(previousKey) ?? null) as OnyxValue; } - return (cache.get(key) ?? null) as OnyxValue; + return (cache.getValue(key) ?? null) as OnyxValue; }, [key, previousKey]); const subscribe = useCallback( From b028d2365376fc86c54edd3cb62e120204380c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 15 Feb 2024 18:17:55 +0000 Subject: [PATCH 12/50] Add condition to ensure only collection member keys can be changed --- lib/useOnyx.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index c451e463..fdc6daab 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -78,6 +78,18 @@ function useOnyxWithSyncExternalStore(key: TKey, options?: const connectionIDRef = useRef(null); const previousKey = usePrevious(key); + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + /** + * This condition will ensure we can only handle collection member keys changing. + */ + if (previousKey !== key && !(previousKey.includes('_') && !previousKey.endsWith('_') && key.includes('_') && !key.endsWith('_'))) { + throw new Error( + `'${previousKey}' key can't be changed to '${key}'. useOnyx() doesn't support changing keys unless they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, + ); + } + }, [previousKey, key]); + /** * According to React docs, `getSnapshot` is a function that returns a snapshot of the data in the store that’s needed by the component. * **While the store has not changed, repeated calls to getSnapshot must return the same value.** From 9716d4bc1015b5736a4913f414732062dc1f7797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 15 Feb 2024 23:54:39 +0000 Subject: [PATCH 13/50] Fix getSnapshot() logic and improve typings --- lib/Onyx.d.ts | 19 +++++++++-- lib/useOnyx.ts | 93 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 20 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index 05bbc0bc..ca4dcb53 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -1,6 +1,6 @@ import {Component} from 'react'; import * as Logger from './Logger'; -import {CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey} from './types'; +import {CollectionKeyBase, DeepRecord, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. @@ -20,6 +20,11 @@ type BaseConnectOptions = { initWithStoredValues?: boolean; }; +type TryGetCachedValueMapping = { + selector?: Selector; + withOnyxInstance?: Component; +}; + /** * Represents the options used in `Onyx.connect()` method. * The type is built from `BaseConnectOptions` and extended to handle key/callback related options. @@ -297,6 +302,15 @@ declare function hasPendingMergeForKey(key: OnyxKey): boolean; */ declare function setMemoryOnlyKeys(keyList: OnyxKey[]): void; +/** + * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined. + * If the requested key is a collection, it will return an object with all the collection members. + */ +declare function tryGetCachedValue( + key: TKey, + mapping?: TryGetCachedValueMapping, +): TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; + declare const Onyx: { connect: typeof connect; disconnect: typeof disconnect; @@ -315,8 +329,9 @@ declare const Onyx: { isSafeEvictionKey: typeof isSafeEvictionKey; METHOD: typeof METHOD; setMemoryOnlyKeys: typeof setMemoryOnlyKeys; + tryGetCachedValue: typeof tryGetCachedValue; isCollectionKey: typeof isCollectionKey; }; export default Onyx; -export {OnyxUpdate, ConnectOptions}; +export {ConnectOptions, OnyxUpdate}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index fdc6daab..f256b4e2 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,12 +1,14 @@ +import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; -import cache from './OnyxCache'; -import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey} from './types'; +import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; import usePrevious from './usePrevious'; type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; -type UseOnyxOptions = { +type SelectorReturn = TKey extends CollectionKeyBase ? OnyxCollection> : TReturnData; + +type UseOnyxOptions = { /** * Determines if this key in this subscription is safe to be evicted. */ @@ -20,15 +22,23 @@ type UseOnyxOptions = { /** * TODO: Check if we still need this flag and associated logic. */ - allowStaleData?: boolean; + // allowStaleData?: boolean; /** * Sets an initial value to be returned by the hook during the first render. */ initialValue?: OnyxValue; + + /** + * If included, this will be used to subscribe to a subset of an Onyx key's data. + * Using this setting on `useOnyx` can have very positive performance benefits because the component will only re-render + * when the subset of data changes. Otherwise, any change of data on any property would normally + * cause the component to re-render (and that can be expensive from a performance standpoint). + */ + selector?: Selector : TReturnData>; }; -function useOnyx(key: TKey, options?: UseOnyxOptions): OnyxValue { +function useOnyx(key: TKey, options?: UseOnyxOptions): OnyxValue { const [value, setValue] = useState>(options?.initialValue ?? (null as OnyxValue)); const connectionIDRef = useRef(null); @@ -74,18 +84,30 @@ function useOnyx(key: TKey, options?: UseOnyxOptions return value; } -function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue { +function isCollectionMemberKey(key: TKey): boolean { + return key.includes('_') && !key.endsWith('_'); +} + +function getCachedValue(key: TKey, selector?: Selector): OnyxValue { + return Onyx.tryGetCachedValue(key, {selector}); +} + +function useOnyxWithSyncExternalStore(key: TKey): OnyxValue; +function useOnyxWithSyncExternalStore(key: TKey, options: Omit, 'selector'>): OnyxValue; +function useOnyxWithSyncExternalStore(key: TKey, options: UseOnyxOptions): SelectorReturn; +function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue | SelectorReturn { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); + const previousDataRef = useRef(null); // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { /** - * This condition will ensure we can only handle collection member keys changing. + * This condition will ensure we can only handle dynamic collection member keys. */ - if (previousKey !== key && !(previousKey.includes('_') && !previousKey.endsWith('_') && key.includes('_') && !key.endsWith('_'))) { + if (previousKey !== key && !(isCollectionMemberKey(previousKey) && isCollectionMemberKey(key))) { throw new Error( - `'${previousKey}' key can't be changed to '${key}'. useOnyx() doesn't support changing keys unless they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, + `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, ); } }, [previousKey, key]); @@ -94,24 +116,59 @@ function useOnyxWithSyncExternalStore(key: TKey, options?: * According to React docs, `getSnapshot` is a function that returns a snapshot of the data in the store that’s needed by the component. * **While the store has not changed, repeated calls to getSnapshot must return the same value.** * If the store changes and the returned value is different (as compared by Object.is), React re-renders the component. - * - * When the `key` is changed (e.g. to get a different record from a collection) and it's not yet in the cache, - * we return the value from the previous key to avoid briefly returning a `null` value to the component, thus avoiding a useless re-render. */ const getSnapshot = useCallback(() => { - if (previousKey !== key && !cache.hasCacheForKey(key)) { - return (cache.getValue(previousKey) ?? null) as OnyxValue; + /** + * Case 1 - We have a normal key without selector + * + * We just return the data from the Onyx cache. + */ + if (!Onyx.isCollectionKey(key) && !options?.selector) { + return (getCachedValue(key) ?? null) as OnyxValue; + } + + /** + * Case 2 - We have a normal key with selector + * + * Since selected data is not directly stored in the cache, we need to generate it with `getCachedValue` + * and deep compare with our previous internal data. + * + * If they are not equal, we update the internal data and return it. + * + * If they are equal, we just return the previous internal data. + */ + if (!Onyx.isCollectionKey(key) && options?.selector) { + const newData = getCachedValue(key, options.selector); + if (!deepEqual(previousDataRef.current, newData)) { + previousDataRef.current = newData; + } + + return (previousDataRef.current ?? null) as OnyxValue; + } + + /** + * Case 3 - We have a collection key with/without selector + * + * Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` + * and deep compare with our previous internal data. + * + * If they are not equal, we update the internal data and return it. + * + * If they are equal, we just return the previous internal data. + */ + const newData = getCachedValue(key, options?.selector); + if (!deepEqual(previousDataRef.current, newData)) { + previousDataRef.current = newData; } - return (cache.getValue(key) ?? null) as OnyxValue; - }, [key, previousKey]); + return (previousDataRef.current ?? null) as OnyxValue; + }, [key, options?.selector]); const subscribe = useCallback( (onStoreChange: () => void) => { connectionIDRef.current = Onyx.connect({ key: key as CollectionKeyBase, - callback: (val: unknown) => { - cache.set(key, val); + callback: () => { onStoreChange(); }, initWithStoredValues: options?.initWithStoredValues, From b5c208c8a0e817f93d72d445caea59dfa4184292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 07:15:47 +0000 Subject: [PATCH 14/50] Implement initialValue logic --- lib/useOnyx.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index f256b4e2..a00c1cfc 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -99,6 +99,7 @@ function useOnyxWithSyncExternalStore(key: TK const connectionIDRef = useRef(null); const previousKey = usePrevious(key); const previousDataRef = useRef(null); + const isFirstRenderRef = useRef(true); // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { @@ -207,6 +208,14 @@ function useOnyxWithSyncExternalStore(key: TK const value = useSyncExternalStore>(subscribe, getSnapshot); + /** + * Return `initialValue` if it's the first render, we don't have anything in the cache and `initialValue` is set. + */ + if (isFirstRenderRef.current && value === null && options?.initialValue !== undefined) { + isFirstRenderRef.current = false; + return options.initialValue; + } + return value; } From 7731c732b9de24874bc5727a74a09dd91a9f45b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 07:31:36 +0000 Subject: [PATCH 15/50] Added comment and minor adjustments --- lib/useOnyx.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index a00c1cfc..5f4bd978 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -89,7 +89,7 @@ function isCollectionMemberKey(key: TKey): boolean { } function getCachedValue(key: TKey, selector?: Selector): OnyxValue { - return Onyx.tryGetCachedValue(key, {selector}); + return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as OnyxValue; } function useOnyxWithSyncExternalStore(key: TKey): OnyxValue; @@ -98,7 +98,7 @@ function useOnyxWithSyncExternalStore(key: TK function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue | SelectorReturn { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); - const previousDataRef = useRef(null); + const previousDataRef = useRef | SelectorReturn | null>(null); const isFirstRenderRef = useRef(true); // eslint-disable-next-line rulesdir/prefer-early-return @@ -125,7 +125,7 @@ function useOnyxWithSyncExternalStore(key: TK * We just return the data from the Onyx cache. */ if (!Onyx.isCollectionKey(key) && !options?.selector) { - return (getCachedValue(key) ?? null) as OnyxValue; + return getCachedValue(key); } /** @@ -144,7 +144,7 @@ function useOnyxWithSyncExternalStore(key: TK previousDataRef.current = newData; } - return (previousDataRef.current ?? null) as OnyxValue; + return previousDataRef.current as SelectorReturn; } /** @@ -162,7 +162,7 @@ function useOnyxWithSyncExternalStore(key: TK previousDataRef.current = newData; } - return (previousDataRef.current ?? null) as OnyxValue; + return previousDataRef.current as OnyxValue | SelectorReturn; }, [key, options?.selector]); const subscribe = useCallback( @@ -170,6 +170,10 @@ function useOnyxWithSyncExternalStore(key: TK connectionIDRef.current = Onyx.connect({ key: key as CollectionKeyBase, callback: () => { + /** + * We don't need to update the Onyx cache again here, when `callback` is called the cache is already + * expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. + */ onStoreChange(); }, initWithStoredValues: options?.initWithStoredValues, From cedee66bd289a90941c8629330884339024d2f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 08:29:49 +0000 Subject: [PATCH 16/50] Implement fetch status logic and make useOnyx return a object --- lib/useOnyx.ts | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 5f4bd978..036daec2 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -38,6 +38,13 @@ type UseOnyxOptions = { selector?: Selector : TReturnData>; }; +type FetchStatus = 'loading' | 'loaded'; + +type UseOnyxData = { + value: TValue; + status: FetchStatus; +}; + function useOnyx(key: TKey, options?: UseOnyxOptions): OnyxValue { const [value, setValue] = useState>(options?.initialValue ?? (null as OnyxValue)); @@ -92,14 +99,18 @@ function getCachedValue(key: TKey, selector?: Selector; } -function useOnyxWithSyncExternalStore(key: TKey): OnyxValue; -function useOnyxWithSyncExternalStore(key: TKey, options: Omit, 'selector'>): OnyxValue; -function useOnyxWithSyncExternalStore(key: TKey, options: UseOnyxOptions): SelectorReturn; -function useOnyxWithSyncExternalStore(key: TKey, options?: UseOnyxOptions): OnyxValue | SelectorReturn { +function useOnyxWithSyncExternalStore(key: TKey): UseOnyxData>; +function useOnyxWithSyncExternalStore(key: TKey, options: Omit, 'selector'>): UseOnyxData>; +function useOnyxWithSyncExternalStore(key: TKey, options: UseOnyxOptions): UseOnyxData>; +function useOnyxWithSyncExternalStore( + key: TKey, + options?: UseOnyxOptions, +): UseOnyxData | SelectorReturn> { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); const previousDataRef = useRef | SelectorReturn | null>(null); const isFirstRenderRef = useRef(true); + const fetchStatusRef = useRef('loading'); // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { @@ -174,6 +185,7 @@ function useOnyxWithSyncExternalStore(key: TK * We don't need to update the Onyx cache again here, when `callback` is called the cache is already * expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. */ + fetchStatusRef.current = 'loaded'; onStoreChange(); }, initWithStoredValues: options?.initWithStoredValues, @@ -186,6 +198,11 @@ function useOnyxWithSyncExternalStore(key: TK } Onyx.disconnect(connectionIDRef.current); + + /** + * Sets the fetch status back to "loading" as we are connecting to a new key. + */ + fetchStatusRef.current = 'loading'; }; }, [key, options?.initWithStoredValues], @@ -210,17 +227,28 @@ function useOnyxWithSyncExternalStore(key: TK } }, [key, options?.canEvict]); - const value = useSyncExternalStore>(subscribe, getSnapshot); + let value = useSyncExternalStore>(subscribe, getSnapshot); - /** - * Return `initialValue` if it's the first render, we don't have anything in the cache and `initialValue` is set. - */ - if (isFirstRenderRef.current && value === null && options?.initialValue !== undefined) { + if (isFirstRenderRef.current) { isFirstRenderRef.current = false; - return options.initialValue; + + /** + * Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. + */ + if (value !== null) { + fetchStatusRef.current = 'loaded'; + } + + /** + * Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. + */ + if (value === null && options?.initialValue !== undefined) { + fetchStatusRef.current = 'loaded'; + value = options.initialValue; + } } - return value; + return {value, status: fetchStatusRef.current}; } export {useOnyx, useOnyxWithSyncExternalStore}; From 49fabbd24c01ba430dae0be2582d55c54b3d8df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 08:33:44 +0000 Subject: [PATCH 17/50] Remove old useOnyx from code --- lib/useOnyx.ts | 61 +++++--------------------------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 036daec2..35062471 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,5 +1,5 @@ import {deepEqual} from 'fast-equals'; -import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react'; +import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; import usePrevious from './usePrevious'; @@ -45,52 +45,6 @@ type UseOnyxData = { status: FetchStatus; }; -function useOnyx(key: TKey, options?: UseOnyxOptions): OnyxValue { - const [value, setValue] = useState>(options?.initialValue ?? (null as OnyxValue)); - - const connectionIDRef = useRef(null); - - useEffect(() => { - connectionIDRef.current = Onyx.connect({ - key: key as CollectionKeyBase, - callback: (val: unknown) => { - setValue(val as OnyxValue); - }, - initWithStoredValues: options?.initWithStoredValues, - waitForCollectionCallback: Onyx.isCollectionKey(key), - }); - - return () => { - if (!connectionIDRef.current) { - return; - } - - Onyx.disconnect(connectionIDRef.current); - }; - }, [key, options?.initWithStoredValues]); - - /** - * Mimics withOnyx's checkEvictableKeys() behavior. - */ - useEffect(() => { - if (options?.canEvict === undefined || !connectionIDRef.current) { - return; - } - - if (!Onyx.isSafeEvictionKey(key)) { - throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); - } - - if (options.canEvict) { - Onyx.removeFromEvictionBlockList(key, connectionIDRef.current); - } else { - Onyx.addToEvictionBlockList(key, connectionIDRef.current); - } - }, [key, options?.canEvict]); - - return value; -} - function isCollectionMemberKey(key: TKey): boolean { return key.includes('_') && !key.endsWith('_'); } @@ -99,13 +53,10 @@ function getCachedValue(key: TKey, selector?: Selector; } -function useOnyxWithSyncExternalStore(key: TKey): UseOnyxData>; -function useOnyxWithSyncExternalStore(key: TKey, options: Omit, 'selector'>): UseOnyxData>; -function useOnyxWithSyncExternalStore(key: TKey, options: UseOnyxOptions): UseOnyxData>; -function useOnyxWithSyncExternalStore( - key: TKey, - options?: UseOnyxOptions, -): UseOnyxData | SelectorReturn> { +function useOnyx(key: TKey): UseOnyxData>; +function useOnyx(key: TKey, options: Omit, 'selector'>): UseOnyxData>; +function useOnyx(key: TKey, options: UseOnyxOptions): UseOnyxData>; +function useOnyx(key: TKey, options?: UseOnyxOptions): UseOnyxData | SelectorReturn> { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); const previousDataRef = useRef | SelectorReturn | null>(null); @@ -251,4 +202,4 @@ function useOnyxWithSyncExternalStore( return {value, status: fetchStatusRef.current}; } -export {useOnyx, useOnyxWithSyncExternalStore}; +export default useOnyx; From 9a7ff48d3e4c429798dd7897bd8b511ab058d1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 09:13:27 +0000 Subject: [PATCH 18/50] Fix selector types --- lib/index.d.ts | 3 +-- lib/index.js | 4 ++-- lib/useOnyx.ts | 25 ++++++++++--------------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 6c356720..6d68b2e9 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,7 +1,7 @@ import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx'; import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState} from './types'; import withOnyx from './withOnyx'; -import {useOnyx, useOnyxWithSyncExternalStore} from './useOnyx'; +import useOnyx from './useOnyx'; export default Onyx; export { @@ -17,5 +17,4 @@ export { Selector, WithOnyxInstanceState, useOnyx, - useOnyxWithSyncExternalStore, }; diff --git a/lib/index.js b/lib/index.js index f6b0a0ef..b50e88d2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ import Onyx from './Onyx'; import withOnyx from './withOnyx'; -import {useOnyx, useOnyxWithSyncExternalStore} from './useOnyx'; +import useOnyx from './useOnyx'; export default Onyx; -export {withOnyx, useOnyx, useOnyxWithSyncExternalStore}; +export {withOnyx, useOnyx}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 35062471..c34ba962 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -6,8 +6,6 @@ import usePrevious from './usePrevious'; type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; -type SelectorReturn = TKey extends CollectionKeyBase ? OnyxCollection> : TReturnData; - type UseOnyxOptions = { /** * Determines if this key in this subscription is safe to be evicted. @@ -35,7 +33,7 @@ type UseOnyxOptions = { * when the subset of data changes. Otherwise, any change of data on any property would normally * cause the component to re-render (and that can be expensive from a performance standpoint). */ - selector?: Selector : TReturnData>; + selector?: Selector; }; type FetchStatus = 'loading' | 'loaded'; @@ -53,13 +51,10 @@ function getCachedValue(key: TKey, selector?: Selector; } -function useOnyx(key: TKey): UseOnyxData>; -function useOnyx(key: TKey, options: Omit, 'selector'>): UseOnyxData>; -function useOnyx(key: TKey, options: UseOnyxOptions): UseOnyxData>; -function useOnyx(key: TKey, options?: UseOnyxOptions): UseOnyxData | SelectorReturn> { +function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); - const previousDataRef = useRef | SelectorReturn | null>(null); + const previousDataRef = useRef(null); const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); @@ -87,7 +82,7 @@ function useOnyx(key: TKey, options?: UseOnyx * We just return the data from the Onyx cache. */ if (!Onyx.isCollectionKey(key) && !options?.selector) { - return getCachedValue(key); + return getCachedValue(key) as TReturnData; } /** @@ -103,10 +98,10 @@ function useOnyx(key: TKey, options?: UseOnyx if (!Onyx.isCollectionKey(key) && options?.selector) { const newData = getCachedValue(key, options.selector); if (!deepEqual(previousDataRef.current, newData)) { - previousDataRef.current = newData; + previousDataRef.current = newData as TReturnData; } - return previousDataRef.current as SelectorReturn; + return previousDataRef.current as TReturnData; } /** @@ -121,10 +116,10 @@ function useOnyx(key: TKey, options?: UseOnyx */ const newData = getCachedValue(key, options?.selector); if (!deepEqual(previousDataRef.current, newData)) { - previousDataRef.current = newData; + previousDataRef.current = newData as TReturnData; } - return previousDataRef.current as OnyxValue | SelectorReturn; + return previousDataRef.current as TReturnData; }, [key, options?.selector]); const subscribe = useCallback( @@ -178,7 +173,7 @@ function useOnyx(key: TKey, options?: UseOnyx } }, [key, options?.canEvict]); - let value = useSyncExternalStore>(subscribe, getSnapshot); + let value = useSyncExternalStore(subscribe, getSnapshot); if (isFirstRenderRef.current) { isFirstRenderRef.current = false; @@ -195,7 +190,7 @@ function useOnyx(key: TKey, options?: UseOnyx */ if (value === null && options?.initialValue !== undefined) { fetchStatusRef.current = 'loaded'; - value = options.initialValue; + value = options.initialValue as TReturnData; } } From c8be6d1bf9459dc5fd25303f67f38abffb657518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 09:23:17 +0000 Subject: [PATCH 19/50] Improve isCollectionKey type --- lib/Onyx.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index ca4dcb53..fba2c745 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -124,7 +124,7 @@ declare function getAllKeys(): Promise>; * Checks to see if the a subscriber's supplied key * is associated with a collection of keys. */ -declare function isCollectionKey(key: OnyxKey): boolean; +declare function isCollectionKey(key: OnyxKey): key is CollectionKeyBase; /** * Checks to see if this key has been flagged as From ee16e80c3de4d9be5c66068c083b0a0b56075493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 09:57:43 +0000 Subject: [PATCH 20/50] Format code --- lib/index.d.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 6d68b2e9..47ea30ac 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -4,17 +4,4 @@ import withOnyx from './withOnyx'; import useOnyx from './useOnyx'; export default Onyx; -export { - CustomTypeOptions, - OnyxCollection, - OnyxEntry, - OnyxUpdate, - withOnyx, - ConnectOptions, - NullishDeep, - KeyValueMapping, - OnyxKey, - Selector, - WithOnyxInstanceState, - useOnyx, -}; +export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, useOnyx}; From ea61b54b27bf654a32e8fce91c5eca739ac7478a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 11:35:44 +0000 Subject: [PATCH 21/50] Update eslint-config-expensify --- .eslintrc.js | 2 +- package-lock.json | 2161 ++++++++++++++------------------------------- package.json | 2 +- 3 files changed, 653 insertions(+), 1512 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1e14a7e1..cd9ed98d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: ['expensify', 'prettier', 'plugin:react-hooks/recommended'], + extends: ['expensify', 'prettier'], plugins: ['react-hooks'], parser: '@typescript-eslint/parser', env: { diff --git a/package-lock.json b/package-lock.json index 9879163c..7aac27a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "eslint": "^8.56.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", @@ -2244,21 +2244,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -2280,18 +2265,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", @@ -2920,21 +2893,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@lwc/eslint-plugin-lwc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@lwc/eslint-plugin-lwc/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -2950,18 +2908,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@lwc/eslint-plugin-lwc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4633,6 +4579,15 @@ "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "dev": true }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escape-sequences": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", @@ -4762,16 +4717,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw==", - "dev": true, - "dependencies": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -4966,12 +4911,6 @@ "node": ">=4" } }, - "node_modules/ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", - "dev": true - }, "node_modules/ast-types/node_modules/tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", @@ -5047,12 +4986,6 @@ "node": ">=4" } }, - "node_modules/axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -5731,12 +5664,6 @@ "node": ">=10" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5827,15 +5754,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -5847,35 +5765,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -6735,9 +6624,9 @@ } }, "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "node_modules/encodeurl": { @@ -6758,6 +6647,19 @@ "once": "^1.4.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", @@ -7083,143 +6985,193 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-expensify": { - "version": "2.0.43", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.43.tgz", - "integrity": "sha512-kLd6NyYbyb3mCB6VH6vu49/RllwNo0rdXcLUUGB7JGny+2N19jOmBJ4/GLKsbpFzvEZEghXfn7BITPRkxVJcgg==", + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, "dependencies": { - "@lwc/eslint-plugin-lwc": "^0.11.0", - "babel-eslint": "^10.1.0", - "eslint": "6.8.0", - "eslint-config-airbnb": "18.0.1", - "eslint-config-airbnb-base": "14.0.0", - "eslint-plugin-es": "^4.1.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0", - "eslint-plugin-rulesdir": "^0.2.0", - "lodash": "^4.17.21", - "underscore": "^1.13.1" + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" } }, - "node_modules/eslint-config-expensify/node_modules/@lwc/eslint-plugin-lwc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-0.11.0.tgz", - "integrity": "sha512-wJOD4XWOH91GaZfypMSKfEeMXqMfvKdsb2gSJ/9FEwJVlziKg1aagtRYJh2ln3DyEZV33tBC/p/dWzIeiwa1tg==", + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, "dependencies": { - "minimatch": "^3.0.4" + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" }, "engines": { - "node": ">=10.0.0" + "node": "^10.12.0 || >=12.0.0" }, "peerDependencies": { - "babel-eslint": "^10", - "eslint": "^6 || ^7" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" } }, - "node_modules/eslint-config-expensify/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "semver": "bin/semver.js" } }, - "node_modules/eslint-config-expensify/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "node_modules/eslint-config-expensify": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.44.tgz", + "integrity": "sha512-fwa7lcQk7llYgqcWA1TX4kcSigYqSVkKGk+anODwYlYSbVbXwzzkQsncsaiWVTM7+eJdk46GmWPeiMAWOGWPvw==", "dev": true, - "engines": { - "node": ">=6" + "dependencies": { + "@lwc/eslint-plugin-lwc": "^1.7.2", + "babel-eslint": "^10.1.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-es": "^4.1.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-react": "^7.18.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-rulesdir": "^0.2.2", + "lodash": "^4.17.21", + "underscore": "^1.13.6" } }, - "node_modules/eslint-config-expensify/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/eslint-config-expensify/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/eslint-config-expensify/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "node_modules/eslint-config-expensify/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", "dev": true, "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" }, "engines": { - "node": ">=4.8" + "node": ">=10.10.0" } }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/eslint-config-expensify/node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/eslint-config-expensify/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "bin": { - "semver": "bin/semver" + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-expensify/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint-config-expensify/node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -7227,269 +7179,42 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^10.12.0 || >=12.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", + "node_modules/eslint-config-expensify/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.7.0" + "node": ">=10" } }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", + "node_modules/eslint-config-expensify/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "dependencies": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "node_modules/eslint-config-expensify/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - }, "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", - "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.14.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=7" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node": ">=4" } }, "node_modules/eslint-config-prettier": { @@ -7888,9 +7613,9 @@ } }, "node_modules/eslint-plugin-rulesdir": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.1.tgz", - "integrity": "sha512-t7rQvEyfE4JZJu6dPl4/uVr6Fr0fxopBhzVbtq3isfOHMKdlIe9xW/5CtIaWZI0E1U+wzAk0lEnZC8laCD5YLA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true, "engines": { "node": ">=4.0.0" @@ -7988,18 +7713,6 @@ "node": ">=4.0" } }, - "node_modules/eslint/node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8016,26 +7729,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/eslint/node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8048,21 +7741,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -8138,33 +7816,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8577,20 +8228,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -8735,31 +8372,16 @@ "bser": "2.1.1" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/file-set": { @@ -8887,23 +8509,38 @@ } }, "node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=4" + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/flow-parser": { @@ -9041,7 +8678,7 @@ "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "node_modules/functions-have-names": { @@ -9165,12 +8802,12 @@ } }, "node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" }, "engines": { "node": ">=8" @@ -9180,12 +8817,15 @@ } }, "node_modules/globals/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -9778,59 +9418,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/inquirer/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/inquirer/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-slot": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", @@ -12315,6 +11902,12 @@ "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -12841,26 +12434,11 @@ }, "node_modules/metro-inspector-proxy/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/metro-inspector-proxy/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/metro-inspector-proxy/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "ms": "2.0.0" } }, "node_modules/metro-inspector-proxy/node_modules/ms": { @@ -12869,20 +12447,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/metro-inspector-proxy/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/metro-inspector-proxy/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13237,12 +12801,6 @@ "ms": "2.0.0" } }, - "node_modules/metro/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/metro/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -13252,15 +12810,6 @@ "node": ">=8" } }, - "node_modules/metro/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/metro/node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -13354,20 +12903,6 @@ "node": ">=0.10.0" } }, - "node_modules/metro/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/metro/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -13561,12 +13096,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -15565,6 +15094,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -15693,15 +15231,6 @@ "node": "6.* || >= 7.*" } }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15725,18 +15254,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", @@ -16720,38 +16237,26 @@ "dev": true }, "node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/string.prototype.matchall": { @@ -16949,18 +16454,19 @@ "dev": true }, "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0.0" } }, "node_modules/table-layout": { @@ -16991,6 +16497,96 @@ "node": ">=4" } }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", @@ -17110,12 +16706,6 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -17150,18 +16740,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -17740,9 +17318,9 @@ } }, "node_modules/v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, "node_modules/v8-to-istanbul": { @@ -18057,53 +17635,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -18211,12 +17748,6 @@ "node": ">=6" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/yargs/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -18230,15 +17761,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -18296,20 +17818,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -19861,15 +19369,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, "ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -19884,12 +19383,6 @@ "requires": { "argparse": "^2.0.1" } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true } } }, @@ -20403,15 +19896,6 @@ "balanced-match": "^1.0.0" } }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, "minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -20420,12 +19904,6 @@ "requires": { "brace-expansion": "^2.0.1" } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true } } }, @@ -21706,6 +21184,12 @@ "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "dev": true }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, "ansi-escape-sequences": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", @@ -21808,16 +21292,6 @@ "sprintf-js": "~1.0.2" } }, - "aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw==", - "dev": true, - "requires": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -21963,12 +21437,6 @@ } } }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -22020,12 +21488,6 @@ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", "dev": true }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, "babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -22534,12 +21996,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -22606,12 +22062,6 @@ "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", "dev": true }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -22621,31 +22071,6 @@ "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } } }, "clone": { @@ -23349,9 +22774,9 @@ "dev": true }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "encodeurl": { @@ -23369,6 +22794,16 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + } + }, "entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", @@ -23667,15 +23102,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -23686,23 +23112,6 @@ "path-exists": "^4.0.0" } }, - "flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "requires": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -23712,15 +23121,6 @@ "is-glob": "^4.0.3" } }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, "ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -23768,325 +23168,187 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + } + } + }, + "eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + } + }, + "eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "eslint-config-expensify": { - "version": "2.0.43", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.43.tgz", - "integrity": "sha512-kLd6NyYbyb3mCB6VH6vu49/RllwNo0rdXcLUUGB7JGny+2N19jOmBJ4/GLKsbpFzvEZEghXfn7BITPRkxVJcgg==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.44.tgz", + "integrity": "sha512-fwa7lcQk7llYgqcWA1TX4kcSigYqSVkKGk+anODwYlYSbVbXwzzkQsncsaiWVTM7+eJdk46GmWPeiMAWOGWPvw==", "dev": true, "requires": { - "@lwc/eslint-plugin-lwc": "^0.11.0", + "@lwc/eslint-plugin-lwc": "^1.7.2", "babel-eslint": "^10.1.0", - "eslint": "6.8.0", - "eslint-config-airbnb": "18.0.1", - "eslint-config-airbnb-base": "14.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-es": "^4.1.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0", - "eslint-plugin-rulesdir": "^0.2.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-react": "^7.18.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-rulesdir": "^0.2.2", "lodash": "^4.17.21", - "underscore": "^1.13.1" + "underscore": "^1.13.6" }, "dependencies": { - "@lwc/eslint-plugin-lwc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-0.11.0.tgz", - "integrity": "sha512-wJOD4XWOH91GaZfypMSKfEeMXqMfvKdsb2gSJ/9FEwJVlziKg1aagtRYJh2ln3DyEZV33tBC/p/dWzIeiwa1tg==", + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", "dev": true, "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", "minimatch": "^3.0.4" } }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - } - }, - "eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - } - }, - "eslint-plugin-react": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", - "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.14.2" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true, - "peer": true, - "requires": {} - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" } }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "isexe": "^2.0.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } } } @@ -24396,9 +23658,9 @@ "dev": true }, "eslint-plugin-rulesdir": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.1.tgz", - "integrity": "sha512-t7rQvEyfE4JZJu6dPl4/uVr6Fr0fxopBhzVbtq3isfOHMKdlIe9xW/5CtIaWZI0E1U+wzAk0lEnZC8laCD5YLA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true }, "eslint-scope": { @@ -24746,17 +24008,6 @@ } } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -24882,22 +24133,13 @@ "bser": "2.1.1" } }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-set": { @@ -25006,20 +24248,31 @@ } }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "flow-parser": { @@ -25120,7 +24373,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "functions-have-names": { @@ -25208,18 +24461,18 @@ } }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" }, "dependencies": { "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } } @@ -25659,52 +24912,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, "internal-slot": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", @@ -27655,6 +26862,12 @@ "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -27887,24 +27100,12 @@ "ms": "2.0.0" } }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -27983,17 +27184,6 @@ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -28291,35 +27481,12 @@ "ms": "2.0.0" } }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -28681,12 +27848,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -30255,6 +29416,12 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -30353,12 +29520,6 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -30368,15 +29529,6 @@ "queue-microtask": "^1.2.2" } }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-array-concat": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", @@ -31188,30 +30340,21 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } } } }, @@ -31358,15 +30501,83 @@ "dev": true }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + } } }, "table-layout": { @@ -31489,12 +30700,6 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -31531,15 +30736,6 @@ } } }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -31981,9 +31177,9 @@ "optional": true }, "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, "v8-to-istanbul": { @@ -32243,29 +31439,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } } } }, @@ -32275,15 +31448,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -32358,12 +31522,6 @@ "yargs-parser": "^18.1.2" }, "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -32374,12 +31532,6 @@ "path-exists": "^4.0.0" } }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -32418,17 +31570,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } } } }, diff --git a/package.json b/package.json index d5e00192..94b74297 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "eslint": "^8.56.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", From 7a0da522fb4a5078dc54ae78994adb0a8cf8a02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 11:46:08 +0000 Subject: [PATCH 22/50] Improve key changing check --- lib/useOnyx.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index c34ba962..bd93e15b 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -58,16 +58,17 @@ function useOnyx>(key: TKey, const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); - // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { /** * This condition will ensure we can only handle dynamic collection member keys. */ - if (previousKey !== key && !(isCollectionMemberKey(previousKey) && isCollectionMemberKey(key))) { - throw new Error( - `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, - ); + if (previousKey === key || (isCollectionMemberKey(previousKey) && isCollectionMemberKey(key))) { + return; } + + throw new Error( + `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, + ); }, [previousKey, key]); /** From c43d4cb27201aaca7eb0562ca3ac7f82ad3e0f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 11:51:08 +0000 Subject: [PATCH 23/50] Improve comments --- lib/useOnyx.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index bd93e15b..f7166003 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -78,7 +78,7 @@ function useOnyx>(key: TKey, */ const getSnapshot = useCallback(() => { /** - * Case 1 - We have a normal key without selector + * Case 1 - We have a non-collection key without selector * * We just return the data from the Onyx cache. */ @@ -87,7 +87,7 @@ function useOnyx>(key: TKey, } /** - * Case 2 - We have a normal key with selector + * Case 2 - We have a non-collection key with selector * * Since selected data is not directly stored in the cache, we need to generate it with `getCachedValue` * and deep compare with our previous internal data. From 461a29cd4017915672916155648d78eaac25a336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 11:51:34 +0000 Subject: [PATCH 24/50] Change fetch status to loaded after onStoreChange --- lib/useOnyx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index f7166003..cc6504c5 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -132,8 +132,8 @@ function useOnyx>(key: TKey, * We don't need to update the Onyx cache again here, when `callback` is called the cache is already * expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. */ - fetchStatusRef.current = 'loaded'; onStoreChange(); + fetchStatusRef.current = 'loaded'; }, initWithStoredValues: options?.initWithStoredValues, waitForCollectionCallback: Onyx.isCollectionKey(key), From c349027fbf9fc2990761707d44307f344f3515c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 11:53:15 +0000 Subject: [PATCH 25/50] Remove hook plugin --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index cd9ed98d..f821efb7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = { extends: ['expensify', 'prettier'], - plugins: ['react-hooks'], parser: '@typescript-eslint/parser', env: { jest: true, From 3c7a14d02b5e06f42b08bf0e38a73eac485744b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 12:07:11 +0000 Subject: [PATCH 26/50] Rename previousDataRef --- lib/useOnyx.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index cc6504c5..a8b237c1 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -54,7 +54,12 @@ function getCachedValue(key: TKey, selector?: Selector>(key: TKey, options?: UseOnyxOptions): UseOnyxData { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); - const previousDataRef = useRef(null); + + /** + * Used to store collection objects or selected data since they aren't stored in the cache. + */ + const currentDataRef = useRef(null); + const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); @@ -98,11 +103,11 @@ function useOnyx>(key: TKey, */ if (!Onyx.isCollectionKey(key) && options?.selector) { const newData = getCachedValue(key, options.selector); - if (!deepEqual(previousDataRef.current, newData)) { - previousDataRef.current = newData as TReturnData; + if (!deepEqual(currentDataRef.current, newData)) { + currentDataRef.current = newData as TReturnData; } - return previousDataRef.current as TReturnData; + return currentDataRef.current as TReturnData; } /** @@ -116,11 +121,11 @@ function useOnyx>(key: TKey, * If they are equal, we just return the previous internal data. */ const newData = getCachedValue(key, options?.selector); - if (!deepEqual(previousDataRef.current, newData)) { - previousDataRef.current = newData as TReturnData; + if (!deepEqual(currentDataRef.current, newData)) { + currentDataRef.current = newData as TReturnData; } - return previousDataRef.current as TReturnData; + return currentDataRef.current as TReturnData; }, [key, options?.selector]); const subscribe = useCallback( From e00b2b23ca491a3d68cdc390c54ce1a50766367e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 16:41:59 +0000 Subject: [PATCH 27/50] Fix useOnyx return value type when key is collection key and selector is defined --- lib/useOnyx.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index a8b237c1..ccb4304d 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -38,8 +38,10 @@ type UseOnyxOptions = { type FetchStatus = 'loading' | 'loaded'; -type UseOnyxData = { - value: TValue; +type UseOnyxDataValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; + +type UseOnyxData = { + value: UseOnyxDataValue; status: FetchStatus; }; @@ -51,14 +53,14 @@ function getCachedValue(key: TKey, selector?: Selector; } -function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { +function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); /** * Used to store collection objects or selected data since they aren't stored in the cache. */ - const currentDataRef = useRef(null); + const currentDataRef = useRef | null>(null); const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); @@ -88,7 +90,7 @@ function useOnyx>(key: TKey, * We just return the data from the Onyx cache. */ if (!Onyx.isCollectionKey(key) && !options?.selector) { - return getCachedValue(key) as TReturnData; + return getCachedValue(key) as UseOnyxDataValue; } /** @@ -104,10 +106,10 @@ function useOnyx>(key: TKey, if (!Onyx.isCollectionKey(key) && options?.selector) { const newData = getCachedValue(key, options.selector); if (!deepEqual(currentDataRef.current, newData)) { - currentDataRef.current = newData as TReturnData; + currentDataRef.current = newData as UseOnyxDataValue; } - return currentDataRef.current as TReturnData; + return currentDataRef.current as UseOnyxDataValue; } /** @@ -122,10 +124,10 @@ function useOnyx>(key: TKey, */ const newData = getCachedValue(key, options?.selector); if (!deepEqual(currentDataRef.current, newData)) { - currentDataRef.current = newData as TReturnData; + currentDataRef.current = newData as UseOnyxDataValue; } - return currentDataRef.current as TReturnData; + return currentDataRef.current as UseOnyxDataValue; }, [key, options?.selector]); const subscribe = useCallback( @@ -179,7 +181,7 @@ function useOnyx>(key: TKey, } }, [key, options?.canEvict]); - let value = useSyncExternalStore(subscribe, getSnapshot); + let value = useSyncExternalStore>(subscribe, getSnapshot); if (isFirstRenderRef.current) { isFirstRenderRef.current = false; @@ -196,7 +198,7 @@ function useOnyx>(key: TKey, */ if (value === null && options?.initialValue !== undefined) { fetchStatusRef.current = 'loaded'; - value = options.initialValue as TReturnData; + value = options.initialValue as UseOnyxDataValue; } } From 8a71c9021d235c0ef351e1877632cce8e7b3a4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 16:48:57 +0000 Subject: [PATCH 28/50] Remove eslint-plugin-react-hooks package --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7aac27a7..daea1e2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.31.10", - "eslint-plugin-react-hooks": "^4.6.0", "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", diff --git a/package.json b/package.json index 94b74297..b2977f0f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.31.10", - "eslint-plugin-react-hooks": "^4.6.0", "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", From 4fa5ab7784ae9be5c6e973765c07a490c0c48b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 16:51:27 +0000 Subject: [PATCH 29/50] Fix lint errors --- lib/storage/__mocks__/index.ts | 11 +++++++++-- tests/unit/cacheEvictionTest.js | 7 ++++++- tests/utils/waitForPromisesToResolve.js | 5 ++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index e202c0f6..3e6f156b 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -15,13 +15,20 @@ const idbKeyvalMock: StorageProvider = { }, multiSet(pairs) { const setPromises = pairs.map(([key, value]) => this.setItem(key, value)); - return new Promise((resolve) => Promise.all(setPromises).then(() => resolve(storageMapInternal))); + return new Promise((resolve) => { + Promise.all(setPromises).then(() => resolve(storageMapInternal)); + }); }, getItem(key) { return Promise.resolve(storageMapInternal[key]); }, multiGet(keys) { - const getPromises = keys.map((key) => new Promise((resolve) => this.getItem(key).then((value) => resolve([key, value])))); + const getPromises = keys.map( + (key) => + new Promise((resolve) => { + this.getItem(key).then((value) => resolve([key, value])); + }), + ); return Promise.all(getPromises) as Promise; }, multiMerge(pairs) { diff --git a/tests/unit/cacheEvictionTest.js b/tests/unit/cacheEvictionTest.js index 59d4c253..ed3d77be 100644 --- a/tests/unit/cacheEvictionTest.js +++ b/tests/unit/cacheEvictionTest.js @@ -43,7 +43,12 @@ test('Cache eviction', () => { // When we set a new key we want to add and force the first attempt to fail const originalSetItem = StorageMock.setItem; - const setItemMock = jest.fn(originalSetItem).mockImplementationOnce(() => new Promise((_resolve, reject) => reject())); + const setItemMock = jest.fn(originalSetItem).mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + reject(); + }), + ); StorageMock.setItem = setItemMock; return Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_ADD}`, {test: 'add'}).then(() => { diff --git a/tests/utils/waitForPromisesToResolve.js b/tests/utils/waitForPromisesToResolve.js index 8ec5a247..be68f88b 100644 --- a/tests/utils/waitForPromisesToResolve.js +++ b/tests/utils/waitForPromisesToResolve.js @@ -1 +1,4 @@ -export default () => new Promise((resolve) => setTimeout(resolve, 0)); +export default () => + new Promise((resolve) => { + setTimeout(resolve, 0); + }); From d10dc64eeeaa411f3f331ddd9409a30b4d18d147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 18:09:16 +0000 Subject: [PATCH 30/50] Simplify getSnapshot() logic --- lib/useOnyx.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index ccb4304d..d695260c 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -94,26 +94,7 @@ function useOnyx>(key: TKey, } /** - * Case 2 - We have a non-collection key with selector - * - * Since selected data is not directly stored in the cache, we need to generate it with `getCachedValue` - * and deep compare with our previous internal data. - * - * If they are not equal, we update the internal data and return it. - * - * If they are equal, we just return the previous internal data. - */ - if (!Onyx.isCollectionKey(key) && options?.selector) { - const newData = getCachedValue(key, options.selector); - if (!deepEqual(currentDataRef.current, newData)) { - currentDataRef.current = newData as UseOnyxDataValue; - } - - return currentDataRef.current as UseOnyxDataValue; - } - - /** - * Case 3 - We have a collection key with/without selector + * Case 2 - We have a non-collection/collection key with/without selector * * Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` * and deep compare with our previous internal data. From ff6f8507e9b8e4c563648cc438ad2b74a03ab154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 16 Feb 2024 18:11:43 +0000 Subject: [PATCH 31/50] Improve typings --- lib/useOnyx.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index d695260c..965cf4f0 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -38,10 +38,10 @@ type UseOnyxOptions = { type FetchStatus = 'loading' | 'loaded'; -type UseOnyxDataValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; +type CachedValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; type UseOnyxData = { - value: UseOnyxDataValue; + value: CachedValue; status: FetchStatus; }; @@ -49,8 +49,8 @@ function isCollectionMemberKey(key: TKey): boolean { return key.includes('_') && !key.endsWith('_'); } -function getCachedValue(key: TKey, selector?: Selector): OnyxValue { - return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as OnyxValue; +function getCachedValue(key: TKey, selector?: Selector): CachedValue { + return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as CachedValue; } function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { @@ -60,7 +60,7 @@ function useOnyx>(key: TKey, /** * Used to store collection objects or selected data since they aren't stored in the cache. */ - const currentDataRef = useRef | null>(null); + const currentDataRef = useRef | null>(null); const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); @@ -90,7 +90,7 @@ function useOnyx>(key: TKey, * We just return the data from the Onyx cache. */ if (!Onyx.isCollectionKey(key) && !options?.selector) { - return getCachedValue(key) as UseOnyxDataValue; + return getCachedValue(key); } /** @@ -105,10 +105,10 @@ function useOnyx>(key: TKey, */ const newData = getCachedValue(key, options?.selector); if (!deepEqual(currentDataRef.current, newData)) { - currentDataRef.current = newData as UseOnyxDataValue; + currentDataRef.current = newData as CachedValue; } - return currentDataRef.current as UseOnyxDataValue; + return currentDataRef.current as CachedValue; }, [key, options?.selector]); const subscribe = useCallback( @@ -162,7 +162,7 @@ function useOnyx>(key: TKey, } }, [key, options?.canEvict]); - let value = useSyncExternalStore>(subscribe, getSnapshot); + let value = useSyncExternalStore>(subscribe, getSnapshot); if (isFirstRenderRef.current) { isFirstRenderRef.current = false; @@ -179,7 +179,7 @@ function useOnyx>(key: TKey, */ if (value === null && options?.initialValue !== undefined) { fetchStatusRef.current = 'loaded'; - value = options.initialValue as UseOnyxDataValue; + value = options.initialValue as CachedValue; } } From 311160f46a8877afa71b0a70210dda302bc97448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 21 Feb 2024 18:07:44 +0000 Subject: [PATCH 32/50] Improve key changing check logic --- lib/Onyx.d.ts | 13 ++++++++++++- lib/Onyx.js | 11 +++++++++++ lib/useOnyx.ts | 17 ++++++++++------- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index fba2c745..2abbed31 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -1,6 +1,6 @@ import {Component} from 'react'; import * as Logger from './Logger'; -import {CollectionKeyBase, DeepRecord, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; +import {CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; /** * Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`. @@ -126,6 +126,15 @@ declare function getAllKeys(): Promise>; */ declare function isCollectionKey(key: OnyxKey): key is CollectionKeyBase; +declare function isCollectionMemberKey(collectionKey: TCollectionKey, key: string): key is `${TCollectionKey}${string}`; + +/** + * Splits a collection member key into the collection key part and the ID part. + * @param key - The collection member key to split. + * @returns A tuple where the first element is the collection part and the second element is the ID part. + */ +declare function splitCollectionMemberKey(key: TKey): [TKey extends `${infer Prefix}_${string}` ? `${Prefix}_` : never, string]; + /** * Checks to see if this key has been flagged as * safe for removal. @@ -331,6 +340,8 @@ declare const Onyx: { setMemoryOnlyKeys: typeof setMemoryOnlyKeys; tryGetCachedValue: typeof tryGetCachedValue; isCollectionKey: typeof isCollectionKey; + isCollectionMemberKey: typeof isCollectionMemberKey; + splitCollectionMemberKey: typeof splitCollectionMemberKey; }; export default Onyx; diff --git a/lib/Onyx.js b/lib/Onyx.js index bed1941a..9a15036e 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -212,6 +212,15 @@ function isCollectionMemberKey(collectionKey, key) { return Str.startsWith(key, collectionKey) && key.length > collectionKey.length; } +/** + * Splits a collection member key into the collection key part and the ID part. + * @param {String} key - The collection member key to split. + * @returns {[String, String]} A tuple where the first element is the collection part and the second element is the ID part. + */ +function splitCollectionMemberKey(key) { + return key.split('_'); +} + /** * Checks to see if a provided key is the exact configured key of our connected subscriber * or if the provided key is a collection member key (in case our configured key is a "collection key") @@ -1628,6 +1637,8 @@ const Onyx = { tryGetCachedValue, hasPendingMergeForKey, isCollectionKey, + isCollectionMemberKey, + splitCollectionMemberKey, }; /** diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 965cf4f0..d70be4a1 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -45,10 +45,6 @@ type UseOnyxData = { status: FetchStatus; }; -function isCollectionMemberKey(key: TKey): boolean { - return key.includes('_') && !key.endsWith('_'); -} - function getCachedValue(key: TKey, selector?: Selector): CachedValue { return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as CachedValue; } @@ -67,14 +63,21 @@ function useOnyx>(key: TKey, useEffect(() => { /** - * This condition will ensure we can only handle dynamic collection member keys. + * These conditions will ensure we can only handle dynamic collection member keys from the same collection. */ - if (previousKey === key || (isCollectionMemberKey(previousKey) && isCollectionMemberKey(key))) { + if (previousKey === key) { + return; + } + + const previousCollectionKey = Onyx.splitCollectionMemberKey(previousKey)[0]; + const collectionKey = Onyx.splitCollectionMemberKey(key)[0]; + + if (Onyx.isCollectionMemberKey(previousCollectionKey, previousKey) && Onyx.isCollectionMemberKey(collectionKey, key) && previousCollectionKey === collectionKey) { return; } throw new Error( - `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys e.g. from 'collection_id1' to 'collection_id2'.`, + `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys from the same collection e.g. from 'collection_id1' to 'collection_id2'.`, ); }, [previousKey, key]); From c0a4fc4fddbbd232893c4b0f385efe04973b6a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 21 Feb 2024 18:12:15 +0000 Subject: [PATCH 33/50] Adjust getSnapshot() comments --- lib/useOnyx.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index d70be4a1..5c9ac95b 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -81,11 +81,6 @@ function useOnyx>(key: TKey, ); }, [previousKey, key]); - /** - * According to React docs, `getSnapshot` is a function that returns a snapshot of the data in the store that’s needed by the component. - * **While the store has not changed, repeated calls to getSnapshot must return the same value.** - * If the store changes and the returned value is different (as compared by Object.is), React re-renders the component. - */ const getSnapshot = useCallback(() => { /** * Case 1 - We have a non-collection key without selector @@ -97,7 +92,7 @@ function useOnyx>(key: TKey, } /** - * Case 2 - We have a non-collection/collection key with/without selector + * Case 2 - We have a non-collection/collection key and/or selector * * Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` * and deep compare with our previous internal data. From 5086b95a162a59a1308a8abbe4dd94a9e835e105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 21 Feb 2024 19:09:56 +0000 Subject: [PATCH 34/50] Stabilizes selector reference to avoid unnecessary calls to getSnapshot() --- lib/useLiveRef.ts | 10 ++++++++++ lib/useOnyx.ts | 12 +++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 lib/useLiveRef.ts diff --git a/lib/useLiveRef.ts b/lib/useLiveRef.ts new file mode 100644 index 00000000..1c60fb94 --- /dev/null +++ b/lib/useLiveRef.ts @@ -0,0 +1,10 @@ +import {useRef} from 'react'; + +function useLiveRef(value: T) { + const ref = useRef(value); + ref.current = value; + + return ref; +} + +export default useLiveRef; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 5c9ac95b..447c141d 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -2,6 +2,7 @@ import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; +import useLiveRef from './useLiveRef'; import usePrevious from './usePrevious'; type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; @@ -53,6 +54,11 @@ function useOnyx>(key: TKey, const connectionIDRef = useRef(null); const previousKey = usePrevious(key); + /** + * Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. + */ + const selectorRef = useLiveRef(options?.selector); + /** * Used to store collection objects or selected data since they aren't stored in the cache. */ @@ -87,7 +93,7 @@ function useOnyx>(key: TKey, * * We just return the data from the Onyx cache. */ - if (!Onyx.isCollectionKey(key) && !options?.selector) { + if (!Onyx.isCollectionKey(key) && !selectorRef.current) { return getCachedValue(key); } @@ -101,13 +107,13 @@ function useOnyx>(key: TKey, * * If they are equal, we just return the previous internal data. */ - const newData = getCachedValue(key, options?.selector); + const newData = getCachedValue(key, selectorRef.current); if (!deepEqual(currentDataRef.current, newData)) { currentDataRef.current = newData as CachedValue; } return currentDataRef.current as CachedValue; - }, [key, options?.selector]); + }, [key, selectorRef]); const subscribe = useCallback( (onStoreChange: () => void) => { From 6fec631772640b600b327567bc968da17fd02d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 21 Feb 2024 19:10:11 +0000 Subject: [PATCH 35/50] Fix comment --- lib/useOnyx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 447c141d..3aef1535 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -98,7 +98,7 @@ function useOnyx>(key: TKey, } /** - * Case 2 - We have a non-collection/collection key and/or selector + * Case 2 - We have a collection key and/or selector * * Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` * and deep compare with our previous internal data. From 7d177b1741a309a7d72b20db69774cbe016d2c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 23 Feb 2024 18:26:01 +0000 Subject: [PATCH 36/50] First implementation of allowStaleData --- lib/useOnyx.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 3aef1535..d72b54a3 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -19,9 +19,9 @@ type UseOnyxOptions = { initWithStoredValues?: boolean; /** - * TODO: Check if we still need this flag and associated logic. + * If set to true, data will be retrieved from cache during the first render even if there is a pending merge for the key. */ - // allowStaleData?: boolean; + allowStaleData?: boolean; /** * Sets an initial value to be returned by the hook during the first render. @@ -124,8 +124,8 @@ function useOnyx>(key: TKey, * We don't need to update the Onyx cache again here, when `callback` is called the cache is already * expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. */ - onStoreChange(); fetchStatusRef.current = 'loaded'; + onStoreChange(); }, initWithStoredValues: options?.initWithStoredValues, waitForCollectionCallback: Onyx.isCollectionKey(key), @@ -166,28 +166,30 @@ function useOnyx>(key: TKey, } }, [key, options?.canEvict]); - let value = useSyncExternalStore>(subscribe, getSnapshot); + let storeValue = useSyncExternalStore>(subscribe, getSnapshot); + let resultValue: CachedValue | null = isFirstRenderRef.current ? null : storeValue; if (isFirstRenderRef.current) { isFirstRenderRef.current = false; /** - * Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. + * Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. */ - if (value !== null) { + if (storeValue === null && options?.initialValue !== undefined) { fetchStatusRef.current = 'loaded'; + storeValue = options.initialValue as CachedValue; } /** - * Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. + * Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. */ - if (value === null && options?.initialValue !== undefined) { + if ((storeValue !== null && !Onyx.hasPendingMergeForKey(key)) || options?.allowStaleData) { fetchStatusRef.current = 'loaded'; - value = options.initialValue as CachedValue; + resultValue = storeValue; } } - return {value, status: fetchStatusRef.current}; + return {value: resultValue as CachedValue, status: fetchStatusRef.current}; } export default useOnyx; From 9df252f87eefa6a90db8084978d3c72250ab5e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 23 Feb 2024 19:22:42 +0000 Subject: [PATCH 37/50] Address review comments --- lib/Onyx.js | 4 ++++ lib/useLiveRef.ts | 4 ++++ lib/useOnyx.ts | 60 ++++++++++++++-------------------------------- lib/usePrevious.ts | 3 +++ 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/lib/Onyx.js b/lib/Onyx.js index 9a15036e..86724e35 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -218,6 +218,10 @@ function isCollectionMemberKey(collectionKey, key) { * @returns {[String, String]} A tuple where the first element is the collection part and the second element is the ID part. */ function splitCollectionMemberKey(key) { + if (!key.includes('_')) { + throw new Error(`Invalid ${key} key provided, only collection keys are allowed.`); + } + return key.split('_'); } diff --git a/lib/useLiveRef.ts b/lib/useLiveRef.ts index 1c60fb94..fb4bef29 100644 --- a/lib/useLiveRef.ts +++ b/lib/useLiveRef.ts @@ -1,5 +1,9 @@ import {useRef} from 'react'; +/** + * Creates a mutable reference to a value, useful when you need to + * maintain a reference to a value that may change over time without triggering re-renders. + */ function useLiveRef(value: T) { const ref = useRef(value); ref.current = value; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index d72b54a3..171ba214 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -24,12 +24,12 @@ type UseOnyxOptions = { allowStaleData?: boolean; /** - * Sets an initial value to be returned by the hook during the first render. + * This value will be returned by the hook on the first render while the data is being read from Onyx. */ initialValue?: OnyxValue; /** - * If included, this will be used to subscribe to a subset of an Onyx key's data. + * This will be used to subscribe to a subset of an Onyx key's data. * Using this setting on `useOnyx` can have very positive performance benefits because the component will only re-render * when the subset of data changes. Otherwise, any change of data on any property would normally * cause the component to re-render (and that can be expensive from a performance standpoint). @@ -54,23 +54,17 @@ function useOnyx>(key: TKey, const connectionIDRef = useRef(null); const previousKey = usePrevious(key); - /** - * Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. - */ + // Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. const selectorRef = useLiveRef(options?.selector); - /** - * Used to store collection objects or selected data since they aren't stored in the cache. - */ + // Used to store collection objects or selected data since they aren't stored in the cache. const currentDataRef = useRef | null>(null); const isFirstRenderRef = useRef(true); const fetchStatusRef = useRef('loading'); useEffect(() => { - /** - * These conditions will ensure we can only handle dynamic collection member keys from the same collection. - */ + // These conditions will ensure we can only handle dynamic collection member keys from the same collection. if (previousKey === key) { return; } @@ -88,25 +82,17 @@ function useOnyx>(key: TKey, }, [previousKey, key]); const getSnapshot = useCallback(() => { - /** - * Case 1 - We have a non-collection key without selector - * - * We just return the data from the Onyx cache. - */ + // Case 1 - We have a non-collection key without selector + // We just return the data from the Onyx cache. if (!Onyx.isCollectionKey(key) && !selectorRef.current) { return getCachedValue(key); } - /** - * Case 2 - We have a collection key and/or selector - * - * Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` - * and deep compare with our previous internal data. - * - * If they are not equal, we update the internal data and return it. - * - * If they are equal, we just return the previous internal data. - */ + // Case 2 - We have a collection key and/or selector + // Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` + // and deep compare with our previous internal data. + // If they are not equal, we update the internal data and return it. + // If they are equal, we just return the previous internal data. const newData = getCachedValue(key, selectorRef.current); if (!deepEqual(currentDataRef.current, newData)) { currentDataRef.current = newData as CachedValue; @@ -120,10 +106,8 @@ function useOnyx>(key: TKey, connectionIDRef.current = Onyx.connect({ key: key as CollectionKeyBase, callback: () => { - /** - * We don't need to update the Onyx cache again here, when `callback` is called the cache is already - * expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. - */ + // We don't need to update the Onyx cache again here, when `callback` is called the cache is already + // expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. fetchStatusRef.current = 'loaded'; onStoreChange(); }, @@ -138,18 +122,14 @@ function useOnyx>(key: TKey, Onyx.disconnect(connectionIDRef.current); - /** - * Sets the fetch status back to "loading" as we are connecting to a new key. - */ + // Sets the fetch status back to "loading" as we are connecting to a new key. fetchStatusRef.current = 'loading'; }; }, [key, options?.initWithStoredValues], ); - /** - * Mimics withOnyx's checkEvictableKeys() behavior. - */ + // Mimics withOnyx's checkEvictableKeys() behavior. useEffect(() => { if (options?.canEvict === undefined || !connectionIDRef.current) { return; @@ -172,17 +152,13 @@ function useOnyx>(key: TKey, if (isFirstRenderRef.current) { isFirstRenderRef.current = false; - /** - * Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. - */ + // Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. if (storeValue === null && options?.initialValue !== undefined) { fetchStatusRef.current = 'loaded'; storeValue = options.initialValue as CachedValue; } - /** - * Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. - */ + // Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. if ((storeValue !== null && !Onyx.hasPendingMergeForKey(key)) || options?.allowStaleData) { fetchStatusRef.current = 'loaded'; resultValue = storeValue; diff --git a/lib/usePrevious.ts b/lib/usePrevious.ts index bc049f32..e03ab7a6 100644 --- a/lib/usePrevious.ts +++ b/lib/usePrevious.ts @@ -1,5 +1,8 @@ import {useEffect, useRef} from 'react'; +/** + * Returns the previous value of the provided value. + */ function usePrevious(value: T): T { const ref = useRef(value); From aaa1397647f384fb1b9700f789129a85d962f129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 27 Feb 2024 19:26:42 +0000 Subject: [PATCH 38/50] Change return format --- lib/useOnyx.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 171ba214..5a8de971 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -41,10 +41,12 @@ type FetchStatus = 'loading' | 'loaded'; type CachedValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; -type UseOnyxData = { - value: CachedValue; - status: FetchStatus; -}; +type UseOnyxData = [ + CachedValue, + { + status: FetchStatus; + }, +]; function getCachedValue(key: TKey, selector?: Selector): CachedValue { return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as CachedValue; @@ -165,7 +167,7 @@ function useOnyx>(key: TKey, } } - return {value: resultValue as CachedValue, status: fetchStatusRef.current}; + return [resultValue as CachedValue, {status: fetchStatusRef.current}]; } export default useOnyx; From 1f62a8f350e66493df3a78fd732482c01a23a51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 5 Mar 2024 18:28:11 +0000 Subject: [PATCH 39/50] Re-write implementation to fix loading states --- lib/Onyx.d.ts | 2 +- lib/index.d.ts | 4 +-- lib/useOnyx.ts | 97 +++++++++++++++++++++++++++----------------------- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index 2abbed31..975c3a0d 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -318,7 +318,7 @@ declare function setMemoryOnlyKeys(keyList: OnyxKey[]): void; declare function tryGetCachedValue( key: TKey, mapping?: TryGetCachedValueMapping, -): TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; +): TKey extends CollectionKeyBase ? OnyxCollection | undefined : OnyxEntry; declare const Onyx: { connect: typeof connect; diff --git a/lib/index.d.ts b/lib/index.d.ts index 47ea30ac..810bf1d0 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,7 +1,7 @@ import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx'; import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState} from './types'; import withOnyx from './withOnyx'; -import useOnyx from './useOnyx'; +import useOnyx, {UseOnyxData} from './useOnyx'; export default Onyx; -export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, useOnyx}; +export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, useOnyx, UseOnyxData}; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 5a8de971..1ca54658 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -48,8 +48,8 @@ type UseOnyxData = [ }, ]; -function getCachedValue(key: TKey, selector?: Selector): CachedValue { - return (Onyx.tryGetCachedValue(key, {selector}) ?? null) as CachedValue; +function getCachedValue(key: TKey, selector?: Selector): CachedValue | undefined { + return Onyx.tryGetCachedValue(key, {selector}) as CachedValue | undefined; } function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { @@ -59,11 +59,17 @@ function useOnyx>(key: TKey, // Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. const selectorRef = useLiveRef(options?.selector); - // Used to store collection objects or selected data since they aren't stored in the cache. - const currentDataRef = useRef | null>(null); + // Stores the previous cached data as it's necessary to compare with new data in `getSnapshot()`. + // We initialize it to `undefined` to simulate that we don't have any value from cache yet. + const cachedValueRef = useRef | undefined>(undefined); - const isFirstRenderRef = useRef(true); - const fetchStatusRef = useRef('loading'); + // Stores the previously data returned by the hook, containing the data from cache and the fetch status. + // We initialize it to `null` and `loading` fetch status to simulate the return data when the hook is loading from the cache. + const dataRef = useRef>([null as CachedValue, {status: 'loading'}]); + + // Indicates if it's the first Onyx connection of this hook or not, as we don't want certain use cases + // in `getSnapshot()` to be satisfied several times. + const isFirstConnectionRef = useRef(true); useEffect(() => { // These conditions will ensure we can only handle dynamic collection member keys from the same collection. @@ -84,24 +90,46 @@ function useOnyx>(key: TKey, }, [previousKey, key]); const getSnapshot = useCallback(() => { - // Case 1 - We have a non-collection key without selector - // We just return the data from the Onyx cache. - if (!Onyx.isCollectionKey(key) && !selectorRef.current) { - return getCachedValue(key); + // We get the data from the cache, supplying a selector too in case it's defined. + // If `newData` is `undefined` it means that the cache doesn't have a value for that key yet. + // If `newData` is `null` or any other value if means that the cache does have a value for that key. + // This difference between `undefined` and other values is crucial and it's used to address the following + // conditions and use cases. + let newData = getCachedValue(key, selectorRef.current); + + // Since the fetch status can be different given the use cases below, we define the variable right away. + let newFetchStatus: FetchStatus | undefined; + + // If we have pending merge operations for the key during the first connection, we set data to `undefined` + // and fetch status to `loading` to simulate that it is still being loaded until we have the most updated data. + // If `allowStaleData` is `true` this logic will be ignored and cached data will be used, even if it's stale data. + if (isFirstConnectionRef.current && Onyx.hasPendingMergeForKey(key) && !options?.allowStaleData) { + newData = undefined; + newFetchStatus = 'loading'; + } + + // If data is not present in cache (if it's `undefined`) and `initialValue` is set during the first connection, + // we set data to `initialValue` and fetch status to `loaded` since we already have some data to return to the consumer. + if (isFirstConnectionRef.current && newData === undefined && options?.initialValue !== undefined) { + newData = options?.initialValue as CachedValue; + newFetchStatus = 'loaded'; } - // Case 2 - We have a collection key and/or selector - // Since both collection objects and selected data are not directly stored in the cache, we need to generate them with `getCachedValue` - // and deep compare with our previous internal data. - // If they are not equal, we update the internal data and return it. - // If they are equal, we just return the previous internal data. - const newData = getCachedValue(key, selectorRef.current); - if (!deepEqual(currentDataRef.current, newData)) { - currentDataRef.current = newData as CachedValue; + // If the previously cached data is different from the new data, we update both cached data + // and the result data to be returned by the hook. + // We can't directly compare the value from `dataRef` with `newData` because we default `undefined` + // to `null` when setting `dataRef` value to ensure we have a consistent return, but we need to be able to differentiate + // between `undefined` and `null` during the comparison, so `cachedValueRef` is used to store the real value without changing + // to `null`. + if (!deepEqual(cachedValueRef.current, newData)) { + cachedValueRef.current = newData as CachedValue; + + // If the new data is `undefined` we default it to `null` to ensure the consumer get a consistent result from the hook. + dataRef.current = [(cachedValueRef.current ?? null) as CachedValue, {status: newFetchStatus ?? 'loaded'}]; } - return currentDataRef.current as CachedValue; - }, [key, selectorRef]); + return dataRef.current; + }, [key, selectorRef, options?.allowStaleData, options?.initialValue]); const subscribe = useCallback( (onStoreChange: () => void) => { @@ -109,8 +137,8 @@ function useOnyx>(key: TKey, key: key as CollectionKeyBase, callback: () => { // We don't need to update the Onyx cache again here, when `callback` is called the cache is already - // expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called. - fetchStatusRef.current = 'loaded'; + // expected to be updated, so we just signal that the store changed and `getSnapshot()` can be called again. + isFirstConnectionRef.current = false; onStoreChange(); }, initWithStoredValues: options?.initWithStoredValues, @@ -123,9 +151,7 @@ function useOnyx>(key: TKey, } Onyx.disconnect(connectionIDRef.current); - - // Sets the fetch status back to "loading" as we are connecting to a new key. - fetchStatusRef.current = 'loading'; + isFirstConnectionRef.current = false; }; }, [key, options?.initWithStoredValues], @@ -148,26 +174,9 @@ function useOnyx>(key: TKey, } }, [key, options?.canEvict]); - let storeValue = useSyncExternalStore>(subscribe, getSnapshot); - let resultValue: CachedValue | null = isFirstRenderRef.current ? null : storeValue; - - if (isFirstRenderRef.current) { - isFirstRenderRef.current = false; - - // Sets the fetch status to "loaded" and `value` to `initialValue` in the first render if we don't have anything in the cache and `initialValue` is set. - if (storeValue === null && options?.initialValue !== undefined) { - fetchStatusRef.current = 'loaded'; - storeValue = options.initialValue as CachedValue; - } - - // Sets the fetch status to "loaded" in the first render if data is already retrieved from cache. - if ((storeValue !== null && !Onyx.hasPendingMergeForKey(key)) || options?.allowStaleData) { - fetchStatusRef.current = 'loaded'; - resultValue = storeValue; - } - } + const data = useSyncExternalStore>(subscribe, getSnapshot); - return [resultValue as CachedValue, {status: fetchStatusRef.current}]; + return data; } export default useOnyx; From 9d9732107d5e7d5ae6d3ccdab15ceaf617686e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Mar 2024 18:33:18 +0000 Subject: [PATCH 40/50] Implement initial tests for useOnyx --- lib/Onyx.js | 6 +- lib/useOnyx.ts | 16 ++- package-lock.json | 95 ++++---------- package.json | 2 +- tests/unit/useOnyxTest.ts | 257 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 76 deletions(-) create mode 100644 tests/unit/useOnyxTest.ts diff --git a/lib/Onyx.js b/lib/Onyx.js index 9949e2cf..87ab39fa 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -218,11 +218,13 @@ function isCollectionMemberKey(collectionKey, key) { * @returns {[String, String]} A tuple where the first element is the collection part and the second element is the ID part. */ function splitCollectionMemberKey(key) { - if (!key.includes('_')) { + const underscoreIndex = key.indexOf('_'); + + if (underscoreIndex === -1) { throw new Error(`Invalid ${key} key provided, only collection keys are allowed.`); } - return key.split('_'); + return [key.substring(0, underscoreIndex + 1), key.substring(underscoreIndex + 1)]; } /** diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 1ca54658..823159e5 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -77,11 +77,17 @@ function useOnyx>(key: TKey, return; } - const previousCollectionKey = Onyx.splitCollectionMemberKey(previousKey)[0]; - const collectionKey = Onyx.splitCollectionMemberKey(key)[0]; - - if (Onyx.isCollectionMemberKey(previousCollectionKey, previousKey) && Onyx.isCollectionMemberKey(collectionKey, key) && previousCollectionKey === collectionKey) { - return; + try { + const previousCollectionKey = Onyx.splitCollectionMemberKey(previousKey)[0]; + const collectionKey = Onyx.splitCollectionMemberKey(key)[0]; + + if (Onyx.isCollectionMemberKey(previousCollectionKey, previousKey) && Onyx.isCollectionMemberKey(collectionKey, key) && previousCollectionKey === collectionKey) { + return; + } + } catch (e) { + throw new Error( + `'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys from the same collection e.g. from 'collection_id1' to 'collection_id2'.`, + ); } throw new Error( diff --git a/package-lock.json b/package-lock.json index d490613d..60e911e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@react-native-community/eslint-config": "^3.2.0", "@react-native/polyfills": "^2.0.0", "@testing-library/jest-native": "^3.4.2", - "@testing-library/react-native": "^7.0.2", + "@testing-library/react-native": "^10.0.0", "@types/jest": "^28.1.8", "@types/lodash": "^4.14.202", "@types/node": "^20.11.5", @@ -3742,12 +3742,12 @@ } }, "node_modules/@testing-library/react-native": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-7.2.0.tgz", - "integrity": "sha512-rDKzJjAAeGgyoJT0gFQiMsIL09chdWcwZyYx6WZHMgm2c5NDqY52hUuyTkzhqddMYWmSRklFphSg7B2HX+246Q==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-10.0.0.tgz", + "integrity": "sha512-aFq2BQReP9ijZsQ83XMLJthGyffFS7vv0YN3ZAvgIgxEfAQ78v0GOMLyoTNyZEM+wDGb4cn1yorHXYUWs8krwA==", "dev": true, "dependencies": { - "pretty-format": "^26.0.1" + "pretty-format": "^27.0.0" }, "peerDependencies": { "react": ">=16.0.0", @@ -3756,51 +3756,29 @@ } }, "node_modules/@testing-library/react-native/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@testing-library/react-native/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/@testing-library/react-native/node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", "react-is": "^17.0.1" }, "engines": { - "node": ">= 10" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/@testing-library/react-native/node_modules/react-is": { @@ -20534,47 +20512,28 @@ } }, "@testing-library/react-native": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-7.2.0.tgz", - "integrity": "sha512-rDKzJjAAeGgyoJT0gFQiMsIL09chdWcwZyYx6WZHMgm2c5NDqY52hUuyTkzhqddMYWmSRklFphSg7B2HX+246Q==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-10.0.0.tgz", + "integrity": "sha512-aFq2BQReP9ijZsQ83XMLJthGyffFS7vv0YN3ZAvgIgxEfAQ78v0GOMLyoTNyZEM+wDGb4cn1yorHXYUWs8krwA==", "dev": true, "requires": { - "pretty-format": "^26.0.1" + "pretty-format": "^27.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true }, "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, diff --git a/package.json b/package.json index 38cdb355..b85b23c3 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@react-native-community/eslint-config": "^3.2.0", "@react-native/polyfills": "^2.0.0", "@testing-library/jest-native": "^3.4.2", - "@testing-library/react-native": "^7.0.2", + "@testing-library/react-native": "^10.0.0", "@types/jest": "^28.1.8", "@types/lodash": "^4.14.202", "@types/node": "^20.11.5", diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts new file mode 100644 index 00000000..b488a347 --- /dev/null +++ b/tests/unit/useOnyxTest.ts @@ -0,0 +1,257 @@ +import {act, renderHook} from '@testing-library/react-native'; +import type {OnyxEntry} from '../../lib'; +import Onyx, {useOnyx} from '../../lib'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import StorageMock from '../../lib/storage'; + +const ONYXKEYS = { + TEST_KEY: 'test', + TEST_KEY_2: 'test2', + COLLECTION: { + TEST_KEY: 'test_', + TEST_KEY_2: 'test2_', + }, +}; + +Onyx.init({ + keys: ONYXKEYS, +}); + +beforeEach(() => Onyx.clear()); + +describe('useOnyx', () => { + describe('dynamic key', () => { + const error = (key1: string, key2: string) => + `'${key1}' key can't be changed to '${key2}'. useOnyx() only supports dynamic keys if they are both collection member keys from the same collection e.g. from 'collection_id1' to 'collection_id2'.`; + + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(jest.fn); + }); + + afterEach(() => { + (console.error as unknown as jest.SpyInstance>).mockRestore(); + }); + + it('should throw an error when changing from a non-collection key to another one', async () => { + const {rerender} = renderHook((key: string) => useOnyx(key), {initialProps: ONYXKEYS.TEST_KEY}); + + try { + await act(async () => { + rerender(ONYXKEYS.TEST_KEY_2); + }); + + fail('Expected to throw an error.'); + } catch (e) { + expect((e as Error).message).toBe(error(ONYXKEYS.TEST_KEY, ONYXKEYS.TEST_KEY_2)); + } + }); + + it('should throw an error when changing from a collection key to another one', async () => { + const {rerender} = renderHook((key: string) => useOnyx(key), {initialProps: ONYXKEYS.COLLECTION.TEST_KEY}); + + try { + await act(async () => { + rerender(ONYXKEYS.COLLECTION.TEST_KEY_2); + }); + + fail('Expected to throw an error.'); + } catch (e) { + expect((e as Error).message).toBe(error(ONYXKEYS.COLLECTION.TEST_KEY, ONYXKEYS.COLLECTION.TEST_KEY_2)); + } + }); + + it('should throw an error when changing from a collection key to a collectiom member key', async () => { + const {rerender} = renderHook((key: string) => useOnyx(key), {initialProps: ONYXKEYS.COLLECTION.TEST_KEY}); + + try { + await act(async () => { + rerender(`${ONYXKEYS.COLLECTION.TEST_KEY}1`); + }); + + fail('Expected to throw an error.'); + } catch (e) { + expect((e as Error).message).toBe(error(ONYXKEYS.COLLECTION.TEST_KEY, `${ONYXKEYS.COLLECTION.TEST_KEY}1`)); + } + }); + + it('should not throw any errors when changing from a collection member key to another one', async () => { + const {rerender} = renderHook((key: string) => useOnyx(key), {initialProps: `${ONYXKEYS.COLLECTION.TEST_KEY}1` as string}); + + try { + await act(async () => { + rerender(`${ONYXKEYS.COLLECTION.TEST_KEY}2`); + }); + } catch (e) { + fail("Expected to don't throw any errors."); + } + }); + }); + + describe('misc', () => { + it('should return value and loaded state when loading cached key', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, 'test'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); + + expect(result.current[0]).toEqual('test'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should initially return null while loading non-cached key, and then return value and loaded state', async () => { + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); + + expect(result.current[0]).toEqual(null); + expect(result.current[1].status).toEqual('loading'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should initially return null and then return cached value after multiple merge operations', async () => { + Onyx.merge(ONYXKEYS.TEST_KEY, 'test1'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); + + expect(result.current[0]).toEqual(null); + expect(result.current[1].status).toEqual('loading'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test3'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return value from cache, and return updated value after a merge operation', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); + + expect(result.current[0]).toEqual('test1'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + + expect(result.current[0]).toEqual('test2'); + expect(result.current[1].status).toEqual('loaded'); + }); + }); + + describe('selector', () => { + it('should return selected data from a non-collection key', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, {id: 'test_id', name: 'test_name'}); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + // @ts-expect-error bypass + selector: (entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name}`, + }), + ); + + expect(result.current[0]).toEqual('id - test_id, name - test_name'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {id: 'changed_id', name: 'changed_name'})); + + expect(result.current[0]).toEqual('id - changed_id, name - changed_name'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return selected data from a collection key', async () => { + // @ts-expect-error bypass + Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: {id: 'entry3_id', name: 'entry3_name'}, + }); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.COLLECTION.TEST_KEY, { + // @ts-expect-error bypass + selector: (entry: OnyxEntry<{id: string; name: string}>) => entry?.id, + }), + ); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual({ + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: 'entry1_id', + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: 'entry2_id', + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: 'entry3_id', + }); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`, null)); + + expect(result.current[0]).toEqual({ + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: 'entry1_id', + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: 'entry3_id', + }); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should not change selected data if a property outside the selector was changed', async () => { + // @ts-expect-error bypass + Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: {id: 'entry3_id', name: 'entry3_name'}, + }); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.COLLECTION.TEST_KEY, { + // @ts-expect-error bypass + selector: (entry: OnyxEntry<{id: string; name: string}>) => entry?.id, + }), + ); + + await act(async () => waitForPromisesToResolve()); + + const oldResult = result.current; + + await act(async () => Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`, {name: 'entry2_changed'})); + + // must be the same reference + expect(oldResult).toBe(result.current); + }); + }); + + describe('initial value', () => { + it('should return initial value from non-cached key and then return null', async () => { + // @ts-expect-error bypass + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initialValue: 'initial value'})); + + expect(result.current[0]).toEqual('initial value'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual(null); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return initial value from cached key and then return cached value', async () => { + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + + // @ts-expect-error bypass + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initialValue: 'initial value'})); + + expect(result.current[0]).toEqual('initial value'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test'); + expect(result.current[1].status).toEqual('loaded'); + }); + }); + + describe('stale data', () => { + // TODO: Not sure if we'll be able to implement tests for these cases. + }); +}); From f107c93a577d6b79227f45a285d4cbd3895ff3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Mar 2024 17:31:37 +0000 Subject: [PATCH 41/50] Address review comments --- lib/Onyx.d.ts | 2 +- lib/index.d.ts | 21 ++++++++++++-- lib/types.d.ts | 6 ++++ lib/useOnyx.ts | 77 +++++++++++++++++++++++--------------------------- 4 files changed, 61 insertions(+), 45 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index 975c3a0d..69475dcd 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -318,7 +318,7 @@ declare function setMemoryOnlyKeys(keyList: OnyxKey[]): void; declare function tryGetCachedValue( key: TKey, mapping?: TryGetCachedValueMapping, -): TKey extends CollectionKeyBase ? OnyxCollection | undefined : OnyxEntry; +): TKey extends CollectionKeyBase ? OnyxCollection | undefined : OnyxEntry | undefined; declare const Onyx: { connect: typeof connect; diff --git a/lib/index.d.ts b/lib/index.d.ts index 810bf1d0..fd7cca46 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,7 +1,22 @@ import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx'; -import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState} from './types'; +import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, OnyxValue} from './types'; import withOnyx from './withOnyx'; -import useOnyx, {UseOnyxData} from './useOnyx'; +import useOnyx, {UseOnyxResult} from './useOnyx'; export default Onyx; -export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, useOnyx, UseOnyxData}; +export { + CustomTypeOptions, + OnyxCollection, + OnyxEntry, + OnyxUpdate, + withOnyx, + ConnectOptions, + NullishDeep, + KeyValueMapping, + OnyxKey, + Selector, + WithOnyxInstanceState, + useOnyx, + UseOnyxResult, + OnyxValue, +}; diff --git a/lib/types.d.ts b/lib/types.d.ts index 1daf7d99..a210c349 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -99,6 +99,11 @@ type CollectionKey = `${CollectionKeyBase}${string}`; */ type OnyxKey = Key | CollectionKey; +/** + * Represents a Onyx value that can be either a single entry or a collection of entries, depending on the `TKey` provided. + */ +type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; + /** * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys * and values are the corresponding values in Onyx's state. @@ -239,6 +244,7 @@ export { OnyxCollection, OnyxEntry, OnyxKey, + OnyxValue, Selector, NullishDeep, WithOnyxInstanceState, diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 823159e5..e6b3aa2c 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,13 +1,11 @@ import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react'; import Onyx from './Onyx'; -import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector} from './types'; +import type {CollectionKeyBase, OnyxCollection, OnyxKey, OnyxValue, Selector} from './types'; import useLiveRef from './useLiveRef'; import usePrevious from './usePrevious'; -type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; - -type UseOnyxOptions = { +type UseOnyxOptions = { /** * Determines if this key in this subscription is safe to be evicted. */ @@ -34,38 +32,37 @@ type UseOnyxOptions = { * when the subset of data changes. Otherwise, any change of data on any property would normally * cause the component to re-render (and that can be expensive from a performance standpoint). */ - selector?: Selector; + selector?: Selector; }; type FetchStatus = 'loading' | 'loaded'; type CachedValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; -type UseOnyxData = [ - CachedValue, - { - status: FetchStatus; - }, -]; +type ResultMetadata = { + status: FetchStatus; +}; + +type UseOnyxResult = [CachedValue, ResultMetadata]; function getCachedValue(key: TKey, selector?: Selector): CachedValue | undefined { return Onyx.tryGetCachedValue(key, {selector}) as CachedValue | undefined; } -function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxData { +function useOnyx>(key: TKey, options?: UseOnyxOptions): UseOnyxResult { const connectionIDRef = useRef(null); const previousKey = usePrevious(key); // Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. const selectorRef = useLiveRef(options?.selector); - // Stores the previous cached data as it's necessary to compare with new data in `getSnapshot()`. + // Stores the previous cached value as it's necessary to compare with the new value in `getSnapshot()`. // We initialize it to `undefined` to simulate that we don't have any value from cache yet. - const cachedValueRef = useRef | undefined>(undefined); + const cachedValueRef = useRef | undefined>(undefined); - // Stores the previously data returned by the hook, containing the data from cache and the fetch status. - // We initialize it to `null` and `loading` fetch status to simulate the return data when the hook is loading from the cache. - const dataRef = useRef>([null as CachedValue, {status: 'loading'}]); + // Stores the previously result returned by the hook, containing the data from cache and the fetch status. + // We initialize it to `null` and `loading` fetch status to simulate the initial result when the hook is loading from the cache. + const resultRef = useRef>([null as CachedValue, {status: 'loading'}]); // Indicates if it's the first Onyx connection of this hook or not, as we don't want certain use cases // in `getSnapshot()` to be satisfied several times. @@ -96,45 +93,41 @@ function useOnyx>(key: TKey, }, [previousKey, key]); const getSnapshot = useCallback(() => { - // We get the data from the cache, supplying a selector too in case it's defined. - // If `newData` is `undefined` it means that the cache doesn't have a value for that key yet. - // If `newData` is `null` or any other value if means that the cache does have a value for that key. + // We get the value from the cache, supplying a selector too in case it's defined. + // If `newValue` is `undefined` it means that the cache doesn't have a value for that key yet. + // If `newValue` is `null` or any other value if means that the cache does have a value for that key. // This difference between `undefined` and other values is crucial and it's used to address the following // conditions and use cases. - let newData = getCachedValue(key, selectorRef.current); + let newValue = getCachedValue(key, selectorRef.current); // Since the fetch status can be different given the use cases below, we define the variable right away. let newFetchStatus: FetchStatus | undefined; - // If we have pending merge operations for the key during the first connection, we set data to `undefined` + // If we have pending merge operations for the key during the first connection, we set the new value to `undefined` // and fetch status to `loading` to simulate that it is still being loaded until we have the most updated data. - // If `allowStaleData` is `true` this logic will be ignored and cached data will be used, even if it's stale data. + // If `allowStaleData` is `true` this logic will be ignored and cached value will be used, even if it's stale data. if (isFirstConnectionRef.current && Onyx.hasPendingMergeForKey(key) && !options?.allowStaleData) { - newData = undefined; + newValue = undefined; newFetchStatus = 'loading'; } // If data is not present in cache (if it's `undefined`) and `initialValue` is set during the first connection, - // we set data to `initialValue` and fetch status to `loaded` since we already have some data to return to the consumer. - if (isFirstConnectionRef.current && newData === undefined && options?.initialValue !== undefined) { - newData = options?.initialValue as CachedValue; + // we set the new value to `initialValue` and fetch status to `loaded` since we already have some data to return to the consumer. + if (isFirstConnectionRef.current && newValue === undefined && options?.initialValue !== undefined) { + newValue = options?.initialValue as CachedValue; newFetchStatus = 'loaded'; } - // If the previously cached data is different from the new data, we update both cached data - // and the result data to be returned by the hook. - // We can't directly compare the value from `dataRef` with `newData` because we default `undefined` - // to `null` when setting `dataRef` value to ensure we have a consistent return, but we need to be able to differentiate - // between `undefined` and `null` during the comparison, so `cachedValueRef` is used to store the real value without changing - // to `null`. - if (!deepEqual(cachedValueRef.current, newData)) { - cachedValueRef.current = newData as CachedValue; - - // If the new data is `undefined` we default it to `null` to ensure the consumer get a consistent result from the hook. - dataRef.current = [(cachedValueRef.current ?? null) as CachedValue, {status: newFetchStatus ?? 'loaded'}]; + // If the previously cached value is different from the new value, we update both cached value + // and the result to be returned by the hook. + if (!deepEqual(cachedValueRef.current, newValue)) { + cachedValueRef.current = newValue as CachedValue; + + // If the new value is `undefined` we default it to `null` to ensure the consumer get a consistent result from the hook. + resultRef.current = [(cachedValueRef.current ?? null) as CachedValue, {status: newFetchStatus ?? 'loaded'}]; } - return dataRef.current; + return resultRef.current; }, [key, selectorRef, options?.allowStaleData, options?.initialValue]); const subscribe = useCallback( @@ -180,9 +173,11 @@ function useOnyx>(key: TKey, } }, [key, options?.canEvict]); - const data = useSyncExternalStore>(subscribe, getSnapshot); + const result = useSyncExternalStore>(subscribe, getSnapshot); - return data; + return result; } export default useOnyx; + +export type {UseOnyxResult}; From d74f7b5c2fd0586586d9c969aac172ce28e9573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Mar 2024 17:46:17 +0000 Subject: [PATCH 42/50] Add tests for stale data --- tests/unit/useOnyxTest.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index b488a347..e34515dd 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -252,6 +252,40 @@ describe('useOnyx', () => { }); describe('stale data', () => { - // TODO: Not sure if we'll be able to implement tests for these cases. + it('should return null and loading state while we have pending merges for the key, and then return updated value and loaded state', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); + + Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test4'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); + + expect(result.current[0]).toEqual(null); + expect(result.current[1].status).toEqual('loading'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test4'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return stale value and loaded state if allowStaleData is true, and then return updated value and loaded state', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); + + Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test4'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {allowStaleData: true})); + + expect(result.current[0]).toEqual('test1'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test4'); + expect(result.current[1].status).toEqual('loaded'); + }); }); }); From 4927f380bce401c744cd5ccc7a5fd36b9682b98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Mar 2024 19:32:58 +0000 Subject: [PATCH 43/50] Implement tests for initWithStoredValues and fix this logic in the hook --- lib/useOnyx.ts | 8 +++- tests/unit/useOnyxTest.ts | 82 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index e6b3aa2c..8e3ae66e 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -62,7 +62,13 @@ function useOnyx>(key: TKey // Stores the previously result returned by the hook, containing the data from cache and the fetch status. // We initialize it to `null` and `loading` fetch status to simulate the initial result when the hook is loading from the cache. - const resultRef = useRef>([null as CachedValue, {status: 'loading'}]); + // However, if `initWithStoredValues` is `true` we set the fetch status to `loaded` since we want to signal that data is ready. + const resultRef = useRef>([ + null as CachedValue, + { + status: options?.initWithStoredValues === false ? 'loaded' : 'loading', + }, + ]); // Indicates if it's the first Onyx connection of this hook or not, as we don't want certain use cases // in `getSnapshot()` to be satisfied several times. diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index e34515dd..be89f8a4 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -221,10 +221,14 @@ describe('useOnyx', () => { }); }); - describe('initial value', () => { + describe('initialValue', () => { it('should return initial value from non-cached key and then return null', async () => { - // @ts-expect-error bypass - const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initialValue: 'initial value'})); + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + // @ts-expect-error bypass + initialValue: 'initial value', + }), + ); expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); @@ -238,8 +242,12 @@ describe('useOnyx', () => { it('should return initial value from cached key and then return cached value', async () => { await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); - // @ts-expect-error bypass - const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initialValue: 'initial value'})); + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + // @ts-expect-error bypass + initialValue: 'initial value', + }), + ); expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); @@ -251,7 +259,7 @@ describe('useOnyx', () => { }); }); - describe('stale data', () => { + describe('allowStaleData', () => { it('should return null and loading state while we have pending merges for the key, and then return updated value and loaded state', async () => { Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); @@ -288,4 +296,66 @@ describe('useOnyx', () => { expect(result.current[1].status).toEqual('loaded'); }); }); + + describe('initWithStoredValues', () => { + it('should return null and loaded state, and after merge return updated value and loaded state', async () => { + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false})); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual(null); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + + expect(result.current[0]).toEqual('test2'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return initial value and loaded state, and after merge return updated value and loaded state', async () => { + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + initWithStoredValues: false, + // @ts-expect-error bypass + initialValue: 'initial value', + }), + ); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('initial value'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + + expect(result.current[0]).toEqual('test2'); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should return selected value and loaded state, and after merge return updated selected value and loaded state', async () => { + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + initWithStoredValues: false, + // @ts-expect-error bypass + selector: (value: OnyxEntry) => `${value}_selected`, + }), + ); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('undefined_selected'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + + expect(result.current[0]).toEqual('test2_selected'); + expect(result.current[1].status).toEqual('loaded'); + }); + }); }); From 96da9f0de9e9a227cf6edb7c908f3554ab534a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Mar 2024 19:52:54 +0000 Subject: [PATCH 44/50] Add another test for selector --- tests/unit/useOnyxTest.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index be89f8a4..24a61a2a 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -219,6 +219,29 @@ describe('useOnyx', () => { // must be the same reference expect(oldResult).toBe(result.current); }); + + it('should always use the current selector reference to return new data', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, {id: 'test_id', name: 'test_name'}); + + let selector = (entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name}`; + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + // @ts-expect-error bypass + selector, + }), + ); + + expect(result.current[0]).toEqual('id - test_id, name - test_name'); + expect(result.current[1].status).toEqual('loaded'); + + selector = (entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name} - selector changed`; + + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {id: 'changed_id', name: 'changed_name'})); + + expect(result.current[0]).toEqual('id - changed_id, name - changed_name - selector changed'); + expect(result.current[1].status).toEqual('loaded'); + }); }); describe('initialValue', () => { From 08ba5572168df731d47e259a03c2cca27d5c9ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 10:41:49 +0000 Subject: [PATCH 45/50] Implement additional test for stale data --- tests/unit/useOnyxTest.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 24a61a2a..3af20ced 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -301,6 +301,29 @@ describe('useOnyx', () => { expect(result.current[1].status).toEqual('loaded'); }); + it('should return initial value and loaded state while we have pending merges for the key, and then return updated value and loaded state', async () => { + Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); + + Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test3'); + Onyx.merge(ONYXKEYS.TEST_KEY, 'test4'); + + const {result} = renderHook(() => + useOnyx(ONYXKEYS.TEST_KEY, { + // @ts-expect-error bypass + initialValue: 'initial value', + }), + ); + + expect(result.current[0]).toEqual('initial value'); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual('test4'); + expect(result.current[1].status).toEqual('loaded'); + }); + it('should return stale value and loaded state if allowStaleData is true, and then return updated value and loaded state', async () => { Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); From a25b50f7f01c885f618568e07697a2702848d1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 11:42:38 +0000 Subject: [PATCH 46/50] Improve CachedValue type --- lib/useOnyx.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 8e3ae66e..dbb0f47e 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -1,5 +1,6 @@ import {deepEqual} from 'fast-equals'; import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react'; +import type {IsEqual} from 'type-fest'; import Onyx from './Onyx'; import type {CollectionKeyBase, OnyxCollection, OnyxKey, OnyxValue, Selector} from './types'; import useLiveRef from './useLiveRef'; @@ -37,7 +38,7 @@ type UseOnyxOptions = { type FetchStatus = 'loading' | 'loaded'; -type CachedValue = TValue extends OnyxValue ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; +type CachedValue = IsEqual> extends true ? TValue : TKey extends CollectionKeyBase ? NonNullable> : TValue; type ResultMetadata = { status: FetchStatus; @@ -104,7 +105,7 @@ function useOnyx>(key: TKey // If `newValue` is `null` or any other value if means that the cache does have a value for that key. // This difference between `undefined` and other values is crucial and it's used to address the following // conditions and use cases. - let newValue = getCachedValue(key, selectorRef.current); + let newValue = getCachedValue(key, selectorRef.current); // Since the fetch status can be different given the use cases below, we define the variable right away. let newFetchStatus: FetchStatus | undefined; @@ -127,7 +128,7 @@ function useOnyx>(key: TKey // If the previously cached value is different from the new value, we update both cached value // and the result to be returned by the hook. if (!deepEqual(cachedValueRef.current, newValue)) { - cachedValueRef.current = newValue as CachedValue; + cachedValueRef.current = newValue; // If the new value is `undefined` we default it to `null` to ensure the consumer get a consistent result from the hook. resultRef.current = [(cachedValueRef.current ?? null) as CachedValue, {status: newFetchStatus ?? 'loaded'}]; From daad8461c8cb397cab2e42512d6d35d4459d79fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 11:53:51 +0000 Subject: [PATCH 47/50] Improve initialValue type --- lib/useOnyx.ts | 2 +- tests/unit/useOnyxTest.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index dbb0f47e..f57be4a9 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -25,7 +25,7 @@ type UseOnyxOptions = { /** * This value will be returned by the hook on the first render while the data is being read from Onyx. */ - initialValue?: OnyxValue; + initialValue?: TReturnValue; /** * This will be used to subscribe to a subset of an Onyx key's data. diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 3af20ced..ed0d13e1 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -248,7 +248,6 @@ describe('useOnyx', () => { it('should return initial value from non-cached key and then return null', async () => { const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { - // @ts-expect-error bypass initialValue: 'initial value', }), ); @@ -267,7 +266,6 @@ describe('useOnyx', () => { const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { - // @ts-expect-error bypass initialValue: 'initial value', }), ); @@ -310,7 +308,6 @@ describe('useOnyx', () => { const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { - // @ts-expect-error bypass initialValue: 'initial value', }), ); @@ -364,9 +361,8 @@ describe('useOnyx', () => { await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => - useOnyx(ONYXKEYS.TEST_KEY, { + useOnyx>(ONYXKEYS.TEST_KEY, { initWithStoredValues: false, - // @ts-expect-error bypass initialValue: 'initial value', }), ); From fd00b7cd5abf2660d8f597509d6ea1a81bd908b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 11:55:24 +0000 Subject: [PATCH 48/50] Export FetchStatus type --- lib/index.d.ts | 3 ++- lib/useOnyx.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index fd7cca46..e69e736e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,7 +1,7 @@ import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx'; import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, OnyxValue} from './types'; import withOnyx from './withOnyx'; -import useOnyx, {UseOnyxResult} from './useOnyx'; +import useOnyx, {UseOnyxResult, FetchStatus} from './useOnyx'; export default Onyx; export { @@ -19,4 +19,5 @@ export { useOnyx, UseOnyxResult, OnyxValue, + FetchStatus, }; diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index f57be4a9..542cf683 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -187,4 +187,4 @@ function useOnyx>(key: TKey export default useOnyx; -export type {UseOnyxResult}; +export type {UseOnyxResult, FetchStatus}; From 555e6f12c4ef6dcd520593e8e33dc32e97fbb53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 13:22:21 +0000 Subject: [PATCH 49/50] Minor fix --- tests/unit/useOnyxTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index ed0d13e1..890a4b7a 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -361,7 +361,7 @@ describe('useOnyx', () => { await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => - useOnyx>(ONYXKEYS.TEST_KEY, { + useOnyx(ONYXKEYS.TEST_KEY, { initWithStoredValues: false, initialValue: 'initial value', }), From 3c25c3bc23eb2b9617b10a71d8c43100edc820b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 8 Mar 2024 17:15:32 +0000 Subject: [PATCH 50/50] Update docs --- API.md | 61 +++++++++++++++++++++++++++++------------------------ lib/Onyx.js | 2 +- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/API.md b/API.md index 02cc989d..098e461d 100644 --- a/API.md +++ b/API.md @@ -22,8 +22,15 @@ cause react to schedule the updates at once instead of after each other. This is and runs it through a reducer function to return a subset of the data according to a selector. The resulting collection will only contain items that are returned by the selector.

+
isCollectionKey(key)Boolean
+

Checks to see if the a subscriber's supplied key +is associated with a collection of keys.

+
isCollectionMemberKey(collectionKey, key)Boolean
+
splitCollectionMemberKey(key)Array.<String>
+

Splits a collection member key into the collection key part and the ID part.

+
tryGetCachedValue(key, mapping)Mixed

Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined. If the requested key is a collection, it will return an object with all the collection members.

@@ -34,7 +41,7 @@ If the requested key is a collection, it will return an object with all the coll
disconnect(connectionID, [keyToRemoveFromEvictionBlocklist])

Remove the listener for a react component

-
scheduleSubscriberUpdate(key, value, [canUpdateSubscriber])Promise
+
scheduleSubscriberUpdate(key, value, prevValue, [canUpdateSubscriber])Promise

Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).

scheduleNotifyCollectionSubscribers(key, value)Promise
@@ -90,13 +97,6 @@ value will be saved to storage after the default value.

setMemoryOnlyKeys(keyList)

When set these keys will not be persisted to storage

-
onClear(callback)
-

Sets the callback to be called when the clear finishes executing.

-
-
subscribeToEvents()
-

Subscribes to the Broadcast channel and executes actions based on the -types of events.

-
init([options])

Initialize the store with actions and listening for storage events

@@ -153,6 +153,18 @@ The resulting collection will only contain items that are returned by the select | selector | String \| function | (see method docs for getSubsetOfData() for full details) | | [withOnyxInstanceState] | Object | | + + +## isCollectionKey(key) ⇒ Boolean +Checks to see if the a subscriber's supplied key +is associated with a collection of keys. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| key | String | + ## isCollectionMemberKey(collectionKey, key) ⇒ Boolean @@ -163,6 +175,18 @@ The resulting collection will only contain items that are returned by the select | collectionKey | String | | key | String | + + +## splitCollectionMemberKey(key) ⇒ Array.<String> +Splits a collection member key into the collection key part and the ID part. + +**Kind**: global function +**Returns**: Array.<String> - A tuple where the first element is the collection part and the second element is the ID part. + +| Param | Type | Description | +| --- | --- | --- | +| key | String | The collection member key to split. | + ## tryGetCachedValue(key, mapping) ⇒ Mixed @@ -221,7 +245,7 @@ Onyx.disconnect(connectionID); ``` -## scheduleSubscriberUpdate(key, value, [canUpdateSubscriber]) ⇒ Promise +## scheduleSubscriberUpdate(key, value, prevValue, [canUpdateSubscriber]) ⇒ Promise Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately). **Kind**: global function @@ -230,6 +254,7 @@ Schedules an update that will be appended to the macro task queue (so it doesn't | --- | --- | --- | | key | String | | | value | \* | | +| prevValue | \* | | | [canUpdateSubscriber] | function | only subscribers that pass this truth test will be updated | **Example** @@ -410,24 +435,6 @@ When set these keys will not be persisted to storage | --- | --- | | keyList | Array.<string> | - - -## onClear(callback) -Sets the callback to be called when the clear finishes executing. - -**Kind**: global function - -| Param | Type | -| --- | --- | -| callback | function | - - - -## subscribeToEvents() -Subscribes to the Broadcast channel and executes actions based on the -types of events. - -**Kind**: global function ## init([options]) diff --git a/lib/Onyx.js b/lib/Onyx.js index 395882d9..1fa40ebc 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -215,7 +215,7 @@ function isCollectionMemberKey(collectionKey, key) { /** * Splits a collection member key into the collection key part and the ID part. * @param {String} key - The collection member key to split. - * @returns {[String, String]} A tuple where the first element is the collection part and the second element is the ID part. + * @returns {Array} A tuple where the first element is the collection part and the second element is the ID part. */ function splitCollectionMemberKey(key) { const underscoreIndex = key.indexOf('_');