Skip to content

Commit f5405c1

Browse files
authored
Merge pull request #41060 from appsmithorg/release
30/06 Daily Promotion
2 parents f041ba1 + f379f16 commit f5405c1

File tree

5 files changed

+616
-142
lines changed

5 files changed

+616
-142
lines changed

app/client/src/ce/workers/Evaluation/evaluationUtils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
WidgetEntity,
3030
DataTreeEntityConfig,
3131
WidgetEntityConfig,
32+
ActionEntityConfig,
3233
} from "ee/entities/DataTree/types";
3334
import type { EvalProps } from "workers/common/DataTreeEvaluator";
3435
import { validateWidgetProperty } from "workers/common/DataTreeEvaluator/validationUtils";
@@ -403,6 +404,16 @@ export function isAction(
403404
);
404405
}
405406

407+
export function isActionConfig(
408+
entity: DataTreeEntityConfig,
409+
): entity is ActionEntityConfig {
410+
return (
411+
typeof entity === "object" &&
412+
"ENTITY_TYPE" in entity &&
413+
entity.ENTITY_TYPE === ENTITY_TYPE.ACTION
414+
);
415+
}
416+
406417
export function isAppsmithEntity(
407418
entity: DataTreeEntity,
408419
): entity is AppsmithEntity {
@@ -413,7 +424,9 @@ export function isAppsmithEntity(
413424
);
414425
}
415426

416-
export function isJSAction(entity: DataTreeEntity): entity is JSActionEntity {
427+
export function isJSAction(
428+
entity: Partial<DataTreeEntity>,
429+
): entity is JSActionEntity {
417430
return (
418431
typeof entity === "object" &&
419432
"ENTITY_TYPE" in entity &&

app/client/src/entities/DependencyMap/DependencyMapUtils.ts

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import {
44
entityTypeCheckForPathDynamicTrigger,
55
getEntityNameAndPropertyPath,
66
IMMEDIATE_PARENT_REGEX,
7+
isActionConfig,
8+
isJSActionConfig,
79
} from "ee/workers/Evaluation/evaluationUtils";
810
import type { ConfigTree } from "entities/DataTree/dataTreeTypes";
911
import { isPathDynamicTrigger } from "utils/DynamicBindingUtils";
12+
import { WorkerEnv } from "workers/Evaluation/handlers/workerEnv";
13+
import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes";
1014

1115
type SortDependencies =
1216
| {
@@ -23,6 +27,9 @@ export class DependencyMapUtils {
2327
): SortDependencies {
2428
const dependencyTree: Array<[string, string | undefined]> = [];
2529
const dependencies = dependencyMap.rawDependencies;
30+
const featureFlags = WorkerEnv.getFeatureFlags();
31+
const isReactiveActionsEnabled =
32+
featureFlags.release_reactive_actions_enabled;
2633

2734
for (const [node, deps] of dependencies.entries()) {
2835
if (deps.size) {
@@ -38,7 +45,7 @@ export class DependencyMapUtils {
3845
.reverse()
3946
.filter((edge) => !!edge);
4047

41-
if (configTree) {
48+
if (configTree && isReactiveActionsEnabled) {
4249
this.detectReactiveDependencyMisuse(dependencyMap, configTree);
4350
}
4451

@@ -163,81 +170,86 @@ export class DependencyMapUtils {
163170
) {
164171
const dependencies = dependencyMap.rawDependencies;
165172

166-
// Helper function to get all transitive dependencies
167-
const getAllTransitiveDependencies = (node: string): Set<string> => {
168-
const allDeps = new Set<string>();
169-
const queue = [node];
173+
for (const node of dependencies.keys()) {
174+
const { entityName: nodeName } = getEntityNameAndPropertyPath(node);
175+
const nodeConfig = configTree[nodeName];
170176

171-
while (queue.length > 0) {
172-
const current = queue.shift()!;
173-
const deps = dependencyMap.getDirectDependencies(current) || [];
177+
const isJSActionEntity = isJSActionConfig(nodeConfig);
178+
const isActionEntity = isActionConfig(nodeConfig);
174179

175-
for (const dep of deps) {
176-
if (!allDeps.has(dep)) {
177-
allDeps.add(dep);
178-
queue.push(dep);
179-
}
180-
}
180+
if (isJSActionEntity) {
181+
// Only continue if at least one function is automatic
182+
const hasAutomaticFunc = Object.values(nodeConfig.meta).some(
183+
(jsFunction) =>
184+
jsFunction.runBehaviour === ActionRunBehaviour.AUTOMATIC,
185+
);
186+
187+
if (!hasAutomaticFunc) continue;
188+
} else if (isActionEntity) {
189+
// Only continue if runBehaviour is AUTOMATIC
190+
if (nodeConfig.runBehaviour !== ActionRunBehaviour.AUTOMATIC) continue;
191+
} else {
192+
// If not a JSAction, or Action, skip
193+
continue;
181194
}
182195

183-
return allDeps;
184-
};
196+
// For each entity, check if both .run and a .data path are present
197+
let hasRun = false;
198+
let hasData = false;
199+
let dataPath = "";
200+
let runPath = "";
185201

186-
for (const [node, deps] of dependencies.entries()) {
187-
// Get all dependencies including transitive ones
188-
const allDeps = new Set<string>();
189-
const queue = Array.from(deps);
202+
const transitiveDeps = this.getAllTransitiveDependencies(
203+
dependencyMap,
204+
node,
205+
);
190206

191-
while (queue.length > 0) {
192-
const dep = queue.shift()!;
207+
for (const dep of transitiveDeps) {
208+
if (this.isTriggerPath(dep, configTree)) {
209+
hasRun = true;
210+
runPath = dep;
211+
}
193212

194-
if (!allDeps.has(dep)) {
195-
allDeps.add(dep);
196-
const depDeps = dependencyMap.getDirectDependencies(dep) || [];
213+
if (this.isDataPath(dep)) {
214+
hasData = true;
215+
dataPath = dep;
216+
}
197217

198-
queue.push(...depDeps);
218+
if (hasRun && hasData) {
219+
throw Object.assign(
220+
new Error(
221+
`Reactive dependency misuse: '${node}' depends on both trigger path '${runPath}' and data path '${dataPath}' from the same entity. This can cause unexpected reactivity.`,
222+
),
223+
{ node, triggerPath: runPath, dataPath },
224+
);
199225
}
200226
}
227+
}
228+
}
201229

202-
// Separate dependencies into trigger paths and data paths
203-
const triggerPaths = Array.from(deps).filter((dep) =>
204-
this.isTriggerPath(dep, configTree),
205-
);
206-
const dataPaths = Array.from(deps).filter((dep) => this.isDataPath(dep));
207-
208-
// For each trigger path, check if there's a data path from the same entity
209-
for (const triggerPath of triggerPaths) {
210-
const triggerEntity = triggerPath.split(".")[0];
211-
212-
// Find data paths from the same entity
213-
const sameEntityDataPaths = dataPaths.filter((dataPath) => {
214-
const dataEntity = dataPath.split(".")[0];
215-
216-
return dataEntity === triggerEntity;
217-
});
218-
219-
if (sameEntityDataPaths.length > 0) {
220-
// Check if any of these data paths depend on the trigger path (directly or indirectly)
221-
for (const dataPath of sameEntityDataPaths) {
222-
const dataPathTransitiveDeps =
223-
getAllTransitiveDependencies(dataPath);
224-
225-
if (dataPathTransitiveDeps.has(triggerPath)) {
226-
const error = new Error(
227-
`Reactive dependency misuse: '${node}' depends on both trigger path '${triggerPath}' and data path '${dataPath}' from the same entity, and '${dataPath}' depends on '${triggerPath}' (directly or indirectly). This can cause unexpected reactivity.`,
228-
);
229-
230-
// Add custom properties
231-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
232-
// @ts-ignore
233-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
234-
(error as any).node = node;
235-
236-
throw error;
237-
}
238-
}
230+
/**
231+
* Returns all transitive dependencies (direct and indirect, no duplicates) for a given node.
232+
*/
233+
static getAllTransitiveDependencies(
234+
dependencyMap: DependencyMap,
235+
node: string,
236+
): string[] {
237+
const dependencies = dependencyMap.rawDependencies;
238+
const visited = new Set<string>();
239+
240+
function traverse(current: string) {
241+
const directDeps = dependencies.get(current) || new Set<string>();
242+
243+
for (const dep of directDeps) {
244+
if (!visited.has(dep)) {
245+
visited.add(dep);
246+
traverse(dep);
239247
}
240248
}
241249
}
250+
251+
traverse(node);
252+
253+
return Array.from(visited);
242254
}
243255
}

0 commit comments

Comments
 (0)