## Description - Using mutative instead of immer, this has reduced the main thread scripting by about 1 second. - Removed a cloneDeep during table onMount which saves about 70ms in main thread scripting. - Bypassed mutative when applying the first tree to reduce the overhead associated to mutative. Fixes #`Issue Number` _or_ Fixes `Issue URL` > [!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" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/13164792224> > Commit: 4cb821723d10198c9db70312a9604df5aa5f80c1 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13164792224&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Thu, 06 Feb 2025 04:21:41 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Dependency** - Integrated a new library to enhance overall state management. - **Refactor** - Updated state update mechanisms across interactive components and data flows. - Improved table widget processing for more consistent behavior. - **Chore** - Removed legacy development-only configuration settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
|
|
import { DataTreeDiffEvent } from "ee/workers/Evaluation/evaluationUtils";
|
|
import { RenderModes } from "constants/WidgetConstants";
|
|
import { ENTITY_TYPE } from "ee/entities/DataTree/types";
|
|
import type { ConfigTree } from "entities/DataTree/dataTreeTypes";
|
|
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
|
|
import { create } from "mutative";
|
|
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";
|
|
import { APP_MODE } from "entities/App";
|
|
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",
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any,
|
|
{},
|
|
new Set(),
|
|
).configEntity,
|
|
Text2: generateDataTreeWidget(
|
|
{
|
|
...BASE_WIDGET_CONFIG,
|
|
...BASE_WIDGET,
|
|
widgetName: "Text2",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any,
|
|
{},
|
|
new Set(),
|
|
).configEntity,
|
|
};
|
|
|
|
const unEvalTree = {
|
|
Text1: generateDataTreeWidget(
|
|
{
|
|
...BASE_WIDGET_CONFIG,
|
|
...BASE_WIDGET,
|
|
widgetName: "Text1",
|
|
text: "Label",
|
|
type: "TEXT_WIDGET",
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any,
|
|
{},
|
|
new Set(),
|
|
).unEvalEntity,
|
|
Text2: generateDataTreeWidget(
|
|
{
|
|
...BASE_WIDGET_CONFIG,
|
|
...BASE_WIDGET,
|
|
widgetName: "Text2",
|
|
text: "{{Text1.text}}",
|
|
dynamicBindingPathList: [{ key: "text" }],
|
|
type: "TEXT_WIDGET",
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any,
|
|
{},
|
|
new Set(),
|
|
).unEvalEntity,
|
|
};
|
|
|
|
describe("evaluateAndPushResponse", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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: "",
|
|
},
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} 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
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return updates.filter((p: any) => !p.rhs.__evaluation__);
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP);
|
|
await evaluator.setupFirstTree(
|
|
unEvalTree,
|
|
configTree,
|
|
{},
|
|
{
|
|
appId: "appId",
|
|
pageId: "pageId",
|
|
timestamp: "timestamp",
|
|
appMode: APP_MODE.PUBLISHED,
|
|
instanceId: "instanceId",
|
|
},
|
|
);
|
|
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({
|
|
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([]);
|
|
});
|
|
|
|
describe("updates", () => {
|
|
test("should generate updates based on the unEvalUpdates", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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: "",
|
|
},
|
|
},
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
] 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,
|
|
[],
|
|
[],
|
|
);
|
|
|
|
expect(webworkerResponse.workerResponse.dependencies).toEqual({
|
|
"Text1.text": ["Text2.text", "Text1"],
|
|
"Text2.text": ["Text2"],
|
|
});
|
|
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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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 ", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const updatedLabelUnevalTree = create(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,
|
|
},
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
});
|