Skip to content

Commit

Permalink
Merge pull request #493 from callstack-internal/audit/app-startup/avo…
Browse files Browse the repository at this point in the history
…id-creating-new-array-in-getAllKeys

[Audit][Implementation] - avoid re-creating array each time getAllKeys is called
  • Loading branch information
cristipaval authored Mar 12, 2024
2 parents 575c37d + 73a1e82 commit ea52baf
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 25 deletions.
43 changes: 32 additions & 11 deletions lib/Onyx.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,12 @@ function get(key) {
/**
* Returns current key names stored in persisted storage
* @private
* @returns {Promise<string[]>}
* @returns {Promise<Set<Key>>}
*/
function getAllKeys() {
// When we've already read stored keys, resolve right away
const storedKeys = cache.getAllKeys();
if (storedKeys.length > 0) {
if (storedKeys.size > 0) {
return Promise.resolve(storedKeys);
}

Expand All @@ -186,7 +186,8 @@ function getAllKeys() {
// Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
const promise = Storage.getAllKeys().then((keys) => {
cache.setAllKeys(keys);
return keys;
// return the updated set of keys
return cache.getAllKeys();
});

return cache.captureTask(taskName, promise);
Expand Down Expand Up @@ -268,10 +269,18 @@ function tryGetCachedValue(key, mapping = {}) {

// It is possible we haven't loaded all keys yet so we do not know if the
// collection actually exists.
if (allCacheKeys.length === 0) {
if (allCacheKeys.size === 0) {
return;
}
const matchingKeys = _.filter(allCacheKeys, (k) => k.startsWith(key));

const matchingKeys = [];
allCacheKeys.forEach((k) => {
if (!k.startsWith(key)) {
return;
}
matchingKeys.push(k);
});

const values = _.reduce(
matchingKeys,
(finalObject, matchedKey) => {
Expand Down Expand Up @@ -374,7 +383,7 @@ function addToEvictionBlockList(key, connectionID) {
function addAllSafeEvictionKeysToRecentlyAccessedList() {
return getAllKeys().then((keys) => {
_.each(evictionAllowList, (safeEvictionKey) => {
_.each(keys, (key) => {
keys.forEach((key) => {
if (!isKeyMatch(safeEvictionKey, key)) {
return;
}
Expand All @@ -390,8 +399,13 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
* @returns {Object}
*/
function getCachedCollection(collectionKey) {
const collectionMemberKeys = _.filter(cache.getAllKeys(), (storedKey) => isCollectionMemberKey(collectionKey, storedKey));

const collectionMemberKeys = [];
cache.getAllKeys().forEach((storedKey) => {
if (!isCollectionMemberKey(collectionKey, storedKey)) {
return;
}
collectionMemberKeys.push(storedKey);
});
return _.reduce(
collectionMemberKeys,
(prev, curr) => {
Expand Down Expand Up @@ -915,7 +929,14 @@ function connect(mapping) {
// We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
// can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
// subscribed to a "collection key" or a single key.
const matchingKeys = _.filter(keys, (key) => isKeyMatch(mapping.key, key));

const matchingKeys = [];
keys.forEach((key) => {
if (!isKeyMatch(mapping.key, key)) {
return;
}
matchingKeys.push(key);
});

// If the key being connected to does not exist we initialize the value with null. For subscribers that connected
// directly via connect() they will simply get a null value sent to them without any information about which key matched
Expand Down Expand Up @@ -1395,7 +1416,7 @@ function clear(keysToPreserve = []) {
// status, or activeClients need to remain in Onyx even when signed out)
// 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
// to null would cause unknown behavior)
_.each(keys, (key) => {
keys.forEach((key) => {
const isKeyToPreserve = _.contains(keysToPreserve, key);
const isDefaultKey = _.has(defaultKeyStates, key);

Expand Down Expand Up @@ -1502,7 +1523,7 @@ function mergeCollection(collectionKey, collection) {
return true;
})
.keys()
.partition((key) => persistedKeys.includes(key))
.partition((key) => persistedKeys.has(key))
.value();

const existingKeyCollection = _.pick(collection, existingKeys);
Expand Down
4 changes: 2 additions & 2 deletions lib/OnyxCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class OnyxCache {
}

/** Get all the storage keys */
getAllKeys(): Key[] {
return Array.from(this.storageKeys);
getAllKeys(): Set<Key> {
return this.storageKeys;
}

/**
Expand Down
18 changes: 9 additions & 9 deletions tests/unit/onyxCacheTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('Onyx', () => {
const allKeys = cache.getAllKeys();

// Then the result should be empty
expect(allKeys).toEqual([]);
expect(allKeys).toEqual(new Set());
});

it('Should keep storage keys', () => {
Expand All @@ -37,7 +37,7 @@ describe('Onyx', () => {

// Then the keys should be stored in cache
const allKeys = cache.getAllKeys();
expect(allKeys).toEqual(['mockKey', 'mockKey2', 'mockKey3']);
expect(allKeys).toEqual(new Set(['mockKey', 'mockKey2', 'mockKey3']));
});

it('Should keep storage keys even when no values are provided', () => {
Expand All @@ -48,7 +48,7 @@ describe('Onyx', () => {

// Then the keys should be stored in cache
const allKeys = cache.getAllKeys();
expect(allKeys).toEqual(['mockKey', 'mockKey2', 'mockKey3']);
expect(allKeys).toEqual(new Set(['mockKey', 'mockKey2', 'mockKey3']));
});

it('Should not store duplicate keys', () => {
Expand All @@ -62,7 +62,7 @@ describe('Onyx', () => {

// Then getAllKeys should not include a duplicate value
const allKeys = cache.getAllKeys();
expect(allKeys).toEqual(['mockKey', 'mockKey2', 'mockKey3']);
expect(allKeys).toEqual(new Set(['mockKey', 'mockKey2', 'mockKey3']));
});
});

Expand Down Expand Up @@ -121,7 +121,7 @@ describe('Onyx', () => {
expect(cache.hasCacheForKey('mockKey')).toBe(false);

// Then but a key should be available
expect(cache.getAllKeys()).toEqual(expect.arrayContaining(['mockKey']));
expect(cache.getAllKeys()).toEqual(new Set(['mockKey']));
});

it('Should not make duplicate keys', () => {
Expand All @@ -135,7 +135,7 @@ describe('Onyx', () => {

// Then getAllKeys should not include a duplicate value
const allKeys = cache.getAllKeys();
expect(allKeys).toEqual(['mockKey', 'mockKey2']);
expect(allKeys).toEqual(new Set(['mockKey', 'mockKey2']));
});
});

Expand All @@ -158,7 +158,7 @@ describe('Onyx', () => {
cache.set('mockKey', {value: 'mockValue'});

// Then but a key should be available
expect(cache.getAllKeys()).toEqual(expect.arrayContaining(['mockKey']));
expect(cache.getAllKeys()).toEqual(new Set(['mockKey']));
});

it('Should overwrite existing cache items for the Given key', () => {
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('Onyx', () => {
// Then a value should not be available in cache
expect(cache.hasCacheForKey('mockKey')).toBe(false);
expect(cache.getValue('mockKey')).not.toBeDefined();
expect(cache.getAllKeys('mockKey').includes('mockKey')).toBe(false);
expect(cache.getAllKeys('mockKey').has('mockKey')).toBe(false);
});
});

Expand Down Expand Up @@ -336,7 +336,7 @@ describe('Onyx', () => {
});

// Then getAllStorage keys should return updated storage keys
expect(cache.getAllKeys()).toEqual(['mockKey', 'mockKey2', 'mockKey3', 'mockKey4']);
expect(cache.getAllKeys()).toEqual(new Set(['mockKey', 'mockKey2', 'mockKey3', 'mockKey4']));
});

it('Should throw if called with anything that is not an object', () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/onyxTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ describe('Onyx', () => {
Onyx.set(ONYX_KEYS.OTHER_TEST, 42)
.then(() => Onyx.getAllKeys())
.then((keys) => {
expect(keys.includes(ONYX_KEYS.OTHER_TEST)).toBe(true);
expect(keys.has(ONYX_KEYS.OTHER_TEST)).toBe(true);
return Onyx.set(ONYX_KEYS.OTHER_TEST, null);
})
.then(() => {
// Checks if cache value is removed.
expect(cache.getAllKeys().length).toBe(0);
expect(cache.getAllKeys().size).toBe(0);

// When cache keys length is 0, we fetch the keys from storage.
return Onyx.getAllKeys();
})
.then((keys) => {
expect(keys.includes(ONYX_KEYS.OTHER_TEST)).toBe(false);
expect(keys.has(ONYX_KEYS.OTHER_TEST)).toBe(false);
}));

it('should set a simple key', () => {
Expand Down

0 comments on commit ea52baf

Please sign in to comment.