From f2a6341c5831e084bb829d646e5b49ba12cf8581 Mon Sep 17 00:00:00 2001 From: Vemparala Surya Vamsi <121419957+vsvamsi1@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:15:24 +0530 Subject: [PATCH] fix: evalTrigger mutation fix (#34106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This fixes a gap in our evaluation flow where we were not sending evaluation updates during an evaluation in the evalTrigger. We have resolved that by sending updates in the evalTrigger, also we have created a separate function called evaluateAndGenerateWebWorkerResponse which unifies the logic between sending updates in evalTrigger as well as evalTreeWithChanges. We have added several unit test cases in this PR to test the evaluation flow. Fixes #33823 > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.All" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 8b7bc93e3d1a8ce93c722a94c8846f9359d40686 > Cypress dashboard. > Tags: `` ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **Refactor** - Improved the evaluation and update process for data tree structures with new helper functions and interfaces. - Enhanced error handling with optional chaining in `setPrevState` function. - **New Features** - Introduced `evaluateAndPushResponse`, `evaluateAndGenerateResponse`, and `getAffectedNodesInTheDataTree` functions for better data tree evaluation and updates. - Added `UpdateTreeResponse` interface for structured update responses. - **Bug Fixes** - Adjusted error handling in the evaluation process to ensure robustness. --- .../Evaluation/__tests__/evaluation.test.ts | 6 +- .../Evaluation/evalTreeWithChanges.test.ts | 532 ++++++++++++++++++ .../workers/Evaluation/evalTreeWithChanges.ts | 211 ++++--- .../Evaluation/handlers/evalTrigger.ts | 12 +- app/client/src/workers/Evaluation/helpers.ts | 4 +- app/client/src/workers/Evaluation/types.ts | 6 + 6 files changed, 682 insertions(+), 89 deletions(-) create mode 100644 app/client/src/workers/Evaluation/evalTreeWithChanges.test.ts diff --git a/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts b/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts index 0caf123f7e..e68382f9cb 100644 --- a/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts +++ b/app/client/src/workers/Evaluation/__tests__/evaluation.test.ts @@ -27,7 +27,7 @@ jest.mock("klona/full", () => ({ }, })); -const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { +export const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { CONTAINER_WIDGET: { defaultProperties: {}, derivedProperties: {}, @@ -231,7 +231,7 @@ const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { }, }; -const BASE_WIDGET = { +export const BASE_WIDGET = { widgetId: "randomID", widgetName: "randomWidgetName", bottomRow: 0, @@ -249,7 +249,7 @@ const BASE_WIDGET = { meta: {}, } as unknown as WidgetEntity; -const BASE_WIDGET_CONFIG = { +export const BASE_WIDGET_CONFIG = { logBlackList: {}, widgetId: "randomID", type: "SKELETON_WIDGET", diff --git a/app/client/src/workers/Evaluation/evalTreeWithChanges.test.ts b/app/client/src/workers/Evaluation/evalTreeWithChanges.test.ts new file mode 100644 index 0000000000..cee6048f50 --- /dev/null +++ b/app/client/src/workers/Evaluation/evalTreeWithChanges.test.ts @@ -0,0 +1,532 @@ +import type { WidgetEntityConfig } from "@appsmith/entities/DataTree/types"; +import { DataTreeDiffEvent } from "@appsmith/workers/Evaluation/evaluationUtils"; +import { RenderModes } from "constants/WidgetConstants"; +import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import type { ConfigTree } from "entities/DataTree/dataTreeTypes"; +import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; +import produce from "immer"; +import type { WidgetEntity } from "plugins/Linting/lib/entity/WidgetEntity"; +import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas"; +import DataTreeEvaluator from "workers/common/DataTreeEvaluator"; +import * as evalTreeWithChanges from "./evalTreeWithChanges"; +export const BASE_WIDGET = { + widgetId: "randomID", + widgetName: "randomWidgetName", + bottomRow: 0, + isLoading: false, + leftColumn: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + renderMode: RenderModes.CANVAS, + rightColumn: 0, + topRow: 0, + type: "SKELETON_WIDGET", + parentId: "0", + version: 1, + ENTITY_TYPE: ENTITY_TYPE.WIDGET, + meta: {}, +} as unknown as WidgetEntity; + +export const BASE_WIDGET_CONFIG = { + logBlackList: {}, + widgetId: "randomID", + type: "SKELETON_WIDGET", + ENTITY_TYPE: ENTITY_TYPE.WIDGET, +} as unknown as WidgetEntityConfig; + +const WIDGET_CONFIG_MAP = { + TEXT_WIDGET: { + defaultProperties: {}, + derivedProperties: { + value: "{{ this.text }}", + }, + metaProperties: {}, + }, +}; + +const configTree: ConfigTree = { + Text1: generateDataTreeWidget( + { + ...BASE_WIDGET_CONFIG, + ...BASE_WIDGET, + widgetName: "Text1", + text: "Label", + type: "TEXT_WIDGET", + } as any, + {}, + new Set(), + ).configEntity, + Text2: generateDataTreeWidget( + { + ...BASE_WIDGET_CONFIG, + ...BASE_WIDGET, + widgetName: "Text2", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: "TEXT_WIDGET", + } as any, + {}, + new Set(), + ).configEntity, +}; + +const unEvalTree = { + Text1: generateDataTreeWidget( + { + ...BASE_WIDGET_CONFIG, + ...BASE_WIDGET, + widgetName: "Text1", + text: "Label", + type: "TEXT_WIDGET", + } as any, + {}, + new Set(), + ).unEvalEntity, + Text2: generateDataTreeWidget( + { + ...BASE_WIDGET_CONFIG, + ...BASE_WIDGET, + widgetName: "Text2", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: "TEXT_WIDGET", + } as any, + {}, + new Set(), + ).unEvalEntity, +}; + +describe("evaluateAndPushResponse", () => { + let pushResponseToMainThreadMock: any; + beforeAll(() => { + pushResponseToMainThreadMock = jest + .spyOn(evalTreeWithChanges, "pushResponseToMainThread") + .mockImplementation(() => {}); // spy on foo + }); + beforeAll(() => { + jest.clearAllMocks(); + }); + test("should call pushResponseToMainThread when we evaluate and push updates", () => { + evalTreeWithChanges.evaluateAndPushResponse( + undefined, + { + unEvalUpdates: [], + evalOrder: [], + jsUpdates: {}, + }, + [], + [], + ); + // check if push response has been called + expect(pushResponseToMainThreadMock).toHaveBeenCalled(); + }); +}); + +describe("getAffectedNodesInTheDataTree", () => { + test("should merge paths from unEvalUpdates and evalOrder", () => { + const result = evalTreeWithChanges.getAffectedNodesInTheDataTree( + [ + { + event: DataTreeDiffEvent.NOOP, + payload: { + propertyPath: "Text2.text", + value: "", + }, + } as any, + ], + ["Text1.text"], + ); + expect(result).toEqual(["Text2.text", "Text1.text"]); + }); + test("should extract unique paths from unEvalUpdates and evalOrder", () => { + const result = evalTreeWithChanges.getAffectedNodesInTheDataTree( + [ + { + event: DataTreeDiffEvent.NOOP, + payload: { + propertyPath: "Text1.text", + value: "", + }, + }, + ], + ["Text1.text"], + ); + expect(result).toEqual(["Text1.text"]); + }); +}); +describe("evaluateAndGenerateResponse", () => { + let evaluator: DataTreeEvaluator; + const UPDATED_LABEL = "updated Label"; + + const getParsedUpdatesFromWebWorkerResp = ( + webworkerResponse: UpdateDataTreeMessageData, + ) => { + const updates = JSON.parse(webworkerResponse.workerResponse.updates); + //scrub out all __evaluation__ patches + return updates.filter((p: any) => !p.rhs.__evaluation__); + }; + beforeEach(() => { + evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP); + evaluator.setupFirstTree(unEvalTree, configTree); + evaluator.evalAndValidateFirstTree(); + }); + + test("inital evaluation successful should be successful", () => { + expect(evaluator.evalTree).toHaveProperty("Text2.text", "Label"); + }); + + test("should respond with default values when dataTreeEvaluator is not provided", () => { + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + undefined, + { + unEvalUpdates: [], + evalOrder: [], + jsUpdates: {}, + }, + [], + [], + ); + const parsedUpdates = getParsedUpdatesFromWebWorkerResp(webworkerResponse); + + expect(parsedUpdates).toEqual([]); + expect(webworkerResponse).toEqual({ + unevalTree: {}, + workerResponse: { + dependencies: {}, + errors: [], + evalMetaUpdates: [], + evaluationOrder: [], + isCreateFirstTree: false, + isNewWidgetAdded: false, + jsUpdates: {}, + jsVarsCreatedEvent: [], + logs: [], + removedPaths: [], + staleMetaIds: [], + unEvalUpdates: [], + undefinedEvalValuesMap: {}, + updates: "[]", + }, + }); + }); + test("should generate no updates when the updateTreeResponse is empty", () => { + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + { + unEvalUpdates: [], + evalOrder: [], + jsUpdates: {}, + }, + [], + [], + ); + const parsedUpdates = getParsedUpdatesFromWebWorkerResp(webworkerResponse); + + expect(parsedUpdates).toEqual([]); + }); + test("should send the new unevalTree in the web worker response", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + expect(evaluator.getOldUnevalTree()).toEqual(unEvalTree); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + // the new unevalTree gets set in setupUpdateTree + expect(evaluator.getOldUnevalTree()).toEqual(updatedLabelUnevalTree); + + const { unevalTree } = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + expect(unevalTree).toEqual(updatedLabelUnevalTree); + }); + + describe("updates", () => { + test("should generate updates based on the unEvalUpdates", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + draft.Text1.text = UPDATED_LABEL; + draft.Text1.label = UPDATED_LABEL; + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + // ignore label Text1.label uneval update and just include Text1.text uneval update + updateTreeResponse.unEvalUpdates = [ + { + event: DataTreeDiffEvent.NOOP, + payload: { + propertyPath: "Text1.text", + value: "", + }, + }, + ] as any; + // the eval tree should have the uneval update but the diff should not be generated because the unEvalUpdates has been altered + expect(evaluator.evalTree).toHaveProperty("Text1.text", UPDATED_LABEL); + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + // Text1.label update should be ignored + expect(parsedUpdates).not.toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text1", "label"], + rhs: UPDATED_LABEL, + }, + ]), + ); + }); + test("should generate updates based on the evalOrder", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + draft.Text1.text = UPDATED_LABEL; + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + // ignore label Text1.label uneval update and just include Text1.text uneval update + // expect(updateTreeResponse.evalOrder).toEqual([]); + updateTreeResponse.evalOrder = []; + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + + // Text1.label update should be ignored + expect(parsedUpdates).not.toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text2", "text"], + rhs: "updated Label", + }, + ]), + ); + }); + test("should generate the correct updates to be sent to the main thread's state when the value tied to a binding changes ", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + expect(parsedUpdates).toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text1", "text"], + rhs: "updated Label", + }, + { + kind: "N", + path: ["Text2", "text"], + rhs: "updated Label", + }, + ]), + ); + + expect(evaluator.evalTree).toHaveProperty("Text2.text", UPDATED_LABEL); + }); + test("should merge additional updates to the dataTree as well as push the updates back to the main thread's state when unEvalUpdates is ignored", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + //set the unEvalUpdates is empty so that evaluation ignores diffing the node + updateTreeResponse.unEvalUpdates = []; + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + ["Text1.text"], + ); + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + expect(parsedUpdates).toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text1", "text"], + rhs: UPDATED_LABEL, + }, + ]), + ); + + expect(evaluator.evalTree).toHaveProperty("Text1.text", UPDATED_LABEL); + }); + }); + + describe("evalMetaUpdates", () => { + test("should add metaUpdates in the webworker's response", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const response = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + const metaUpdates = [ + { + widgetId: unEvalTree.Text1.widgetId, + metaPropertyPath: ["someMetaValuePath"], + value: "someValue", + }, + ]; + const { workerResponse } = + evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + response, + metaUpdates, + [], + ); + + expect(workerResponse.evalMetaUpdates).toEqual(metaUpdates); + }); + test("should sanitise metaUpdates in the webworker's response and strip out non serialisable properties", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const response = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + const metaUpdates = [ + { + widgetId: unEvalTree.Text1.widgetId, + metaPropertyPath: ["someMetaValuePath"], + value: function () {}, + }, + ]; + const { workerResponse } = + evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + response, + metaUpdates, + [], + ); + + // the function properties should be stripped out + expect(workerResponse.evalMetaUpdates).toEqual([ + { + widgetId: unEvalTree.Text1.widgetId, + metaPropertyPath: ["someMetaValuePath"], + }, + ]); + }); + }); + + describe("unEvalUpdates", () => { + test("should add unEvalUpdates to the web worker response", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + expect(webworkerResponse.workerResponse.unEvalUpdates).toEqual([ + { + event: DataTreeDiffEvent.NOOP, + payload: { propertyPath: "Text1.text", value: "" }, + }, + ]); + expect(parsedUpdates).toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text1", "text"], + rhs: UPDATED_LABEL, + }, + ]), + ); + }); + test("should ignore generating updates when unEvalUpdates is empty", () => { + const updatedLabelUnevalTree = produce(unEvalTree, (draft: any) => { + if (draft.Text1?.text) { + draft.Text1.text = UPDATED_LABEL; + } + }); + const updateTreeResponse = evaluator.setupUpdateTree( + updatedLabelUnevalTree, + configTree, + ); + //set the evalOrder is empty so that evaluation ignores diffing the node + updateTreeResponse.unEvalUpdates = []; + + const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse( + evaluator, + updateTreeResponse, + [], + [], + ); + const parsedUpdates = + getParsedUpdatesFromWebWorkerResp(webworkerResponse); + + expect(parsedUpdates).not.toEqual( + expect.arrayContaining([ + { + kind: "N", + path: ["Text1", "text"], + rhs: UPDATED_LABEL, + }, + ]), + ); + }); + }); +}); diff --git a/app/client/src/workers/Evaluation/evalTreeWithChanges.ts b/app/client/src/workers/Evaluation/evalTreeWithChanges.ts index 8835a78d48..9618a43a37 100644 --- a/app/client/src/workers/Evaluation/evalTreeWithChanges.ts +++ b/app/client/src/workers/Evaluation/evalTreeWithChanges.ts @@ -1,105 +1,58 @@ -import type { DataTree, UnEvalTree } from "entities/DataTree/dataTreeTypes"; import { dataTreeEvaluator } from "./handlers/evalTree"; -import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils"; import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types"; -import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils"; import { makeEntityConfigsAsObjProperties } from "@appsmith/workers/Evaluation/dataTreeUtils"; -import type { EvalTreeResponseData } from "./types"; +import type { EvalTreeResponseData, UpdateTreeResponse } from "./types"; 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, getNewDataTreeUpdates, uniqueOrderUpdatePaths, } from "./helpers"; +import type DataTreeEvaluator from "workers/common/DataTreeEvaluator"; +import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils"; + +const getDefaultEvalResponse = (): EvalTreeResponseData => ({ + updates: "[]", + dependencies: {}, + errors: [], + evalMetaUpdates: [], + evaluationOrder: [], + jsUpdates: {}, + logs: [], + unEvalUpdates: [], + isCreateFirstTree: false, + staleMetaIds: [], + removedPaths: [], + isNewWidgetAdded: false, + undefinedEvalValuesMap: {}, + jsVarsCreatedEvent: [], +}); export function evalTreeWithChanges( updatedValuePaths: string[][], metaUpdates: EvalMetaUpdates = [], ) { - let evalOrder: string[] = []; - let jsUpdates: Record = {}; - let unEvalUpdates: DataTreeDiff[] = []; - const isCreateFirstTree = false; - let dataTree: DataTree = {}; - const errors: EvalError[] = []; - const logs: any[] = []; - const dependencies: DependencyMap = {}; - let evalMetaUpdates: EvalMetaUpdates = [...metaUpdates]; - let staleMetaIds: string[] = []; - const removedPaths: Array<{ entityId: string; fullpath: string }> = []; - let unevalTree: UnEvalTree = {}; - + let setupUpdateTreeResponse = {} as UpdateTreeResponse; if (dataTreeEvaluator) { - const setupUpdateTreeResponse = + setupUpdateTreeResponse = dataTreeEvaluator.setupUpdateTreeWithDifferences(updatedValuePaths); - - evalOrder = setupUpdateTreeResponse.evalOrder; - unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates; - jsUpdates = setupUpdateTreeResponse.jsUpdates; - - const updateResponse = dataTreeEvaluator.evalAndValidateSubTree( - evalOrder, - dataTreeEvaluator.oldConfigTree, - unEvalUpdates, - ); - - dataTree = makeEntityConfigsAsObjProperties(dataTreeEvaluator.evalTree, { - evalProps: dataTreeEvaluator.evalProps, - }); - - /** Make sure evalMetaUpdates is sanitized to prevent postMessage failure */ - evalMetaUpdates = JSON.parse( - JSON.stringify([...evalMetaUpdates, ...updateResponse.evalMetaUpdates]), - ); - - staleMetaIds = updateResponse.staleMetaIds; - unevalTree = dataTreeEvaluator.getOldUnevalTree(); } - const allUnevalUpdates = unEvalUpdates.map( - (update) => update.payload.propertyPath, - ); - const completeEvalOrder = uniqueOrderUpdatePaths([ - ...allUnevalUpdates, - ...evalOrder, - ]); - const setterAndLocalStorageUpdates = getNewDataTreeUpdates( - uniqueOrderUpdatePaths(updatedValuePaths.map((val) => val.join("."))), - dataTree, + const setterAndLocalStorageUpdatePaths = uniqueOrderUpdatePaths( + updatedValuePaths.map((val) => val.join(".")), ); - const updates = generateOptimisedUpdatesAndSetPrevState( - dataTree, + evaluateAndPushResponse( dataTreeEvaluator, - completeEvalOrder, - setterAndLocalStorageUpdates, + setupUpdateTreeResponse, + metaUpdates, + setterAndLocalStorageUpdatePaths, ); +} - const evalTreeResponse: EvalTreeResponseData = { - updates, - dependencies, - errors, - evalMetaUpdates, - evaluationOrder: evalOrder, - jsUpdates, - logs, - unEvalUpdates, - isCreateFirstTree, - staleMetaIds, - removedPaths, - isNewWidgetAdded: false, - undefinedEvalValuesMap: dataTreeEvaluator?.undefinedEvalValuesMap || {}, - jsVarsCreatedEvent: [], - }; - - const data: UpdateDataTreeMessageData = { - workerResponse: evalTreeResponse, - unevalTree, - }; - +export const pushResponseToMainThread = (data: UpdateDataTreeMessageData) => { sendMessage.call(self, { messageType: MessageType.DEFAULT, body: { @@ -107,4 +60,106 @@ export function evalTreeWithChanges( method: MAIN_THREAD_ACTION.UPDATE_DATATREE, }, }); -} +}; + +export const getAffectedNodesInTheDataTree = ( + unEvalUpdates: DataTreeDiff[], + evalOrder: string[], +) => { + const allUnevalUpdates = unEvalUpdates.map( + (update) => update.payload.propertyPath, + ); + // merge unevalUpdate paths and evalOrder paths + return uniqueOrderUpdatePaths([...allUnevalUpdates, ...evalOrder]); +}; + +export const evaluateAndPushResponse = ( + dataTreeEvaluator: DataTreeEvaluator | undefined, + setupUpdateTreeResponse: UpdateTreeResponse, + metaUpdates: EvalMetaUpdates, + additionalPathsAddedAsUpdates: string[], +) => { + const response = evaluateAndGenerateResponse( + dataTreeEvaluator, + setupUpdateTreeResponse, + metaUpdates, + additionalPathsAddedAsUpdates, + ); + return pushResponseToMainThread(response); +}; + +export const evaluateAndGenerateResponse = ( + dataTreeEvaluator: DataTreeEvaluator | undefined, + setupUpdateTreeResponse: UpdateTreeResponse, + metaUpdates: EvalMetaUpdates, + additionalPathsAddedAsUpdates: string[], +): UpdateDataTreeMessageData => { + // generate default response first and later add updates to it + const defaultResponse = getDefaultEvalResponse(); + + if (!dataTreeEvaluator) { + const updates = generateOptimisedUpdatesAndSetPrevState( + {}, + dataTreeEvaluator, + [], + ); + defaultResponse.updates = updates; + defaultResponse.evalMetaUpdates = [...(metaUpdates || [])]; + return { + workerResponse: defaultResponse, + unevalTree: {}, + }; + } + + const { evalOrder, jsUpdates, unEvalUpdates } = setupUpdateTreeResponse; + defaultResponse.evaluationOrder = evalOrder; + defaultResponse.unEvalUpdates = unEvalUpdates; + defaultResponse.jsUpdates = jsUpdates; + + const updateResponse = dataTreeEvaluator.evalAndValidateSubTree( + evalOrder, + dataTreeEvaluator.oldConfigTree, + unEvalUpdates, + ); + + const dataTree = makeEntityConfigsAsObjProperties( + dataTreeEvaluator.evalTree, + { + evalProps: dataTreeEvaluator.evalProps, + }, + ); + + /** Make sure evalMetaUpdates is sanitized to prevent postMessage failure */ + defaultResponse.evalMetaUpdates = JSON.parse( + JSON.stringify([...(metaUpdates || []), ...updateResponse.evalMetaUpdates]), + ); + + defaultResponse.staleMetaIds = updateResponse.staleMetaIds; + const unevalTree = dataTreeEvaluator.getOldUnevalTree(); + + // when additional paths are required to be added as updates, we extract the updates from the data tree using these paths. + const additionalUpdates = getNewDataTreeUpdates( + additionalPathsAddedAsUpdates, + dataTree, + ); + // the affected paths is a combination of the eval order and the uneval updates + // we use this collection to limit the diff between the old and new data tree + const affectedNodePaths = getAffectedNodesInTheDataTree( + unEvalUpdates, + evalOrder, + ); + + defaultResponse.updates = generateOptimisedUpdatesAndSetPrevState( + dataTree, + dataTreeEvaluator, + affectedNodePaths, + additionalUpdates, + ); + dataTreeEvaluator.undefinedEvalValuesMap = + dataTreeEvaluator.undefinedEvalValuesMap || {}; + + return { + workerResponse: defaultResponse, + unevalTree, + }; +}; diff --git a/app/client/src/workers/Evaluation/handlers/evalTrigger.ts b/app/client/src/workers/Evaluation/handlers/evalTrigger.ts index 836e82bc67..4893ef9c0a 100644 --- a/app/client/src/workers/Evaluation/handlers/evalTrigger.ts +++ b/app/client/src/workers/Evaluation/handlers/evalTrigger.ts @@ -1,6 +1,7 @@ import { dataTreeEvaluator } from "./evalTree"; import type { EvalWorkerASyncRequest } from "../types"; import ExecutionMetaData from "../fns/utils/ExecutionMetaData"; +import { evaluateAndPushResponse } from "../evalTreeWithChanges"; export default async function (request: EvalWorkerASyncRequest) { const { data } = request; @@ -26,14 +27,13 @@ export default async function (request: EvalWorkerASyncRequest) { //TODO: the evalTrigger can be optimised to not diff all JS actions { isAllAffected: true, ids: [] }, ); - - dataTreeEvaluator.evalAndValidateSubTree( - evalOrder, - unEvalTree.configTree, - unEvalUpdates, + evaluateAndPushResponse( + dataTreeEvaluator, + { evalOrder, unEvalUpdates, jsUpdates: {} }, + [], + [], ); } - return dataTreeEvaluator.evaluateTriggers( dynamicTrigger, dataTreeEvaluator.getEvalTree(), diff --git a/app/client/src/workers/Evaluation/helpers.ts b/app/client/src/workers/Evaluation/helpers.ts index 0276ed8f2b..c0199046a1 100644 --- a/app/client/src/workers/Evaluation/helpers.ts +++ b/app/client/src/workers/Evaluation/helpers.ts @@ -383,13 +383,13 @@ export const generateOptimisedUpdatesAndSetPrevState = ( mergeAdditionalUpdates?: any, ) => { const { error, serialisedUpdates } = generateSerialisedUpdates( - dataTreeEvaluator.getPrevState(), + dataTreeEvaluator?.getPrevState() || {}, dataTree, constrainedDiffPaths, mergeAdditionalUpdates, ); - if (error) { + if (error && dataTreeEvaluator?.errors) { dataTreeEvaluator.errors.push(error); } dataTreeEvaluator?.setPrevState(dataTree); diff --git a/app/client/src/workers/Evaluation/types.ts b/app/client/src/workers/Evaluation/types.ts index e151e520eb..42169630fa 100644 --- a/app/client/src/workers/Evaluation/types.ts +++ b/app/client/src/workers/Evaluation/types.ts @@ -63,3 +63,9 @@ export interface EvalTreeResponseData { webworkerTelemetry?: Record; updates: string; } + +export interface UpdateTreeResponse { + unEvalUpdates: DataTreeDiff[]; + evalOrder: string[]; + jsUpdates: Record; +}