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

RFC: GraphQL @defer #3093

Merged
merged 20 commits into from
Jul 24, 2023
Merged

RFC: GraphQL @defer #3093

merged 20 commits into from
Jul 24, 2023

Conversation

calvincestari
Copy link
Member

@calvincestari calvincestari commented Jun 27, 2023

Read the formatted RFC here.

Execution

  1. low-priority
  2. calvincestari

Networking

  1. calvincestari
  2. calvincestari
  3. calvincestari
  4. calvincestari
  5. feature

General

  1. calvincestari

@netlify
Copy link

netlify bot commented Jun 27, 2023

Deploy Preview for apollo-ios-docs canceled.

Name Link
🔨 Latest commit 7353999
🔍 Latest deploy log https://app.netlify.com/sites/apollo-ios-docs/deploys/64bec05ea0ab2c00079615e7

@calvincestari calvincestari marked this pull request as ready for review June 30, 2023 21:50
@calvincestari
Copy link
Member Author

This is ready for review and comment. The section on the generated models is the weakest of the RFC right now, I will put more time into that when I get back. If you have comments/ideas for that though please comment on it and I will address it.

.fragment(EntityFragment.self, deferred: true),
] }
```

Copy link

@BoD BoD Jul 3, 2023

Choose a reason for hiding this comment

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

Just for a bit of context, here's how it works on Apollo Kotlin:

  • fragments spread/inline fragments generate a dedicated field in the generated model
  • before @defer, this field could be nullable if:
    • its type condition means the fields won't always be returned
    • or it has a @skip or @include (unless it's trivial)
  • after @defer: now there's a 3rd case where this field can be nullable: if it has @defer. If it's null it means you haven't received this fragment yet.


The most simple solution is to change the deferred property type to an optional version of that type. This hides detail though because you wouldn't be able to tell whether the value is `nil` because the response data has been received yet (i.e.: deferred) or whether the data was returned and it was explicitly `null`. It also gets more complicated when a type is already optional; would that result in a Swift double-optional type? As we learnt with the legacy implementation of GraphQL nullability, double-optionals are difficult to interpret and easy lead to errors.

I explored Swift's property wrappers but they suffer from the limitation of not being able to be applied to a computed property. All model properties are computed properties because they simply route access the value in the underlying dictionary data storage. It would be nice to be able to simply annotate fragments and fields with some like `@Deferred` but it doesn't look like that is possible.
Copy link
Contributor

@Iron-Ham Iron-Ham Jul 3, 2023

Choose a reason for hiding this comment

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

Not pressing: Investigate macros for this purpose? I haven't played much with macros outside of an iOS 17 dummy target, so I can't say what their overall level of backwards-compatibility would be. I hope that as a language feature it should be compatible, but who's to say. 🤷🏽

Copy link
Member Author

Choose a reason for hiding this comment

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

Macros might be a way to obscure the implementation in the generated code so the generated models wouldn't need to be regenerated if the underlying implementation changed but I don't think macros can provide a mechanism to expose the 'state' and values of deferred fragments?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I thought about this a lot. The problem is there really isn't any amount of generated code that could really make this work. We actually want to be able to have a "metadata" accessor on a property. The only way to do this is through the projectedValue of a property wrapper.

We could wrap the value in some struct that had the metadata and the value, but the 99% of the time you just wanted to value and not the "state" metadata, you'd need to call myProperty.value which is definitely not what we want here.

We just wanted to be able to use that sweet swift syntactic sugar that only exists with property wrappers, but they have another limitation that prevents us from using them right now.

}
```

Another way which may be a bit more intuitive is to make the `server` case on `Source` have an associated value since `cache` sources will always be complete. The cache could return partial responses for deferred operations but for the initial implementation we will probably only write the cache record once all deferred fragments have been received.
Copy link
Contributor

@Iron-Ham Iron-Ham Jul 3, 2023

Choose a reason for hiding this comment

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

The cache could return partial responses for deferred operations but for the initial implementation we will probably only write the cache record once all deferred fragments have been received.

Just thinking on edge cases here. Let's imagine this scenario:

  • User triggers a network request.
  • One of the response fields is @deferred as it takes, let's say 5 seconds to resolve.
  • The user navigates away from the view prior to receiving the @deferred field, ending all ongoing network requests for that view.

In this case, there would be no cache write at all, despite the majority of data being retrieved (and valid!). This perhaps could lead to the unintended effect wherein a partial fetch that should have updated the values of various entity types across the cache don't. Perhaps fine for an MVP implementation, but something that should be communicated out & documented.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes I can see that use case. I think this is something we'll iterate towards still though instead of making it operate that way from the first release.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great call out @Iron-Ham. I think we're all aligned this is something we want to implement, just not for the MVP. And yes, we should clearly document this behavior.

@calvincestari
Copy link
Member Author

I've updated the section on the generated models adding "Optional fragments" which aligns with @BoD's comment earlier.

@calvincestari
Copy link
Member Author

I'm going to be creating issues/tasks for the parts of the RFC that are less contentious and need to happen to be able to effectively support the @defer work.

@calvincestari calvincestari added this to the `@defer` support milestone Jul 11, 2023
@calvincestari calvincestari self-assigned this Jul 11, 2023
@calvincestari calvincestari linked an issue Jul 13, 2023 that may be closed by this pull request
@calvincestari
Copy link
Member Author

@AnthonyMDev / @Iron-Ham - do you have any feedback on the completion handler changes?

Working on the defer protocol parser today I realized there is the label property which can be part of the deferred response and I wonder if we need to feed that back to the user too or just swallow it? Is there value in passing it back to the user through the completion data?

@calvincestari
Copy link
Member Author

Working on the defer protocol parser today I realized there is the label property which can be part of the deferred response and I wonder if we need to feed that back to the user too or just swallow it? Is there value in passing it back to the user through the completion data?

Thinking about this more today I don't believe there is any value in passing back the label to the user. Users work with models and models are typed which is a much stronger 'reference' than a label string. If anything it should maybe be a property on deferred fragment instances.

@calvincestari
Copy link
Member Author

@Iron-Ham, I've updated the Generated Models section with an option (4) that would make fragments optional. I'd love to get your thoughts on it when you get a moment - thanks.

Design/3093-graphql-defer.md Outdated Show resolved Hide resolved
}
```

4. Optional fragments (disabling field merging) - optional types are only needed when fragment fields are merged into entity selection sets. If field merging were disabled automatically for deferred fragments then the solution is simplified and we only need to alter the deferred fragments to be optional. Consuming the result data is intuitive too where a `nil` fragment value would indicate that the fragment data has not yet been received (i.e.: deferred) and when the complete response is received the fragment value is populated and the result sent to the client. This seems a more elegant and ergonimic way to indicate the status of deferred data but complicates the understanding of field merging.
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 is probably the best approach we have. I do believe there is one specific situation where we already allow optional fragments. I think it had to do with having a conditionally included fragment with a @include/skip directive. We should double check that and make sure that we maintain consistency and clarity here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Noted - thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 -- I think this is a brilliant approach.

Design/3093-graphql-defer.md Show resolved Hide resolved
// proceed ahead on the request chain
}

let errorHandler: (() -> Void) = {
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll just need to make sure that we have adequate testing around this to ensure these closures don't cause retain cycles. Easy to capture self here and cause memory leaks.

Design/3093-graphql-defer.md Outdated Show resolved Hide resolved
case complete
}

public let response: Response
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, this is what I was referring to above! :)

I don't like calling this Response though. It's not actually the response data, it's just indicating if the response is partial/complete. Think we just need some consideration on naming here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Noted

Copy link
Member

Choose a reason for hiding this comment

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

I do like the idea of providing this data to the user through an enum. As for naming, yea just Response maybe isn't the best, thinking about what we are trying to represent which is the status of the result, maybe something like ResultState? ResponseResult doesn't really feel right, and ResultData would make me think it is data and not a state representation.

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 naming we can settle on in Slack or the PR. It seems like we're aligned on the state being available at the root of GraphQLResult and not only on the server source case.

Design/3093-graphql-defer.md Show resolved Hide resolved
Design/3093-graphql-defer.md Outdated Show resolved Hide resolved
}
```

2. Another way which may be a bit more intuitive is to make the `server` case on `Source` have an associated value since `cache` sources will always be complete. The cache could return partial responses for deferred operations but for the initial implementation we will probably only write the cache record once all deferred fragments have been received. This solution becomes invalid though once the cache can return partial responses, with that in mind maybe option 1 is better.
Copy link
Member

Choose a reason for hiding this comment

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

Agreed, ultimately would be great to be able to get partial responses from cache but for this implementation I think option 1 is fine.

@AnthonyMDev
Copy link
Contributor

Thinking about this more today I don't believe there is any value in passing back the label to the user. Users work with models and models are typed which is a much stronger 'reference' than a label string. If anything it should maybe be a property on deferred fragment instances.

I'm not sure about this. There may be some valuable functionality that this could unlock, but I don't think it needs to be part of the MVP either way. We can see if we get any user feedback requesting us to expose this later.

@calvincestari
Copy link
Member Author

calvincestari commented Jul 19, 2023

Thinking about this more today I don't believe there is any value in passing back the label to the user. Users work with models and models are typed which is a much stronger 'reference' than a label string. If anything it should maybe be a property on deferred fragment instances.

I'm not sure about this. There may be some valuable functionality that this could unlock, but I don't think it needs to be part of the MVP either way. We can see if we get any user feedback requesting us to expose this later.

I initially thought there might be value too until I went back and read the RFCs description/purpose of the label property. To me it reads that it's helpful for data patching and not so much once you have a complete response.

label: String

  • A unique label across all @defer and @stream directives in an operation.
  • This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragment.
  • If provided, the GraphQL Server must add it to the payload.

@calvincestari
Copy link
Member Author

I've updated the RFC with decisions on the preferred option where there were multiple and cleaned up some of the text based on review comments.

In summary:

  1. Update graphql-js dependency - option 3 is the preferred solution.
  2. Generated models - option 4 is the preferred solution.
  3. Completion handler - option 1 is the preferred solution.

@calvincestari calvincestari changed the base branch from main to feature/defer July 24, 2023 18:25
@calvincestari calvincestari merged commit 834ac4a into feature/defer Jul 24, 2023
@calvincestari calvincestari deleted the spike/defer branch July 24, 2023 18:26
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.

Feature: @defer support
5 participants