From 31dac488fec40876ee0ac16ce763f8d5a74983ee Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Mar 2024 17:46:15 +0100 Subject: [PATCH 01/16] Prepare remaining files for Onyx.js migration --- lib/OnyxCache.ts | 36 ++++++++++++-------------- lib/PerformanceUtils.ts | 9 +++---- lib/storage/__mocks__/index.ts | 10 +++---- lib/storage/providers/IDBKeyVal.ts | 8 +++--- lib/storage/providers/SQLiteStorage.ts | 3 --- lib/storage/providers/types.ts | 24 +++++++---------- lib/{types.d.ts => types.ts} | 10 ++++--- 7 files changed, 42 insertions(+), 58 deletions(-) rename lib/{types.d.ts => types.ts} (94%) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 77eb2b4b3..29351168b 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -1,9 +1,7 @@ import {deepEqual} from 'fast-equals'; import bindAll from 'lodash/bindAll'; -import type {Key, Value} from './storage/providers/types'; import utils from './utils'; - -type StorageMap = Record; +import type {OnyxKey, OnyxValue} from './types'; /** * In memory cache providing data by reference @@ -11,19 +9,19 @@ type StorageMap = Record; */ class OnyxCache { /** Cache of all the storage keys available in persistent storage */ - private storageKeys: Set; + storageKeys: Set; /** Unique list of keys maintained in access order (most recent at the end) */ - private recentKeys: Set; + private recentKeys: Set; /** A map of cached values */ - private storageMap: StorageMap; + private storageMap: Record; /** * Captured pending tasks for already running storage methods * Using a map yields better performance on operations such a delete */ - private pendingPromises: Map>; + private pendingPromises: Map>; /** Maximum size of the keys store din cache */ private maxRecentKeysSize = 0; @@ -54,7 +52,7 @@ class OnyxCache { } /** Get all the storage keys */ - getAllKeys(): Key[] { + getAllKeys(): OnyxKey[] { return Array.from(this.storageKeys); } @@ -62,7 +60,7 @@ class OnyxCache { * Get a cached value from storage * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. */ - getValue(key: Key, shouldReindexCache = true): Value { + getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { if (shouldReindexCache) { this.addToAccessedKeys(key); } @@ -70,14 +68,14 @@ class OnyxCache { } /** Check whether cache has data for the given key */ - hasCacheForKey(key: Key): boolean { + hasCacheForKey(key: OnyxKey): boolean { return this.storageMap[key] !== undefined; } /** Saves a key in the storage keys list * Serves to keep the result of `getAllKeys` up to date */ - addKey(key: Key): void { + addKey(key: OnyxKey): void { this.storageKeys.add(key); } @@ -85,7 +83,7 @@ class OnyxCache { * Set's a key value in cache * Adds the key to the storage keys list as well */ - set(key: Key, value: Value): Value { + set(key: OnyxKey, value: OnyxValue): OnyxValue { this.addKey(key); this.addToAccessedKeys(key); this.storageMap[key] = value; @@ -94,7 +92,7 @@ class OnyxCache { } /** Forget the cached value for the given key */ - drop(key: Key): void { + drop(key: OnyxKey): void { delete this.storageMap[key]; this.storageKeys.delete(key); this.recentKeys.delete(key); @@ -104,7 +102,7 @@ class OnyxCache { * Deep merge data to cache, any non existing keys will be created * @param data - a map of (cache) key - values */ - merge(data: StorageMap): void { + merge(data: Record): void { if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs'); } @@ -146,8 +144,8 @@ class OnyxCache { * provided from this function * @param taskName - unique name given for the task */ - getTaskPromise(taskName: string): Promise | undefined { - return this.pendingPromises.get(taskName); + getTaskPromise(taskName: string): Promise { + return this.pendingPromises.get(taskName) as Promise; } /** @@ -155,7 +153,7 @@ class OnyxCache { * hook up to the promise if it's still pending * @param taskName - unique name for the task */ - captureTask(taskName: string, promise: Promise): Promise { + captureTask(taskName: string, promise: Promise): Promise { const returnPromise = promise.finally(() => { this.pendingPromises.delete(taskName); }); @@ -166,7 +164,7 @@ class OnyxCache { } /** Adds a key to the top of the recently accessed keys */ - private addToAccessedKeys(key: Key): void { + addToAccessedKeys(key: OnyxKey): void { this.recentKeys.delete(key); this.recentKeys.add(key); } @@ -198,7 +196,7 @@ class OnyxCache { } /** Check if the value has changed */ - hasValueChanged(key: Key, value: Value): boolean { + hasValueChanged(key: OnyxKey, value: OnyxValue): boolean { return !deepEqual(this.storageMap[key], value); } } diff --git a/lib/PerformanceUtils.ts b/lib/PerformanceUtils.ts index b378eb283..832aaa2a6 100644 --- a/lib/PerformanceUtils.ts +++ b/lib/PerformanceUtils.ts @@ -1,5 +1,7 @@ import lodashTransform from 'lodash/transform'; import {deepEqual} from 'fast-equals'; +import type {Mapping} from './Onyx'; +import type {OnyxKey} from './types'; type UnknownObject = Record; @@ -10,11 +12,6 @@ type LogParams = { newValue?: unknown; }; -type Mapping = Record & { - key: string; - displayName: string; -}; - let debugSetState = false; function setShouldDebugSetState(debug: boolean) { @@ -44,7 +41,7 @@ function diffObject( /** * Provide insights into why a setState() call occurred by diffing the before and after values. */ -function logSetStateCall(mapping: Mapping, previousValue: unknown, newValue: unknown, caller: string, keyThatChanged: string) { +function logSetStateCall(mapping: Mapping, previousValue: unknown, newValue: unknown, caller: string, keyThatChanged?: string) { if (!debugSetState) { return; } diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index 3e6f156b5..81425418b 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -1,8 +1,9 @@ +import type {OnyxKey, OnyxValue} from '../../types'; import utils from '../../utils'; -import type {Key, KeyValuePairList, Value} from '../providers/types'; +import type {KeyValuePairList} from '../providers/types'; import type StorageProvider from '../providers/types'; -let storageMapInternal: Record = {}; +let storageMapInternal: Record = {}; const set = jest.fn((key, value) => { storageMapInternal[key] = value; @@ -35,7 +36,7 @@ const idbKeyvalMock: StorageProvider = { pairs.forEach(([key, value]) => { const existingValue = storageMapInternal[key]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = utils.fastMerge(existingValue as any, value); + const newValue = utils.fastMerge(existingValue as any, value as any); set(key, newValue); }); @@ -65,8 +66,6 @@ const idbKeyvalMock: StorageProvider = { getDatabaseSize() { return Promise.resolve({bytesRemaining: 0, bytesUsed: 99999}); }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setMemoryOnlyKeys() {}, }; const idbKeyvalMockSpy = { @@ -86,7 +85,6 @@ const idbKeyvalMockSpy = { storageMapInternal = data; }), getDatabaseSize: jest.fn(idbKeyvalMock.getDatabaseSize), - setMemoryOnlyKeys: jest.fn(idbKeyvalMock.setMemoryOnlyKeys), }; export default idbKeyvalMockSpy; diff --git a/lib/storage/providers/IDBKeyVal.ts b/lib/storage/providers/IDBKeyVal.ts index bc429bc26..fa3562e71 100644 --- a/lib/storage/providers/IDBKeyVal.ts +++ b/lib/storage/providers/IDBKeyVal.ts @@ -2,7 +2,7 @@ import type {UseStore} from 'idb-keyval'; import {set, keys, getMany, setMany, get, clear, del, delMany, createStore, promisifyRequest} from 'idb-keyval'; import utils from '../../utils'; import type StorageProvider from './types'; -import type {Value} from './types'; +import type {OnyxValue} from '../../types'; // We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB // which might not be available in certain environments that load the bundle (e.g. electron main process). @@ -21,13 +21,13 @@ const provider: StorageProvider = { getCustomStore()('readwrite', (store) => { // Note: we are using the manual store transaction here, to fit the read and update // of the items in one transaction to achieve best performance. - const getValues = Promise.all(pairs.map(([key]) => promisifyRequest(store.get(key)))); + const getValues = Promise.all(pairs.map(([key]) => promisifyRequest(store.get(key)))); return getValues.then((values) => { const upsertMany = pairs.map(([key, value], index) => { const prev = values[index]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = utils.fastMerge(prev as any, value); + const newValue = utils.fastMerge(prev as any, value as any); return promisifyRequest(store.put(newValue, key)); }); return Promise.all(upsertMany); @@ -39,8 +39,6 @@ const provider: StorageProvider = { }, multiSet: (pairs) => setMany(pairs, getCustomStore()), clear: () => clear(getCustomStore()), - // eslint-disable-next-line @typescript-eslint/no-empty-function - setMemoryOnlyKeys: () => {}, getAllKeys: () => keys(getCustomStore()), getItem: (key) => get(key, getCustomStore()) diff --git a/lib/storage/providers/SQLiteStorage.ts b/lib/storage/providers/SQLiteStorage.ts index 6011277bc..36eb36227 100644 --- a/lib/storage/providers/SQLiteStorage.ts +++ b/lib/storage/providers/SQLiteStorage.ts @@ -93,9 +93,6 @@ const provider: StorageProvider = { }); }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setMemoryOnlyKeys: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function keepInstancesSync: () => {}, }; diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index 3f8113729..e4723036f 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -1,18 +1,17 @@ import type {BatchQueryResult, QueryResult} from 'react-native-quick-sqlite'; +import type {OnyxKey, OnyxValue} from '../../types'; -type Key = string; -type Value = IDBValidKey; -type KeyValuePair = [Key, Value]; -type KeyList = Key[]; +type KeyValuePair = [OnyxKey, OnyxValue]; +type KeyList = OnyxKey[]; type KeyValuePairList = KeyValuePair[]; -type OnStorageKeyChanged = (key: Key, value: Value | null) => void; +type OnStorageKeyChanged = (key: OnyxKey, value: OnyxValue | null) => void; type StorageProvider = { /** * Gets the value of a given key or return `null` if it's not available in storage */ - getItem: (key: Key) => Promise; + getItem: (key: OnyxKey) => Promise; /** * Get multiple key-value pairs for the given array of keys in a batch @@ -22,7 +21,7 @@ type StorageProvider = { /** * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string */ - setItem: (key: Key, value: Value) => Promise; + setItem: (key: OnyxKey, value: OnyxValue) => Promise; /** * Stores multiple key-value pairs in a batch @@ -39,7 +38,7 @@ type StorageProvider = { * @param changes - the delta for a specific key * @param modifiedData - the pre-merged data from `Onyx.applyMerge` */ - mergeItem: (key: Key, changes: Value, modifiedData: Value) => Promise; + mergeItem: (key: OnyxKey, changes: OnyxValue, modifiedData: OnyxValue) => Promise; /** * Returns all keys available in storage @@ -49,7 +48,7 @@ type StorageProvider = { /** * Removes given key and its value from storage */ - removeItem: (key: Key) => Promise; + removeItem: (key: OnyxKey) => Promise; /** * Removes given keys and their values from storage @@ -61,11 +60,6 @@ type StorageProvider = { */ clear: () => Promise; - /** - * Sets memory only keys - */ - setMemoryOnlyKeys: () => void; - /** * Gets the total bytes of the database file */ @@ -78,4 +72,4 @@ type StorageProvider = { }; export default StorageProvider; -export type {Value, Key, KeyList, KeyValuePairList}; +export type {KeyList, KeyValuePair, KeyValuePairList, OnStorageKeyChanged}; diff --git a/lib/types.d.ts b/lib/types.ts similarity index 94% rename from lib/types.d.ts rename to lib/types.ts index a210c349d..00e52a42a 100644 --- a/lib/types.d.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ -import {Merge} from 'type-fest'; -import {BuiltIns} from 'type-fest/source/internal'; +import type {Merge} from 'type-fest'; +import type {BuiltIns} from 'type-fest/source/internal'; /** * Represents a deeply nested record. It maps keys to values, @@ -76,6 +76,7 @@ type TypeOptions = Merge< * } * ``` */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface interface CustomTypeOptions {} /** @@ -102,7 +103,7 @@ 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; +type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; /** * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys @@ -193,6 +194,7 @@ type ExtractOnyxCollectionValue = TOnyxCollection extends NonNu type NonTransformableTypes = | BuiltIns + // eslint-disable-next-line @typescript-eslint/no-explicit-any | ((...args: any[]) => unknown) | Map | Set @@ -234,7 +236,7 @@ type NullishObjectDeep = { */ type WithOnyxInstanceState = (TOnyxProps & {loading: boolean}) | undefined; -export { +export type { CollectionKey, CollectionKeyBase, CustomTypeOptions, From e8bcecfcc1b15809d96989f90ac7b8d1e5a286a1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Mar 2024 17:49:00 +0100 Subject: [PATCH 02/16] Bring back setMemoryKeys --- lib/storage/__mocks__/index.ts | 3 +++ lib/storage/providers/IDBKeyVal.ts | 2 ++ lib/storage/providers/SQLiteStorage.ts | 3 +++ lib/storage/providers/types.ts | 5 +++++ 4 files changed, 13 insertions(+) diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index 81425418b..60e7c221e 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -66,6 +66,8 @@ const idbKeyvalMock: StorageProvider = { getDatabaseSize() { return Promise.resolve({bytesRemaining: 0, bytesUsed: 99999}); }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setMemoryOnlyKeys() {}, }; const idbKeyvalMockSpy = { @@ -85,6 +87,7 @@ const idbKeyvalMockSpy = { storageMapInternal = data; }), getDatabaseSize: jest.fn(idbKeyvalMock.getDatabaseSize), + setMemoryOnlyKeys: jest.fn(idbKeyvalMock.setMemoryOnlyKeys), }; export default idbKeyvalMockSpy; diff --git a/lib/storage/providers/IDBKeyVal.ts b/lib/storage/providers/IDBKeyVal.ts index fa3562e71..13d4cf7dd 100644 --- a/lib/storage/providers/IDBKeyVal.ts +++ b/lib/storage/providers/IDBKeyVal.ts @@ -38,6 +38,8 @@ const provider: StorageProvider = { return provider.setItem(key, modifiedData); }, multiSet: (pairs) => setMany(pairs, getCustomStore()), + // eslint-disable-next-line @typescript-eslint/no-empty-function + setMemoryOnlyKeys: () => {}, clear: () => clear(getCustomStore()), getAllKeys: () => keys(getCustomStore()), getItem: (key) => diff --git a/lib/storage/providers/SQLiteStorage.ts b/lib/storage/providers/SQLiteStorage.ts index 36eb36227..6011277bc 100644 --- a/lib/storage/providers/SQLiteStorage.ts +++ b/lib/storage/providers/SQLiteStorage.ts @@ -93,6 +93,9 @@ const provider: StorageProvider = { }); }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setMemoryOnlyKeys: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function keepInstancesSync: () => {}, }; diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index e4723036f..48f5112be 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -60,6 +60,11 @@ type StorageProvider = { */ clear: () => Promise; + /** + * Sets memory only keys + */ + setMemoryOnlyKeys: () => void; + /** * Gets the total bytes of the database file */ From 174471cc655e3eeec2257c1eab3f658413c1ffc8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Mar 2024 18:00:22 +0100 Subject: [PATCH 03/16] Minor types improvements --- lib/storage/__mocks__/index.ts | 3 +-- lib/storage/providers/IDBKeyVal.ts | 5 ++--- lib/storage/providers/types.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index 60e7c221e..e027ff93c 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -35,8 +35,7 @@ const idbKeyvalMock: StorageProvider = { multiMerge(pairs) { pairs.forEach(([key, value]) => { const existingValue = storageMapInternal[key]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = utils.fastMerge(existingValue as any, value as any); + const newValue = utils.fastMerge(existingValue!, value!); set(key, newValue); }); diff --git a/lib/storage/providers/IDBKeyVal.ts b/lib/storage/providers/IDBKeyVal.ts index 13d4cf7dd..29277dc30 100644 --- a/lib/storage/providers/IDBKeyVal.ts +++ b/lib/storage/providers/IDBKeyVal.ts @@ -26,8 +26,7 @@ const provider: StorageProvider = { return getValues.then((values) => { const upsertMany = pairs.map(([key, value], index) => { const prev = values[index]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = utils.fastMerge(prev as any, value as any); + const newValue = utils.fastMerge(prev!, value!); return promisifyRequest(store.put(newValue, key)); }); return Promise.all(upsertMany); @@ -38,9 +37,9 @@ const provider: StorageProvider = { return provider.setItem(key, modifiedData); }, multiSet: (pairs) => setMany(pairs, getCustomStore()), + clear: () => clear(getCustomStore()), // eslint-disable-next-line @typescript-eslint/no-empty-function setMemoryOnlyKeys: () => {}, - clear: () => clear(getCustomStore()), getAllKeys: () => keys(getCustomStore()), getItem: (key) => get(key, getCustomStore()) diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index 48f5112be..4a0e2df10 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -77,4 +77,4 @@ type StorageProvider = { }; export default StorageProvider; -export type {KeyList, KeyValuePair, KeyValuePairList, OnStorageKeyChanged}; +export type {KeyList, KeyValuePair, KeyValuePairList}; From aa3dbbe905b1a28077840f705a6f4f368fb8ac91 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Mar 2024 18:04:40 +0100 Subject: [PATCH 04/16] Final code adjustements --- lib/OnyxCache.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 29351168b..e7697c6a2 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -21,7 +21,7 @@ class OnyxCache { * Captured pending tasks for already running storage methods * Using a map yields better performance on operations such a delete */ - private pendingPromises: Map>; + private pendingPromises: Map>; /** Maximum size of the keys store din cache */ private maxRecentKeysSize = 0; @@ -126,7 +126,7 @@ class OnyxCache { * * @param keys - an array of keys */ - setAllKeys(keys: Key[]) { + setAllKeys(keys: OnyxKey[]) { this.storageKeys = new Set(keys); } @@ -145,7 +145,13 @@ class OnyxCache { * @param taskName - unique name given for the task */ getTaskPromise(taskName: string): Promise { - return this.pendingPromises.get(taskName) as Promise; + const promise = this.pendingPromises.get(taskName); + + if (!promise) { + throw new Error(`No pending task found for ${taskName}`); + } + + return promise; } /** From 24baecdf958b8282c85e0a598ff17bb3b58f56b9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Mar 2024 18:22:29 +0100 Subject: [PATCH 05/16] Fix tests and use existing type --- lib/Onyx.d.ts | 1 + lib/OnyxCache.ts | 6 +----- lib/PerformanceUtils.ts | 4 ++-- lib/storage/WebStorage.ts | 5 +++-- tests/unit/useOnyxTest.ts | 26 +++++++++++++------------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index 8bf30964f..3caa12194 100644 --- a/lib/Onyx.d.ts +++ b/lib/Onyx.d.ts @@ -18,6 +18,7 @@ type BaseConnectOptions = { statePropertyName?: string; withOnyxInstance?: Component; initWithStoredValues?: boolean; + displayName?: string; }; type TryGetCachedValueMapping = { diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index e7697c6a2..3349a8b65 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -144,13 +144,9 @@ class OnyxCache { * provided from this function * @param taskName - unique name given for the task */ - getTaskPromise(taskName: string): Promise { + getTaskPromise(taskName: string): Promise | undefined { const promise = this.pendingPromises.get(taskName); - if (!promise) { - throw new Error(`No pending task found for ${taskName}`); - } - return promise; } diff --git a/lib/PerformanceUtils.ts b/lib/PerformanceUtils.ts index 832aaa2a6..d3ee9456e 100644 --- a/lib/PerformanceUtils.ts +++ b/lib/PerformanceUtils.ts @@ -1,7 +1,7 @@ import lodashTransform from 'lodash/transform'; import {deepEqual} from 'fast-equals'; -import type {Mapping} from './Onyx'; import type {OnyxKey} from './types'; +import type {ConnectOptions} from './Onyx'; type UnknownObject = Record; @@ -41,7 +41,7 @@ function diffObject( /** * Provide insights into why a setState() call occurred by diffing the before and after values. */ -function logSetStateCall(mapping: Mapping, previousValue: unknown, newValue: unknown, caller: string, keyThatChanged?: string) { +function logSetStateCall(mapping: ConnectOptions, previousValue: unknown, newValue: unknown, caller: string, keyThatChanged?: string) { if (!debugSetState) { return; } diff --git a/lib/storage/WebStorage.ts b/lib/storage/WebStorage.ts index 439215b90..6621a084b 100644 --- a/lib/storage/WebStorage.ts +++ b/lib/storage/WebStorage.ts @@ -3,8 +3,9 @@ * when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when * data changes and then stay up-to-date with everything happening in Onyx. */ +import type {OnyxKey} from '../types'; import Storage from './providers/IDBKeyVal'; -import type {KeyList, Key} from './providers/types'; +import type {KeyList} from './providers/types'; import type StorageProvider from './providers/types'; const SYNC_ONYX = 'SYNC_ONYX'; @@ -12,7 +13,7 @@ const SYNC_ONYX = 'SYNC_ONYX'; /** * Raise an event thorough `localStorage` to let other tabs know a value changed */ -function raiseStorageSyncEvent(onyxKey: Key) { +function raiseStorageSyncEvent(onyxKey: OnyxKey) { global.localStorage.setItem(SYNC_ONYX, onyxKey); global.localStorage.removeItem(SYNC_ONYX); } diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 890a4b7ad..eaf55c957 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -98,7 +98,7 @@ describe('useOnyx', () => { }); 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'); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test'}); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); @@ -107,7 +107,7 @@ describe('useOnyx', () => { await act(async () => waitForPromisesToResolve()); - expect(result.current[0]).toEqual('test'); + expect(result.current[0]).toEqual({test: 'test'}); expect(result.current[1].status).toEqual('loaded'); }); @@ -262,7 +262,7 @@ describe('useOnyx', () => { }); it('should return initial value from cached key and then return cached value', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test'}); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { @@ -275,7 +275,7 @@ describe('useOnyx', () => { await act(async () => waitForPromisesToResolve()); - expect(result.current[0]).toEqual('test'); + expect(result.current[0]).toEqual({test: 'test'}); expect(result.current[1].status).toEqual('loaded'); }); }); @@ -342,7 +342,7 @@ describe('useOnyx', () => { 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'); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test1'}); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false})); @@ -351,14 +351,14 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual(null); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); - expect(result.current[0]).toEqual('test2'); + expect(result.current[0]).toEqual({test: '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'); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test1'}); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { @@ -372,20 +372,20 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); - expect(result.current[0]).toEqual('test2'); + expect(result.current[0]).toEqual({test: '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'); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test1'}); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { initWithStoredValues: false, // @ts-expect-error bypass - selector: (value: OnyxEntry) => `${value}_selected`, + selector: (value: OnyxEntry<{test: string}>) => `${value?.test}_selected`, }), ); @@ -394,7 +394,7 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('undefined_selected'); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); expect(result.current[0]).toEqual('test2_selected'); expect(result.current[1].status).toEqual('loaded'); From 4b5b0e31586a26c815be8819bdad07d9c929de57 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 12 Mar 2024 13:17:23 +0100 Subject: [PATCH 06/16] Remove unnecessary code --- lib/OnyxCache.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 3349a8b65..7bc0a6590 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -145,9 +145,7 @@ class OnyxCache { * @param taskName - unique name given for the task */ getTaskPromise(taskName: string): Promise | undefined { - const promise = this.pendingPromises.get(taskName); - - return promise; + return this.pendingPromises.get(taskName); } /** From 82b7af319c69704ba2db8fdd5a4695cd28254820 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 13:08:57 +0100 Subject: [PATCH 07/16] Add changes after Fabio review --- lib/DevTools.ts | 2 +- lib/index.ts | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/DevTools.ts b/lib/DevTools.ts index 76be784e9..21f572e1c 100644 --- a/lib/DevTools.ts +++ b/lib/DevTools.ts @@ -44,7 +44,7 @@ class DevTools { if ((options && options.remote) || typeof window === 'undefined' || !reduxDevtools) { return; } - // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-explicit-any + return reduxDevtools.connect(options); } catch (e) { console.error(ERROR_LABEL, e); diff --git a/lib/index.ts b/lib/index.ts index b16537f56..58c423ff3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,9 +1,24 @@ import Onyx from './Onyx'; import type {OnyxUpdate, ConnectOptions} from './Onyx'; -import type {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState} from './types'; +import type {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState, OnyxValue} from './types'; +import type {UseOnyxResult, FetchStatus} from './useOnyx'; import useOnyx from './useOnyx'; import withOnyx from './withOnyx'; export default Onyx; export {withOnyx, useOnyx}; -export type {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector, WithOnyxInstanceState}; +export type { + CustomTypeOptions, + OnyxCollection, + OnyxEntry, + OnyxUpdate, + ConnectOptions, + NullishDeep, + KeyValueMapping, + OnyxKey, + Selector, + WithOnyxInstanceState, + UseOnyxResult, + OnyxValue, + FetchStatus, +}; From 4ec36480e0d4a33da00404d23214fd4561056bca Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:42:57 +0100 Subject: [PATCH 08/16] Add generic types for StorageProvider --- lib/storage/providers/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index 4a0e2df10..203ff3e4f 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -1,17 +1,17 @@ import type {BatchQueryResult, QueryResult} from 'react-native-quick-sqlite'; import type {OnyxKey, OnyxValue} from '../../types'; -type KeyValuePair = [OnyxKey, OnyxValue]; +type KeyValuePair = [OnyxKey, OnyxValue]; type KeyList = OnyxKey[]; type KeyValuePairList = KeyValuePair[]; -type OnStorageKeyChanged = (key: OnyxKey, value: OnyxValue | null) => void; +type OnStorageKeyChanged = (key: TKey, value: OnyxValue | null) => void; type StorageProvider = { /** * Gets the value of a given key or return `null` if it's not available in storage */ - getItem: (key: OnyxKey) => Promise; + getItem: (key: TKey) => Promise | null>; /** * Get multiple key-value pairs for the given array of keys in a batch @@ -21,7 +21,7 @@ type StorageProvider = { /** * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string */ - setItem: (key: OnyxKey, value: OnyxValue) => Promise; + setItem: (key: TKey, value: OnyxValue) => Promise; /** * Stores multiple key-value pairs in a batch @@ -38,7 +38,7 @@ type StorageProvider = { * @param changes - the delta for a specific key * @param modifiedData - the pre-merged data from `Onyx.applyMerge` */ - mergeItem: (key: OnyxKey, changes: OnyxValue, modifiedData: OnyxValue) => Promise; + mergeItem: (key: TKey, changes: OnyxValue, modifiedData: OnyxValue) => Promise; /** * Returns all keys available in storage From f5d9ed9dac34972ae6250d89a37ce33b4234b5b2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:43:11 +0100 Subject: [PATCH 09/16] Remove public keyword --- lib/DevTools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DevTools.ts b/lib/DevTools.ts index 21f572e1c..22b884c1b 100644 --- a/lib/DevTools.ts +++ b/lib/DevTools.ts @@ -90,7 +90,7 @@ class DevTools { /** * This clears the internal state of the DevTools, preserving the keys included in `keysToPreserve` */ - public clearState(keysToPreserve: string[] = []): void { + clearState(keysToPreserve: string[] = []): void { const newState = Object.entries(this.state).reduce((obj: Record, [key, value]) => { // eslint-disable-next-line no-param-reassign obj[key] = keysToPreserve.includes(key) ? value : this.defaultState[key]; From dd46d8699e04fec706254ef7a0cf089074e958db Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:43:35 +0100 Subject: [PATCH 10/16] Adjust OnyxValue types in OnyxCache --- lib/OnyxCache.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index 65d9662b5..f139b0410 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -15,13 +15,13 @@ class OnyxCache { private recentKeys: Set; /** A map of cached values */ - private storageMap: Record; + private storageMap: Record>; /** * Captured pending tasks for already running storage methods * Using a map yields better performance on operations such a delete */ - private pendingPromises: Map>; + private pendingPromises: Map | OnyxKey[]>>; /** Maximum size of the keys store din cache */ private maxRecentKeysSize = 0; @@ -60,7 +60,7 @@ class OnyxCache { * Get a cached value from storage * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. */ - getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { + getValue(key: OnyxKey, shouldReindexCache = true): OnyxValue { if (shouldReindexCache) { this.addToAccessedKeys(key); } @@ -83,7 +83,7 @@ class OnyxCache { * Set's a key value in cache * Adds the key to the storage keys list as well */ - set(key: OnyxKey, value: OnyxValue): OnyxValue { + set(key: OnyxKey, value: OnyxValue): OnyxValue { this.addKey(key); this.addToAccessedKeys(key); this.storageMap[key] = value; @@ -102,7 +102,7 @@ class OnyxCache { * Deep merge data to cache, any non existing keys will be created * @param data - a map of (cache) key - values */ - merge(data: Record): void { + merge(data: Record>): void { if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs'); } @@ -144,7 +144,7 @@ class OnyxCache { * provided from this function * @param taskName - unique name given for the task */ - getTaskPromise(taskName: string): Promise | undefined { + getTaskPromise(taskName: string): Promise | OnyxKey[]> | undefined { return this.pendingPromises.get(taskName); } @@ -153,7 +153,7 @@ class OnyxCache { * hook up to the promise if it's still pending * @param taskName - unique name for the task */ - captureTask(taskName: string, promise: Promise): Promise { + captureTask(taskName: string, promise: Promise>): Promise> { const returnPromise = promise.finally(() => { this.pendingPromises.delete(taskName); }); @@ -196,7 +196,7 @@ class OnyxCache { } /** Check if the value has changed */ - hasValueChanged(key: OnyxKey, value: OnyxValue): boolean { + hasValueChanged(key: OnyxKey, value: OnyxValue): boolean { return !deepEqual(this.storageMap[key], value); } } From d936372b5408b551974d6c214bd7083473a06fdc Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:43:52 +0100 Subject: [PATCH 11/16] Remove default value in OnyxValue --- lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 00e52a42a..ddbbbf782 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -103,7 +103,7 @@ 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; +type OnyxValue = TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; /** * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys From b00e03611bbc85bdb610b4d02a8a6636d7e4cb02 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:44:16 +0100 Subject: [PATCH 12/16] Adjust utils, provde better typings --- lib/utils.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 56e8d7a48..857791384 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import type {OnyxKey} from './types'; +import type {OnyxEntry, OnyxKey, OnyxValue} from './types'; type EmptyObject = Record; type EmptyValue = EmptyObject | null | undefined; @@ -14,7 +14,7 @@ function isEmptyObject(obj: T | EmptyValue): obj is EmptyValue { /** * Checks whether the given value can be merged. It has to be an object, but not an array, RegExp or Date. */ -function isMergeableObject(value: unknown): boolean { +function isMergeableObject(value: unknown): value is Record { const nonNullObject = value != null ? typeof value === 'object' : false; return nonNullObject && Object.prototype.toString.call(value) !== '[object RegExp]' && Object.prototype.toString.call(value) !== '[object Date]' && !Array.isArray(value); } @@ -26,15 +26,15 @@ function isMergeableObject(value: unknown): boolean { * @param shouldRemoveNullObjectValues - If true, null object values will be removed. * @returns - The merged object. */ -function mergeObject>(target: TTarget, source: TTarget, shouldRemoveNullObjectValues = true): TTarget { +function mergeObject>(target: OnyxEntry, source: TValue, shouldRemoveNullObjectValues = true): TValue { const destination: Record = {}; if (isMergeableObject(target)) { // lodash adds a small overhead so we don't use it here const targetKeys = Object.keys(target); for (let i = 0; i < targetKeys.length; ++i) { const key = targetKeys[i]; - const sourceValue = source?.[key as keyof TTarget]; - const targetValue = target?.[key as keyof TTarget]; + const sourceValue = source?.[key]; + const targetValue = target?.[key]; // If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object const isSourceOrTargetNull = targetValue === null || sourceValue === null; @@ -46,11 +46,11 @@ function mergeObject>(target } } - const sourceKeys = Object.keys(source); + const sourceKeys = Object.keys(source!); for (let i = 0; i < sourceKeys.length; ++i) { const key = sourceKeys[i]; - const sourceValue = source?.[key as keyof TTarget]; - const targetValue = target?.[key as keyof TTarget]; + const sourceValue = source?.[key]; + const targetValue = target?.[key]; // If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object const shouldOmitSourceKey = shouldRemoveNullObjectValues && sourceValue === null; @@ -63,14 +63,14 @@ function mergeObject>(target if (isSourceKeyMergable && targetValue) { // eslint-disable-next-line no-use-before-define - destination[key] = fastMerge(targetValue, sourceValue as typeof targetValue, shouldRemoveNullObjectValues); + destination[key] = fastMerge(targetValue as OnyxValue, sourceValue, shouldRemoveNullObjectValues); } else if (!shouldRemoveNullObjectValues || sourceValue !== null) { destination[key] = sourceValue; } } } - return destination as TTarget; + return destination as TValue; } /** @@ -80,7 +80,7 @@ function mergeObject>(target * On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values. * To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations. */ -function fastMerge>(target: TTarget, source: TTarget, shouldRemoveNullObjectValues = true): TTarget { +function fastMerge>(target: TValue, source: TValue, shouldRemoveNullObjectValues = true): TValue { // We have to ignore arrays and nullish values here, // otherwise "mergeObject" will throw an error, // because it expects an object as "source" From 66456065e8f87e37f1219b7d13498fdbc986e532 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Mar 2024 14:44:30 +0100 Subject: [PATCH 13/16] Adjust storage code --- lib/storage/__mocks__/index.ts | 8 ++++---- lib/storage/providers/IDBKeyVal.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index e027ff93c..88587407e 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -3,7 +3,7 @@ import utils from '../../utils'; import type {KeyValuePairList} from '../providers/types'; import type StorageProvider from '../providers/types'; -let storageMapInternal: Record = {}; +let storageMapInternal: Record> = {}; const set = jest.fn((key, value) => { storageMapInternal[key] = value; @@ -20,8 +20,8 @@ const idbKeyvalMock: StorageProvider = { Promise.all(setPromises).then(() => resolve(storageMapInternal)); }); }, - getItem(key) { - return Promise.resolve(storageMapInternal[key]); + getItem(key: TKey) { + return Promise.resolve(storageMapInternal[key] as OnyxValue); }, multiGet(keys) { const getPromises = keys.map( @@ -35,7 +35,7 @@ const idbKeyvalMock: StorageProvider = { multiMerge(pairs) { pairs.forEach(([key, value]) => { const existingValue = storageMapInternal[key]; - const newValue = utils.fastMerge(existingValue!, value!); + const newValue = utils.fastMerge(existingValue, value); set(key, newValue); }); diff --git a/lib/storage/providers/IDBKeyVal.ts b/lib/storage/providers/IDBKeyVal.ts index 29277dc30..d98ceea28 100644 --- a/lib/storage/providers/IDBKeyVal.ts +++ b/lib/storage/providers/IDBKeyVal.ts @@ -2,7 +2,7 @@ import type {UseStore} from 'idb-keyval'; import {set, keys, getMany, setMany, get, clear, del, delMany, createStore, promisifyRequest} from 'idb-keyval'; import utils from '../../utils'; import type StorageProvider from './types'; -import type {OnyxValue} from '../../types'; +import type {OnyxKey, OnyxValue} from '../../types'; // We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB // which might not be available in certain environments that load the bundle (e.g. electron main process). @@ -21,12 +21,12 @@ const provider: StorageProvider = { getCustomStore()('readwrite', (store) => { // Note: we are using the manual store transaction here, to fit the read and update // of the items in one transaction to achieve best performance. - const getValues = Promise.all(pairs.map(([key]) => promisifyRequest(store.get(key)))); + const getValues = Promise.all(pairs.map(([key]) => promisifyRequest>(store.get(key)))); return getValues.then((values) => { const upsertMany = pairs.map(([key, value], index) => { const prev = values[index]; - const newValue = utils.fastMerge(prev!, value!); + const newValue = utils.fastMerge(prev, value); return promisifyRequest(store.put(newValue, key)); }); return Promise.all(upsertMany); From edcb91f26d9fd6a0248bf32be16b484aed5d06db Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 14 Mar 2024 15:40:54 +0100 Subject: [PATCH 14/16] Make fastMerge generic --- lib/utils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 857791384..69e4ad996 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ -import type {OnyxEntry, OnyxKey, OnyxValue} from './types'; + +import type {OnyxKey} from './types'; type EmptyObject = Record; type EmptyValue = EmptyObject | null | undefined; @@ -26,7 +27,7 @@ function isMergeableObject(value: unknown): value is Record { * @param shouldRemoveNullObjectValues - If true, null object values will be removed. * @returns - The merged object. */ -function mergeObject>(target: OnyxEntry, source: TValue, shouldRemoveNullObjectValues = true): TValue { +function mergeObject>(target: TObject | null, source: TObject, shouldRemoveNullObjectValues = true): TObject { const destination: Record = {}; if (isMergeableObject(target)) { // lodash adds a small overhead so we don't use it here @@ -46,7 +47,7 @@ function mergeObject>(target: OnyxEntry>(target: OnyxEntry, sourceValue, shouldRemoveNullObjectValues); + destination[key] = fastMerge(targetValue as TObject, sourceValue, shouldRemoveNullObjectValues); } else if (!shouldRemoveNullObjectValues || sourceValue !== null) { destination[key] = sourceValue; } } } - return destination as TValue; + return destination as TObject; } /** @@ -80,13 +81,14 @@ function mergeObject>(target: OnyxEntry>(target: TValue, source: TValue, shouldRemoveNullObjectValues = true): TValue { +function fastMerge>(target: TObject | null, source: TObject | null, shouldRemoveNullObjectValues = true): TObject | null { // We have to ignore arrays and nullish values here, // otherwise "mergeObject" will throw an error, // because it expects an object as "source" if (Array.isArray(source) || source === null || source === undefined) { return source; } + return mergeObject(target, source, shouldRemoveNullObjectValues); } From 888295d45961dcdc64ea8cf0c542869b5513b9ea Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 14 Mar 2024 16:11:24 +0100 Subject: [PATCH 15/16] Revert useOnyx tests changes and adjust OnyxValue type --- lib/storage/__mocks__/index.ts | 2 +- lib/storage/providers/IDBKeyVal.ts | 2 +- lib/types.ts | 2 +- tests/unit/useOnyxTest.ts | 26 +++++++++++++------------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index 4c3c82579..a88e6f953 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -35,7 +35,7 @@ const idbKeyvalMock: StorageProvider = { multiMerge(pairs) { pairs.forEach(([key, value]) => { const existingValue = storageMapInternal[key]; - const newValue = utils.fastMerge(existingValue, value); + const newValue = utils.fastMerge(existingValue as Record, value as Record); set(key, newValue); }); diff --git a/lib/storage/providers/IDBKeyVal.ts b/lib/storage/providers/IDBKeyVal.ts index cd4f706bc..6312d0e3e 100644 --- a/lib/storage/providers/IDBKeyVal.ts +++ b/lib/storage/providers/IDBKeyVal.ts @@ -26,7 +26,7 @@ const provider: StorageProvider = { return getValues.then((values) => { const upsertMany = pairs.map(([key, value], index) => { const prev = values[index]; - const newValue = utils.fastMerge(prev, value); + const newValue = utils.fastMerge(prev as Record, value as Record); return promisifyRequest(store.put(newValue, key)); }); return Promise.all(upsertMany); diff --git a/lib/types.ts b/lib/types.ts index ddbbbf782..c3a3044f6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -103,7 +103,7 @@ 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; +type OnyxValue = string extends TKey ? unknown : TKey extends CollectionKeyBase ? OnyxCollection : OnyxEntry; /** * Represents a mapping of Onyx keys to values, where keys are either normal or collection Onyx keys diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index eaf55c957..c34575ef5 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -98,7 +98,7 @@ describe('useOnyx', () => { }); 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: 'test'}); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test' as unknown); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); @@ -107,7 +107,7 @@ describe('useOnyx', () => { await act(async () => waitForPromisesToResolve()); - expect(result.current[0]).toEqual({test: 'test'}); + expect(result.current[0]).toEqual('test'); expect(result.current[1].status).toEqual('loaded'); }); @@ -262,7 +262,7 @@ describe('useOnyx', () => { }); it('should return initial value from cached key and then return cached value', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, {test: 'test'}); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { @@ -275,7 +275,7 @@ describe('useOnyx', () => { await act(async () => waitForPromisesToResolve()); - expect(result.current[0]).toEqual({test: 'test'}); + expect(result.current[0]).toEqual('test'); expect(result.current[1].status).toEqual('loaded'); }); }); @@ -342,7 +342,7 @@ describe('useOnyx', () => { 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, {test: 'test1'}); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false})); @@ -351,14 +351,14 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual(null); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); - expect(result.current[0]).toEqual({test: '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, {test: 'test1'}); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { @@ -372,20 +372,20 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('initial value'); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); - expect(result.current[0]).toEqual({test: '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, {test: 'test1'}); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { initWithStoredValues: false, // @ts-expect-error bypass - selector: (value: OnyxEntry<{test: string}>) => `${value?.test}_selected`, + selector: (value: OnyxEntry) => `${value}_selected`, }), ); @@ -394,7 +394,7 @@ describe('useOnyx', () => { expect(result.current[0]).toEqual('undefined_selected'); expect(result.current[1].status).toEqual('loaded'); - await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, {test: 'test2'})); + await act(async () => Onyx.merge(ONYXKEYS.TEST_KEY, 'test2')); expect(result.current[0]).toEqual('test2_selected'); expect(result.current[1].status).toEqual('loaded'); From 8379d9ac6986d396c11ebd9c76841042da3a68c4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 14 Mar 2024 16:40:41 +0100 Subject: [PATCH 16/16] Remove assertion --- 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 c34575ef5..890a4b7ad 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -98,7 +98,7 @@ describe('useOnyx', () => { }); 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' as unknown); + await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY));