fix: Batch operations on appsmith store for performance gains (#19247)

This commit is contained in:
arunvjn 2023-01-10 10:23:08 +05:30 committed by GitHub
parent dc7582f35b
commit c8063743a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 295 additions and 213 deletions

View File

@ -1154,7 +1154,7 @@ Cypress.Commands.add("ValidatePaginationInputDataV2", () => {
Cypress.Commands.add("CheckForPageSaveError", () => {
cy.get("body").then(($ele) => {
if ($ele.find(commonlocators.saveStatusError).length) {
cy.reload()
cy.reload();
}
});
});

View File

@ -65,8 +65,7 @@ export const EVALUATE_REDUX_ACTIONS = [
// App Data
ReduxActionTypes.SET_APP_MODE,
ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
ReduxActionTypes.UPDATE_APP_STORE,
ReduxActionTypes.SET_USER_CURRENT_GEO_LOCATION,
// Widgets
ReduxActionTypes.UPDATE_LAYOUT,

View File

@ -24,7 +24,6 @@ import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsRe
import { GenerateTemplatePageRequest } from "api/PageApi";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import { Replayable } from "entities/Replay/ReplayEntity/ReplayEditor";
import { StoreValueActionDescription } from "@appsmith/entities/DataTree/actionTriggers";
export interface FetchPageListPayload {
applicationId: string;
@ -348,30 +347,12 @@ export const setAppMode = (payload: APP_MODE): ReduxAction<APP_MODE> => {
};
};
export const updateAppStoreEvaluated = (
storeValueAction?: StoreValueActionDescription["payload"],
) => ({
type: ReduxActionTypes.UPDATE_APP_STORE_EVALUATED,
payload: storeValueAction,
});
export const updateAppTransientStore = (
export const updateAppStore = (
payload: Record<string, unknown>,
storeValueAction?: StoreValueActionDescription["payload"],
): EvaluationReduxAction<Record<string, unknown>> => ({
type: ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE,
payload,
postEvalActions: [updateAppStoreEvaluated(storeValueAction)],
});
export const updateAppPersistentStore = (
payload: Record<string, unknown>,
storeValueAction?: StoreValueActionDescription["payload"],
): EvaluationReduxAction<Record<string, unknown>> => {
return {
type: ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE,
type: ReduxActionTypes.UPDATE_APP_STORE,
payload,
postEvalActions: [updateAppStoreEvaluated(storeValueAction)],
};
};

View File

@ -477,9 +477,7 @@ export const ReduxActionTypes = {
TOGGLE_PROPERTY_PANE_WIDGET_NAME_EDIT:
"TOGGLE_PROPERTY_PANE_WIDGET_NAME_EDIT",
SET_PROPERTY_PANE_WIDTH: "SET_PROPERTY_PANE_WIDTH",
UPDATE_APP_PERSISTENT_STORE: "UPDATE_APP_PERSISTENT_STORE",
UPDATE_APP_TRANSIENT_STORE: "UPDATE_APP_TRANSIENT_STORE",
UPDATE_APP_STORE_EVALUATED: "UPDATE_APP_STORE_EVALUATED",
UPDATE_APP_STORE: "UPDATE_APP_STORE",
SET_ACTION_TO_EXECUTE_ON_PAGELOAD: "SET_ACTION_TO_EXECUTE_ON_PAGELOAD",
TOGGLE_ACTION_EXECUTE_ON_LOAD_SUCCESS:
"TOGGLE_ACTION_EXECUTE_ON_LOAD_SUCCESS",

View File

@ -106,7 +106,6 @@ export type StoreValueActionDescription = ActionDescriptionInterface<
key: string;
value: string;
persist: boolean;
uniqueActionRequestId: string;
},
"STORE_VALUE"
>;

View File

@ -16,10 +16,6 @@ import {
setAppVersionOnWorkerSaga,
} from "sagas/EvaluationsSaga";
import navigateActionSaga from "sagas/ActionExecution/NavigateActionSaga";
import storeValueLocally, {
clearLocalStore,
removeLocalValue,
} from "sagas/ActionExecution/StoreActionSaga";
import downloadSaga from "sagas/ActionExecution/DownloadActionSaga";
import copySaga from "sagas/ActionExecution/CopyActionSaga";
import resetWidgetActionSaga from "sagas/ActionExecution/ResetWidgetActionSaga";
@ -93,15 +89,6 @@ export function* executeActionTriggers(
case "CLOSE_MODAL":
yield call(closeModalSaga, trigger);
break;
case "STORE_VALUE":
yield call(storeValueLocally, trigger.payload);
break;
case "REMOVE_VALUE":
yield call(removeLocalValue, trigger.payload);
break;
case "CLEAR_STORE":
yield call(clearLocalStore);
break;
case "DOWNLOAD":
yield call(downloadSaga, trigger.payload);
break;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-types */
import { DataTree, DataTreeEntity } from "entities/DataTree/dataTreeFactory";
import { set } from "lodash";
import set from "lodash/set";
import {
ActionDescription,
ActionTriggerFunctionNames,
@ -10,12 +10,12 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { isAction, isAppsmithEntity, isTrueObject } from "./evaluationUtils";
import { EvalContext } from "workers/Evaluation/evaluate";
import { ActionCalledInSyncFieldError } from "workers/Evaluation/errorModifier";
import { initStoreFns } from "workers/Evaluation/fns/storeFns";
import {
ActionDescriptionWithExecutionType,
ActionDispatcherWithExecutionType,
PLATFORM_FUNCTIONS,
} from "@appsmith/workers/Evaluation/PlatformFunctions";
declare global {
/** All identifiers added to the worker global scope should also
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
@ -226,8 +226,14 @@ export const addDataTreeToContext = (args: {
export const addPlatformFunctionsToEvalContext = (context: any) => {
for (const [funcName, fn] of platformFunctionEntries) {
context[funcName] = pusher.bind({}, fn);
Object.defineProperty(context, funcName, {
value: pusher.bind({}, fn),
enumerable: false,
writable: true,
configurable: true,
});
}
initStoreFns(context);
};
export const getAllAsyncFunctions = (dataTree: DataTree) => {
@ -242,7 +248,7 @@ export const getAllAsyncFunctions = (dataTree: DataTree) => {
}
}
for (const [name] of platformFunctionEntries) {
for (const name of Object.values(ActionTriggerFunctionNames)) {
asyncFunctionNameMap[name] = true;
}

View File

@ -26,9 +26,10 @@ export const EVAL_WORKER_ACTIONS = {
...EVAL_WORKER_ASYNC_ACTION,
};
export const MAIN_THREAD_ACTION = {
PROCESS_TRIGGER: "PROCESS_TRIGGER",
PROCESS_LOGS: "PROCESS_LOGS",
LINT_TREE: "LINT_TREE",
PROCESS_JS_FUNCTION_EXECUTION: "PROCESS_JS_FUNCTION_EXECUTION",
};
export enum MAIN_THREAD_ACTION {
PROCESS_TRIGGER = "PROCESS_TRIGGER",
PROCESS_STORE_UPDATES = "PROCESS_STORE_UPDATES",
PROCESS_LOGS = "PROCESS_LOGS",
LINT_TREE = "LINT_TREE",
PROCESS_JS_FUNCTION_EXECUTION = "PROCESS_JS_FUNCTION_EXECUTION",
}

View File

@ -154,7 +154,7 @@ export class DataTreeFactory {
...appData,
// combine both persistent and transient state with the transient state
// taking precedence in case the key is the same
store: { ...appData.store.persistent, ...appData.store.transient },
store: appData.store,
theme,
} as DataTreeAppsmith;
(dataTree.appsmith as DataTreeAppsmith).ENTITY_TYPE = ENTITY_TYPE.APPSMITH;

View File

@ -1,5 +1,5 @@
import { fetchApplication } from "actions/applicationActions";
import { setAppMode, updateAppPersistentStore } from "actions/pageActions";
import { setAppMode, updateAppStore } from "actions/pageActions";
import {
ApplicationPayload,
ReduxActionErrorTypes,
@ -69,9 +69,7 @@ export default abstract class AppEngine {
if (!apiCalls)
throw new PageNotFoundError(`Cannot find page with id: ${pageId}`);
const application: ApplicationPayload = yield select(getCurrentApplication);
yield put(
updateAppPersistentStore(getPersistentAppStore(application.id, branch)),
);
yield put(updateAppStore(getPersistentAppStore(application.id, branch)));
const toLoadPageId: string = pageId || (yield select(getDefaultPageId));
this._urlRedirect = URLGeneratorFactory.create(
application.applicationVersion,

View File

@ -23,10 +23,7 @@ export type UrlDataState = {
fullPath: string;
};
export type AppStoreState = {
transient: Record<string, unknown>;
persistent: Record<string, unknown>;
};
export type AppStoreState = Record<string, unknown>;
export type AppDataState = {
mode?: APP_MODE;
@ -55,10 +52,7 @@ const initialState: AppDataState = {
hash: "",
fullPath: "",
},
store: {
transient: {},
persistent: {},
},
store: {},
geolocation: {
canBeRequested: "geolocation" in navigator,
currentPosition: {},
@ -93,28 +87,13 @@ const appReducer = createReducer(initialState, {
URL: action.payload,
};
},
[ReduxActionTypes.UPDATE_APP_TRANSIENT_STORE]: (
[ReduxActionTypes.UPDATE_APP_STORE]: (
state: AppDataState,
action: ReduxAction<Record<string, unknown>>,
) => {
return {
...state,
store: {
...state.store,
transient: action.payload,
},
};
},
[ReduxActionTypes.UPDATE_APP_PERSISTENT_STORE]: (
state: AppDataState,
action: ReduxAction<Record<string, unknown>>,
) => {
return {
...state,
store: {
...state.store,
persistent: action.payload,
},
store: action.payload,
};
},
[ReduxActionTypes.SET_USER_CURRENT_GEO_LOCATION]: (

View File

@ -1,94 +1,62 @@
import { put, select, take } from "redux-saga/effects";
import { put, select } from "redux-saga/effects";
import { getAppStoreName } from "constants/AppConstants";
import localStorage from "utils/localStorage";
import {
updateAppPersistentStore,
updateAppTransientStore,
} from "actions/pageActions";
import { updateAppStore } from "actions/pageActions";
import AppsmithConsole from "utils/AppsmithConsole";
import { getAppStoreData } from "selectors/entitiesSelector";
import {
ClearStoreActionDescription,
RemoveValueActionDescription,
StoreValueActionDescription,
} from "@appsmith/entities/DataTree/actionTriggers";
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { AppStoreState } from "reducers/entityReducers/appReducer";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { Severity, LOG_CATEGORY } from "entities/AppsmithConsole";
import moment from "moment";
export default function* storeValueLocally(
action: StoreValueActionDescription["payload"],
) {
if (action.persist) {
const applicationId: string = yield select(getCurrentApplicationId);
const branch: string | undefined = yield select(getCurrentGitBranch);
const appStoreName = getAppStoreName(applicationId, branch);
const existingStore = localStorage.getItem(appStoreName) || "{}";
const parsedStore = JSON.parse(existingStore);
parsedStore[action.key] = action.value;
const storeString = JSON.stringify(parsedStore);
localStorage.setItem(appStoreName, storeString);
yield put(updateAppPersistentStore(parsedStore, action));
AppsmithConsole.info({
text: `store('${action.key}', '${action.value}', true)`,
});
} else {
const existingStore: AppStoreState = yield select(getAppStoreData);
const newTransientStore = {
...existingStore.transient,
[action.key]: action.value,
};
yield put(updateAppTransientStore(newTransientStore, action));
AppsmithConsole.info({
text: `store('${action.key}', '${action.value}', false)`,
});
}
/* It is possible that user calls multiple storeValue function together, in such case we need to track completion of each action separately
We use uniqueActionRequestId to differentiate each storeValueAction here.
*/
while (true) {
const returnedAction: StoreValueActionDescription = yield take(
ReduxActionTypes.UPDATE_APP_STORE_EVALUATED,
);
if (!returnedAction?.payload?.uniqueActionRequestId) {
break;
}
type StoreOperation =
| StoreValueActionDescription
| ClearStoreActionDescription
| RemoveValueActionDescription;
const { uniqueActionRequestId } = returnedAction.payload;
if (uniqueActionRequestId === action.uniqueActionRequestId) {
break;
}
}
}
export function* removeLocalValue(
action: RemoveValueActionDescription["payload"],
) {
export function* handleStoreOperations(triggers: StoreOperation[]) {
const applicationId: string = yield select(getCurrentApplicationId);
const branch: string | undefined = yield select(getCurrentGitBranch);
const appStoreName = getAppStoreName(applicationId, branch);
const existingStore = localStorage.getItem(appStoreName) || "{}";
const parsedStore = JSON.parse(existingStore);
delete parsedStore[action.key];
const storeString = JSON.stringify(parsedStore);
const existingLocalStore = localStorage.getItem(appStoreName) || "{}";
let parsedLocalStore = JSON.parse(existingLocalStore);
let currentStore: AppStoreState = yield select(getAppStoreData);
const logs: string[] = [];
for (const t of triggers) {
const { type } = t;
if (type === "STORE_VALUE") {
const { key, persist, value } = t.payload;
if (persist) {
parsedLocalStore[key] = value;
}
currentStore[key] = value;
logs.push(`storeValue('${key}', '${value}', ${persist})`);
} else if (type === "REMOVE_VALUE") {
const { key } = t.payload;
delete parsedLocalStore[key];
delete currentStore[key];
logs.push(`removeValue('${key}')`);
} else if (type === "CLEAR_STORE") {
parsedLocalStore = {};
currentStore = {};
logs.push(`clearStore()`);
}
}
yield put(updateAppStore(currentStore));
const storeString = JSON.stringify(parsedLocalStore);
localStorage.setItem(appStoreName, storeString);
yield put(updateAppPersistentStore(parsedStore));
const existingTransientStore: AppStoreState = yield select(getAppStoreData);
delete existingTransientStore.transient?.[action.key];
yield put(updateAppTransientStore(existingTransientStore.transient));
AppsmithConsole.info({
text: `remove('${action.key}')`,
});
}
export function* clearLocalStore() {
const applicationId: string = yield select(getCurrentApplicationId);
const branch: string | undefined = yield select(getCurrentGitBranch);
const appStoreName = getAppStoreName(applicationId, branch);
localStorage.setItem(appStoreName, "{}");
yield put(updateAppPersistentStore({}));
yield put(updateAppTransientStore({}));
AppsmithConsole.info({
text: `clear()`,
});
AppsmithConsole.addLogs(
logs.map((text) => ({
text,
severity: Severity.INFO,
category: LOG_CATEGORY.USER_GENERATED,
timestamp: moment().format("hh:mm:ss"),
})),
);
}

View File

@ -617,8 +617,8 @@ export function* storeLogs(
entityId: string,
) {
AppsmithConsole.addLogs(
logs.reduce((acc: Log[], log: LogObject) => {
acc.push({
logs.map((log: LogObject) => {
return {
text: createLogTitleString(log.data),
logData: log.data,
source: {
@ -629,9 +629,8 @@ export function* storeLogs(
severity: log.severity,
timestamp: log.timestamp,
category: LOG_CATEGORY.USER_GENERATED,
});
return acc;
}, []),
};
}),
);
}

View File

@ -113,6 +113,7 @@ import {
import { BatchedJSExecutionData } from "reducers/entityReducers/jsActionsReducer";
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
import { MessageType, TMessage } from "utils/MessageUtil";
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
import { ActionDescription } from "@appsmith/entities/DataTree/actionTriggers";
const evalWorker = new GracefulWorkerService(
@ -390,6 +391,9 @@ export function* handleEvalWorkerMessage(message: TMessage<any>) {
yield call(evalWorker.respond, message.messageId, result);
break;
}
case MAIN_THREAD_ACTION.PROCESS_STORE_UPDATES: {
yield call(handleStoreOperations, data);
}
}
yield call(evalErrorHandler, data?.errors || []);
}

View File

@ -316,64 +316,66 @@ describe("Add functions", () => {
const key = "some";
const value = "thing";
const persist = false;
const uniqueActionRequestId = "kjebd";
// @ts-expect-error: mockReturnValueOnce is not available on uniqueId
uniqueId.mockReturnValueOnce(uniqueActionRequestId);
expect(evalContext.storeValue(key, value, persist)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("STORE_VALUE", {
data: {
trigger: {
type: "STORE_VALUE",
payload: {
key,
value,
persist,
uniqueActionRequestId,
},
},
eventType: undefined,
},
method: "PROCESS_TRIGGER",
}),
jest.useFakeTimers();
expect(evalContext.storeValue(key, value, persist)).resolves.toStrictEqual(
{},
);
jest.runAllTimers();
expect(workerEventMock).lastCalledWith({
messageType: "DEFAULT",
body: {
data: [
{
payload: {
key: "some",
persist: false,
value: "thing",
},
type: "STORE_VALUE",
},
],
method: "PROCESS_STORE_UPDATES",
},
});
});
it("removeValue works", () => {
const key = "some";
expect(evalContext.removeValue(key)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("REMOVE_VALUE", {
data: {
trigger: {
type: "REMOVE_VALUE",
jest.useFakeTimers();
expect(evalContext.removeValue(key)).resolves.toStrictEqual({});
jest.runAllTimers();
expect(workerEventMock).lastCalledWith({
messageType: "DEFAULT",
body: {
data: [
{
payload: {
key,
},
type: "REMOVE_VALUE",
},
eventType: undefined,
},
method: "PROCESS_TRIGGER",
}),
);
],
method: "PROCESS_STORE_UPDATES",
},
});
});
it("clearStore works", () => {
expect(evalContext.clearStore()).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("CLEAR_STORE", {
data: {
trigger: {
type: "CLEAR_STORE",
jest.useFakeTimers();
expect(evalContext.clearStore()).resolves.toStrictEqual({});
jest.runAllTimers();
expect(workerEventMock).lastCalledWith({
messageType: "DEFAULT",
body: {
data: [
{
payload: null,
type: "CLEAR_STORE",
},
eventType: undefined,
},
method: "PROCESS_TRIGGER",
}),
);
],
method: "PROCESS_STORE_UPDATES",
},
});
});
it("download works", () => {

View File

@ -250,10 +250,7 @@ describe("isFunctionAsync", () => {
if (typeof testFunc === "string") {
testFunc = eval(testFunc);
}
functionDeterminer.setupEval({}, {});
addPlatformFunctionsToEvalContext(self);
const actual = functionDeterminer.isFunctionAsync(testFunc);
expect(actual).toBe(testCase.expected);
}

View File

@ -19,7 +19,6 @@ import { DOM_APIS } from "./SetupDOM";
import { JSLibraries, libraryReservedIdentifiers } from "../common/JSLibrary";
import { errorModifier, FoundPromiseInSyncEvalError } from "./errorModifier";
import { addDataTreeToContext } from "@appsmith/workers/Evaluation/Actions";
import { PLATFORM_FUNCTIONS } from "@appsmith/workers/Evaluation/PlatformFunctions";
export type EvalResult = {
result: any;
@ -83,7 +82,6 @@ function resetWorkerGlobalScope() {
continue;
if (JSLibraries.find((lib) => lib.accessor.includes(key))) continue;
if (libraryReservedIdentifiers[key]) continue;
if (PLATFORM_FUNCTIONS[key]) continue;
try {
// @ts-expect-error: Types are not available
delete self[key];

View File

@ -0,0 +1,16 @@
const _originalFetch = self.fetch;
export default function interceptAndOverrideHttpRequest() {
Object.defineProperty(self, "fetch", {
writable: false,
configurable: false,
value: function(...args: any) {
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
return;
}
const request = new Request(args[0], { ...args[1], credentials: "omit" });
return _originalFetch(request);
},
});
}

View File

@ -0,0 +1,54 @@
import {
RemoveValueActionDescription,
StoreValueActionDescription,
} from "ce/entities/DataTree/actionTriggers";
import set from "lodash/set";
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
import { addFn } from "./utils/fnGuard";
import { TriggerCollector } from "./utils/TriggerCollector";
export function initStoreFns(ctx: typeof globalThis) {
const triggerCollector = new TriggerCollector(
MAIN_THREAD_ACTION.PROCESS_STORE_UPDATES,
);
function storeValue(key: string, value: string, persist = true) {
const requestPayload: StoreValueActionDescription = {
type: "STORE_VALUE",
payload: {
key,
value,
persist,
},
};
set(self, ["appsmith", "store", key], value);
triggerCollector.collect(requestPayload);
return Promise.resolve({});
}
function removeValue(key: string) {
const requestPayload: RemoveValueActionDescription = {
type: "REMOVE_VALUE",
payload: {
key,
},
};
//@ts-expect-error no types for store
delete self.appsmith.store[key];
triggerCollector.collect(requestPayload);
return Promise.resolve({});
}
function clearStore() {
//@ts-expect-error no types for store
self.appsmith.store = {};
triggerCollector.collect({
type: "CLEAR_STORE",
payload: null,
});
return Promise.resolve({});
}
addFn(ctx, "storeValue", storeValue);
addFn(ctx, "removeValue", removeValue);
addFn(ctx, "clearStore", clearStore);
}

View File

@ -0,0 +1,45 @@
import { ActionCalledInSyncFieldError } from "../errorModifier";
import { createEvaluationContext } from "../evaluate";
import { dataTreeEvaluator } from "../handlers/evalTree";
export const _internalSetTimeout = self.setTimeout;
export const _internalClearTimeout = self.clearTimeout;
export default function overrideTimeout() {
Object.defineProperty(self, "setTimeout", {
writable: true,
configurable: true,
value: function(cb: (...args: any) => any, delay: number, ...args: any) {
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
throw new ActionCalledInSyncFieldError("setTimeout");
}
const evalContext = createEvaluationContext({
dataTree: dataTreeEvaluator?.evalTree || {},
resolvedFunctions: dataTreeEvaluator?.resolvedFunctions || {},
isTriggerBased: true,
});
return _internalSetTimeout(
function(...args: any) {
self.ALLOW_ASYNC = true;
Object.assign(self, evalContext);
cb(...args);
},
delay,
...args,
);
},
});
Object.defineProperty(self, "clearTimeout", {
writable: true,
configurable: true,
value: function(timerId: number) {
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
throw new ActionCalledInSyncFieldError("clearTimeout");
}
return _internalClearTimeout(timerId);
},
});
}

View File

@ -0,0 +1,21 @@
import { MessageType, sendMessage } from "utils/MessageUtil";
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
export class TriggerCollector {
private triggers: unknown[] = [];
constructor(private requestType: MAIN_THREAD_ACTION) {}
collect = (trigger: unknown) => {
if (this.triggers.length === 0) {
queueMicrotask(() => {
sendMessage.call(self, {
messageType: MessageType.DEFAULT,
body: {
method: this.requestType,
data: this.triggers,
},
});
this.triggers = [];
});
}
this.triggers.push(trigger);
};
}

View File

@ -0,0 +1,26 @@
import { ActionCalledInSyncFieldError } from "workers/Evaluation/errorModifier";
export function addFn(
ctx: typeof globalThis,
fnName: string,
fn: (...args: any[]) => any,
fnGuards = [isAsyncGuard],
) {
Object.defineProperty(ctx, fnName, {
value: function(...args: any[]) {
for (const guard of fnGuards) {
guard(fn, fnName);
}
return fn(...args);
},
configurable: true,
writable: true,
enumerable: false,
});
}
export function isAsyncGuard(_: (...args: any[]) => any, fnName: string) {
if (self.ALLOW_ASYNC) return;
self.IS_ASYNC = true;
throw new ActionCalledInSyncFieldError(fnName);
}

View File

@ -53,7 +53,7 @@ import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import { Severity } from "entities/AppsmithConsole";
import { JSLibraries } from "workers/common/JSLibrary";
import { MessageType, sendMessage } from "utils/MessageUtil";
import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions";
import { ActionTriggerFunctionNames } from "ce/entities/DataTree/actionTriggers";
export function getlintErrorsFromTree(
pathsToLint: string[],
@ -68,7 +68,11 @@ export function getlintErrorsFromTree(
skipEntityFunctions: true,
});
addPlatformFunctionsToEvalContext(evalContext);
const platformFnNamesMap = Object.values(ActionTriggerFunctionNames).reduce(
(acc, name) => ({ ...acc, [name]: true }),
{} as { [x: string]: boolean },
);
Object.assign(evalContext, platformFnNamesMap);
const evalContextWithOutFunctions = createEvaluationContext({
dataTree: unEvalTree,