chore: optimise first evaluation by adding worker scope cache (CE) (#38068)

## Description
- Add evalContextCache to reduce the number of times the evalContext is created from the contextTree
- remove resetWorkerGlobalScope execution before every evalSync
- Instantiate the evalWorker in src/index.tsx


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/12295048843>
> Commit: 04b1e859b02282ba9efa96aea25acc9f20098061
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12295048843&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 12 Dec 2024 12:21:04 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 Features**
- Introduced a new property `actionNames` to enhance action tracking
within JavaScript action entities.
- Added a new feature flag `release_evaluation_scope_cache` for improved
feature management.
- Implemented a new function `isPropertyAnEntityAction` to identify
action properties in JavaScript entities.
- Enhanced the `loadAppEntities` method to improve JavaScript library
loading processes.
- Updated the evaluation context initialization process to utilize
`getDataTreeContext`.
- Expanded the `WIDGET_CONFIG_MAP` to include detailed configurations
for various widget types.

- **Bug Fixes**
- Enhanced error handling for unsafe function calls in evaluation logic.
- Improved error handling and logging for library installation and
uninstallation processes.

- **Tests**
- Expanded test coverage for action bindings and dependencies in the
`DataTreeEvaluator`.
- Updated tests to validate the new `actionNames` property and its
integration.
- Modified tests to ensure correct functionality of the `evaluateSync`
function.
- Added new test cases to assess the behavior of the evaluator with
widget updates.

- **Chores**
- Streamlined imports and initialization of worker instances across
various files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Diljit 2024-12-13 09:44:19 +05:30 committed by GitHub
parent 4ea5021e88
commit abb0878388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 201 additions and 89 deletions

View File

@ -191,6 +191,7 @@ describe("generateDataTreeJSAction", () => {
myVar1: "SMART_SUBSTITUTE", myVar1: "SMART_SUBSTITUTE",
myVar2: "SMART_SUBSTITUTE", myVar2: "SMART_SUBSTITUTE",
}, },
actionNames: new Set(["myFun2", "myFun1"]),
}; };
const resultData = generateDataTreeJSAction(jsCollection); const resultData = generateDataTreeJSAction(jsCollection);
@ -389,6 +390,7 @@ describe("generateDataTreeJSAction", () => {
myVar1: "SMART_SUBSTITUTE", myVar1: "SMART_SUBSTITUTE",
myVar2: "SMART_SUBSTITUTE", myVar2: "SMART_SUBSTITUTE",
}, },
actionNames: new Set(["myFun2", "myFun1"]),
}; };
const result = generateDataTreeJSAction(jsCollection); const result = generateDataTreeJSAction(jsCollection);

View File

@ -46,7 +46,7 @@ export const generateDataTreeJSAction = (
const dependencyMap: DependencyMap = {}; const dependencyMap: DependencyMap = {};
dependencyMap["body"] = []; dependencyMap["body"] = [];
const actions = js.config.actions; const actions = js.config.actions || [];
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const actionsData: Record<string, any> = {}; const actionsData: Record<string, any> = {};
@ -89,6 +89,7 @@ export const generateDataTreeJSAction = (
dynamicBindingPathList: dynamicBindingPathList, dynamicBindingPathList: dynamicBindingPathList,
variables: listVariables, variables: listVariables,
dependencyMap: dependencyMap, dependencyMap: dependencyMap,
actionNames: new Set(actions.map((action) => action.name)),
}, },
}; };
}; };

View File

@ -96,6 +96,7 @@ export interface JSActionEntityConfig extends EntityConfig {
moduleId?: string; moduleId?: string;
moduleInstanceId?: string; moduleInstanceId?: string;
isPublic?: boolean; isPublic?: boolean;
actionNames: Set<string>;
} }
export interface JSActionEntity { export interface JSActionEntity {

View File

@ -43,6 +43,7 @@ export const FEATURE_FLAG = {
"release_table_custom_loading_state_enabled", "release_table_custom_loading_state_enabled",
release_custom_widget_ai_builder: "release_custom_widget_ai_builder", release_custom_widget_ai_builder: "release_custom_widget_ai_builder",
ab_request_new_integration_enabled: "ab_request_new_integration_enabled", ab_request_new_integration_enabled: "ab_request_new_integration_enabled",
release_evaluation_scope_cache: "release_evaluation_scope_cache",
release_table_html_column_type_enabled: release_table_html_column_type_enabled:
"release_table_html_column_type_enabled", "release_table_html_column_type_enabled",
} as const; } as const;
@ -83,6 +84,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_table_custom_loading_state_enabled: false, release_table_custom_loading_state_enabled: false,
release_custom_widget_ai_builder: false, release_custom_widget_ai_builder: false,
ab_request_new_integration_enabled: false, ab_request_new_integration_enabled: false,
release_evaluation_scope_cache: false,
release_table_html_column_type_enabled: false, release_table_html_column_type_enabled: false,
}; };

View File

