diff --git a/lib/DevTools.ts b/lib/DevTools.ts index 76be784e..22b884c1 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); @@ -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]; diff --git a/lib/Onyx.d.ts b/lib/Onyx.d.ts index 3ab35031..c39d316a 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 9ec447ed..f139b041 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 | OnyxKey[]>>; /** Maximum size of the keys store din cache */ private maxRecentKeysSize = 0; @@ -54,7 +52,7 @@ class OnyxCache { } /** Get all the storage keys */ - getAllKeys(): Set { + getAllKeys(): Set { return 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'); } @@ -128,7 +126,7 @@ class OnyxCache { * * @param keys - an array of keys */ - setAllKeys(keys: Key[]) { + setAllKeys(keys: OnyxKey[]) { this.storageKeys = new Set(keys); } @@ -146,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); } @@ -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 b378eb28..d3ee9456 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 {OnyxKey} from './types'; +import type {ConnectOptions} from './Onyx'; 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: ConnectOptions, previousValue: unknown, newValue: unknown, caller: string, keyThatChanged?: string) { if (!debugSetState) { return; } diff --git a/lib/index.ts b/lib/index.ts index b16537f5..58c423ff 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, +}; diff --git a/lib/storage/WebStorage.ts b/lib/storage/WebStorage.ts index 439215b9..6621a084 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/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index ba2ce0b6..a88e6f95 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; @@ -19,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( @@ -34,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); + 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 cd9d3ba3..6312d0e3 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 {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,13 +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]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = utils.fastMerge(prev as any, 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/storage/providers/types.ts b/lib/storage/providers/types.ts index 34e85116..353190f0 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: 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: Key) => Promise; + getItem: (key: TKey) => Promise | null>; /** * 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: TKey, 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: TKey, 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 @@ -73,4 +72,4 @@ type StorageProvider = { }; export default StorageProvider; -export type {Value, Key, KeyList, KeyValuePairList}; +export type {KeyList, KeyValuePair, KeyValuePairList}; 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 a210c349..c3a3044f 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 = 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 @@ -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, diff --git a/lib/utils.ts b/lib/utils.ts index 56e8d7a4..69e4ad99 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ + import type {OnyxKey} from './types'; type EmptyObject = Record; @@ -14,7 +15,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 +27,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: 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 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; @@ -49,8 +50,8 @@ function mergeObject>(target 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 +64,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 TObject, sourceValue, shouldRemoveNullObjectValues); } else if (!shouldRemoveNullObjectValues || sourceValue !== null) { destination[key] = sourceValue; } } } - return destination as TTarget; + return destination as TObject; } /** @@ -80,13 +81,14 @@ 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: 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); }