Skip to content

Commit b2d0034

Browse files
WIP
1 parent d2955f1 commit b2d0034

2 files changed

Lines changed: 124 additions & 44 deletions

File tree

lib/Onyx.js

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ const reduceCollectionWithSelector = (collection, selector, withOnyxInstanceStat
146146
{},
147147
);
148148

149+
/**
150+
* Returns if the key is a computed key.
151+
*
152+
* @param {Mixed} key
153+
* @returns {boolean}
154+
*/
155+
function isComputedKey(key) {
156+
return typeof key === 'object' && 'compute' in key;
157+
}
158+
149159
/**
150160
* Get some data from the store
151161
*
@@ -251,10 +261,6 @@ function isSafeEvictionKey(testKey) {
251261
return _.some(evictionAllowList, (key) => isKeyMatch(key, testKey));
252262
}
253263

254-
function isComputedKey(key) {
255-
return typeof key === 'object' && 'compute' in key;
256-
}
257-
258264
function getCacheKey(key) {
259265
return isComputedKey(key) ? key.cacheKey : key;
260266
}
@@ -279,7 +285,6 @@ function tryGetCachedValue(key, mapping = {}) {
279285
const dependencies = _.mapObject(key.dependencies || {}, (dependencyKey) =>
280286
tryGetCachedValue(
281287
dependencyKey,
282-
283288
// TODO: We could support full mapping here.
284289
{key: dependencyKey},
285290
),
@@ -853,6 +858,15 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
853858
.then((val) => sendDataToConnection(mapping, val, undefined, true));
854859
}
855860

861+
function computeAndSendData(mapping, dependencies) {
862+
let val = cache.getValue(mapping.key.cacheKey);
863+
if (val === undefined) {
864+
val = mapping.key.compute(dependencies);
865+
cache.set(mapping.key.cacheKey, val);
866+
}
867+
sendDataToConnection(mapping, val, mapping.key.cacheKey, true);
868+
}
869+
856870
/**
857871
* Subscribes a react component's state directly to a store key
858872
*
@@ -886,43 +900,49 @@ function connect(mapping) {
886900
callbackToStateMapping[connectionID] = mapping;
887901
callbackToStateMapping[connectionID].connectionID = connectionID;
888902

889-
if (mapping.initWithStoredValues === false) {
903+
if (isComputedKey(mapping.key)) {
904+
deferredInitTask.promise
905+
.then(() => addKeyToRecentlyAccessedIfNeeded(mapping))
906+
.then(() => {
907+
const mappingDependencies = mapping.key.dependencies || {};
908+
const dependenciesCount = _.size(mappingDependencies);
909+
if (dependenciesCount === 0) {
910+
// If we have no dependencies we can send the computed value immediately.
911+
computeAndSendData(mapping, {});
912+
} else {
913+
callbackToStateMapping[connectionID].dependencyConnections = [];
914+
915+
const dependencies = {};
916+
_.each(mappingDependencies, (dependency, mappingKey) => {
917+
// Create a mapping of dependent cache keys so when a key changes, all dependent keys
918+
// can also be cleared from the cache.
919+
const cacheKey = getCacheKey(dependency);
920+
dependentCacheKeys[cacheKey] = dependentCacheKeys[cacheKey] || new Set();
921+
dependentCacheKeys[cacheKey].add(mapping.key.cacheKey);
922+
923+
// Connect to dependencies.
924+
const dependencyConnection = connect({
925+
key: dependency,
926+
waitForCollectionCallback: true,
927+
callback: (value) => {
928+
dependencies[mappingKey] = value;
929+
930+
// Once all dependencies are ready, compute the value and send it to the connection.
931+
if (_.size(dependencies) === dependenciesCount) {
932+
computeAndSendData(mapping, dependencies);
933+
}
934+
},
935+
});
936+
937+
// Store dependency connections so we can disconnect them later.
938+
callbackToStateMapping[connectionID].dependencyConnections.push(dependencyConnection);
939+
});
940+
}
941+
});
890942
return connectionID;
891943
}
892944

893-
if (isComputedKey(mapping.key)) {
894-
deferredInitTask.promise.then(() => {
895-
callbackToStateMapping[connectionID].dependencyConnections = [];
896-
897-
const mappingDependencies = mapping.key.dependencies || {};
898-
const dependenciesCount = _.size(mappingDependencies);
899-
const dependencies = {};
900-
_.each(mappingDependencies, (dependency, mappingKey) => {
901-
const cacheKey = getCacheKey(dependency);
902-
dependentCacheKeys[cacheKey] = dependentCacheKeys[cacheKey] || new Set();
903-
dependentCacheKeys[cacheKey].add(mapping.key.cacheKey);
904-
const dependencyConnection = connect({
905-
key: dependency,
906-
waitForCollectionCallback: true,
907-
callback: (value) => {
908-
dependencies[mappingKey] = value;
909-
910-
if (_.size(dependencies) === dependenciesCount) {
911-
let val = cache.getValue(mapping.key.cacheKey);
912-
if (val === undefined) {
913-
val = mapping.key.compute(dependencies);
914-
cache.set(mapping.key.cacheKey, val);
915-
}
916-
917-
sendDataToConnection(mapping, val, mapping.key.cacheKey, true);
918-
}
919-
},
920-
});
921-
922-
// TODO: Disconnect those on disconnect.
923-
callbackToStateMapping[connectionID].dependencyConnections.push(dependencyConnection);
924-
});
925-
});
945+
if (mapping.initWithStoredValues === false) {
926946
return connectionID;
927947
}
928948

tests/unit/onyxComputedKeyTest.js

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import _ from 'underscore';
22
import Onyx from '../../lib';
33
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
4+
import StorageMock from '../../lib/storage';
45

56
const ONYX_KEYS = {
67
COLLECTION: {
@@ -36,14 +37,15 @@ describe('Onyx computed keys', () => {
3637
TEST_COMPUTED_KEY_1.compute.mockClear();
3738
TEST_COMPUTED_KEY_2.compute.mockClear();
3839

39-
return Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, {
40-
[`${ONYX_KEYS.COLLECTION.TEST_KEY}3`]: {ID: 3},
41-
[`${ONYX_KEYS.COLLECTION.TEST_KEY}1`]: {ID: 1},
42-
[`${ONYX_KEYS.COLLECTION.TEST_KEY}2`]: {ID: 2},
43-
});
40+
return Promise.all([
41+
StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}3`, {ID: 3}),
42+
StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, {ID: 1}),
43+
StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}2`, {ID: 2}),
44+
]);
4445
});
4546

4647
afterEach(() => {
48+
StorageMock.clear();
4749
Onyx.disconnect(connectionID);
4850
return Onyx.clear();
4951
});
@@ -108,10 +110,68 @@ describe('Onyx computed keys', () => {
108110

109111
return waitForPromisesToResolve()
110112
.then(() => {
113+
expect(callback).toHaveBeenCalledTimes(1);
111114
expect(TEST_COMPUTED_KEY_1.compute).toHaveBeenCalledTimes(1);
115+
Onyx.disconnect(connectionID);
116+
connectionID = Onyx.connect({
117+
key: TEST_COMPUTED_KEY_1,
118+
callback,
119+
});
112120
})
121+
.then(waitForPromisesToResolve)
122+
.then(() => {
123+
expect(callback).toHaveBeenCalledTimes(2);
124+
expect(TEST_COMPUTED_KEY_1.compute).toHaveBeenCalledTimes(1);
125+
});
126+
});
127+
128+
it('handles multiple connects at the same time', () => {
129+
const callback = jest.fn();
130+
const connections = [
131+
Onyx.connect({
132+
key: TEST_COMPUTED_KEY_1,
133+
callback,
134+
}),
135+
Onyx.connect({
136+
key: TEST_COMPUTED_KEY_1,
137+
callback,
138+
}),
139+
Onyx.connect({
140+
key: TEST_COMPUTED_KEY_2,
141+
callback,
142+
}),
143+
];
144+
145+
return waitForPromisesToResolve()
113146
.then(() => {
147+
expect(callback).toHaveBeenCalledTimes(3);
114148
expect(TEST_COMPUTED_KEY_1.compute).toHaveBeenCalledTimes(1);
149+
expect(TEST_COMPUTED_KEY_2.compute).toHaveBeenCalledTimes(1);
150+
})
151+
.then(() => {
152+
connections.forEach(Onyx.disconnect);
153+
});
154+
});
155+
156+
it('disconnects dependencies', () => {
157+
const callback = jest.fn();
158+
connectionID = Onyx.connect({
159+
key: TEST_COMPUTED_KEY_1,
160+
callback,
161+
});
162+
163+
return waitForPromisesToResolve()
164+
.then(() => {
165+
expect(callback).toHaveBeenCalledTimes(1);
166+
Onyx.disconnect(connectionID);
167+
})
168+
.then(() =>
169+
Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, {
170+
[`${ONYX_KEYS.COLLECTION.TEST_KEY}4`]: {ID: 4},
171+
}),
172+
)
173+
.then(() => {
174+
expect(callback).toHaveBeenCalledTimes(1);
115175
});
116176
});
117177
});

0 commit comments

Comments
 (0)