@ -40,8 +40,7 @@ export enum ExecutionType {
/** /**
* This method returns new dataTree with entity function and platform function * This method returns new dataTree with entity function and platform function
*/ */
export const addDataTreeToContext = (args: { export const getDataTreeContext = (args: {
EVAL_CONTEXT: EvalContext;
dataTree: Readonly<DataTree>; dataTree: Readonly<DataTree>;
removeEntityFunctions?: boolean; removeEntityFunctions?: boolean;
isTriggerBased: boolean; isTriggerBased: boolean;
@ -50,10 +49,11 @@ export const addDataTreeToContext = (args: {
const { const {
configTree, configTree,
dataTree, dataTree,
EVAL_CONTEXT,
isTriggerBased, isTriggerBased,
removeEntityFunctions = false, removeEntityFunctions = false,
} = args; } = args;
const EVAL_CONTEXT: EvalContext = {};
const dataTreeEntries = Object.entries(dataTree); const dataTreeEntries = Object.entries(dataTree);
const entityFunctionCollection: Record<string, Record<string, Function>> = {}; const entityFunctionCollection: Record<string, Record<string, Function>> = {};
@ -95,16 +95,23 @@ export const addDataTreeToContext = (args: {
); );
} }
if (removeEntityFunctions) if (removeEntityFunctions) {
return removeEntityFunctionsFromEvalContext( removeEntityFunctionsFromEvalContext(
entityFunctionCollection, entityFunctionCollection,
EVAL_CONTEXT, EVAL_CONTEXT,
); );
if (!isTriggerBased) return; return EVAL_CONTEXT;
}
if (!isTriggerBased) {
return EVAL_CONTEXT;
}
// if eval is not trigger based i.e., sync eval then we skip adding entity function to evalContext // if eval is not trigger based i.e., sync eval then we skip adding entity function to evalContext
addEntityFunctionsToEvalContext(EVAL_CONTEXT, entityFunctionCollection); addEntityFunctionsToEvalContext(EVAL_CONTEXT, entityFunctionCollection);
return EVAL_CONTEXT;
}; };
export const addEntityFunctionsToEvalContext = ( export const addEntityFunctionsToEvalContext = (

View File

@ -1104,6 +1104,19 @@ export const isNotEntity = (entity: DataTreeEntity) => {
export const isEntityAction = (entity: DataTreeEntity) => { export const isEntityAction = (entity: DataTreeEntity) => {
return isAction(entity); return isAction(entity);
}; };
export const isPropertyAnEntityAction = (
entity: DataTreeEntity,
propertyPath: string,
entityConfig: DataTreeEntityConfig,
) => {
if (!isJSAction(entity)) return false;
const { actionNames } = entityConfig as JSActionEntityConfig;
return actionNames.has(propertyPath);
};
export const convertMicroDiffToDeepDiff = ( export const convertMicroDiffToDeepDiff = (
microDiffDifferences: Difference[], microDiffDifferences: Difference[],
): Diff<unknown, unknown>[] => ): Diff<unknown, unknown>[] =>

View File

@ -25,7 +25,6 @@ import {
waitForSegmentInit, waitForSegmentInit,
waitForFetchUserSuccess, waitForFetchUserSuccess,
} from "ee/sagas/userSagas"; } from "ee/sagas/userSagas";
import { waitForFetchEnvironments } from "ee/sagas/EnvironmentSagas";
import { fetchJSCollectionsForView } from "actions/jsActionActions"; import { fetchJSCollectionsForView } from "actions/jsActionActions";
import { import {
fetchAppThemesAction, fetchAppThemesAction,
@ -154,14 +153,6 @@ export default class AppViewerEngine extends AppEngine {
yield call(waitForSegmentInit, true); yield call(waitForSegmentInit, true);
endSpan(waitForSegmentSpan); endSpan(waitForSegmentSpan);
const waitForEnvironmentsSpan = startNestedSpan(
"AppViewerEngine.waitForFetchEnvironments",
rootSpan,
);
yield call(waitForFetchEnvironments);
endSpan(waitForEnvironmentsSpan);
yield put(fetchAllPageEntityCompletion([executePageLoadActions()])); yield put(fetchAllPageEntityCompletion([executePageLoadActions()]));
endSpan(loadAppEntitiesSpan); endSpan(loadAppEntitiesSpan);

View File

@ -1,5 +1,7 @@
// This file must be executed as early as possible to ensure the preloads are triggered ASAP // This file must be executed as early as possible to ensure the preloads are triggered ASAP
import "./preload-route-chunks"; import "./preload-route-chunks";
// Initialise eval worker instance
import "utils/workerInstances";
import React from "react"; import React from "react";
import "./wdyr"; import "./wdyr";

View File

@ -103,7 +103,8 @@ import log from "loglevel";
import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse"; import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse";
import type { AppState } from "ee/reducers"; import type { AppState } from "ee/reducers";
import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "ee/constants/ApiConstants"; import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "ee/constants/ApiConstants";
import { evaluateActionBindings, evalWorker } from "sagas/EvaluationsSaga"; import { evaluateActionBindings } from "sagas/EvaluationsSaga";
import { evalWorker } from "utils/workerInstances";
import { isBlobUrl, parseBlobUrl } from "utils/AppsmithUtils"; import { isBlobUrl, parseBlobUrl } from "utils/AppsmithUtils";
import { getType, Types } from "utils/TypeHelpers"; import { getType, Types } from "utils/TypeHelpers";
import { matchPath } from "react-router"; import { matchPath } from "react-router";

View File

@ -5,7 +5,7 @@ import { showToastOnExecutionError } from "sagas/ActionExecution/errorUtils";
import { setUserCurrentGeoLocation } from "actions/browserRequestActions"; import { setUserCurrentGeoLocation } from "actions/browserRequestActions";
import type { Channel } from "redux-saga"; import type { Channel } from "redux-saga";
import { channel } from "redux-saga"; import { channel } from "redux-saga";
import { evalWorker } from "sagas/EvaluationsSaga"; import { evalWorker } from "utils/workerInstances";
import type { import type {
TGetGeoLocationDescription, TGetGeoLocationDescription,
TWatchGeoLocationDescription, TWatchGeoLocationDescription,

View File

@ -12,10 +12,10 @@ import type { TMessage } from "utils/MessageUtil";
import { MessageType } from "utils/MessageUtil"; import { MessageType } from "utils/MessageUtil";
import type { ResponsePayload } from "../sagas/EvaluationsSaga"; import type { ResponsePayload } from "../sagas/EvaluationsSaga";
import { import {
evalWorker,
executeTriggerRequestSaga, executeTriggerRequestSaga,
updateDataTreeHandler, updateDataTreeHandler,
} from "../sagas/EvaluationsSaga"; } from "../sagas/EvaluationsSaga";
import { evalWorker } from "utils/workerInstances";
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga"; import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
import type { EvalTreeResponseData } from "workers/Evaluation/types"; import type { EvalTreeResponseData } from "workers/Evaluation/types";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";

View File

@ -2,8 +2,8 @@ import {
defaultAffectedJSObjects, defaultAffectedJSObjects,
evalQueueBuffer, evalQueueBuffer,
evaluateTreeSaga, evaluateTreeSaga,
evalWorker,
} from "./EvaluationsSaga"; } from "./EvaluationsSaga";
import { evalWorker } from "utils/workerInstances";
import { expectSaga } from "redux-saga-test-plan"; import { expectSaga } from "redux-saga-test-plan";
import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions"; import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
import { select } from "redux-saga/effects"; import { select } from "redux-saga/effects";

View File

@ -25,7 +25,7 @@ import {
import { getMetaWidgets, getWidgets, getWidgetsMeta } from "sagas/selectors"; import { getMetaWidgets, getWidgets, getWidgetsMeta } from "sagas/selectors";
import type { WidgetTypeConfigMap } from "WidgetProvider/factory"; import type { WidgetTypeConfigMap } from "WidgetProvider/factory";
import WidgetFactory from "WidgetProvider/factory"; import WidgetFactory from "WidgetProvider/factory";
import { GracefulWorkerService } from "utils/WorkerUtil"; import { evalWorker } from "utils/workerInstances";
import type { EvalError, EvaluationError } from "utils/DynamicBindingUtils"; import type { EvalError, EvaluationError } from "utils/DynamicBindingUtils";
import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils"; import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions"; import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
@ -120,18 +120,6 @@ import { getInstanceId } from "ee/selectors/tenantSelectors";
const APPSMITH_CONFIGS = getAppsmithConfigs(); const APPSMITH_CONFIGS = getAppsmithConfigs();
export const evalWorker = new GracefulWorkerService(
new Worker(
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
{
type: "module",
// Note: the `Worker` part of the name is slightly important LinkRelPreload_spec.js
// relies on it to find workers in the list of all requests.
name: "evalWorker",
},
),
);
let widgetTypeConfigMap: WidgetTypeConfigMap; let widgetTypeConfigMap: WidgetTypeConfigMap;
export function* updateDataTreeHandler( export function* updateDataTreeHandler(
@ -902,5 +890,3 @@ export default function* evaluationSagaListeners() {
} }
} }
} }
export { evalWorker as EvalWorker };

View File

@ -21,7 +21,7 @@ import { getCurrentApplicationId } from "selectors/editorSelectors";
import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService"; import CodemirrorTernService from "utils/autocomplete/CodemirrorTernService";
import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions"; import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
import { validateResponse } from "./ErrorSagas"; import { validateResponse } from "./ErrorSagas";
import { EvalWorker } from "./EvaluationsSaga"; import { evalWorker as EvalWorker } from "utils/workerInstances";
import log from "loglevel"; import log from "loglevel";
import { APP_MODE } from "entities/App"; import { APP_MODE } from "entities/App";
import { getAppMode } from "ee/selectors/applicationSelectors"; import { getAppMode } from "ee/selectors/applicationSelectors";

View File

@ -41,6 +41,7 @@ import type { EvalTreeResponseData } from "workers/Evaluation/types";
import { endSpan, startRootSpan } from "UITelemetry/generateTraces"; import { endSpan, startRootSpan } from "UITelemetry/generateTraces";
import { getJSActionPathNameToDisplay } from "ee/utils/actionExecutionUtils"; import { getJSActionPathNameToDisplay } from "ee/utils/actionExecutionUtils";
import { showToastOnExecutionError } from "./ActionExecution/errorUtils"; import { showToastOnExecutionError } from "./ActionExecution/errorUtils";
import { waitForFetchEnvironments } from "ee/sagas/EnvironmentSagas";
let successfulBindingsMap: SuccessfulBindingMap | undefined; let successfulBindingsMap: SuccessfulBindingMap | undefined;
@ -190,6 +191,9 @@ export function* logSuccessfulBindings(
} }
export function* postEvalActionDispatcher(actions: Array<AnyReduxAction>) { export function* postEvalActionDispatcher(actions: Array<AnyReduxAction>) {
// Wait for environments api fetch before dispatching actions
yield call(waitForFetchEnvironments);
for (const action of actions) { for (const action of actions) {
yield put(action); yield put(action);
} }

View File

@ -0,0 +1,13 @@
import { GracefulWorkerService } from "./WorkerUtil";
export const evalWorker = new GracefulWorkerService(
new Worker(
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
{
type: "module",
// Note: the `Worker` part of the name is slightly important LinkRelPreload_spec.js
// relies on it to find workers in the list of all requests.
name: "evalWorker",
},
),
);

View File

@ -5,7 +5,7 @@ import { EvalErrorTypes, getEvalValuePath } from "utils/DynamicBindingUtils";
import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils"; import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils";
import { parseJSObject, isJSFunctionProperty } from "@shared/ast"; import { parseJSObject, isJSFunctionProperty } from "@shared/ast";
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator"; import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import evaluateSync from "workers/Evaluation/evaluate"; import { evaluateSync } from "workers/Evaluation/evaluate";
import type { DataTreeDiff } from "ee/workers/Evaluation/evaluationUtils"; import type { DataTreeDiff } from "ee/workers/Evaluation/evaluationUtils";
import { import {
DataTreeDiffEvent, DataTreeDiffEvent,

View File

@ -195,9 +195,6 @@ describe("saveResolvedFunctionsAndJSUpdates", function () {
{ {
key: "myFun2", key: "myFun2",
}, },
{
key: "myFun2",
},
], ],
bindingPaths: { bindingPaths: {
body: "SMART_SUBSTITUTE", body: "SMART_SUBSTITUTE",
@ -216,6 +213,7 @@ describe("saveResolvedFunctionsAndJSUpdates", function () {
pluginType: "JS", pluginType: "JS",
name: "JSObject1", name: "JSObject1",
actionId: "64013546b956c26882acc587", actionId: "64013546b956c26882acc587",
actionNames: new Set(["myFun1", "myFun2"]),
} as JSActionEntityConfig, } as JSActionEntityConfig,
}; };
const entityName = "JSObject1"; const entityName = "JSObject1";

View File

@ -6,7 +6,7 @@ import type { EvalContext } from "workers/Evaluation/evaluate";
import { createEvaluationContext } from "workers/Evaluation/evaluate"; import { createEvaluationContext } from "workers/Evaluation/evaluate";
import { MessageType } from "utils/MessageUtil"; import { MessageType } from "utils/MessageUtil";
import { import {
addDataTreeToContext, getDataTreeContext,
addPlatformFunctionsToEvalContext, addPlatformFunctionsToEvalContext,
} from "ee/workers/Evaluation/Actions"; } from "ee/workers/Evaluation/Actions";
import TriggerEmitter, { BatchKey } from "../fns/utils/TriggerEmitter"; import TriggerEmitter, { BatchKey } from "../fns/utils/TriggerEmitter";
@ -548,12 +548,13 @@ describe("Test addDataTreeToContext method", () => {
const evalContext: EvalContext = {}; const evalContext: EvalContext = {};
beforeAll(() => { beforeAll(() => {
addDataTreeToContext({ const EVAL_CONTEXT = getDataTreeContext({
EVAL_CONTEXT: evalContext,
dataTree: dataTree as unknown as DataTree, dataTree: dataTree as unknown as DataTree,
configTree, configTree,
isTriggerBased: true, isTriggerBased: true,
}); });
Object.assign(evalContext, EVAL_CONTEXT);
addPlatformFunctionsToEvalContext(evalContext); addPlatformFunctionsToEvalContext(evalContext);
}); });

View File

@ -1,4 +1,5 @@
import evaluate, { import {
evaluateSync,
createEvaluationContext, createEvaluationContext,
evaluateAsync, evaluateAsync,
} from "workers/Evaluation/evaluate"; } from "workers/Evaluation/evaluate";
@ -54,29 +55,29 @@ describe("evaluateSync", () => {
}); });
it("unescapes string before evaluation", () => { it("unescapes string before evaluation", () => {
const js = '\\"Hello!\\"'; const js = '\\"Hello!\\"';
const response = evaluate(js, {}, false); const response = evaluateSync(js, {}, false);
expect(response.result).toBe("Hello!"); expect(response.result).toBe("Hello!");
}); });
it("evaluate string post unescape in v1", () => { it("evaluate string post unescape in v1", () => {
const js = '[1, 2, 3].join("\\\\n")'; const js = '[1, 2, 3].join("\\\\n")';
const response = evaluate(js, {}, false); const response = evaluateSync(js, {}, false);
expect(response.result).toBe("1\n2\n3"); expect(response.result).toBe("1\n2\n3");
}); });
it("evaluate string without unescape in v2", () => { it("evaluate string without unescape in v2", () => {
self.evaluationVersion = 2; self.evaluationVersion = 2;
const js = '[1, 2, 3].join("\\n")'; const js = '[1, 2, 3].join("\\n")';
const response = evaluate(js, {}, false); const response = evaluateSync(js, {}, false);
expect(response.result).toBe("1\n2\n3"); expect(response.result).toBe("1\n2\n3");
}); });
it("throws error for undefined js", () => { it("throws error for undefined js", () => {
// @ts-expect-error: Types are not available // @ts-expect-error: Types are not available
expect(() => evaluate(undefined, {})).toThrow(TypeError); expect(() => evaluateSync(undefined, {})).toThrow(TypeError);
}); });
it("Returns for syntax errors", () => { it("Returns for syntax errors", () => {
const response1 = evaluate("wrongJS", {}, false); const response1 = evaluateSync("wrongJS", {}, false);
expect(response1).toStrictEqual({ expect(response1).toStrictEqual({
result: undefined, result: undefined,
@ -100,7 +101,7 @@ describe("evaluateSync", () => {
}, },
], ],
}); });
const response2 = evaluate("{}.map()", {}, false); const response2 = evaluateSync("{}.map()", {}, false);
expect(response2).toStrictEqual({ expect(response2).toStrictEqual({
result: undefined, result: undefined,
@ -130,21 +131,21 @@ describe("evaluateSync", () => {
}); });
it("evaluates value from data tree", () => { it("evaluates value from data tree", () => {
const js = "Input1.text"; const js = "Input1.text";
const response = evaluate(js, dataTree, false); const response = evaluateSync(js, dataTree, false);
expect(response.result).toBe("value"); expect(response.result).toBe("value");
}); });
it("disallows unsafe function calls", () => { it("disallows unsafe function calls", () => {
const js = "setImmediate(() => {}, 100)"; const js = "setImmediate(() => {}, 100)";
const response = evaluate(js, dataTree, false); const response = evaluateSync(js, dataTree, false);
expect(response).toStrictEqual({ expect(response).toStrictEqual({
result: undefined, result: undefined,
errors: [ errors: [
{ {
errorMessage: { errorMessage: {
name: "ReferenceError", name: "TypeError",
message: "setImmediate is not defined", message: "setImmediate is not a function",
}, },
errorType: "PARSE", errorType: "PARSE",
kind: { kind: {
@ -166,51 +167,51 @@ describe("evaluateSync", () => {
}); });
it("has access to extra library functions", () => { it("has access to extra library functions", () => {
const js = "_.add(1,2)"; const js = "_.add(1,2)";
const response = evaluate(js, dataTree, false); const response = evaluateSync(js, dataTree, false);
expect(response.result).toBe(3); expect(response.result).toBe(3);
}); });
it("evaluates functions with callback data", () => { it("evaluates functions with callback data", () => {
const js = "(arg1, arg2) => arg1.value + arg2"; const js = "(arg1, arg2) => arg1.value + arg2";
const callbackData = [{ value: "test" }, "1"]; const callbackData = [{ value: "test" }, "1"];
const response = evaluate(js, dataTree, false, {}, callbackData); const response = evaluateSync(js, dataTree, false, {}, callbackData);
expect(response.result).toBe("test1"); expect(response.result).toBe("test1");
}); });
it("handles EXPRESSIONS with new lines", () => { it("handles EXPRESSIONS with new lines", () => {
let js = "\n"; let js = "\n";
let response = evaluate(js, dataTree, false); let response = evaluateSync(js, dataTree, false);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
js = "\n\n\n"; js = "\n\n\n";
response = evaluate(js, dataTree, false); response = evaluateSync(js, dataTree, false);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
}); });
it("handles TRIGGERS with new lines", () => { it("handles TRIGGERS with new lines", () => {
let js = "\n"; let js = "\n";
let response = evaluate(js, dataTree, false, undefined, undefined); let response = evaluateSync(js, dataTree, false, undefined, undefined);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
js = "\n\n\n"; js = "\n\n\n";
response = evaluate(js, dataTree, false, undefined, undefined); response = evaluateSync(js, dataTree, false, undefined, undefined);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
}); });
it("handles ANONYMOUS_FUNCTION with new lines", () => { it("handles ANONYMOUS_FUNCTION with new lines", () => {
let js = "\n"; let js = "\n";
let response = evaluate(js, dataTree, false, undefined, undefined); let response = evaluateSync(js, dataTree, false, undefined, undefined);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
js = "\n\n\n"; js = "\n\n\n";
response = evaluate(js, dataTree, false, undefined, undefined); response = evaluateSync(js, dataTree, false, undefined, undefined);
expect(response.errors.length).toBe(0); expect(response.errors.length).toBe(0);
}); });
it("has access to this context", () => { it("has access to this context", () => {
const js = "this.contextVariable"; const js = "this.contextVariable";
const thisContext = { contextVariable: "test" }; const thisContext = { contextVariable: "test" };
const response = evaluate(js, dataTree, false, { thisContext }); const response = evaluateSync(js, dataTree, false, { thisContext });
expect(response.result).toBe("test"); expect(response.result).toBe("test");
// there should not be any error when accessing "this" variables // there should not be any error when accessing "this" variables
@ -220,7 +221,7 @@ describe("evaluateSync", () => {
it("has access to additional global context", () => { it("has access to additional global context", () => {
const js = "contextVariable"; const js = "contextVariable";
const globalContext = { contextVariable: "test" }; const globalContext = { contextVariable: "test" };
const response = evaluate(js, dataTree, false, { globalContext }); const response = evaluateSync(js, dataTree, false, { globalContext });
expect(response.result).toBe("test"); expect(response.result).toBe("test");
expect(response.errors).toHaveLength(0); expect(response.errors).toHaveLength(0);

View File

@ -18,7 +18,6 @@ import WidgetFactory from "WidgetProvider/factory";
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget"; import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
import { sortObjectWithArray } from "../../../utils/treeUtils"; import { sortObjectWithArray } from "../../../utils/treeUtils";
import klona from "klona"; import klona from "klona";
import { APP_MODE } from "entities/App"; import { APP_MODE } from "entities/App";
const klonaFullSpy = jest.fn(); const klonaFullSpy = jest.fn();

View File

@ -23,7 +23,7 @@ import {
PrimitiveErrorModifier, PrimitiveErrorModifier,
TypeErrorModifier, TypeErrorModifier,
} from "./errorModifier"; } from "./errorModifier";
import { addDataTreeToContext } from "ee/workers/Evaluation/Actions"; import { getDataTreeContext } from "ee/workers/Evaluation/Actions";
import { set } from "lodash"; import { set } from "lodash";
import { klona } from "klona"; import { klona } from "klona";
import { getEntityNameAndPropertyPath } from "ee/workers/Evaluation/evaluationUtils"; import { getEntityNameAndPropertyPath } from "ee/workers/Evaluation/evaluationUtils";
@ -103,7 +103,7 @@ const ignoreGlobalObjectKeys = new Set([
"location", "location",
]); ]);
function resetWorkerGlobalScope() { export function resetWorkerGlobalScope() {
const jsLibraryAccessorSet = JSLibraryAccessor.getSet(); const jsLibraryAccessorSet = JSLibraryAccessor.getSet();
for (const key of Object.keys(self)) { for (const key of Object.keys(self)) {
@ -273,14 +273,15 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
Object.assign(EVAL_CONTEXT, context.globalContext); Object.assign(EVAL_CONTEXT, context.globalContext);
} }
addDataTreeToContext({ const dataTreeContext = getDataTreeContext({
EVAL_CONTEXT,
dataTree, dataTree,
configTree, configTree,
removeEntityFunctions: !!removeEntityFunctions, removeEntityFunctions: !!removeEntityFunctions,
isTriggerBased, isTriggerBased,
}); });
Object.assign(EVAL_CONTEXT, dataTreeContext);
overrideEvalContext(EVAL_CONTEXT, context?.overrideContext); overrideEvalContext(EVAL_CONTEXT, context?.overrideContext);
return EVAL_CONTEXT; return EVAL_CONTEXT;
@ -365,7 +366,7 @@ export function setEvalContext({
Object.assign(self, evalContext); Object.assign(self, evalContext);
} }
export default function evaluateSync( export function evaluateSync(
userScript: string, userScript: string,
dataTree: DataTree, dataTree: DataTree,
isJSCollection: boolean, isJSCollection: boolean,
@ -373,7 +374,8 @@ export default function evaluateSync(
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
evalArguments?: Array<any>, evalArguments?: Array<any>,
configTree?: ConfigTree, configTree: ConfigTree = {},
evalContextCache?: EvalContext,
): EvalResult { ): EvalResult {
return (function () { return (function () {
const errors: EvaluationError[] = []; const errors: EvaluationError[] = [];
@ -394,16 +396,34 @@ export default function evaluateSync(
}; };
} }
resetWorkerGlobalScope(); self["$isDataField"] = true;
const EVAL_CONTEXT: EvalContext = {};
setEvalContext({ ///// Adding callback data
dataTree, EVAL_CONTEXT.ARGUMENTS = evalArguments;
configTree, //// Adding contextual data not part of data tree
isDataField: true, EVAL_CONTEXT.THIS_CONTEXT = context?.thisContext || {};
isTriggerBased: isJSCollection,
context, if (context?.globalContext) {
evalArguments, Object.assign(EVAL_CONTEXT, context.globalContext);
}); }
if (evalContextCache) {
Object.assign(EVAL_CONTEXT, evalContextCache);
} else {
const dataTreeContext = getDataTreeContext({
dataTree,
configTree,
removeEntityFunctions: false,
isTriggerBased: isJSCollection,
});
Object.assign(EVAL_CONTEXT, dataTreeContext);
}
overrideEvalContext(EVAL_CONTEXT, context?.overrideContext);
Object.assign(self, EVAL_CONTEXT);
try { try {
result = indirectEval(script); result = indirectEval(script);

View File

@ -15,7 +15,7 @@ import type {
import { isWidget } from "ee/workers/Evaluation/evaluationUtils"; import { isWidget } from "ee/workers/Evaluation/evaluationUtils";
import { klona } from "klona"; import { klona } from "klona";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils"; import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import evaluateSync, { setEvalContext } from "../evaluate"; import { evaluateSync, setEvalContext } from "../evaluate";
import type { DescendantWidgetMap } from "sagas/WidgetOperationUtils"; import type { DescendantWidgetMap } from "sagas/WidgetOperationUtils";
import type { MetaState } from "reducers/entityReducers/metaReducer"; import type { MetaState } from "reducers/entityReducers/metaReducer";
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";

View File

@ -1,4 +1,3 @@
import { createMessage, customJSLibraryMessages } from "ee/constants/messages";
import difference from "lodash/difference"; import difference from "lodash/difference";
import type { Def } from "tern"; import type { Def } from "tern";
import { invalidEntityIdentifiers } from "workers/common/DependencyMap/utils"; import { invalidEntityIdentifiers } from "workers/common/DependencyMap/utils";
@ -34,7 +33,7 @@ enum LibraryInstallError {
class ImportError extends Error { class ImportError extends Error {
code = LibraryInstallError.ImportError; code = LibraryInstallError.ImportError;
constructor(url: string) { constructor(url: string) {
super(createMessage(customJSLibraryMessages.IMPORT_URL_ERROR, url)); super(`The script at ${url} cannot be installed.`);
this.name = "ImportError"; this.name = "ImportError";
} }
} }
@ -42,7 +41,7 @@ class ImportError extends Error {
class TernDefinitionError extends Error { class TernDefinitionError extends Error {
code = LibraryInstallError.TernDefinitionError; code = LibraryInstallError.TernDefinitionError;
constructor(name: string) { constructor(name: string) {
super(createMessage(customJSLibraryMessages.DEFS_FAILED_ERROR, name)); super(`Failed to generate autocomplete definitions for ${name}.`);
this.name = "TernDefinitionError"; this.name = "TernDefinitionError";
} }
} }

View File

@ -1,9 +1,7 @@
import type { FeatureFlags } from "ee/entities/FeatureFlag"; import type { FeatureFlags } from "ee/entities/FeatureFlag";
export class WorkerEnv { export class WorkerEnv {
// TODO: Fix this the next time the file is edited static flags: FeatureFlags = {} as FeatureFlags;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static flags: any;
static cloudHosting: boolean; static cloudHosting: boolean;
static setFeatureFlags(featureFlags: FeatureFlags) { static setFeatureFlags(featureFlags: FeatureFlags) {

View File

@ -38,6 +38,7 @@ import type { DataTreeDiff } from "ee/workers/Evaluation/evaluationUtils";
import { import {
convertMicroDiffToDeepDiff, convertMicroDiffToDeepDiff,
getAllPathsBasedOnDiffPaths, getAllPathsBasedOnDiffPaths,
isPropertyAnEntityAction,
} from "ee/workers/Evaluation/evaluationUtils"; } from "ee/workers/Evaluation/evaluationUtils";
import { import {
@ -86,8 +87,11 @@ import {
EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_REFERENCE_REGEX,
THIS_DOT_PARAMS_KEY, THIS_DOT_PARAMS_KEY,
} from "constants/AppsmithActionConstants/ActionConstants"; } from "constants/AppsmithActionConstants/ActionConstants";
import type { EvalResult, EvaluateContext } from "workers/Evaluation/evaluate"; import {
import evaluateSync, { evaluateSync,
resetWorkerGlobalScope,
type EvalResult,
type EvaluateContext,
evaluateAsync, evaluateAsync,
setEvalContext, setEvalContext,
} from "workers/Evaluation/evaluate"; } from "workers/Evaluation/evaluate";
@ -148,6 +152,8 @@ import {
EComputationCacheName, EComputationCacheName,
type ICacheProps, type ICacheProps,
} from "../AppComputationCache/types"; } from "../AppComputationCache/types";
import { getDataTreeContext } from "ee/workers/Evaluation/Actions";
import { WorkerEnv } from "workers/Evaluation/handlers/workerEnv";
type SortedDependencies = Array<string>; type SortedDependencies = Array<string>;
export interface EvalProps { export interface EvalProps {
@ -1059,6 +1065,8 @@ export default class DataTreeEvaluator {
staleMetaIds: string[]; staleMetaIds: string[];
contextTree: DataTree; contextTree: DataTree;
} { } {
resetWorkerGlobalScope();
const safeTree = klonaJSON(unEvalTree); const safeTree = klonaJSON(unEvalTree);
const dataStore = DataStore.getDataStore(); const dataStore = DataStore.getDataStore();
const dataStoreClone = klonaJSON(dataStore); const dataStoreClone = klonaJSON(dataStore);
@ -1084,6 +1092,16 @@ export default class DataTreeEvaluator {
const { isFirstTree, metaWidgets, unevalUpdates } = options; const { isFirstTree, metaWidgets, unevalUpdates } = options;
let staleMetaIds: string[] = []; let staleMetaIds: string[] = [];
let evalContextCache;
if (WorkerEnv.flags.release_evaluation_scope_cache) {
evalContextCache = getDataTreeContext({
dataTree: contextTree,
configTree: oldConfigTree,
isTriggerBased: false,
});
}
try { try {
for (const fullPropertyPath of evaluationOrder) { for (const fullPropertyPath of evaluationOrder) {
const { entityName, propertyPath } = const { entityName, propertyPath } =
@ -1093,6 +1111,11 @@ export default class DataTreeEvaluator {
if (!isWidgetActionOrJsObject(entity)) continue; if (!isWidgetActionOrJsObject(entity)) continue;
// Skip evaluations for actions in JSObjects
if (isPropertyAnEntityAction(entity, propertyPath, entityConfig)) {
continue;
}
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let unEvalPropertyValue = get(contextTree as any, fullPropertyPath); let unEvalPropertyValue = get(contextTree as any, fullPropertyPath);
@ -1144,6 +1167,7 @@ export default class DataTreeEvaluator {
contextData, contextData,
undefined, undefined,
fullPropertyPath, fullPropertyPath,
evalContextCache,
); );
} catch (error) { } catch (error) {
this.errors.push({ this.errors.push({
@ -1209,6 +1233,13 @@ export default class DataTreeEvaluator {
set(contextTree, fullPropertyPath, parsedValue); set(contextTree, fullPropertyPath, parsedValue);
set(safeTree, fullPropertyPath, klonaJSON(parsedValue)); set(safeTree, fullPropertyPath, klonaJSON(parsedValue));
if (
WorkerEnv.flags.release_evaluation_scope_cache &&
evalContextCache
) {
set(evalContextCache, fullPropertyPath, klonaJSON(parsedValue));
}
staleMetaIds = staleMetaIds.concat( staleMetaIds = staleMetaIds.concat(
getStaleMetaStateIds({ getStaleMetaStateIds({
entity: widgetEntity, entity: widgetEntity,
@ -1254,6 +1285,18 @@ export default class DataTreeEvaluator {
set(contextTree, fullPropertyPath, evalPropertyValue); set(contextTree, fullPropertyPath, evalPropertyValue);
set(safeTree, fullPropertyPath, klonaJSON(evalPropertyValue)); set(safeTree, fullPropertyPath, klonaJSON(evalPropertyValue));
if (
WorkerEnv.flags.release_evaluation_scope_cache &&
evalContextCache
) {
set(
evalContextCache,
fullPropertyPath,
klonaJSON(evalPropertyValue),
);
}
break; break;
} }
case ENTITY_TYPE.JSACTION: { case ENTITY_TYPE.JSACTION: {
@ -1294,6 +1337,18 @@ export default class DataTreeEvaluator {
set(contextTree, fullPropertyPath, evalValue); set(contextTree, fullPropertyPath, evalValue);
set(safeTree, fullPropertyPath, valueForSafeTree); set(safeTree, fullPropertyPath, valueForSafeTree);
if (
WorkerEnv.flags.release_evaluation_scope_cache &&
evalContextCache
) {
set(
evalContextCache,
fullPropertyPath,
klonaJSON(evalPropertyValue),
);
}
JSObjectCollection.setVariableValue(evalValue, fullPropertyPath); JSObjectCollection.setVariableValue(evalValue, fullPropertyPath);
JSObjectCollection.setPrevUnEvalState({ JSObjectCollection.setPrevUnEvalState({
fullPath: fullPropertyPath, fullPath: fullPropertyPath,
@ -1306,6 +1361,17 @@ export default class DataTreeEvaluator {
default: default:
set(contextTree, fullPropertyPath, evalPropertyValue); set(contextTree, fullPropertyPath, evalPropertyValue);
set(safeTree, fullPropertyPath, klonaJSON(evalPropertyValue)); set(safeTree, fullPropertyPath, klonaJSON(evalPropertyValue));
if (
WorkerEnv.flags.release_evaluation_scope_cache &&
evalContextCache
) {
set(
evalContextCache,
fullPropertyPath,
klonaJSON(evalPropertyValue),
);
}
} }
} }
} catch (error) { } catch (error) {
@ -1417,6 +1483,7 @@ export default class DataTreeEvaluator {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
callBackData?: Array<any>, callBackData?: Array<any>,
fullPropertyPath?: string, fullPropertyPath?: string,
evalContextCache?: EvaluateContext,
) { ) {
// Get the {{binding}} bound values // Get the {{binding}} bound values
let entity: DataTreeEntity | undefined = undefined; let entity: DataTreeEntity | undefined = undefined;
@ -1467,6 +1534,7 @@ export default class DataTreeEvaluator {
!!entity && isAnyJSAction(entity), !!entity && isAnyJSAction(entity),
contextData, contextData,
callBackData, callBackData,
evalContextCache,
); );
if (fullPropertyPath && evalErrors.length) { if (fullPropertyPath && evalErrors.length) {
@ -1560,6 +1628,7 @@ export default class DataTreeEvaluator {
// TODO: Fix this the next time the file is edited // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
callbackData?: Array<any>, callbackData?: Array<any>,
evalContextCache?: EvaluateContext,
): EvalResult { ): EvalResult {
let evalResponse: EvalResult; let evalResponse: EvalResult;
@ -1574,6 +1643,8 @@ export default class DataTreeEvaluator {
isJSObject, isJSObject,
contextData, contextData,
callbackData, callbackData,
{},
evalContextCache,
); );
} catch (error) { } catch (error) {
evalResponse = { evalResponse = {

View File

@ -768,6 +768,7 @@ describe("isDataField", () => {
dependencyMap: { dependencyMap: {
body: ["myFun2", "myFun1"], body: ["myFun2", "myFun1"],
}, },
actionNames: new Set(["myFun1", "myFun2"]),
}, },
JSObject2: { JSObject2: {
actionId: "644242aeadc0936a9b0e71cc", actionId: "644242aeadc0936a9b0e71cc",
@ -821,6 +822,7 @@ describe("isDataField", () => {
dependencyMap: { dependencyMap: {
body: ["myFun2", "myFun1"], body: ["myFun2", "myFun1"],
}, },
actionNames: new Set(["myFun1", "myFun2"]),
}, },
MainContainer: { MainContainer: {
defaultProps: {}, defaultProps: {},