@@ -193,6 +193,7 @@ import {
193193 type CacheInvalidateConfig ,
194194 type CachePopulateConfig ,
195195 type EntityCacheConfig ,
196+ type RequestScopedFieldConfig ,
196197} from '../../router-configuration/types' ;
197198import { printTypeNode } from '@graphql-tools/merge' ;
198199import {
@@ -205,6 +206,7 @@ import {
205206 nonExternalConditionalFieldWarning ,
206207 singleSubgraphInputFieldOneOfWarning ,
207208 unimplementedInterfaceOutputTypeWarning ,
209+ requestScopedSingleFieldWarning ,
208210} from '../warnings/warnings' ;
209211import { upsertDirectiveSchemaAndEntityDefinitions , upsertParentsAndChildren } from './walkers' ;
210212import {
@@ -357,6 +359,7 @@ import {
357359 MAX_AGE ,
358360 NEGATIVE_CACHE_TTL ,
359361 PARTIAL_CACHE_LOAD ,
362+ REQUEST_SCOPED ,
360363 SHADOW_MODE ,
361364} from '../../utils/string-constants' ;
362365import { MAX_INT32 } from '../../utils/integer-constants' ;
@@ -390,6 +393,7 @@ import {
390393 type ValidateDirectiveParams ,
391394 type CachePopulateDirectiveNode ,
392395 type EntityCacheDirectiveNode ,
396+ type RequestScopedDirectiveNode ,
393397} from './types/types' ;
394398import { newConfigurationData , newFieldSetConditionData } from '../../router-configuration/utils' ;
395399import { type ImplementationErrors , type InvalidFieldImplementation } from '../../utils/types' ;
@@ -4239,6 +4243,83 @@ export class NormalizationFactory {
42394243 } ;
42404244 }
42414245
4246+ extractRequestScopedFields ( ) {
4247+ // Gather fields annotated with @openfed__requestScoped across all types in this subgraph.
4248+ // A field is both a reader and writer of the coordinate L1 — no receiver/provider.
4249+ // Fields with the same `key` share the same L1 entry: whichever is resolved first
4250+ // populates it, subsequent ones inject from it.
4251+ type Collected = {
4252+ typeName : string ;
4253+ fieldName : string ;
4254+ fieldCoords : string ;
4255+ key : string ;
4256+ l1Key : string ;
4257+ } ;
4258+ const collected : Array < Collected > = [ ] ;
4259+
4260+ for ( const [ , parentData ] of this . parentDefinitionDataByTypeName ) {
4261+ if ( parentData . kind !== Kind . OBJECT_TYPE_DEFINITION && parentData . kind !== Kind . INTERFACE_TYPE_DEFINITION ) {
4262+ continue ;
4263+ }
4264+ const typeName = getParentTypeName ( parentData ) ;
4265+ for ( const [ fieldName , fieldData ] of parentData . fieldDataByName ) {
4266+ const directives = fieldData . directivesByName . get ( REQUEST_SCOPED ) ;
4267+ if ( ! directives ) {
4268+ continue ;
4269+ }
4270+ // validateDirectives() (run earlier in normalize()) has already guaranteed a single, non-repeated
4271+ // @openfed__requestScoped with a required String `key`, so the generic ConstDirectiveNode can be
4272+ // narrowed to the precise typed node — mirroring handleComposeDirective()/ComposeDirectiveNode.
4273+ const directive = directives [ 0 ] as RequestScopedDirectiveNode ;
4274+ const keyArg = directive . arguments . find ( ( arg ) => arg . name . value === KEY ) ;
4275+ if ( ! keyArg ) {
4276+ continue ;
4277+ }
4278+
4279+ const fieldCoords = `${ typeName } .${ fieldName } ` ;
4280+ const key = keyArg . value . value ;
4281+ const l1Key = `${ this . subgraphName } .${ key } ` ;
4282+ collected . push ( { typeName, fieldName, fieldCoords, key, l1Key } ) ;
4283+ }
4284+ }
4285+
4286+ if ( collected . length === 0 ) {
4287+ return ;
4288+ }
4289+
4290+ // Warn when a key is used on only one field — @openfed__requestScoped is meaningless
4291+ // unless >= 2 fields share the key (there'd be no second reader to benefit).
4292+ const coordsByKey = new Map < string , Array < string > > ( ) ;
4293+ for ( const c of collected ) {
4294+ getValueOrDefault ( coordsByKey , c . key , ( ) => [ ] ) . push ( c . fieldCoords ) ;
4295+ }
4296+ for ( const [ key , coordsList ] of coordsByKey ) {
4297+ if ( coordsList . length == 1 ) {
4298+ this . warnings . push (
4299+ requestScopedSingleFieldWarning ( {
4300+ subgraphName : this . subgraphName ,
4301+ key,
4302+ fieldCoords : coordsList [ 0 ] ,
4303+ } ) ,
4304+ ) ;
4305+ }
4306+ }
4307+
4308+ // Group by type and attach to configurationData.
4309+ const byType = new Map < string , Array < RequestScopedFieldConfig > > ( ) ;
4310+ for ( const c of collected ) {
4311+ const list = byType . get ( c . typeName ) ?? [ ] ;
4312+ list . push ( { fieldName : c . fieldName , typeName : c . typeName , l1Key : c . l1Key } ) ;
4313+ byType . set ( c . typeName , list ) ;
4314+ }
4315+ for ( const [ typeName , fields ] of byType ) {
4316+ const configurationData = getValueOrDefault ( this . configurationDataByTypeName , typeName , ( ) =>
4317+ newConfigurationData ( false , typeName ) ,
4318+ ) ;
4319+ configurationData . entityCaching = { ...configurationData . entityCaching , requestScopedFields : fields } ;
4320+ }
4321+ }
4322+
42424323 addFieldNamesToConfigurationData ( fieldDataByFieldName : Map < string , FieldData > , configurationData : ConfigurationData ) {
42434324 const externalFieldNames = new Set < string > ( ) ;
42444325 for ( const [ fieldName , fieldData ] of fieldDataByFieldName ) {
@@ -4512,6 +4593,8 @@ export class NormalizationFactory {
45124593 // per-field caching directives (@openfed__cacheInvalidate etc.) — must run after entityCache
45134594 // (reads entityCacheConfigByTypeName)
45144595 this . processRootFieldCacheDirectives ( ) ;
4596+ // this is where @openfed__requestScoped configurations are added to the ConfigurationData
4597+ this . extractRequestScopedFields ( ) ;
45154598 // Check that explicitly defined operations types are valid objects and that their fields are also valid
45164599 for ( const operationType of Object . values ( OperationTypeNode ) ) {
45174600 const operationTypeNode = this . schemaData . operationTypes . get ( operationType ) ;
0 commit comments