Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Selection set generation for @defer #3176

Closed
wants to merge 16 commits into from

Conversation

calvincestari
Copy link
Member

@calvincestari calvincestari commented Aug 5, 2023

Closes #3141

Introduces IsDeferred expression logic which is used in the selection set template to:

  • Add Operation-level hasDeferredFragments property
  • Add deferred property to inline and named fragment selections
  • Mark deferred fragment accessors as Optional

If there are other tests you think would be beneficial, I'm sure there are, please let me know and get them added.

@@ -173,6 +181,27 @@ public class CompilationResult: JavaScriptObject {

lazy var directives: [Directive]? = self["directives"]

lazy var isDeferred: IsDeferred = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated for both InlineFragment and FragmentSpread, I'm not sure if there is a better way to do it; they're both just simple JavaScriptObject types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protocol Deferrable {
  var directives: [Directive]? { get }
}

fileprivate extension Deferrable where Self: JavaScriptObject {
  func getIsDeferred() -> IsDeferred {
    ...
  }
}

public class InlineFragment: Deferrable {
  ...
  lazy var isDeferred: IsDeferred = getIsDeferred()
}

Wish we could easily namespace this Deferrable protocol in CompilationResult. Until we break CompilationResult into it's own package, we might want to call the protocol CompilationResult_Deferrable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol and extension can have the fileprivate scope which is effectively the same as putting it into it's own package for this particular case.

@@ -54,6 +54,21 @@ extension IR {
self.conditions = [condition]
}

/// A failable initializer to convert from an array of `CompilationResult.InclusionCondition`.
init?(_ conditions: [CompilationResult.InclusionCondition]?) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AnthonyMDev - I've added this since we determined that ScopeCondition should match the inclusion conditions as well as the parent type. Problem is that the named fragment passed into the scope condition is from CompilationResult and the two InclusionCondition types can't be compared. This is a simple conversion similar to how we treat conversions for the IR.InclusionCondition.Result type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could instead have a custom == operator that will compare CompilationResult.InclusionConditions to IR.InclusionConditions. That might be a better solution than an initializer that should never be used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with including this initializer is that it doesn't have an accurate 1:1 way to map. For @skip with no variable, we would be omitting the entire fragment. The preconditionFailure here seems way too easy to accidentally make occur and cause a crash.

I think that a custom matching function that is fileprivate to the file that is using it is a better approach. It's not an actual ==, so I would make it a function with a more descriptive name.

@@ -3,9 +3,6 @@ import OrderedCollections

extension IR {

// TODO: Write tests that two inline fragments with same type and inclusion conditions,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we should also be adding tests for this behavior to the IR building tests, not just the SelectionSetTemplateTests. Making sure the actual IR is structured properly is important and having those tests makes it a LOT easier to debug and figure out the cause of bugs than just the template tests.

func DeferredProperties(
_ definition: IR.Definition
) -> TemplateString {
func isDeferred(_ selectionSet: IR.SelectionSet) -> Bool {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AnthonyMDev - needing your feedback here whether this is the best way to be determining at an operation-level whether any fragments are deferred; I think it should be since we're only doing direct selections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code should be valid, but I'm wondering if we could prevent another iteration through the entire selection set here.

What if we added hasDeferredFragments to the RootFieldBuilder.Result. We could keep track of this as a var on the RootFieldBuilder, and while building, if we encounter a deferred fragment, we just set that to true.

private func scopeCondition(
for conditionalSelectionSet: ConditionallyIncludable,
in parentTypePath: SelectionSet.TypeInfo,
isDeferred: CompilationResult.IsDeferred
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change the type of this parameter to IR.IsDeferred and do ``IsDeferred(compilationResult: namedFragment.isDeferred)` on line 247.

.field("__typename", String.self),
.include(if: "filter", [
.inlineFragment(AsDogIfFilter.self, deferred: true),
.inlineFragment(AsDogIfFilter.self, deferred: .if("other")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's going to generate two separate selection sets both named AsDogIfFilter, which is not going to compile. We're going to need to handle this more carefully.

@@ -3,9 +3,6 @@ import OrderedCollections

extension IR {

// TODO: Write tests that two inline fragments with same type and inclusion conditions,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we should also be adding tests for this behavior to the IR building tests, not just the SelectionSetTemplateTests. Making sure the actual IR is structured properly is important and having those tests makes it a LOT easier to debug and figure out the cause of bugs than just the template tests.

func DeferredProperties(
_ definition: IR.Definition
) -> TemplateString {
func isDeferred(_ selectionSet: IR.SelectionSet) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code should be valid, but I'm wondering if we could prevent another iteration through the entire selection set here.

What if we added hasDeferredFragments to the RootFieldBuilder.Result. We could keep track of this as a var on the RootFieldBuilder, and while building, if we encounter a deferred fragment, we just set that to true.

type: type,
conditions: inclusionResult.conditions,
isDeferred: IsDeferred(compilationResult: isDeferred)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes also should really be tested in tests on the RootFieldBuilder, not just the SelectionSetTemplateTests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants