Merge pull request #32102 from appsmithorg/release

chore: Daily promotion part 2 for 27th March, 2024
This commit is contained in:
Trisha Anand 2024-03-27 15:35:20 +05:30 committed by GitHub
commit b47c563b32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 752 additions and 942 deletions

View File

@ -2,6 +2,7 @@ import {
agHelper,
deployMode,
dataSources,
assertHelper,
locators,
draggableWidgets,
} from "../../../support/Objects/ObjectsCore";
@ -86,6 +87,7 @@ describe("PgAdmin Clone App", { tags: ["@tag.Datasource"] }, function () {
cy.xpath(appPage.submitButton).click({ force: true });
agHelper.AssertElementVisibility(appPage.addColumn);
cy.xpath(appPage.submitButton).first().click({ force: true });
assertHelper.AssertNetworkStatus("@postExecute");
cy.xpath(appPage.closeButton).click({ force: true });
cy.xpath(appPage.addNewtable).should("be.visible");
// viewing the table's columns by clicking on view button

View File

@ -196,9 +196,8 @@ function setPropertyPaneSectionState(propertySectionState) {
)) {
cy.get("body").then(($body) => {
if (
$body.find(
`${propertySectionClass(sectionName)} .t--chevron-icon.rotate-180`,
).length >
$body.find(`${propertySectionClass(sectionName)} .t--chevron-icon`)
.length >
0 !==
shouldSectionOpen
) {
@ -214,9 +213,8 @@ function verifyPropertyPaneSectionState(propertySectionState) {
)) {
cy.get("body").then(($body) => {
const isSectionOpen =
$body.find(
`${propertySectionClass(sectionName)} .t--chevron-icon.rotate-180`,
).length > 0;
$body.find(`${propertySectionClass(sectionName)} .t--chevron-icon`)
.length > 0;
expect(isSectionOpen).to.equal(shouldSectionOpen);
});
}

View File

@ -180,9 +180,8 @@ function setPropertyPaneSectionState(propertySectionState) {
)) {
cy.get("body").then(($body) => {
if (
$body.find(
`${propertySectionClass(sectionName)} .t--chevron-icon.rotate-180`,
).length >
$body.find(`${propertySectionClass(sectionName)} .t--chevron-icon`)
.length >
0 !==
shouldSectionOpen
) {
@ -198,9 +197,8 @@ function verifyPropertyPaneSectionState(propertySectionState) {
)) {
cy.get("body").then(($body) => {
const isSectionOpen =
$body.find(
`${propertySectionClass(sectionName)} .t--chevron-icon.rotate-180`,
).length > 0;
$body.find(`${propertySectionClass(sectionName)} .t--chevron-icon`)
.length > 0;
expect(isSectionOpen).to.equal(shouldSectionOpen);
});
}

View File

@ -1,7 +1,6 @@
# To run only limited tests - give the spec names in below format:
cypress/e2e/Regression/ClientSide/Templates/Fork_Template_spec.js
# For running all specs - uncomment below:
#cypress/e2e/**/**/*

View File

@ -4,7 +4,7 @@ import { intersection } from "lodash";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { QueryActionConfig } from "entities/Action";
import type { DatasourceConfiguration } from "entities/Datasource";
import type { DiffWithReferenceState } from "workers/Evaluation/helpers";
import type { DiffWithNewTreeState } from "workers/Evaluation/helpers";
import {
EVALUATE_REDUX_ACTIONS,
EVAL_AND_LINT_REDUX_ACTIONS,
@ -57,8 +57,8 @@ export function shouldLog(action: ReduxAction<unknown>) {
}
export const setEvaluatedTree = (
updates: DiffWithReferenceState[],
): ReduxAction<{ updates: DiffWithReferenceState[] }> => {
updates: DiffWithNewTreeState[],
): ReduxAction<{ updates: DiffWithNewTreeState[] }> => {
return {
type: ReduxActionTypes.SET_EVALUATED_TREE,
payload: { updates },

View File

@ -1,8 +1,5 @@
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import { makeEntityConfigsAsObjProperties } from "@appsmith/workers/Evaluation/dataTreeUtils";
import { smallDataSet } from "workers/Evaluation/__tests__/generateOpimisedUpdates.test";
import produce from "immer";
import { cloneDeep } from "lodash";
const unevalTreeFromMainThread = {
Api2: {
@ -131,122 +128,22 @@ const unevalTreeFromMainThread = {
describe("7. Test util methods", () => {
describe("makeDataTreeEntityConfigAsProperty", () => {
it("should not introduce __evaluation__ property", () => {
it("should not introduce __evaluation__ property to the widget state when the corresponding evalProps isn't there for a widget", () => {
const dataTree = makeEntityConfigsAsObjProperties(
unevalTreeFromMainThread as unknown as DataTree,
);
expect(dataTree.Api2).not.toHaveProperty("__evaluation__");
});
describe("identicalEvalPathsPatches decompress updates", () => {
it("should decompress identicalEvalPathsPatches updates into evalProps and not in state", () => {
const state = {
Table1: {
filteredTableData: smallDataSet,
selectedRows: [],
pageSize: 0,
},
} as any;
it("should introduce __evaluation__ property to the widget state when it has a corresponding evalProps", () => {
const dataTree = makeEntityConfigsAsObjProperties(
unevalTreeFromMainThread as unknown as DataTree,
{
evalProps: { Api2: { __evaluation__: { errors: { someVal: [] } } } },
},
);
const identicalEvalPathsPatches = {
"Table1.__evaluation__.evaluatedValues.['filteredTableData']":
"Table1.filteredTableData",
};
const evalProps = {
Table1: {
__evaluation__: {
evaluatedValues: {
someProp: "abc",
},
errors: {
someProp1: "abc",
},
},
},
} as any;
const dataTree = makeEntityConfigsAsObjProperties(state as any, {
sanitizeDataTree: true,
evalProps,
identicalEvalPathsPatches,
});
// only errors should be attached to dataTree evaluatedValues should be excluded
const expectedState = produce(state, (draft: any) => {
draft.Table1.__evaluation__ = { errors: { someProp1: "abc" } };
});
expect(dataTree).toEqual(expectedState);
//evalProps should have decompressed updates in it coming from identicalEvalPathsPatches
const expectedEvalProps = produce(evalProps, (draft: any) => {
draft.Table1.__evaluation__.evaluatedValues.filteredTableData =
smallDataSet;
});
expect(evalProps).toEqual(expectedEvalProps);
});
it("should not make any updates to evalProps when the identicalEvalPathsPatches is empty", () => {
const state = {
Table1: {
filteredTableData: smallDataSet,
selectedRows: [],
pageSize: 0,
},
} as any;
const identicalEvalPathsPatches = {};
const initialEvalProps = {} as any;
const evalProps = cloneDeep(initialEvalProps);
const dataTree = makeEntityConfigsAsObjProperties(state, {
sanitizeDataTree: true,
evalProps,
identicalEvalPathsPatches,
});
expect(dataTree).toEqual(dataTree);
//evalProps not be mutated with any updates
expect(evalProps).toEqual(initialEvalProps);
});
it("should ignore non relevant identicalEvalPathsPatches updates into evalProps and state", () => {
const state = {
Table1: {
filteredTableData: smallDataSet,
selectedRows: [],
pageSize: 0,
},
} as any;
//ignore non existent widget state
const identicalEvalPathsPatches = {
"SomeWidget.__evaluation__.evaluatedValues.['filteredTableData']":
"SomeWidget.filteredTableData",
};
const initialEvalProps = {
Table1: {
__evaluation__: {
evaluatedValues: {
someProp: "abc",
},
errors: {
someProp1: "efg",
},
},
},
} as any;
const evalProps = cloneDeep(initialEvalProps);
const dataTree = makeEntityConfigsAsObjProperties(state, {
sanitizeDataTree: true,
evalProps,
identicalEvalPathsPatches,
});
const expectedState = produce(state, (draft: any) => {
// evaluatedValues should not be attached to dataTree only errors should be attached
draft.Table1.__evaluation__ = { errors: { someProp1: "efg" } };
});
expect(dataTree).toEqual(expectedState);
//evalProps not be mutated with any updates
expect(evalProps).toEqual(initialEvalProps);
});
expect(dataTree.Api2).toHaveProperty("__evaluation__");
});
});
});

View File

@ -1,5 +1,5 @@
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import { get, isObject, set } from "lodash";
import { isObject, set } from "lodash";
import { klona } from "klona/json";
import type { EvalProps } from "workers/common/DataTreeEvaluator";
@ -12,14 +12,9 @@ export function makeEntityConfigsAsObjProperties(
option = {} as {
sanitizeDataTree?: boolean;
evalProps?: EvalProps;
identicalEvalPathsPatches?: Record<string, string>;
},
): DataTree {
const {
evalProps,
identicalEvalPathsPatches,
sanitizeDataTree = true,
} = option;
const { evalProps, sanitizeDataTree = true } = option;
const newDataTree: DataTree = {};
for (const entityName of Object.keys(dataTree)) {
const entity = dataTree[entityName];
@ -31,24 +26,6 @@ export function makeEntityConfigsAsObjProperties(
if (!evalProps) return dataTreeToReturn;
//clean up deletes widget states
Object.entries(identicalEvalPathsPatches || {}).forEach(
([evalPath, statePath]) => {
const [entity] = statePath.split(".");
if (!dataTreeToReturn[entity]) {
delete identicalEvalPathsPatches?.[evalPath];
}
},
);
// decompressIdenticalEvalPaths
Object.entries(identicalEvalPathsPatches || {}).forEach(
([evalPath, statePath]) => {
const referencePathValue = get(dataTreeToReturn, statePath);
set(evalProps, evalPath, referencePathValue);
},
);
for (const [entityName, entityEvalProps] of Object.entries(evalProps)) {
if (!entityEvalProps.__evaluation__) continue;
set(

View File

@ -194,8 +194,8 @@ export const PropertySection = memo((props: PropertySectionProps) => {
)}
{props.collapsible && (
<Icon
className={`ml-auto t--chevron-icon ${isOpen ? "rotate-180" : ""}`}
name="arrow-up-s-line"
className={`ml-auto t--chevron-icon`}
name={isOpen ? "expand-less" : "expand-more"}
size="md"
/>
)}

View File

@ -1,12 +1,10 @@
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { Diff } from "deep-diff";
import { applyChange } from "deep-diff";
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import { createImmerReducer } from "utils/ReducerUtils";
import * as Sentry from "@sentry/react";
import { get } from "lodash";
import type { DiffWithReferenceState } from "workers/Evaluation/helpers";
import type { DiffWithNewTreeState } from "workers/Evaluation/helpers";
export type EvaluatedTreeState = DataTree;
@ -17,7 +15,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, {
state: EvaluatedTreeState,
action: ReduxAction<{
dataTree: DataTree;
updates: DiffWithReferenceState[];
updates: DiffWithNewTreeState[];
removedPaths: [string];
}>,
) => {
@ -26,23 +24,13 @@ const evaluatedTreeReducer = createImmerReducer(initialState, {
return state;
}
for (const update of updates) {
// Null check for typescript
if (!Array.isArray(update.path) || update.path.length === 0) {
continue;
}
try {
//these are the decompression updates, there are cases where identical values are present in the state
//over here we have the path which has the identical value and apply as an update
if (update.kind === "referenceState") {
const { path, referencePath } = update;
const patch = {
kind: "N",
path,
rhs: get(state, referencePath),
} as Diff<DataTree, DataTree>;
applyChange(state, undefined, patch);
if (update.kind === "newTree") {
return update.rhs;
} else {
if (!update.path || update.path.length === 0) {
continue;
}
applyChange(state, undefined, update);
}
} catch (e) {

View File

@ -1,9 +1,9 @@
import log from "loglevel";
import type { DiffWithReferenceState } from "workers/Evaluation/helpers";
import type { DiffWithNewTreeState } from "workers/Evaluation/helpers";
export const parseUpdatesAndDeleteUndefinedUpdates = (
updates: string,
): DiffWithReferenceState[] => {
): DiffWithNewTreeState[] => {
let parsedUpdates = [];
try {
//Parse updates from a string
@ -12,6 +12,7 @@ export const parseUpdatesAndDeleteUndefinedUpdates = (
log.error("Failed to parse updates", e, updates);
return [];
}
//delete all undefined properties from the state
const { deleteUpdates, regularUpdates } = parsedUpdates.reduce(
(acc: any, curr: any) => {

View File

@ -13,7 +13,11 @@ import { MessageType, sendMessage } from "utils/MessageUtil";
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas";
import type { JSUpdate } from "utils/JSPaneUtils";
import { generateOptimisedUpdatesAndSetPrevState } from "./helpers";
import {
generateOptimisedUpdatesAndSetPrevState,
getNewDataTreeUpdates,
uniqueOrderUpdatePaths,
} from "./helpers";
export function evalTreeWithChanges(
updatedValuePaths: string[][],
@ -49,8 +53,6 @@ export function evalTreeWithChanges(
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
identicalEvalPathsPatches:
dataTreeEvaluator.getEvalPathsIdenticalToState(),
});
/** Make sure evalMetaUpdates is sanitized to prevent postMessage failure */
@ -62,11 +64,26 @@ export function evalTreeWithChanges(
unevalTree = dataTreeEvaluator.getOldUnevalTree();
configTree = dataTreeEvaluator.oldConfigTree;
}
const allUnevalUpdates = unEvalUpdates.map(
(update) => update.payload.propertyPath,
);
const completeEvalOrder = uniqueOrderUpdatePaths([
...allUnevalUpdates,
...evalOrder,
]);
const setterAndLocalStorageUpdates = getNewDataTreeUpdates(
uniqueOrderUpdatePaths(updatedValuePaths.map((val) => val.join("."))),
dataTree,
);
const updates = generateOptimisedUpdatesAndSetPrevState(
dataTree,
dataTreeEvaluator,
completeEvalOrder,
setterAndLocalStorageUpdates,
);
const evalTreeResponse: EvalTreeResponseData = {
updates,
dependencies,

View File

@ -87,8 +87,6 @@ function resetWidgetMetaProperty(
const oldUnEvalTree = dataTreeEvaluator.getOldUnevalTree();
const configTree = dataTreeEvaluator.getConfigTree();
const evalProps = dataTreeEvaluator.getEvalProps();
const evalPathsIdenticalToState =
dataTreeEvaluator.getEvalPathsIdenticalToState();
const evaluatedEntity = evalTree[widget.widgetName];
const evaluatedEntityConfig = configTree[
@ -135,7 +133,6 @@ function resetWidgetMetaProperty(
evalPropertyValue: finalValue,
unEvalPropertyValue: expressionToEvaluate,
evalProps,
evalPathsIdenticalToState,
});
evalMetaUpdates.push({

View File

@ -9,6 +9,7 @@ import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types";
import { makeEntityConfigsAsObjProperties } from "@appsmith/workers/Evaluation/dataTreeUtils";
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
import { serialiseToBigInt } from "@appsmith/workers/Evaluation/evaluationUtils";
import {
CrashingError,
getSafeToRenderDataTree,
@ -22,7 +23,10 @@ import { clearAllIntervals } from "../fns/overrides/interval";
import JSObjectCollection from "workers/Evaluation/JSObject/Collection";
import { getJSVariableCreatedEvents } from "../JSObject/JSVariableEvents";
import { errorModifier } from "../errorModifier";
import { generateOptimisedUpdatesAndSetPrevState } from "../helpers";
import {
generateOptimisedUpdatesAndSetPrevState,
uniqueOrderUpdatePaths,
} from "../helpers";
import DataStore from "../dataStore";
import type { TransmissionErrorHandler } from "../fns/utils/Messenger";
import { MessageType, sendMessage } from "utils/MessageUtil";
@ -76,6 +80,7 @@ export function evalTree(request: EvalWorkerSyncRequest) {
canvasWidgets = widgets;
canvasWidgetsMeta = widgetsMeta;
metaWidgetsCache = metaWidgets;
let isNewTree = false;
try {
if (!dataTreeEvaluator) {
@ -107,10 +112,9 @@ export function evalTree(request: EvalWorkerSyncRequest) {
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
identicalEvalPathsPatches:
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
});
staleMetaIds = dataTreeResponse.staleMetaIds;
isNewTree = true;
} else if (dataTreeEvaluator.hasCyclicalDependency || forceEvaluation) {
if (dataTreeEvaluator && !isEmpty(allActionValidationConfig)) {
//allActionValidationConfigs may not be set in dataTreeEvaluator. Therefore, set it explicitly via setter method
@ -150,8 +154,6 @@ export function evalTree(request: EvalWorkerSyncRequest) {
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
identicalEvalPathsPatches:
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
});
staleMetaIds = dataTreeResponse.staleMetaIds;
} else {
@ -193,8 +195,6 @@ export function evalTree(request: EvalWorkerSyncRequest) {
dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, {
evalProps: dataTreeEvaluator.evalProps,
identicalEvalPathsPatches:
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
});
evalMetaUpdates = JSON.parse(
@ -229,21 +229,42 @@ export function evalTree(request: EvalWorkerSyncRequest) {
makeEntityConfigsAsObjProperties(unevalTree, {
sanitizeDataTree: false,
evalProps: dataTreeEvaluator?.evalProps,
identicalEvalPathsPatches:
dataTreeEvaluator?.getEvalPathsIdenticalToState(),
}),
widgetTypeConfigMap,
configTree,
);
unEvalUpdates = [];
isNewTree = true;
}
const jsVarsCreatedEvent = getJSVariableCreatedEvents(jsUpdates);
const updates = generateOptimisedUpdatesAndSetPrevState(
dataTree,
dataTreeEvaluator,
);
let updates;
if (isNewTree) {
try {
//for new tree send the whole thing, don't diff at all
updates = serialiseToBigInt([{ kind: "newTree", rhs: dataTree }]);
dataTreeEvaluator?.setPrevState(dataTree);
} catch (e) {
updates = "[]";
}
isNewTree = false;
} else {
const allUnevalUpdates = unEvalUpdates.map(
(update) => update.payload.propertyPath,
);
const completeEvalOrder = uniqueOrderUpdatePaths([
...allUnevalUpdates,
...evalOrder,
]);
updates = generateOptimisedUpdatesAndSetPrevState(
dataTree,
dataTreeEvaluator,
completeEvalOrder,
);
}
const evalTreeResponse: EvalTreeResponseData = {
updates,

View File

@ -1,22 +1,33 @@
import { serialiseToBigInt } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { WidgetEntity } from "@appsmith//entities/DataTree/types";
import type { Diff } from "deep-diff";
import { diff } from "deep-diff";
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import equal from "fast-deep-equal";
import { get, isNumber, isObject, set } from "lodash";
import { get, isObject, set } from "lodash";
import { isMoment } from "moment";
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
export const fn_keys: string = "__fn_keys__";
export interface DiffReferenceState {
kind: "referenceState";
path: any[];
referencePath: string;
export const uniqueOrderUpdatePaths = (updatePaths: string[]) =>
Array.from(new Set(updatePaths)).sort((a, b) => b.length - a.length);
export const getNewDataTreeUpdates = (paths: string[], dataTree: object) =>
paths.map((path) => {
const segmentedPath = path.split(".");
return {
kind: "N",
path: segmentedPath,
rhs: get(dataTree, segmentedPath),
};
});
export interface DiffNewTreeState {
kind: "newTree";
rhs: any;
}
export type DiffWithReferenceState =
| Diff<DataTree, DataTree>
| DiffReferenceState;
export type DiffWithNewTreeState = Diff<DataTree, DataTree> | DiffNewTreeState;
// Finds the first index which is a duplicate value
// Returns -1 if there are no duplicates
// Returns the index of the first duplicate entry it finds
@ -72,29 +83,6 @@ export const countOccurrences = (
};
const LARGE_COLLECTION_SIZE = 100;
// for object paths which have a "." in the object key like "a.['b.c']"
const REGEX_NESTED_OBJECT_PATH = /(.+)\.\[\'(.*)\'\]/;
const generateWithKey = (basePath: any, key: any) => {
const segmentedPath = [...basePath, key];
if (isNumber(key)) {
return {
path: basePath.join(".") + ".[" + key + "]",
segmentedPath,
};
}
if (key.includes(".")) {
return {
path: basePath.join(".") + ".['" + key + "']",
segmentedPath,
};
}
return {
path: basePath.join(".") + "." + key,
segmentedPath,
};
};
export const stringifyFnsInObject = (
userObject: Record<string, unknown>,
@ -170,68 +158,45 @@ const isLargeCollection = (val: any) => {
return size > LARGE_COLLECTION_SIZE;
};
const normaliseEvalPath = (identicalEvalPathsPatches: any) =>
Object.keys(identicalEvalPathsPatches || {}).reduce(
(acc: any, evalPath: string) => {
//for object paths which have a "." in the object key like "a.['b.c']", we need to extract these
// paths and break them to appropriate patch paths
const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH);
if (!matches || !matches.length) {
//regular paths like "a.b.c"
acc[evalPath] = identicalEvalPathsPatches[evalPath];
return acc;
}
const [, firstSeg, nestedPathSeg] = matches;
// normalise non nested paths like "a.['b']"
if (!nestedPathSeg.includes(".")) {
const key = [firstSeg, nestedPathSeg].join(".");
acc[key] = identicalEvalPathsPatches[evalPath];
return acc;
}
// object paths which have a "." like "a.['b.c']"
const key = [firstSeg, `['${nestedPathSeg}']`].join(".");
acc[key] = identicalEvalPathsPatches[evalPath];
return acc;
},
{},
);
const getReducedDataTree = (
dataTree: DataTree,
constrainedDiffPaths: string[],
): DataTree => {
const withErrors = Object.keys(dataTree).reduce((acc: any, key: string) => {
const widgetValue = dataTree[key] as WidgetEntity;
acc[key] = {
__evaluation__: {
errors: widgetValue.__evaluation__?.errors,
},
};
return acc;
}, {});
return constrainedDiffPaths.reduce((acc: DataTree, key: string) => {
set(acc, key, get(dataTree, key));
return acc;
}, withErrors);
};
const generateDiffUpdates = (
oldDataTree: any,
dataTree: any,
ignoreLargeKeys: any,
): DiffWithReferenceState[] => {
const attachDirectly: DiffWithReferenceState[] = [];
const ignoreLargeKeysHasBeenAttached = new Set();
const attachLater: DiffWithReferenceState[] = [];
oldDataTree: DataTree,
dataTree: DataTree,
constrainedDiffPaths: string[],
): Diff<DataTree, DataTree>[] => {
const attachDirectly: Diff<DataTree, DataTree>[] = [];
const attachLater: Diff<DataTree, DataTree>[] = [];
// we are reducing the data tree to only the paths that are being diffed
const oldData = getReducedDataTree(oldDataTree, constrainedDiffPaths);
const newData = getReducedDataTree(dataTree, constrainedDiffPaths);
const updates =
diff(oldDataTree, dataTree, (path, key) => {
diff(oldData, newData, (path, key) => {
if (!path.length || key === "__evaluation__") return false;
const { path: setPath, segmentedPath } = generateWithKey(path, key);
const segmentedPath = [...path, key];
// if ignore path is present...this segment of code generates the data compression patches
if (!!ignoreLargeKeys[setPath]) {
const originalStateVal = get(oldDataTree, segmentedPath);
const correspondingStatePath = ignoreLargeKeys[setPath];
const statePathValue = get(dataTree, correspondingStatePath);
if (!equal(originalStateVal, statePathValue)) {
//reference state patches are a patch that does not have a patch value but it provides a path which contains the same value
//this is helpful in making the payload sent to the main thread small
attachLater.push({
kind: "referenceState",
path: segmentedPath,
referencePath: correspondingStatePath,
});
}
ignoreLargeKeysHasBeenAttached.add(setPath);
return true;
}
const rhs = get(dataTree, segmentedPath);
const rhs = get(dataTree, segmentedPath) as DataTree;
const lhs = get(oldDataTree, segmentedPath);
const lhs = get(oldDataTree, segmentedPath) as DataTree;
//when a moment value changes we do not want the inner moment object updates, we just want the ISO result of it
// which we get during the serialisation process we perform at latter steps
@ -250,7 +215,6 @@ const generateDiffUpdates = (
if (lhs !== undefined) {
attachDirectly.push({ kind: "D", lhs, path: segmentedPath });
}
// if the lhs is also undefined ignore diff on this node
return true;
}
@ -280,20 +244,107 @@ const generateDiffUpdates = (
return [...updates, ...largeDataSetUpdates];
};
const correctUndefinedUpdatesToDeletesOrNew = (
updates: Diff<DataTree, DataTree>[],
) =>
updates.reduce(
(acc, update) => {
const { kind, lhs, path, rhs } = update as any;
if (kind === "E") {
if (lhs === undefined && rhs !== undefined) {
acc.push({ kind: "N", path, rhs });
}
if (lhs !== undefined && rhs === undefined) {
acc.push({ path, lhs, kind: "D" });
}
if (lhs !== undefined && rhs !== undefined) {
acc.push(update);
}
return acc;
}
acc.push(update);
return acc;
},
[] as Diff<DataTree, DataTree>[],
);
// whenever an element in a collection is set to undefined, we need to send the entire collection as an update
const generateRootWidgetUpdates = (
updates: Diff<DataTree, DataTree>[],
newDataTree: DataTree,
oldDataTree: DataTree,
): Diff<DataTree, DataTree>[] =>
updates
.filter(
(v) =>
v.kind === "D" &&
v.path &&
typeof v.path[v.path.length - 1] === "number",
)
.map(
({ path }: any) => {
const pathCopy = [...path];
pathCopy.pop();
return {
kind: "E",
path: pathCopy,
lhs: get(oldDataTree, pathCopy) as DataTree,
rhs: get(newDataTree, pathCopy) as DataTree,
}; //push the parent path
},
[] as Diff<DataTree, DataTree>[],
);
// when a root collection is updated, we need to scrub out updates that are inside the root collection
const getScrubbedOutUpdatesWhenRootCollectionIsUpdated = (
updates: Diff<DataTree, DataTree>[],
rootCollectionUpdates: Diff<DataTree, DataTree>[],
) => {
const rootCollectionPaths = rootCollectionUpdates
.filter((update) => update?.path?.length)
.map((update) => (update.path as string[]).join("."));
return (
updates
.map((update: any) => ({ update, condensedPath: update.path.join(".") }))
.filter(
({ condensedPath }) =>
!rootCollectionPaths.some((p) => condensedPath.startsWith(p)),
)
// remove the condensedPath from the update
.map(({ update }) => update)
);
};
export const generateOptimisedUpdates = (
oldDataTree: any,
dataTree: any,
identicalEvalPathsPatches?: Record<string, string>,
): DiffWithReferenceState[] => {
const ignoreLargeKeys = normaliseEvalPath(identicalEvalPathsPatches);
const updates = generateDiffUpdates(oldDataTree, dataTree, ignoreLargeKeys);
return updates;
oldDataTree: DataTree,
dataTree: DataTree,
// these are the paths that the diff is limited to, this is a performance optimisation and through this we don't have to diff the entire data tree
constrainedDiffPaths: string[],
): Diff<DataTree, DataTree>[] => {
const updates = generateDiffUpdates(
oldDataTree,
dataTree,
constrainedDiffPaths,
);
const correctedUpdates = correctUndefinedUpdatesToDeletesOrNew(updates);
const rootCollectionUpdates = generateRootWidgetUpdates(
correctedUpdates,
dataTree,
oldDataTree,
);
const scrubedOutUpdates = getScrubbedOutUpdatesWhenRootCollectionIsUpdated(
correctedUpdates,
rootCollectionUpdates,
);
return [...scrubedOutUpdates, ...rootCollectionUpdates];
};
export const generateSerialisedUpdates = (
prevState: any,
currentState: any,
identicalEvalPathsPatches: any,
prevState: DataTree,
currentState: DataTree,
constrainedDiffPaths: string[],
mergeAdditionalUpdates?: any,
): {
serialisedUpdates: string;
error?: { type: string; message: string };
@ -301,13 +352,14 @@ export const generateSerialisedUpdates = (
const updates = generateOptimisedUpdates(
prevState,
currentState,
identicalEvalPathsPatches,
constrainedDiffPaths,
);
//remove lhs from diff to reduce the size of diff upload,
//it is not necessary to send lhs and we can make the payload to transfer to the main thread smaller for quicker transfer
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const removedLhs = updates.map(({ lhs, ...rest }: any) => rest);
let removedLhs = updates.map(({ lhs, ...rest }: any) => rest);
removedLhs = [...removedLhs, ...(mergeAdditionalUpdates || [])];
try {
// serialise bigInt values and convert the updates to a string over here to minismise the cost of transfer
@ -325,16 +377,16 @@ export const generateSerialisedUpdates = (
};
export const generateOptimisedUpdatesAndSetPrevState = (
dataTree: any,
dataTree: DataTree,
dataTreeEvaluator: any,
constrainedDiffPaths: string[],
mergeAdditionalUpdates?: any,
) => {
const identicalEvalPathsPatches =
dataTreeEvaluator?.getEvalPathsIdenticalToState();
const { error, serialisedUpdates } = generateSerialisedUpdates(
dataTreeEvaluator.getPrevState(),
dataTree,
identicalEvalPathsPatches,
constrainedDiffPaths,
mergeAdditionalUpdates,
);
if (error) {

View File

@ -114,7 +114,6 @@ import {
import { getFixedTimeDifference, replaceThisDotParams } from "./utils";
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
import {
setToEvalPathsIdenticalToState,
validateActionProperty,
validateAndParseWidgetProperty,
validateWidgetProperty,
@ -134,7 +133,6 @@ export interface EvalProps {
[entityName: string]: DataTreeEvaluationProps;
}
export type EvalPathsIdenticalToState = Record<string, string>;
export default class DataTreeEvaluator {
dependencyMap: DependencyMap = new DependencyMap();
sortedDependencies: SortedDependencies = [];
@ -164,9 +162,6 @@ export default class DataTreeEvaluator {
* Sanitized eval values and errors
*/
evalProps: EvalProps = {};
//when attaching values to __evaluations__ segment of the state there are cases where this value is identical to the widget property
//in those cases do not it to the dataTree and update this map. The main thread can decompress these updates and we can minimise the data transfer
evalPathsIdenticalToState: EvalPathsIdenticalToState = {};
undefinedEvalValuesMap: Record<string, boolean> = {};
prevState = {};
@ -187,9 +182,6 @@ export default class DataTreeEvaluator {
this.widgetConfigMap = widgetConfigMap;
}
getEvalPathsIdenticalToState(): EvalPathsIdenticalToState {
return this.evalPathsIdenticalToState || {};
}
getEvalTree() {
return this.evalTree;
}
@ -343,12 +335,11 @@ export default class DataTreeEvaluator {
dataTree: DataTree,
option: {
evalProps: EvalProps;
evalPathsIdenticalToState: EvalPathsIdenticalToState;
},
configTree: ConfigTree,
) {
const unParsedEvalTree = this.getUnParsedEvalTree();
const { evalPathsIdenticalToState, evalProps } = option;
const { evalProps } = option;
for (const [entityName, entity] of Object.entries(dataTree)) {
if (!isWidget(entity)) continue;
const entityConfig = configTree[entityName] as WidgetEntityConfig;
@ -363,33 +354,15 @@ export default class DataTreeEvaluator {
get(entity, propertyPath),
);
// Pass it through parse
const { isValid, messages, parsed, transformed } =
validateWidgetProperty(validationConfig, value, entity, propertyPath);
const { isValid, messages, parsed } = validateWidgetProperty(
validationConfig,
value,
entity,
propertyPath,
);
set(entity, propertyPath, parsed);
const evaluatedValue = isValid
? parsed
: isUndefined(transformed)
? value
: transformed;
const isParsedValueTheSame = parsed === evaluatedValue;
const evalPath = getEvalValuePath(fullPropertyPath, {
isPopulated: false,
fullPath: true,
});
setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState,
evalProps,
isParsedValueTheSame,
fullPropertyPath,
value: evaluatedValue,
});
resetValidationErrorsForEntityProperty({
evalProps,
fullPropertyPath,
@ -442,7 +415,6 @@ export default class DataTreeEvaluator {
evaluatedTree,
{
evalProps: this.evalProps,
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
},
this.oldConfigTree,
);
@ -1096,7 +1068,6 @@ export default class DataTreeEvaluator {
evalPropertyValue,
unEvalPropertyValue,
evalProps: this.evalProps,
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
});
parsedValue = this.getParsedValueForWidgetProperty({
@ -1155,16 +1126,6 @@ export default class DataTreeEvaluator {
if (!propertyPath) continue;
const evalPath = getEvalValuePath(fullPropertyPath);
setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
evalProps: this.evalProps,
isParsedValueTheSame: true,
fullPropertyPath,
value: evalPropertyValue,
});
/**
* Perf optimization very specific to handling actions
* Fields like Api1.data doesn't get evaluated since it is not in dynamicBindingPathList
@ -1206,35 +1167,11 @@ export default class DataTreeEvaluator {
? prevEvaluatedValue
: evalPropertyValue;
const evalPath = getEvalValuePath(fullPropertyPath, {
isPopulated: true,
//what is the purpose of this argument
fullPath: true,
});
/** Variables defined in a JS object are not reactive.
* Their evaluated values need to be reset only when the variable is modified by the user.
* When uneval value of a js variable hasn't changed, it means that the previously evaluated values are in both trees already */
if (skipVariableValueAssignment) {
setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
evalProps: this.evalProps,
isParsedValueTheSame: true,
fullPropertyPath,
value: evalValue,
});
} else {
if (!skipVariableValueAssignment) {
const valueForSafeTree = klona(evalValue);
setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState: this.evalPathsIdenticalToState,
evalProps: this.evalProps,
isParsedValueTheSame: true,
fullPropertyPath,
value: valueForSafeTree,
});
set(contextTree, fullPropertyPath, evalValue);
set(safeTree, fullPropertyPath, valueForSafeTree);
JSObjectCollection.setVariableValue(evalValue, fullPropertyPath);

View File

@ -5,10 +5,8 @@ import type {
WidgetEntityConfig,
} from "@appsmith/entities/DataTree/types";
import type { ConfigTree } from "entities/DataTree/dataTreeTypes";
import { isObject, isUndefined, set } from "lodash";
import type { EvaluationError } from "utils/DynamicBindingUtils";
import {
getEvalValuePath,
isPathDynamicTrigger,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
@ -18,47 +16,11 @@ import {
resetValidationErrorsForEntityProperty,
} from "@appsmith/workers/Evaluation/evaluationUtils";
import { validate } from "workers/Evaluation/validations";
import type { EvalPathsIdenticalToState, EvalProps } from ".";
import type { EvalProps } from ".";
import type { ValidationResponse } from "constants/WidgetValidation";
const LARGE_COLLECTION_SIZE = 100;
const getIsLargeCollection = (val: any) => {
if (!Array.isArray(val)) return false;
const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length;
const size = val.length * rowSize;
return size > LARGE_COLLECTION_SIZE;
};
export function setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState,
evalProps,
fullPropertyPath,
isParsedValueTheSame,
value,
}: {
evalPath: string;
evalPathsIdenticalToState: EvalPathsIdenticalToState;
evalProps: EvalProps;
isParsedValueTheSame: boolean;
fullPropertyPath: string;
value: unknown;
}) {
const isLargeCollection = getIsLargeCollection(value);
if (isParsedValueTheSame && isLargeCollection) {
evalPathsIdenticalToState[evalPath] = fullPropertyPath;
} else {
delete evalPathsIdenticalToState[evalPath];
set(evalProps, evalPath, value);
}
}
export function validateAndParseWidgetProperty({
configTree,
evalPathsIdenticalToState,
evalPropertyValue,
evalProps,
fullPropertyPath,
@ -71,7 +33,6 @@ export function validateAndParseWidgetProperty({
evalPropertyValue: unknown;
unEvalPropertyValue: string;
evalProps: EvalProps;
evalPathsIdenticalToState: EvalPathsIdenticalToState;
}): unknown {
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
@ -82,26 +43,20 @@ export function validateAndParseWidgetProperty({
const widgetConfig = configTree[widget.widgetName] as WidgetEntityConfig;
const validation = widgetConfig.validationPaths[propertyPath];
const { isValid, messages, parsed, transformed } = validateWidgetProperty(
const { isValid, messages, parsed } = validateWidgetProperty(
validation,
evalPropertyValue,
widget,
propertyPath,
);
let evaluatedValue;
// remove already present validation errors
resetValidationErrorsForEntityProperty({
evalProps,
fullPropertyPath,
});
if (isValid) {
evaluatedValue = parsed;
} else {
evaluatedValue = isUndefined(transformed) ? evalPropertyValue : transformed;
if (!isValid) {
const evalErrors: EvaluationError[] =
messages?.map((message) => {
return {
@ -120,21 +75,6 @@ export function validateAndParseWidgetProperty({
});
}
const evalPath = getEvalValuePath(fullPropertyPath, {
isPopulated: false,
fullPath: true,
});
const isParsedValueTheSame = parsed === evaluatedValue;
setToEvalPathsIdenticalToState({
evalPath,
evalPathsIdenticalToState,
evalProps,
isParsedValueTheSame,
fullPropertyPath,
value: evaluatedValue,
});
return parsed;
}