Skip to content

Commit c81b469

Browse files
committed
Merge branch 'main' into feature/sqliteprovider-improvements
2 parents c51e083 + e8ba3ab commit c81b469

6 files changed

Lines changed: 73 additions & 5 deletions

File tree

lib/Onyx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function init({
4040
enablePerformanceMetrics = false,
4141
enableDevTools = true,
4242
skippableCollectionMemberIDs = [],
43+
snapshotMergeKeys = [],
4344
}: InitOptions): void {
4445
if (enablePerformanceMetrics) {
4546
GlobalSettings.setPerformanceMetricsEnabled(true);
@@ -51,6 +52,7 @@ function init({
5152
Storage.init();
5253

5354
OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));
55+
OnyxUtils.setSnapshotMergeKeys(new Set(snapshotMergeKeys));
5456

5557
if (shouldSyncMultipleInstances) {
5658
Storage.keepInstancesSync?.((key, value) => {

lib/OnyxUtils.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {deepEqual} from 'fast-equals';
22
import type {ValueOf} from 'type-fest';
3-
import lodashPick from 'lodash/pick';
43
import _ from 'underscore';
54
import DevTools from './DevTools';
65
import * as Logger from './Logger';
@@ -103,6 +102,8 @@ const deferredInitTask = createDeferredTask();
103102

104103
// Holds a set of collection member IDs which updates will be ignored when using Onyx methods.
105104
let skippableCollectionMemberIDs = new Set<string>();
105+
// Holds a set of keys that should always be merged into snapshot entries.
106+
let snapshotMergeKeys = new Set<string>();
106107

107108
function getSnapshotKey(): OnyxKey | null {
108109
return snapshotKey;
@@ -143,13 +144,27 @@ function getSkippableCollectionMemberIDs(): Set<string> {
143144
return skippableCollectionMemberIDs;
144145
}
145146

147+
/**
148+
* Getter - returns the snapshot merge keys allowlist.
149+
*/
150+
function getSnapshotMergeKeys(): Set<string> {
151+
return snapshotMergeKeys;
152+
}
153+
146154
/**
147155
* Setter - sets the skippable collection member IDs.
148156
*/
149157
function setSkippableCollectionMemberIDs(ids: Set<string>): void {
150158
skippableCollectionMemberIDs = ids;
151159
}
152160

161+
/**
162+
* Setter - sets the snapshot merge keys allowlist.
163+
*/
164+
function setSnapshotMergeKeys(keys: Set<string>): void {
165+
snapshotMergeKeys = keys;
166+
}
167+
153168
/**
154169
* Sets the initial values for the Onyx store
155170
*
@@ -1234,7 +1249,15 @@ function updateSnapshots<TKey extends OnyxKey>(data: Array<OnyxUpdate<TKey>>, me
12341249
}
12351250

12361251
const oldValue = updatedData[key] || {};
1237-
const newValue = lodashPick(value, Object.keys(snapshotData[key]));
1252+
1253+
// Snapshot entries are stored as a "shape" of the last known data per key, so by default we only
1254+
// merge fields that already exist in the snapshot to avoid unintentionally bloating snapshot data.
1255+
// Some clients need specific fields (like pending status) even when they are missing in the snapshot,
1256+
// so we allow an explicit, opt-in list of keys to always include during snapshot merges.
1257+
const snapshotExistingKeys = Object.keys(snapshotData[key] || {});
1258+
const allowedNewKeys = getSnapshotMergeKeys();
1259+
const keysToCopy = new Set([...snapshotExistingKeys, ...allowedNewKeys]);
1260+
const newValue = typeof value === 'object' && value !== null ? utils.pick(value as Record<string, unknown>, [...keysToCopy]) : {};
12381261

12391262
updatedData = {...updatedData, [key]: Object.assign(oldValue, newValue)};
12401263
}
@@ -1704,6 +1727,8 @@ const OnyxUtils = {
17041727
unsubscribeFromKey,
17051728
getSkippableCollectionMemberIDs,
17061729
setSkippableCollectionMemberIDs,
1730+
getSnapshotMergeKeys,
1731+
setSnapshotMergeKeys,
17071732
storeKeyBySubscriptions,
17081733
deleteKeyBySubscriptions,
17091734
addKeyToRecentlyAccessedIfNeeded,

lib/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,15 @@ type InitOptions = {
425425
* Additionally, any subscribers from these keys to won't receive any data from Onyx.
426426
*/
427427
skippableCollectionMemberIDs?: string[];
428+
429+
/**
430+
* A list of field names that should always be merged into snapshot entries even if those fields are
431+
* missing in the snapshot. Snapshots are saved "views" of a key's data used to populate read-only
432+
* or cached lists, and by default Onyx only merges fields that already exist in that saved view.
433+
* Use this to opt-in to additional fields that must appear in snapshots (for example, pending flags)
434+
* without hardcoding app-specific logic inside Onyx.
435+
*/
436+
snapshotMergeKeys?: string[];
428437
};
429438

430439
// eslint-disable-next-line @typescript-eslint/no-explicit-any

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-onyx",
3-
"version": "3.0.32",
3+
"version": "3.0.33",
44
"author": "Expensify, Inc.",
55
"homepage": "https://expensify.com",
66
"description": "State management for React Native",

tests/unit/onyxTest.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Onyx.init({
3434
[ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default',
3535
},
3636
skippableCollectionMemberIDs: ['skippable-id'],
37+
snapshotMergeKeys: ['pendingAction', 'pendingFields'],
3738
});
3839

3940
describe('Onyx', () => {
@@ -1576,6 +1577,37 @@ describe('Onyx', () => {
15761577
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: finalValue}}, snapshot1);
15771578
});
15781579

1580+
it('should merge allowlisted keys into Snapshot even if they were missing', async () => {
1581+
const cat = `${ONYX_KEYS.COLLECTION.ANIMALS}cat`;
1582+
const snapshot1 = `${ONYX_KEYS.COLLECTION.SNAPSHOT}1`;
1583+
1584+
const initialValue = {name: 'Fluffy'};
1585+
const finalValue = {
1586+
name: 'Kitty',
1587+
pendingAction: 'delete',
1588+
pendingFields: {preview: 'delete'},
1589+
other: 'ignored',
1590+
};
1591+
1592+
await Onyx.set(cat, initialValue);
1593+
await Onyx.set(snapshot1, {data: {[cat]: initialValue}});
1594+
1595+
const callback = jest.fn();
1596+
1597+
Onyx.connect({
1598+
key: ONYX_KEYS.COLLECTION.SNAPSHOT,
1599+
callback,
1600+
});
1601+
1602+
await waitForPromisesToResolve();
1603+
1604+
await Onyx.update([{key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}]);
1605+
1606+
expect(callback).toBeCalledTimes(2);
1607+
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue}}, snapshot1);
1608+
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: {name: 'Kitty', pendingAction: 'delete', pendingFields: {preview: 'delete'}}}}, snapshot1);
1609+
});
1610+
15791611
describe('update', () => {
15801612
it('should squash all updates of collection-related keys into a single mergeCollection call', () => {
15811613
const connections: Connection[] = [];

0 commit comments

Comments
 (0)