Fix cyclical dependency issue when Api returns an error (#5333)
Co-authored-by: Trisha Anand <trisha@appsmith.com>
This commit is contained in:
parent
df38f9d311
commit
eae2e46b9d
|
|
@ -187,7 +187,6 @@ export default class DataTreeEvaluator {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
this.logs.push({
|
this.logs.push({
|
||||||
differences,
|
|
||||||
evaluationOrder,
|
evaluationOrder,
|
||||||
sortedDependencies: this.sortedDependencies,
|
sortedDependencies: this.sortedDependencies,
|
||||||
inverse: this.inverseDependencyMap,
|
inverse: this.inverseDependencyMap,
|
||||||
|
|
@ -750,195 +749,190 @@ export default class DataTreeEvaluator {
|
||||||
// In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW)
|
// In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW)
|
||||||
// TODO: Optimise by only getting paths of changed node
|
// TODO: Optimise by only getting paths of changed node
|
||||||
this.allKeys = getAllPaths(unEvalDataTree);
|
this.allKeys = getAllPaths(unEvalDataTree);
|
||||||
|
const translatedDiffs = differences.map(
|
||||||
|
translateDiffEventToDataTreeDiffEvent,
|
||||||
|
);
|
||||||
|
this.logs.push({ differences, translatedDiffs });
|
||||||
// Transform the diff library events to Appsmith evaluator events
|
// Transform the diff library events to Appsmith evaluator events
|
||||||
differences
|
_.flatten(translatedDiffs).forEach((dataTreeDiff) => {
|
||||||
.map(translateDiffEventToDataTreeDiffEvent)
|
const entityName = dataTreeDiff.payload.propertyPath.split(".")[0];
|
||||||
.forEach((dataTreeDiff) => {
|
let entity = unEvalDataTree[entityName];
|
||||||
const entityName = dataTreeDiff.payload.propertyPath.split(".")[0];
|
if (dataTreeDiff.event === DataTreeDiffEvent.DELETE) {
|
||||||
let entity = unEvalDataTree[entityName];
|
entity = this.oldUnEvalTree[entityName];
|
||||||
if (dataTreeDiff.event === DataTreeDiffEvent.DELETE) {
|
}
|
||||||
entity = this.oldUnEvalTree[entityName];
|
const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop";
|
||||||
}
|
|
||||||
const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop";
|
|
||||||
|
|
||||||
if (entityType !== "noop") {
|
if (entityType !== "noop") {
|
||||||
switch (dataTreeDiff.event) {
|
switch (dataTreeDiff.event) {
|
||||||
case DataTreeDiffEvent.NEW: {
|
case DataTreeDiffEvent.NEW: {
|
||||||
// If a new entity/property was added, add all the internal bindings for this entity to the global dependency map
|
// If a new entity/property was added, add all the internal bindings for this entity to the global dependency map
|
||||||
if (
|
if (
|
||||||
(isWidget(entity) || isAction(entity)) &&
|
(isWidget(entity) || isAction(entity)) &&
|
||||||
!this.isDynamicLeaf(
|
!this.isDynamicLeaf(
|
||||||
unEvalDataTree,
|
|
||||||
dataTreeDiff.payload.propertyPath,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const entityDependencyMap: DependencyMap = this.listEntityDependencies(
|
|
||||||
entity,
|
|
||||||
entityName,
|
|
||||||
);
|
|
||||||
if (Object.keys(entityDependencyMap).length) {
|
|
||||||
didUpdateDependencyMap = true;
|
|
||||||
// The entity might already have some dependencies,
|
|
||||||
// so we just want to update those
|
|
||||||
Object.entries(entityDependencyMap).forEach(
|
|
||||||
([entityDependent, entityDependencies]) => {
|
|
||||||
if (this.dependencyMap[entityDependent]) {
|
|
||||||
this.dependencyMap[
|
|
||||||
entityDependent
|
|
||||||
] = this.dependencyMap[entityDependent].concat(
|
|
||||||
entityDependencies,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.dependencyMap[
|
|
||||||
entityDependent
|
|
||||||
] = entityDependencies;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Either a new entity or a new property path has been added. Go through existing dynamic bindings and
|
|
||||||
// find out if a new dependency has to be created because the property path used in the binding just became
|
|
||||||
// eligible
|
|
||||||
const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings(
|
|
||||||
unEvalDataTree,
|
unEvalDataTree,
|
||||||
dataTreeDiff.payload.propertyPath,
|
dataTreeDiff.payload.propertyPath,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const entityDependencyMap: DependencyMap = this.listEntityDependencies(
|
||||||
|
entity,
|
||||||
|
entityName,
|
||||||
);
|
);
|
||||||
// We have found some bindings which are related to the new property path and hence should be added to the
|
if (Object.keys(entityDependencyMap).length) {
|
||||||
// global dependency map
|
|
||||||
if (Object.keys(possibleReferencesInOldBindings).length) {
|
|
||||||
didUpdateDependencyMap = true;
|
didUpdateDependencyMap = true;
|
||||||
Object.assign(
|
// The entity might already have some dependencies,
|
||||||
this.dependencyMap,
|
// so we just want to update those
|
||||||
possibleReferencesInOldBindings,
|
Object.entries(entityDependencyMap).forEach(
|
||||||
|
([entityDependent, entityDependencies]) => {
|
||||||
|
if (this.dependencyMap[entityDependent]) {
|
||||||
|
this.dependencyMap[entityDependent] = this.dependencyMap[
|
||||||
|
entityDependent
|
||||||
|
].concat(entityDependencies);
|
||||||
|
} else {
|
||||||
|
this.dependencyMap[entityDependent] = entityDependencies;
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case DataTreeDiffEvent.DELETE: {
|
// Either a new entity or a new property path has been added. Go through existing dynamic bindings and
|
||||||
// Add to removedPaths as they have been deleted from the evalTree
|
// find out if a new dependency has to be created because the property path used in the binding just became
|
||||||
removedPaths.push(dataTreeDiff.payload.propertyPath);
|
// eligible
|
||||||
// If an existing widget was deleted, remove all the bindings from the global dependency map
|
const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings(
|
||||||
if (
|
unEvalDataTree,
|
||||||
(isWidget(entity) || isAction(entity)) &&
|
dataTreeDiff.payload.propertyPath,
|
||||||
dataTreeDiff.payload.propertyPath === entityName
|
);
|
||||||
) {
|
// We have found some bindings which are related to the new property path and hence should be added to the
|
||||||
const entityDependencies = this.listEntityDependencies(
|
// global dependency map
|
||||||
entity,
|
if (Object.keys(possibleReferencesInOldBindings).length) {
|
||||||
entityName,
|
didUpdateDependencyMap = true;
|
||||||
);
|
Object.assign(
|
||||||
Object.keys(entityDependencies).forEach((widgetDep) => {
|
this.dependencyMap,
|
||||||
didUpdateDependencyMap = true;
|
possibleReferencesInOldBindings,
|
||||||
delete this.dependencyMap[widgetDep];
|
);
|
||||||
});
|
}
|
||||||
}
|
break;
|
||||||
// Either an existing entity or an existing property path has been deleted. Update the global dependency map
|
}
|
||||||
// by removing the bindings from the same.
|
case DataTreeDiffEvent.DELETE: {
|
||||||
Object.keys(this.dependencyMap).forEach((dependencyPath) => {
|
debugger;
|
||||||
|
// Add to removedPaths as they have been deleted from the evalTree
|
||||||
|
removedPaths.push(dataTreeDiff.payload.propertyPath);
|
||||||
|
// If an existing widget was deleted, remove all the bindings from the global dependency map
|
||||||
|
if (
|
||||||
|
(isWidget(entity) || isAction(entity)) &&
|
||||||
|
dataTreeDiff.payload.propertyPath === entityName
|
||||||
|
) {
|
||||||
|
const entityDependencies = this.listEntityDependencies(
|
||||||
|
entity,
|
||||||
|
entityName,
|
||||||
|
);
|
||||||
|
Object.keys(entityDependencies).forEach((widgetDep) => {
|
||||||
didUpdateDependencyMap = true;
|
didUpdateDependencyMap = true;
|
||||||
if (
|
delete this.dependencyMap[widgetDep];
|
||||||
isChildPropertyPath(
|
|
||||||
dataTreeDiff.payload.propertyPath,
|
|
||||||
dependencyPath,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
delete this.dependencyMap[dependencyPath];
|
|
||||||
} else {
|
|
||||||
const toRemove: Array<string> = [];
|
|
||||||
this.dependencyMap[dependencyPath].forEach(
|
|
||||||
(dependantPath) => {
|
|
||||||
if (
|
|
||||||
isChildPropertyPath(
|
|
||||||
dataTreeDiff.payload.propertyPath,
|
|
||||||
dependantPath,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
dependenciesOfRemovedPaths.push(dependencyPath);
|
|
||||||
toRemove.push(dependantPath);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.dependencyMap[dependencyPath] = _.difference(
|
|
||||||
this.dependencyMap[dependencyPath],
|
|
||||||
toRemove,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
// Either an existing entity or an existing property path has been deleted. Update the global dependency map
|
||||||
case DataTreeDiffEvent.EDIT: {
|
// by removing the bindings from the same.
|
||||||
// We only care if the difference is in dynamic bindings since static values do not need
|
Object.keys(this.dependencyMap).forEach((dependencyPath) => {
|
||||||
// an evaluation.
|
didUpdateDependencyMap = true;
|
||||||
if (
|
if (
|
||||||
(isWidget(entity) || isAction(entity)) &&
|
isChildPropertyPath(
|
||||||
typeof dataTreeDiff.payload.value === "string"
|
dataTreeDiff.payload.propertyPath,
|
||||||
|
dependencyPath,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const entity: DataTreeAction | DataTreeWidget = unEvalDataTree[
|
delete this.dependencyMap[dependencyPath];
|
||||||
entityName
|
} else {
|
||||||
] as DataTreeAction | DataTreeWidget;
|
const toRemove: Array<string> = [];
|
||||||
const fullPropertyPath = dataTreeDiff.payload.propertyPath;
|
this.dependencyMap[dependencyPath].forEach((dependantPath) => {
|
||||||
const entityPropertyPath = fullPropertyPath.substring(
|
if (
|
||||||
fullPropertyPath.indexOf(".") + 1,
|
isChildPropertyPath(
|
||||||
);
|
dataTreeDiff.payload.propertyPath,
|
||||||
const isABindingPath = isPathADynamicBinding(
|
dependantPath,
|
||||||
entity,
|
)
|
||||||
entityPropertyPath,
|
) {
|
||||||
);
|
dependenciesOfRemovedPaths.push(dependencyPath);
|
||||||
if (isABindingPath) {
|
toRemove.push(dependantPath);
|
||||||
didUpdateDependencyMap = true;
|
|
||||||
|
|
||||||
const { jsSnippets } = getDynamicBindings(
|
|
||||||
dataTreeDiff.payload.value,
|
|
||||||
);
|
|
||||||
const correctSnippets = jsSnippets.filter(
|
|
||||||
(jsSnippet) => !!jsSnippet,
|
|
||||||
);
|
|
||||||
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the
|
|
||||||
// dependencies for this property path with the newly found dependencies
|
|
||||||
if (correctSnippets.length) {
|
|
||||||
this.dependencyMap[fullPropertyPath] = correctSnippets;
|
|
||||||
} else {
|
|
||||||
// The dependency on this property path has been removed. Delete this property path from the global
|
|
||||||
// dependency map
|
|
||||||
delete this.dependencyMap[fullPropertyPath];
|
|
||||||
}
|
}
|
||||||
if (isAction(entity)) {
|
});
|
||||||
// Actions have a defined dependency map that should always be maintained
|
this.dependencyMap[dependencyPath] = _.difference(
|
||||||
if (entityPropertyPath in entity.dependencyMap) {
|
this.dependencyMap[dependencyPath],
|
||||||
const entityDependenciesName = entity.dependencyMap[
|
toRemove,
|
||||||
entityPropertyPath
|
);
|
||||||
].map((dep) => `${entityName}.${dep}`);
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Filter only the paths which exist in the appsmith world to avoid cyclical dependencies
|
case DataTreeDiffEvent.EDIT: {
|
||||||
const filteredEntityDependencies = entityDependenciesName.filter(
|
// We only care if the difference is in dynamic bindings since static values do not need
|
||||||
(path) => this.allKeys.hasOwnProperty(path),
|
// an evaluation.
|
||||||
);
|
if (
|
||||||
|
(isWidget(entity) || isAction(entity)) &&
|
||||||
|
typeof dataTreeDiff.payload.value === "string"
|
||||||
|
) {
|
||||||
|
const entity: DataTreeAction | DataTreeWidget = unEvalDataTree[
|
||||||
|
entityName
|
||||||
|
] as DataTreeAction | DataTreeWidget;
|
||||||
|
const fullPropertyPath = dataTreeDiff.payload.propertyPath;
|
||||||
|
const entityPropertyPath = fullPropertyPath.substring(
|
||||||
|
fullPropertyPath.indexOf(".") + 1,
|
||||||
|
);
|
||||||
|
const isABindingPath = isPathADynamicBinding(
|
||||||
|
entity,
|
||||||
|
entityPropertyPath,
|
||||||
|
);
|
||||||
|
if (isABindingPath) {
|
||||||
|
didUpdateDependencyMap = true;
|
||||||
|
|
||||||
// Now assign these existing dependent paths to the property path in dependencyMap
|
const { jsSnippets } = getDynamicBindings(
|
||||||
if (fullPropertyPath in this.dependencyMap) {
|
dataTreeDiff.payload.value,
|
||||||
this.dependencyMap[
|
);
|
||||||
fullPropertyPath
|
const correctSnippets = jsSnippets.filter(
|
||||||
] = this.dependencyMap[fullPropertyPath].concat(
|
(jsSnippet) => !!jsSnippet,
|
||||||
filteredEntityDependencies,
|
);
|
||||||
);
|
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the
|
||||||
} else {
|
// dependencies for this property path with the newly found dependencies
|
||||||
this.dependencyMap[
|
if (correctSnippets.length) {
|
||||||
fullPropertyPath
|
this.dependencyMap[fullPropertyPath] = correctSnippets;
|
||||||
] = filteredEntityDependencies;
|
} else {
|
||||||
}
|
// The dependency on this property path has been removed. Delete this property path from the global
|
||||||
|
// dependency map
|
||||||
|
delete this.dependencyMap[fullPropertyPath];
|
||||||
|
}
|
||||||
|
if (isAction(entity)) {
|
||||||
|
// Actions have a defined dependency map that should always be maintained
|
||||||
|
if (entityPropertyPath in entity.dependencyMap) {
|
||||||
|
const entityDependenciesName = entity.dependencyMap[
|
||||||
|
entityPropertyPath
|
||||||
|
].map((dep) => `${entityName}.${dep}`);
|
||||||
|
|
||||||
|
// Filter only the paths which exist in the appsmith world to avoid cyclical dependencies
|
||||||
|
const filteredEntityDependencies = entityDependenciesName.filter(
|
||||||
|
(path) => this.allKeys.hasOwnProperty(path),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now assign these existing dependent paths to the property path in dependencyMap
|
||||||
|
if (fullPropertyPath in this.dependencyMap) {
|
||||||
|
this.dependencyMap[fullPropertyPath] = this.dependencyMap[
|
||||||
|
fullPropertyPath
|
||||||
|
].concat(filteredEntityDependencies);
|
||||||
|
} else {
|
||||||
|
this.dependencyMap[
|
||||||
|
fullPropertyPath
|
||||||
|
] = filteredEntityDependencies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
const diffCalcEnd = performance.now();
|
const diffCalcEnd = performance.now();
|
||||||
const subDepCalcStart = performance.now();
|
const subDepCalcStart = performance.now();
|
||||||
if (didUpdateDependencyMap) {
|
if (didUpdateDependencyMap) {
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@ export function getEntityNameAndPropertyPath(
|
||||||
|
|
||||||
export const translateDiffEventToDataTreeDiffEvent = (
|
export const translateDiffEventToDataTreeDiffEvent = (
|
||||||
difference: Diff<any, any>,
|
difference: Diff<any, any>,
|
||||||
): DataTreeDiff => {
|
): DataTreeDiff | DataTreeDiff[] => {
|
||||||
const result: DataTreeDiff = {
|
let result: DataTreeDiff | DataTreeDiff[] = {
|
||||||
payload: {
|
payload: {
|
||||||
propertyPath: "",
|
propertyPath: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
|
@ -126,25 +126,53 @@ export const translateDiffEventToDataTreeDiffEvent = (
|
||||||
propertyPath,
|
propertyPath,
|
||||||
value: difference.rhs,
|
value: difference.rhs,
|
||||||
};
|
};
|
||||||
} else {
|
} else if (difference.lhs === undefined || difference.rhs === undefined) {
|
||||||
// Handle static value changes that change structure that can lead to
|
// Handle static value changes that change structure that can lead to
|
||||||
// old bindings being eligible
|
// old bindings being eligible
|
||||||
if (
|
if (difference.lhs === undefined && isTrueObject(difference.rhs)) {
|
||||||
difference.lhs === undefined &&
|
|
||||||
typeof difference.rhs === "object"
|
|
||||||
) {
|
|
||||||
result.event = DataTreeDiffEvent.NEW;
|
result.event = DataTreeDiffEvent.NEW;
|
||||||
result.payload = { propertyPath };
|
result.payload = { propertyPath };
|
||||||
}
|
}
|
||||||
if (
|
if (difference.rhs === undefined && isTrueObject(difference.lhs)) {
|
||||||
difference.rhs === undefined &&
|
|
||||||
typeof difference.lhs === "object"
|
|
||||||
) {
|
|
||||||
result.event = DataTreeDiffEvent.DELETE;
|
result.event = DataTreeDiffEvent.DELETE;
|
||||||
result.payload = { propertyPath };
|
result.payload = { propertyPath };
|
||||||
}
|
}
|
||||||
}
|
} else if (
|
||||||
|
isTrueObject(difference.lhs) &&
|
||||||
|
!isTrueObject(difference.rhs)
|
||||||
|
) {
|
||||||
|
// This will happen for static value changes where a property went
|
||||||
|
// from being an object to any other type like string or number
|
||||||
|
// in such a case we want to delete all nested paths of the
|
||||||
|
// original lhs object
|
||||||
|
|
||||||
|
result = Object.keys(difference.lhs).map((diffKey) => {
|
||||||
|
const path = `${propertyPath}.${diffKey}`;
|
||||||
|
return {
|
||||||
|
event: DataTreeDiffEvent.DELETE,
|
||||||
|
payload: {
|
||||||
|
propertyPath: path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
!isTrueObject(difference.lhs) &&
|
||||||
|
isTrueObject(difference.rhs)
|
||||||
|
) {
|
||||||
|
// This will happen for static value changes where a property went
|
||||||
|
// from being any other type like string or number to an object
|
||||||
|
// in such a case we want to add all nested paths of the
|
||||||
|
// new rhs object
|
||||||
|
result = Object.keys(difference.rhs).map((diffKey) => {
|
||||||
|
const path = `${propertyPath}.${diffKey}`;
|
||||||
|
return {
|
||||||
|
event: DataTreeDiffEvent.NEW,
|
||||||
|
payload: {
|
||||||
|
propertyPath: path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "A": {
|
case "A": {
|
||||||
|
|
@ -546,3 +574,9 @@ export const addErrorToEntityProperty = (
|
||||||
}
|
}
|
||||||
return dataTree;
|
return dataTree;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For the times when you need to know if something truly an object like { a: 1, b: 2}
|
||||||
|
// typeof, lodash.isObject and others will return false positives for things like array, null, etc
|
||||||
|
export const isTrueObject = (item: unknown): boolean => {
|
||||||
|
return Object.prototype.toString.call(item) === "[object Object]";
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user