From 16e25a77030427bbd417eabf9740455531f3ebcc Mon Sep 17 00:00:00 2001 From: Hugo Striedinger Date: Thu, 25 Jun 2026 17:09:36 -0400 Subject: [PATCH] Add `dynamic` flag to opt provided variables out of value memoization Provided-variable providers are memoized process-wide: withProvidedVariables pins each provider's first return value in a module-level cache to enforce purity. On a server, where one process serves many requests/viewers, a provider that reads per-request state legitimately varies per request, which pins stale values and emits spurious purity warnings. Add an opt-in `dynamic?: boolean` on the provider object. When true, withProvidedVariables uses the freshly resolved value and skips both the cache and the warning; default behavior is unchanged. relay-typegen now emits an optional `dynamic?: boolean` on the generated provider type so a provider exporting it still typechecks against the exact object type. Docs, tests, and a fixture are included; generated artifacts and typegen snapshots regenerated. --- compiler/crates/relay-typegen/src/write.rs | 13 +- .../query-mixed-provided-variables.expected | 4 + .../query-only-provided-variables.expected | 3 + ...-provided-variables-custom-scalar.expected | 1 + ...sTestWithProvidedVariablesQuery.graphql.js | 3 +- ...DEPRECATEDTest_ProvidedVarQuery.graphql.js | 4 +- ...ueryProvidedVariablesTest_Query.graphql.js | 4 +- ...yProvidedVariablesTest_badQuery.graphql.js | 3 +- ...eTest_UserArgManyFragmentsQuery.graphql.js | 4 +- ...Test_UserArgSingleFragmentQuery.graphql.js | 3 +- ...nDescriptorTestCycleWithPVQuery.graphql.js | 3 +- ...yResponseNormalizerTest_pvQuery.graphql.js | 4 +- .../relay-runtime/util/RelayConcreteNode.d.ts | 3 +- .../relay-runtime/util/RelayConcreteNode.js | 9 +- ...withProvidedVariablesTest1Query.graphql.js | 3 +- ...withProvidedVariablesTest2Query.graphql.js | 3 +- ...withProvidedVariablesTest3Query.graphql.js | 4 +- ...withProvidedVariablesTest4Query.graphql.js | 4 +- ...withProvidedVariablesTest5Query.graphql.js | 4 +- ...withProvidedVariablesTest6Query.graphql.js | 3 +- ...hProvidedVariablesTest7Fragment.graphql.js | 82 +++++++++ ...withProvidedVariablesTest7Query.graphql.js | 167 ++++++++++++++++++ .../provideDynamicValue.relayprovider.js | 16 ++ .../__tests__/withProvidedVariables-test.js | 51 ++++++ .../util/withProvidedVariables.js | 10 +- .../graphql/graphql-directives.mdx | 19 ++ 26 files changed, 407 insertions(+), 20 deletions(-) create mode 100644 packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Fragment.graphql.js create mode 100644 packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Query.graphql.js create mode 100644 packages/relay-runtime/util/__tests__/provideDynamicValue.relayprovider.js diff --git a/compiler/crates/relay-typegen/src/write.rs b/compiler/crates/relay-typegen/src/write.rs index 548ed7fbf1634..9ccd94ceb494f 100644 --- a/compiler/crates/relay-typegen/src/write.rs +++ b/compiler/crates/relay-typegen/src/write.rs @@ -822,17 +822,26 @@ fn generate_provided_variables_type( encountered_enums, custom_scalars, ))); - let provider_module = Prop::KeyValuePair(KeyValuePairProp { + let provider_get = Prop::KeyValuePair(KeyValuePairProp { key: "get".intern(), read_only: true, optional: false, value: provider_func, }); + let provider_dynamic = Prop::KeyValuePair(KeyValuePairProp { + key: "dynamic".intern(), + read_only: true, + optional: true, + value: AST::Boolean, + }); Some(Prop::KeyValuePair(KeyValuePairProp { key: def.name.item.0, read_only: true, optional: false, - value: AST::ExactObject(ExactObject::new(vec![provider_module])), + value: AST::ExactObject(ExactObject::new(vec![ + provider_get, + provider_dynamic, + ])), })) }) .collect_vec(); diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-mixed-provided-variables.expected b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-mixed-provided-variables.expected index 1e82580596399..709a68bbb16da 100644 --- a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-mixed-provided-variables.expected +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-mixed-provided-variables.expected @@ -80,12 +80,15 @@ export type queryMixedProvidedVar_MultiFragment = { "__relay_internal__pv__skipFirstnameProvider": require('skipFirstnameProvider') } as { readonly __relay_internal__pv__includeNameProvider: { + readonly dynamic?: boolean, readonly get: () => CustomBoolean, }, readonly __relay_internal__pv__numberOfFriendsProvider: { + readonly dynamic?: boolean, readonly get: () => number, }, readonly __relay_internal__pv__skipFirstnameProvider: { + readonly dynamic?: boolean, readonly get: () => CustomBoolean, }, }); @@ -109,6 +112,7 @@ export type queryMixedProvidedVar_OneFragment = { "__relay_internal__pv__includeNameProvider": require('includeNameProvider') } as { readonly __relay_internal__pv__includeNameProvider: { + readonly dynamic?: boolean, readonly get: () => CustomBoolean, }, }); diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-only-provided-variables.expected b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-only-provided-variables.expected index 683c44b75a83f..93ce0cb4985bf 100644 --- a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-only-provided-variables.expected +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-only-provided-variables.expected @@ -56,9 +56,11 @@ export type queryOnlyProvidedVar_MultiFragment = { "__relay_internal__pv__numberOfFriendsProvider": require('numberOfFriendsProvider') } as { readonly __relay_internal__pv__includeNameProvider: { + readonly dynamic?: boolean, readonly get: () => CustomBoolean, }, readonly __relay_internal__pv__numberOfFriendsProvider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); @@ -80,6 +82,7 @@ export type queryOnlyProvidedVar_OneFragment = { "__relay_internal__pv__includeNameProvider": require('includeNameProvider') } as { readonly __relay_internal__pv__includeNameProvider: { + readonly dynamic?: boolean, readonly get: () => CustomBoolean, }, }); diff --git a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-provided-variables-custom-scalar.expected b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-provided-variables-custom-scalar.expected index ea0d667dba329..fdc698cdaa99c 100644 --- a/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-provided-variables-custom-scalar.expected +++ b/compiler/crates/relay-typegen/tests/generate_flow/fixtures/query-provided-variables-custom-scalar.expected @@ -32,6 +32,7 @@ export type testQuery = { "__relay_internal__pv__includeNameProvider": require('includeNameProvider') } as { readonly __relay_internal__pv__includeNameProvider: { + readonly dynamic?: boolean, readonly get: () => ?JSON, }, }); diff --git a/packages/react-relay/__tests__/__generated__/LiveResolversTestWithProvidedVariablesQuery.graphql.js b/packages/react-relay/__tests__/__generated__/LiveResolversTestWithProvidedVariablesQuery.graphql.js index c31dcbaec7f1c..6d7729cdd9804 100644 --- a/packages/react-relay/__tests__/__generated__/LiveResolversTestWithProvidedVariablesQuery.graphql.js +++ b/packages/react-relay/__tests__/__generated__/LiveResolversTestWithProvidedVariablesQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<28410ebf64ee51ccfba092c7bf62d537>> + * @generated SignedSource<<106bcd3da2641e16cd6a98cb6921ba96>> * @flow * @lightSyntaxTransform */ @@ -39,6 +39,7 @@ export type LiveResolversTestWithProvidedVariablesQuery = { "__relay_internal__pv__HelloWorldProviderrelayprovider": require('../../../relay-runtime/store/__tests__/resolvers/HelloWorldProvider.relayprovider') } as { readonly __relay_internal__pv__HelloWorldProviderrelayprovider: { + readonly dynamic?: boolean, readonly get: () => string, }, }); diff --git a/packages/react-relay/relay-hooks/__tests__/__generated__/preloadQueryDEPRECATEDTest_ProvidedVarQuery.graphql.js b/packages/react-relay/relay-hooks/__tests__/__generated__/preloadQueryDEPRECATEDTest_ProvidedVarQuery.graphql.js index 15034faa15a29..5acdac2770a9c 100644 --- a/packages/react-relay/relay-hooks/__tests__/__generated__/preloadQueryDEPRECATEDTest_ProvidedVarQuery.graphql.js +++ b/packages/react-relay/relay-hooks/__tests__/__generated__/preloadQueryDEPRECATEDTest_ProvidedVarQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<8d108fc93ef63b70ffeeff0127149cae>> + * @generated SignedSource<> * @flow * @lightSyntaxTransform */ @@ -35,9 +35,11 @@ export type preloadQueryDEPRECATEDTest_ProvidedVarQuery = { "__relay_internal__pv__RelayProvider_returnsFalserelayprovider": require('../RelayProvider_returnsFalse.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_returnsFalserelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, readonly __relay_internal__pv__RelayProvider_returnsTruerelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_Query.graphql.js b/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_Query.graphql.js index d8cad74aeeacb..1e459ec2f8607 100644 --- a/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_Query.graphql.js +++ b/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<046579603f12a05ff5576f9776ac4d09>> + * @generated SignedSource<<663ed24061d2a772f14b9af8b59f6f8d>> * @flow * @lightSyntaxTransform */ @@ -36,9 +36,11 @@ export type usePreloadedQueryProvidedVariablesTest_Query = { "__relay_internal__pv__RelayProvider_returnsFalserelayprovider": require('../RelayProvider_returnsFalse.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_returnsFalserelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, readonly __relay_internal__pv__RelayProvider_returnsTruerelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_badQuery.graphql.js b/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_badQuery.graphql.js index 6ae7355e8d480..795989dd8f41c 100644 --- a/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_badQuery.graphql.js +++ b/packages/react-relay/relay-hooks/__tests__/__generated__/usePreloadedQueryProvidedVariablesTest_badQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<52f5b26016dc43fa8fddd3c38d2c4558>> + * @generated SignedSource<<09f66198bbfc74cec5591b11fe64d6c0>> * @flow * @lightSyntaxTransform */ @@ -34,6 +34,7 @@ export type usePreloadedQueryProvidedVariablesTest_badQuery = { "__relay_internal__pv__RelayProvider_impurerelayprovider": require('../RelayProvider_impure.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_impurerelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgManyFragmentsQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgManyFragmentsQuery.graphql.js index 35671597f3378..26075afb34675 100644 --- a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgManyFragmentsQuery.graphql.js +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgManyFragmentsQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<318514b39e468b63b6cbb3b363a1694d>> + * @generated SignedSource<<5f09388e6e61ccaf12629c6385deb2e4>> * @flow * @lightSyntaxTransform */ @@ -37,9 +37,11 @@ export type RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgManyFra "__relay_internal__pv__RelayProvider_pictureScalerelayprovider": require('../RelayProvider_pictureScale.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_pictureScalerelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, readonly __relay_internal__pv__RelayProvider_returnsTruerelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgSingleFragmentQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgSingleFragmentQuery.graphql.js index a36ac6365d36c..73c5bef951249 100644 --- a/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgSingleFragmentQuery.graphql.js +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgSingleFragmentQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<7411e13e378d84ce987860106d33a265>> + * @generated SignedSource<> * @flow * @lightSyntaxTransform */ @@ -34,6 +34,7 @@ export type RelayModernEnvironmentExecuteWithProvidedVariableTest_UserArgSingleF "__relay_internal__pv__RelayProvider_returnsTruerelayprovider": require('../RelayProvider_returnsTrue.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_returnsTruerelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayModernOperationDescriptorTestCycleWithPVQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayModernOperationDescriptorTestCycleWithPVQuery.graphql.js index 8c46bdb5dc77d..e180be14e87fd 100644 --- a/packages/relay-runtime/store/__tests__/__generated__/RelayModernOperationDescriptorTestCycleWithPVQuery.graphql.js +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayModernOperationDescriptorTestCycleWithPVQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<> + * @generated SignedSource<<880939da02cd9c9ec14b1d0c1ea82775>> * @flow * @lightSyntaxTransform */ @@ -32,6 +32,7 @@ export type RelayModernOperationDescriptorTestCycleWithPVQuery = { "__relay_internal__pv__RelayProvider_returnsCyclicrelayprovider": require('../RelayProvider_returnsCyclic.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_returnsCyclicrelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest_pvQuery.graphql.js b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest_pvQuery.graphql.js index 69ec3a61769b0..526f020aeb045 100644 --- a/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest_pvQuery.graphql.js +++ b/packages/relay-runtime/store/__tests__/__generated__/RelayResponseNormalizerTest_pvQuery.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<154e699ff79d825f082b33db712f5075>> + * @generated SignedSource<<427d49bf4ca9a9f731f6785d9a52f5e3>> * @flow * @lightSyntaxTransform */ @@ -36,9 +36,11 @@ export type RelayResponseNormalizerTest_pvQuery = { "__relay_internal__pv__RelayProvider_returnsFalserelayprovider": require('../RelayProvider_returnsFalse.relayprovider') } as { readonly __relay_internal__pv__RelayProvider_returnsFalserelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, readonly __relay_internal__pv__RelayProvider_returnsTruerelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, }); diff --git a/packages/relay-runtime/util/RelayConcreteNode.d.ts b/packages/relay-runtime/util/RelayConcreteNode.d.ts index 6db8b5eb1e513..5ec1e91eb4170 100644 --- a/packages/relay-runtime/util/RelayConcreteNode.d.ts +++ b/packages/relay-runtime/util/RelayConcreteNode.d.ts @@ -112,9 +112,10 @@ export const RelayConcreteNode: { }; export interface ProvidedVariablesType { - readonly [key: string]: { get(): unknown }; + readonly [key: string]: ProvidedVariableType; } export interface ProvidedVariableType { get(): unknown; + readonly dynamic?: boolean; } diff --git a/packages/relay-runtime/util/RelayConcreteNode.js b/packages/relay-runtime/util/RelayConcreteNode.js index 464c413f397e5..6332f715cc60b 100644 --- a/packages/relay-runtime/util/RelayConcreteNode.js +++ b/packages/relay-runtime/util/RelayConcreteNode.js @@ -39,9 +39,14 @@ export type NormalizationRootNode = | ConcreteRequest | NormalizationSplitOperation; -export type ProvidedVariableType = {get(): unknown}; +export type ProvidedVariableType = { + get(): unknown, + readonly dynamic?: boolean, +}; -export type ProvidedVariablesType = {readonly [key: string]: {get(): unknown}}; +export type ProvidedVariablesType = { + readonly [key: string]: ProvidedVariableType, +}; /** * Contains the parameters required for executing a GraphQL request. diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest1Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest1Query.graphql.js index 3452ecc90ff26..211028840a5ec 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest1Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest1Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<> + * @generated SignedSource<<0717812b1d5daf0b971dc2ec19a608f0>> * @flow * @lightSyntaxTransform */ @@ -32,6 +32,7 @@ export type withProvidedVariablesTest1Query = { "__relay_internal__pv__provideNumberOfFriendsrelayprovider": require('../provideNumberOfFriends.relayprovider') } as { readonly __relay_internal__pv__provideNumberOfFriendsrelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest2Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest2Query.graphql.js index 21fbfd42b14fa..b988f6cb63f0c 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest2Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest2Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<39e43ed725c5981c4d76d6e0475cce83>> + * @generated SignedSource<<77bf5e4ac0556906fb3979abff953ae3>> * @flow * @lightSyntaxTransform */ @@ -34,6 +34,7 @@ export type withProvidedVariablesTest2Query = { "__relay_internal__pv__provideNumberOfFriendsrelayprovider": require('../provideNumberOfFriends.relayprovider') } as { readonly __relay_internal__pv__provideNumberOfFriendsrelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest3Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest3Query.graphql.js index 81378bd70e7ca..58e7aad4b2291 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest3Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest3Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<0e93acb3dbb88992f6aec63bd7d525c9>> + * @generated SignedSource<<80844e836bfeb9ad54fbe476756453ae>> * @flow * @lightSyntaxTransform */ @@ -33,9 +33,11 @@ export type withProvidedVariablesTest3Query = { "__relay_internal__pv__provideIncludeUserNamesrelayprovider": require('../provideIncludeUserNames.relayprovider') } as { readonly __relay_internal__pv__provideIncludeUserNamesrelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, readonly __relay_internal__pv__provideNumberOfFriendsrelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest4Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest4Query.graphql.js index dd741e3cbffe1..13dc70be4493b 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest4Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest4Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<4da0bd090d5b463ef3d45b39fbc2d5eb>> + * @generated SignedSource<<9df1832bec04b39381549bba44507ce4>> * @flow * @lightSyntaxTransform */ @@ -34,9 +34,11 @@ export type withProvidedVariablesTest4Query = { "__relay_internal__pv__provideIncludeUserNamesrelayprovider": require('../provideIncludeUserNames.relayprovider') } as { readonly __relay_internal__pv__provideIncludeUserNamesrelayprovider: { + readonly dynamic?: boolean, readonly get: () => boolean, }, readonly __relay_internal__pv__provideNumberOfFriendsrelayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest5Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest5Query.graphql.js index 3d9aad4213445..d9e471f0a3e25 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest5Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest5Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<4142c76beaa020ff0c23deb081fc19ce>> + * @generated SignedSource<<02f3190839a5853c2d09be92666bfc8e>> * @flow * @lightSyntaxTransform */ @@ -33,9 +33,11 @@ export type withProvidedVariablesTest5Query = { "__relay_internal__pv__provideRandomNumber_invalid2relayprovider": require('../provideRandomNumber_invalid2.relayprovider') } as { readonly __relay_internal__pv__provideRandomNumber_invalid1relayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, readonly __relay_internal__pv__provideRandomNumber_invalid2relayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest6Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest6Query.graphql.js index 86287565d691d..8d5bd35e15b7a 100644 --- a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest6Query.graphql.js +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest6Query.graphql.js @@ -6,7 +6,7 @@ * * @oncall relay * - * @generated SignedSource<<06e2c710ac749f0507546980efb5c5d1>> + * @generated SignedSource<<99e55f6e388da364b10dcc8b5a1c73d7>> * @flow * @lightSyntaxTransform */ @@ -32,6 +32,7 @@ export type withProvidedVariablesTest6Query = { "__relay_internal__pv__provideRandomNumber_invalid1relayprovider": require('../provideRandomNumber_invalid1.relayprovider') } as { readonly __relay_internal__pv__provideRandomNumber_invalid1relayprovider: { + readonly dynamic?: boolean, readonly get: () => number, }, }); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Fragment.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Fragment.graphql.js new file mode 100644 index 0000000000000..32c3421155343 --- /dev/null +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Fragment.graphql.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<62044c0b8f32381d18157a0cb71deb48>> + * @flow + * @lightSyntaxTransform + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type withProvidedVariablesTest7Fragment$fragmentType: FragmentType; +export type withProvidedVariablesTest7Fragment$data = { + readonly profile_picture: ?{ + readonly uri: ?string, + }, + readonly $fragmentType: withProvidedVariablesTest7Fragment$fragmentType, +}; +export type withProvidedVariablesTest7Fragment$key = { + readonly $data?: withProvidedVariablesTest7Fragment$data, + readonly $fragmentSpreads: withProvidedVariablesTest7Fragment$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [ + { + "kind": "RootArgument", + "name": "__relay_internal__pv__provideDynamicValuerelayprovider" + } + ], + "kind": "Fragment", + "metadata": null, + "name": "withProvidedVariablesTest7Fragment", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "scale", + "variableName": "__relay_internal__pv__provideDynamicValuerelayprovider" + } + ], + "concreteType": "Image", + "kind": "LinkedField", + "name": "profile_picture", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "uri", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*:: as any*/).hash = "37a45014ce2e2e58371a52a5429eaf90"; +} + +module.exports = ((node/*:: as any*/)/*:: as Fragment< + withProvidedVariablesTest7Fragment$fragmentType, + withProvidedVariablesTest7Fragment$data, +>*/); diff --git a/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Query.graphql.js b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Query.graphql.js new file mode 100644 index 0000000000000..78968c1d3fa28 --- /dev/null +++ b/packages/relay-runtime/util/__tests__/__generated__/withProvidedVariablesTest7Query.graphql.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { withProvidedVariablesTest7Fragment$fragmentType } from "./withProvidedVariablesTest7Fragment.graphql"; +export type withProvidedVariablesTest7Query$variables = {}; +export type withProvidedVariablesTest7Query$data = { + readonly node: ?{ + readonly $fragmentSpreads: withProvidedVariablesTest7Fragment$fragmentType, + }, +}; +export type withProvidedVariablesTest7Query = { + response: withProvidedVariablesTest7Query$data, + variables: withProvidedVariablesTest7Query$variables, +}; +({ + "__relay_internal__pv__provideDynamicValuerelayprovider": require('../provideDynamicValue.relayprovider') +} as { + readonly __relay_internal__pv__provideDynamicValuerelayprovider: { + readonly dynamic?: boolean, + readonly get: () => number, + }, +}); +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "id", + "value": 4 + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "withProvidedVariablesTest7Query", + "selections": [ + { + "alias": null, + "args": (v0/*:: as any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "withProvidedVariablesTest7Fragment" + } + ], + "storageKey": "node(id:4)" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "__relay_internal__pv__provideDynamicValuerelayprovider" + } + ], + "kind": "Operation", + "name": "withProvidedVariablesTest7Query", + "selections": [ + { + "alias": null, + "args": (v0/*:: as any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "scale", + "variableName": "__relay_internal__pv__provideDynamicValuerelayprovider" + } + ], + "concreteType": "Image", + "kind": "LinkedField", + "name": "profile_picture", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "uri", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": "node(id:4)" + } + ] + }, + "params": { + "cacheID": "cccc8d13add9921b3325b27527df8a08", + "id": null, + "metadata": {}, + "name": "withProvidedVariablesTest7Query", + "operationKind": "query", + "text": "query withProvidedVariablesTest7Query(\n $__relay_internal__pv__provideDynamicValuerelayprovider: Float!\n) {\n node(id: 4) {\n __typename\n ...withProvidedVariablesTest7Fragment\n id\n }\n}\n\nfragment withProvidedVariablesTest7Fragment on User {\n profile_picture(scale: $__relay_internal__pv__provideDynamicValuerelayprovider) {\n uri\n }\n}\n", + "providedVariables": { + "__relay_internal__pv__provideDynamicValuerelayprovider": require('../provideDynamicValue.relayprovider') + } + } +}; +})(); + +if (__DEV__) { + (node/*:: as any*/).hash = "38546e2e378acd0148f4293aa159519c"; +} + +module.exports = ((node/*:: as any*/)/*:: as Query< + withProvidedVariablesTest7Query$variables, + withProvidedVariablesTest7Query$data, +>*/); diff --git a/packages/relay-runtime/util/__tests__/provideDynamicValue.relayprovider.js b/packages/relay-runtime/util/__tests__/provideDynamicValue.relayprovider.js new file mode 100644 index 0000000000000..98b07d1d87554 --- /dev/null +++ b/packages/relay-runtime/util/__tests__/provideDynamicValue.relayprovider.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall relay + */ + +let counter = 0; +export function get(): number { + return counter++; +} +export const dynamic = true; diff --git a/packages/relay-runtime/util/__tests__/withProvidedVariables-test.js b/packages/relay-runtime/util/__tests__/withProvidedVariables-test.js index cbc0f93525e59..d83d028aa86a9 100644 --- a/packages/relay-runtime/util/__tests__/withProvidedVariables-test.js +++ b/packages/relay-runtime/util/__tests__/withProvidedVariables-test.js @@ -307,4 +307,55 @@ describe('withProvidedVariables', () => { ).toEqual(0); }); }); + + describe('When a provider is marked dynamic', () => { + it('resolves a fresh value on every call and never warns', () => { + const userQuery = graphql` + query withProvidedVariablesTest7Query { + node(id: 4) { + ...withProvidedVariablesTest7Fragment @dangerously_unaliased_fixme + } + } + `; + graphql` + fragment withProvidedVariablesTest7Fragment on User + @argumentDefinitions( + dynamicProvider: { + type: "Float!" + provider: "./provideDynamicValue.relayprovider" + } + ) { + profile_picture(scale: $dynamicProvider) { + uri + } + } + `; + + const userVariables = {}; + // disallowWarnings() (top of file) fails the test if the pure-function + // warning fires, so this also asserts that a dynamic provider never warns + // even though it returns a different value each call. + let vars = withProvidedVariables( + userVariables, + userQuery.params.providedVariables, + ); + expect( + vars.__relay_internal__pv__provideDynamicValuerelayprovider, + ).toEqual(0); + vars = withProvidedVariables( + userVariables, + userQuery.params.providedVariables, + ); + expect( + vars.__relay_internal__pv__provideDynamicValuerelayprovider, + ).toEqual(1); + vars = withProvidedVariables( + userVariables, + userQuery.params.providedVariables, + ); + expect( + vars.__relay_internal__pv__provideDynamicValuerelayprovider, + ).toEqual(2); + }); + }); }); diff --git a/packages/relay-runtime/util/withProvidedVariables.js b/packages/relay-runtime/util/withProvidedVariables.js index 04911bd16a231..f410056b4c2ef 100644 --- a/packages/relay-runtime/util/withProvidedVariables.js +++ b/packages/relay-runtime/util/withProvidedVariables.js @@ -35,9 +35,17 @@ function withProvidedVariables( // $FlowFixMe[unsafe-object-assign] Object.assign(operationVariables, userSuppliedVariables); Object.keys(providedVariables).forEach((varName: string) => { - const providerFunction = providedVariables[varName].get; + const provider = providedVariables[varName]; + const providerFunction = provider.get; const providerResult = providerFunction(); + // `dynamic` providers are not expected to be pure, so use the fresh value + // and skip the memoization cache (and its purity warning) + if (provider.dynamic === true) { + operationVariables[varName] = providerResult; + return; + } + // people like to ignore these warnings, so use the cache to // enforce that we only compute the value the first time if (!debugCache.has(providerFunction)) { diff --git a/website/docs/api-reference/graphql/graphql-directives.mdx b/website/docs/api-reference/graphql/graphql-directives.mdx index ce656649b2857..1396d084c6d6f 100644 --- a/website/docs/api-reference/graphql/graphql-directives.mdx +++ b/website/docs/api-reference/graphql/graphql-directives.mdx @@ -77,6 +77,12 @@ To add a provided variable: - ensure that `[JSModule].relayprovider.js` exists and exports a `get()` function - `get` should return the same value on every call for a given run. + - if the value can legitimately differ between runs (for example, it depends + on per-request or per-session state), also export `dynamic: true`. Relay + then resolves the provider fresh on every operation instead of memoizing the + first value, which is important when one runtime serves many requests, such + as during server-side rendering. `get` should still return a consistent + value within a single run. ```graphql fragment TodoItem_item on TodoList @@ -101,6 +107,19 @@ export default { }; ``` +A `dynamic` provider whose value depends on the current run: + +```javascript +// Viewer_IsLoggedIn.relayprovider.js +export default { + // resolved fresh on every operation rather than memoized + dynamic: true, + get(): boolean { + return getCurrentViewer().isLoggedIn; + }, +}; +``` + Notes: