@@ -4,9 +4,13 @@ import {
44 entityTypeCheckForPathDynamicTrigger ,
55 getEntityNameAndPropertyPath ,
66 IMMEDIATE_PARENT_REGEX ,
7+ isActionConfig ,
8+ isJSActionConfig ,
79} from "ee/workers/Evaluation/evaluationUtils" ;
810import type { ConfigTree } from "entities/DataTree/dataTreeTypes" ;
911import { isPathDynamicTrigger } from "utils/DynamicBindingUtils" ;
12+ import { WorkerEnv } from "workers/Evaluation/handlers/workerEnv" ;
13+ import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes" ;
1014
1115type 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