Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function init({
enablePerformanceMetrics = false,
enableDevTools = true,
skippableCollectionMemberIDs = [],
snapshotMergeKeys = [],
}: InitOptions): void {
if (enablePerformanceMetrics) {
GlobalSettings.setPerformanceMetricsEnabled(true);
Expand All @@ -51,6 +52,7 @@ function init({
Storage.init();

OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));
OnyxUtils.setSnapshotMergeKeys(new Set(snapshotMergeKeys));

if (shouldSyncMultipleInstances) {
Storage.keepInstancesSync?.((key, value) => {
Expand Down
29 changes: 27 additions & 2 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {deepEqual} from 'fast-equals';
import type {ValueOf} from 'type-fest';
import lodashPick from 'lodash/pick';
import _ from 'underscore';
import DevTools from './DevTools';
import * as Logger from './Logger';
Expand Down Expand Up @@ -103,6 +102,8 @@ const deferredInitTask = createDeferredTask();

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

function getSnapshotKey(): OnyxKey | null {
return snapshotKey;
Expand Down Expand Up @@ -143,13 +144,27 @@ function getSkippableCollectionMemberIDs(): Set<string> {
return skippableCollectionMemberIDs;
}

/**
* Getter - returns the snapshot merge keys allowlist.
*/
function getSnapshotMergeKeys(): Set<string> {
return snapshotMergeKeys;
}

/**
* Setter - sets the skippable collection member IDs.
*/
function setSkippableCollectionMemberIDs(ids: Set<string>): void {
skippableCollectionMemberIDs = ids;
}

/**
* Setter - sets the snapshot merge keys allowlist.
*/
function setSnapshotMergeKeys(keys: Set<string>): void {
snapshotMergeKeys = keys;
}

/**
* Sets the initial values for the Onyx store
*
Expand Down Expand Up @@ -1234,7 +1249,15 @@ function updateSnapshots<TKey extends OnyxKey>(data: Array<OnyxUpdate<TKey>>, me
}

const oldValue = updatedData[key] || {};
const newValue = lodashPick(value, Object.keys(snapshotData[key]));

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

updatedData = {...updatedData, [key]: Object.assign(oldValue, newValue)};
}
Expand Down Expand Up @@ -1704,6 +1727,8 @@ const OnyxUtils = {
unsubscribeFromKey,
getSkippableCollectionMemberIDs,
setSkippableCollectionMemberIDs,
getSnapshotMergeKeys,
setSnapshotMergeKeys,
storeKeyBySubscriptions,
deleteKeyBySubscriptions,
addKeyToRecentlyAccessedIfNeeded,
Expand Down
9 changes: 9 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,15 @@ type InitOptions = {
* Additionally, any subscribers from these keys to won't receive any data from Onyx.
*/
skippableCollectionMemberIDs?: string[];

/**
* A list of field names that should always be merged into snapshot entries even if those fields are
* missing in the snapshot. Snapshots are saved "views" of a key's data used to populate read-only
* or cached lists, and by default Onyx only merges fields that already exist in that saved view.
* Use this to opt-in to additional fields that must appear in snapshots (for example, pending flags)
* without hardcoding app-specific logic inside Onyx.
*/
snapshotMergeKeys?: string[];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/onyxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Onyx.init({
[ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default',
},
skippableCollectionMemberIDs: ['skippable-id'],
snapshotMergeKeys: ['pendingAction', 'pendingFields'],
});

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

it('should merge allowlisted keys into Snapshot even if they were missing', async () => {
const cat = `${ONYX_KEYS.COLLECTION.ANIMALS}cat`;
const snapshot1 = `${ONYX_KEYS.COLLECTION.SNAPSHOT}1`;

const initialValue = {name: 'Fluffy'};
const finalValue = {
name: 'Kitty',
pendingAction: 'delete',
pendingFields: {preview: 'delete'},
other: 'ignored',
};

await Onyx.set(cat, initialValue);
await Onyx.set(snapshot1, {data: {[cat]: initialValue}});

const callback = jest.fn();

Onyx.connect({
key: ONYX_KEYS.COLLECTION.SNAPSHOT,
callback,
});

await waitForPromisesToResolve();

await Onyx.update([{key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}]);

expect(callback).toBeCalledTimes(2);
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue}}, snapshot1);
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: {name: 'Kitty', pendingAction: 'delete', pendingFields: {preview: 'delete'}}}}, snapshot1);
});

describe('update', () => {
it('should squash all updates of collection-related keys into a single mergeCollection call', () => {
const connections: Connection[] = [];
Expand Down