Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Expensify/react-native-onyx into ch…
Browse files Browse the repository at this point in the history
…ore/add-metrics
  • Loading branch information
hannojg committed Nov 27, 2024
2 parents 0494c31 + 0ff5851 commit 6e5275f
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 58 deletions.
24 changes: 24 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ value will be saved to storage after the default value.</p>
<dt><a href="#update">update(data)</a> ⇒</dt>
<dd><p>Insert API responses and lifecycle data into Onyx</p>
</dd>
<dt><a href="#setCollection">setCollection(collectionKey, collection)</a></dt>
<dd><p>Sets a collection by replacing all existing collection members with new values.
Any existing collection members not included in the new data will be removed.</p>
</dd>
</dl>

<a name="init"></a>
Expand Down Expand Up @@ -209,3 +213,23 @@ Insert API responses and lifecycle data into Onyx
| --- | --- |
| data | An array of objects with update expressions |

<a name="setCollection"></a>

## setCollection(collectionKey, collection)
Sets a collection by replacing all existing collection members with new values.
Any existing collection members not included in the new data will be removed.

**Kind**: global function

| Param | Description |
| --- | --- |
| collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` |
| collection | Object collection keyed by individual collection member keys and values |

**Example**
```js
Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, {
[`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
[`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
});
```
78 changes: 57 additions & 21 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
OnyxUpdate,
OnyxValue,
OnyxInput,
OnyxMethodMap,
} from './types';
import OnyxUtils from './OnyxUtils';
import logMessages from './logMessages';
Expand Down Expand Up @@ -607,7 +608,7 @@ function updateSnapshots(data: OnyxUpdate[]) {
function update(data: OnyxUpdate[]): Promise<void> {
// First, validate the Onyx object is in the format we expect
data.forEach(({onyxMethod, key, value}) => {
if (![OnyxUtils.METHOD.CLEAR, OnyxUtils.METHOD.SET, OnyxUtils.METHOD.MERGE, OnyxUtils.METHOD.MERGE_COLLECTION, OnyxUtils.METHOD.MULTI_SET].includes(onyxMethod)) {
if (!Object.values(OnyxUtils.METHOD).includes(onyxMethod)) {
throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
}
if (onyxMethod === OnyxUtils.METHOD.MULTI_SET) {
Expand Down Expand Up @@ -644,18 +645,14 @@ function update(data: OnyxUpdate[]): Promise<void> {
let clearPromise: Promise<void> = Promise.resolve();

data.forEach(({onyxMethod, key, value}) => {
switch (onyxMethod) {
case OnyxUtils.METHOD.SET:
enqueueSetOperation(key, value);
break;
case OnyxUtils.METHOD.MERGE:
enqueueMergeOperation(key, value);
break;
case OnyxUtils.METHOD.MERGE_COLLECTION: {
const handlers: Record<OnyxMethodMap[keyof OnyxMethodMap], (k: typeof key, v: typeof value) => void> = {
[OnyxUtils.METHOD.SET]: enqueueSetOperation,
[OnyxUtils.METHOD.MERGE]: enqueueMergeOperation,
[OnyxUtils.METHOD.MERGE_COLLECTION]: () => {
const collection = value as Collection<CollectionKey, unknown, unknown>;
if (!OnyxUtils.isValidNonEmptyCollectionForMerge(collection)) {
Logger.logInfo('mergeCollection enqueued within update() with invalid or empty value. Skipping this operation.');
break;
return;
}

// Confirm all the collection keys belong to the same parent
Expand All @@ -664,18 +661,15 @@ function update(data: OnyxUpdate[]): Promise<void> {
const mergedCollection: OnyxInputKeyValueMapping = collection;
collectionKeys.forEach((collectionKey) => enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]));
}

break;
}
case OnyxUtils.METHOD.MULTI_SET:
Object.entries(value).forEach(([entryKey, entryValue]) => enqueueSetOperation(entryKey, entryValue));
break;
case OnyxUtils.METHOD.CLEAR:
},
[OnyxUtils.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v as Collection<CollectionKey, unknown, unknown>)),
[OnyxUtils.METHOD.MULTI_SET]: (k, v) => Object.entries(v as Partial<OnyxInputKeyValueMapping>).forEach(([entryKey, entryValue]) => enqueueSetOperation(entryKey, entryValue)),
[OnyxUtils.METHOD.CLEAR]: () => {
clearPromise = clear();
break;
default:
break;
}
},
};

handlers[onyxMethod](key, value);
});

// Group all the collection-related keys and update each collection in a single `mergeCollection` call.
Expand Down Expand Up @@ -736,6 +730,47 @@ function update(data: OnyxUpdate[]): Promise<void> {
.then(() => undefined);
}

type BaseCollection<TMap> = Record<string, TMap | null>;

/**
* Sets a collection by replacing all existing collection members with new values.
* Any existing collection members not included in the new data will be removed.
*
* @example
* Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, {
* [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
* [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
* });
*
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
* @param collection Object collection keyed by individual collection member keys and values
*/
function setCollection<TKey extends CollectionKeyBase, TMap extends string>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TMap>): Promise<void> {
const newCollectionKeys = Object.keys(collection);

if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, newCollectionKeys)) {
Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
return Promise.resolve();
}

return OnyxUtils.getAllKeys().then((persistedKeys) => {
const mutableCollection: BaseCollection<TMap> = {...collection};

persistedKeys.forEach((key) => {
if (!key.startsWith(collectionKey)) {
return;
}
if (newCollectionKeys.includes(key)) {
return;
}

mutableCollection[key] = null;
});

return multiSet(mutableCollection);
});
}

const Onyx = {
METHOD: OnyxUtils.METHOD,
connect,
Expand All @@ -744,6 +779,7 @@ const Onyx = {
multiSet,
merge,
mergeCollection,
setCollection,
update,
clear,
init,
Expand Down
2 changes: 2 additions & 0 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const METHOD = {
SET: 'set',
MERGE: 'merge',
MERGE_COLLECTION: 'mergecollection',
SET_COLLECTION: 'setcollection',
MULTI_SET: 'multiset',
CLEAR: 'clear',
} as const;
Expand Down Expand Up @@ -1465,4 +1466,5 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
subscribeToKey = decorateWithMetrics(subscribeToKey, 'OnyxUtils.subscribeToKey');
});

export type {OnyxMethod};
export default OnyxUtils;
74 changes: 41 additions & 33 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Merge} from 'type-fest';
import type {BuiltIns} from 'type-fest/source/internal';
import type OnyxUtils from './OnyxUtils';
import type {WithOnyxInstance, WithOnyxState} from './withOnyx/types';
import type {OnyxMethod} from './OnyxUtils';

/**
* Utility type that excludes `null` from the type `TValue`.
Expand Down Expand Up @@ -395,41 +396,46 @@ type OnyxMergeInput<TKey extends OnyxKey> = OnyxInput<TKey>;
*/
type OnyxMergeCollectionInput<TKey extends OnyxKey, TMap = object> = Collection<TKey, NonNullable<OnyxInput<TKey>>, TMap>;

type OnyxMethodMap = typeof OnyxUtils.METHOD;

// Maps onyx methods to their corresponding value types
type OnyxMethodValueMap = {
[OnyxUtils.METHOD.SET]: {
key: OnyxKey;
value: OnyxSetInput<OnyxKey>;
};
[OnyxUtils.METHOD.MULTI_SET]: {
key: OnyxKey;
value: OnyxMultiSetInput;
};
[OnyxUtils.METHOD.MERGE]: {
key: OnyxKey;
value: OnyxMergeInput<OnyxKey>;
};
[OnyxUtils.METHOD.CLEAR]: {
key: OnyxKey;
value?: undefined;
};
[OnyxUtils.METHOD.MERGE_COLLECTION]: {
key: CollectionKeyBase;
value: OnyxMergeCollectionInput<CollectionKeyBase>;
};
[OnyxUtils.METHOD.SET_COLLECTION]: {
key: CollectionKeyBase;
value: OnyxMergeCollectionInput<CollectionKeyBase>;
};
};

/**
* Represents different kinds of updates that can be passed to `Onyx.update()` method. It is a discriminated union of
* different update methods (`SET`, `MERGE`, `MERGE_COLLECTION`), each with their own key and value structure.
* OnyxUpdate type includes all onyx methods used in OnyxMethodValueMap.
* If a new method is added to OnyxUtils.METHOD constant, it must be added to OnyxMethodValueMap type.
* Otherwise it will show static type errors.
*/
type OnyxUpdate =
| {
[TKey in OnyxKey]:
| {
onyxMethod: typeof OnyxUtils.METHOD.SET;
key: TKey;
value: OnyxSetInput<TKey>;
}
| {
onyxMethod: typeof OnyxUtils.METHOD.MULTI_SET;
key: TKey;
value: OnyxMultiSetInput;
}
| {
onyxMethod: typeof OnyxUtils.METHOD.MERGE;
key: TKey;
value: OnyxMergeInput<TKey>;
}
| {
onyxMethod: typeof OnyxUtils.METHOD.CLEAR;
key: TKey;
value?: undefined;
};
}[OnyxKey]
| {
[TKey in CollectionKeyBase]: {
onyxMethod: typeof OnyxUtils.METHOD.MERGE_COLLECTION;
key: TKey;
value: OnyxMergeCollectionInput<TKey>;
};
}[CollectionKeyBase];
type OnyxUpdate = {
[Method in OnyxMethod]: {
onyxMethod: Method;
} & OnyxMethodValueMap[Method];
}[OnyxMethod];

/**
* Represents the options used in `Onyx.init()` method.
Expand Down Expand Up @@ -513,6 +519,8 @@ export type {
OnyxMultiSetInput,
OnyxMergeInput,
OnyxMergeCollectionInput,
OnyxMethod,
OnyxMethodMap,
OnyxUpdate,
OnyxValue,
Selector,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-onyx",
"version": "2.0.80",
"version": "2.0.82",
"author": "Expensify, Inc.",
"homepage": "https://expensify.com",
"description": "State management for React Native",
Expand Down
Loading

0 comments on commit 6e5275f

Please sign in to comment.