@@ -4,9 +4,13 @@ import {
4
4
entityTypeCheckForPathDynamicTrigger ,
5
5
getEntityNameAndPropertyPath ,
6
6
IMMEDIATE_PARENT_REGEX ,
7
+ isActionConfig ,
8
+ isJSActionConfig ,
7
9
} from "ee/workers/Evaluation/evaluationUtils" ;
8
10
import type { ConfigTree } from "entities/DataTree/dataTreeTypes" ;
9
11
import { isPathDynamicTrigger } from "utils/DynamicBindingUtils" ;
12
+ import { WorkerEnv } from "workers/Evaluation/handlers/workerEnv" ;
13
+ import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes" ;
10
14
11
15
type SortDependencies =
12
16
| {
@@ -23,6 +27,9 @@ export class DependencyMapUtils {
23
27
) : SortDependencies {
24
28
const dependencyTree : Array < [ string , string | undefined ] > = [ ] ;
25
29
const dependencies = dependencyMap . rawDependencies ;
30
+ const featureFlags = WorkerEnv . getFeatureFlags ( ) ;
31
+ const isReactiveActionsEnabled =
32
+ featureFlags . release_reactive_actions_enabled ;
26
33
27
34
for ( const [ node , deps ] of dependencies . entries ( ) ) {
28
35
if ( deps . size ) {
@@ -38,7 +45,7 @@ export class DependencyMapUtils {
38
45
. reverse ( )
39
46
. filter ( ( edge ) => ! ! edge ) ;
40
47
41
- if ( configTree ) {
48
+ if ( configTree && isReactiveActionsEnabled ) {
42
49
this . detectReactiveDependencyMisuse ( dependencyMap , configTree ) ;
43
50
}
44
51
@@ -163,81 +170,86 @@ export class DependencyMapUtils {
163
170
) {
164
171
const dependencies = dependencyMap . rawDependencies ;
165
172
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 ] ;
170
176
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 ) ;
174
179
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 ;
181
194
}
182
195
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 = "" ;
185
201
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
+ ) ;
190
206
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
+ }
193
212
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
+ }
197
217
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
+ ) ;
199
225
}
200
226
}
227
+ }
228
+ }
201
229
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 ) ;
239
247
}
240
248
}
241
249
}
250
+
251
+ traverse ( node ) ;
252
+
253
+ return Array . from ( visited ) ;
242
254
}
243
255
}
0 commit comments