Skip to content
Draft
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
11 changes: 10 additions & 1 deletion lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ function prepareKeyValuePairsForStorage(
const pairs: StorageKeyValuePair[] = [];

for (const [key, value] of Object.entries(data)) {
if (value === null) {
if (value === null || (Array.isArray(value) && value.length === 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we want to do the same for empty objects? Or just empty arrays?

remove(key, isProcessingCollectionUpdate);
continue;
}
Expand Down Expand Up @@ -1335,6 +1335,15 @@ function setWithRetry<TKey extends OnyxKey>({key, value, options}: SetParams<TKe
return Promise.resolve();
}

// Empty arrays are treated as null to prevent them from poisoning the cache.
// When a key holds [], subsequent merge operations with objects are rejected as
// incompatible, breaking important flows. Normalizing [] to null here ensures
// the key is removed and future merges can apply cleanly.
if (Array.isArray(value) && value.length === 0) {
// eslint-disable-next-line no-param-reassign
value = null;
}

const existingValue = cache.get(key, false);

// If the existing value as well as the new value are null, we can return early.
Expand Down
113 changes: 113 additions & 0 deletions tests/unit/onyxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,119 @@ describe('Onyx', () => {
});
});

it('should treat empty arrays as null in Onyx.set and remove the key', () => {
let testKeyValue: unknown;

connection = Onyx.connect({
key: ONYX_KEYS.TEST_KEY,
initWithStoredValues: false,
callback: (value) => {
testKeyValue = value;
},
});

return Onyx.set(ONYX_KEYS.TEST_KEY, {existing: 'data'})
.then(() => {
expect(testKeyValue).toEqual({existing: 'data'});

// Setting an empty array should remove the key, same as setting null
return Onyx.set(ONYX_KEYS.TEST_KEY, []);
})
.then(() => {
expect(testKeyValue).toBeUndefined();
});
});

it('should treat empty arrays as null in Onyx.multiSet and remove the key', () => {
let testKeyValue: unknown;
let otherTestKeyValue: unknown;

connection = Onyx.connect({
key: ONYX_KEYS.TEST_KEY,
initWithStoredValues: false,
callback: (value) => {
testKeyValue = value;
},
});

connection = Onyx.connect({
key: ONYX_KEYS.OTHER_TEST,
initWithStoredValues: false,
callback: (value) => {
otherTestKeyValue = value;
},
});

return Onyx.multiSet({
[ONYX_KEYS.TEST_KEY]: {existing: 'data'},
[ONYX_KEYS.OTHER_TEST]: 'otherData',
})
.then(() => {
expect(testKeyValue).toEqual({existing: 'data'});
expect(otherTestKeyValue).toEqual('otherData');

return Onyx.multiSet({
[ONYX_KEYS.TEST_KEY]: [],
[ONYX_KEYS.OTHER_TEST]: [],
});
})
.then(() => {
expect(testKeyValue).toBeUndefined();
expect(otherTestKeyValue).toBeUndefined();
});
});

it('should allow merging objects after an empty array was set (empty array clears the key)', () => {
let testKeyValue: unknown;

connection = Onyx.connect({
key: ONYX_KEYS.TEST_KEY,
initWithStoredValues: false,
callback: (value) => {
testKeyValue = value;
},
});

return Onyx.set(ONYX_KEYS.TEST_KEY, [])
.then(() => {
expect(testKeyValue).toBeUndefined();

// Merging an object should succeed because the key was removed, not stuck as []
return Onyx.merge(ONYX_KEYS.TEST_KEY, {name: 'test'});
})
.then(() => {
expect(testKeyValue).toEqual({name: 'test'});
});
});

it('should treat empty arrays as null in Onyx.update with SET method', () => {
let testKeyValue: unknown;

connection = Onyx.connect({
key: ONYX_KEYS.TEST_KEY,
initWithStoredValues: false,
callback: (value) => {
testKeyValue = value;
},
});

return Onyx.set(ONYX_KEYS.TEST_KEY, {existing: 'data'})
.then(() => {
expect(testKeyValue).toEqual({existing: 'data'});

return Onyx.update([
{
onyxMethod: Onyx.METHOD.SET,
key: ONYX_KEYS.TEST_KEY,
value: [],
},
]);
})
.then(() => {
expect(testKeyValue).toBeUndefined();
});
});

it('should ignore top-level and remove nested `undefined` values in Onyx.merge', () => {
let testKeyValue: unknown;

Expand Down
Loading