-
Notifications
You must be signed in to change notification settings - Fork 734
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: Networking Updates #1340
Comments
Overall this looks great. I only have 2 small questions:
|
|
Heh, this was kinda funny to me.
Should the request creator live on all requests? Said another way, what's the rationale behind only allowing
+1 to this, but definitely not critical for the context of this RFC. I feel like typed errors could be it's own whole RFC/PR too!
I'm a little bit fuzzy on what this change is. If not a All told, this seems solid! One of our big use cases in the GitHub app for the current delegate implementation of this is to logout on HTTP 401 status codes, so it seems like that should make this a little simpler (just need an interceptor to throw an error when those responses come back). Holler if you want some feedback directly on #1341 as well! |
Ha, that's what I get for working on this doc for so long, I completely glossed that over 🙃
My thought that was only
Basically, we have the
I tried about 15 different workarounds for this and this is the only one that wasn't monstrously over-complicated - everything else involved some mild-to-completely bizarre type erasure strategies. I'm not totally against those, but I think in this case the benefits of going that way were vastly outweighed just changing the gating of this a bit. |
and if you have specific feedback on #1341 I'd love to hear it - just be aware that there's definitely a lot that's WIP (thus the TODOs) |
One of the things that's nice about the Concretely, this kind of approach would probably mean the Sometimes you want to terminate the chain with something that makes an HTTP request, but other times the terminal link might provide its own data (for example, mock data during tests). Sometimes you might want to split a request between multiple downstream links, or choose among several servers that can handle different kinds of requests (or load-balance the same type of request between multiple servers, entirely on the client). It's hard to represent branching structures like that as an array, because it's no longer really a list, but a dynamic tree. But if each interceptor gets to make its own decisions about how it passes requests to the rest of the chain (and how it handles the responses), the branching can be hidden as an implementation detail, with each interceptor abstracting over everything downstream from it. I don't know enough Swift to anticipate specific ways in which this approach might be tricky, but it seems like it should be possible to give the interceptor chain more of a recursive, potentially tree-like structure. That's worked pretty well on the web, in the sense that I haven't had to worry very much about the For reference, here's an overview of |
I think one huge, huge difference is that things like cancellation, retry, and thread management can be handled through javascript's That just isn't doable without either using Combine (which would require dropping everything below iOS 12, and is unfortunately a non-starter with a number of our larger users) or adding some kind of Reactive library as a dependency (which I am loathe to do because of the massive number of dependency conflicts it could introduce). However, I think you're right that splitting out methods for request setup vs response handling is a good idea - this could at least help reduce the number of things that need to be optional on Will futz with this tomorrow. |
@designatednerd I'm glad you brought that up! While I agree that I should also mention that On the topic of cancellation, one approach that does work is to pass some sort of context object explicitly down through the chain (and back up), so that each link/interceptor can check whether the request has been cancelled at points where aborting would be safe. On the topic of retrying, the I think we can agree it's important for interceptors to be able to perform any kind of async work as an implementation detail, which requires a uniformly asynchronous API. Both |
from this error it looks like you changed the signature of func handleErrorAsync<ParsedValue: Parseable, Operation: GraphQLOperation, TypedError: Error>(
error: TypedError,
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<ParsedValue>,
completion: @escaping (Result<ParsedValue, TypedError>) -> Void) while I was thinking something like: func handleErrorAsync<ParsedValue: Parseable, Operation: GraphQLOperation, TypedError: Error>(
error: Error, // keep this one untyped so that you can pass anything you need
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<ParsedValue>,
completion: @escaping (Result<ParsedValue, TypedError>) -> Void) // but allow users to wrap the internal errors in their own custom types so that you can give a default implementation that does not wrap the error type: extension ApolloErrorInterceptor where TypedError == Error {
// default interceptor with a untyped completionHandler sorry I wasn't clear with the previous message. Thank you for fiddling with this!
@designatednerd would you prefer to move this conversation in a separate issue? |
@TizianoCoroneo Yeah if I left it with |
@benjamn I think one thing that's helpful context to understand is that the basis for this architecture is the In Still futzing around, but I thought that'd be useful context. |
@TizianoCoroneo I've pushed some stuff to a branch ominously named All: I did make some improvements that make it easier to reason about what data's coming back through (basically: Got rid of the need for |
I tried to play around with it, and I confirm that the ominous name of the branch is accurate. I found no way to avoid having the |
Still looking at some of the stuff @benjamn and I were talking about, but I've updated the issue to match what's in the code at the moment. TL;DR - I was able to simplify by constraining to |
More updates:
@benjamn: While I think the linked-list idea is more flexible, I don't know the additional flexibility it provides gets you much more than what we have in "This interceptor can be async and take as long as it pleases to do stuff, so you can fire off a bunch of additional network requests and wait for them to call I think the overwhelming majority of interceptors are not going to do that, and I think the simplicity, especially from a retry standpoint, of "Here's a list of interceptors in the order they should be executed, GO!" is a lot better fit for the way this is used on iOS than a linked list would be. |
More updates are up - I don't think any fundamentally change anything already in here, but I do have a couple questions:
|
Not using it here 🤷♂️ feels like a pretty cumbersome API to use effectively right now, so removing it seems alright, as long as we don't think others are using it.
I think it's alright to remove this in favor of |
Wholeheartedly agreed, I want to come up with something way better (and that does not involve the word
I don't think it's a great idea because there's a bunch of stuff with delegates that would be a bit of a hot mess to handle. |
I'm going to close out this RFC as I'm getting ready to beta off of #1341 - I just updated the client initialization documentation, which hopefully will give a good idea of how this should all be working. In terms of differences from what's outlined in the RFC, at this time the main difference is that I moved retry counting off the Further comments should be left on #1341 - thank you all for the feedback! |
Wow I'm late to the party! And because I'm late to it it would be great if there were some migration docs detailing what has changed with examples of before and after? Do such docs exist? |
There's not really a migration doc, but there's been an extensive update to the doc about creating a client that should give you a good idea of the what and why with the new system. |
Further to this @wuf810, the networking stack will be the focus of our attention in future 2.x releases. |
Thanks @designatednerd and @calvincestari I've found the docs on creating the client and think I'm OK to progress now. I'll try to keep up to date in the future so I can at least be part of the discussion! PS And thanks for the heads up on the coming changes to the network stack. |
Actually there is one thing I'm not sure on. How do I replicate the the HTTPNetworkTransportPreflightDelegate methods? I know I can use interceptors but is there a way to distinguish the shouldSend and willSend methods? And I guess the same question for the HTTPNetworkTransportRetryDelegate. Where in the chain do I intercept this? Thanks. |
So the preflight delegate can basically be replaced by anything prior to the There's a There isn't really a |
Thanks again for the reply. I've definitely got it now. Sorry I needed to get up to speed as I never implemented this code originally and had to first work out what they were doing and how to update to the new approach. I love the new interceptors! |
This is the technical outline of a proposal to make major changes to our primary networking interface from the current
HTTPNetworkTransport
to aRequestChainNetworkTransport
which uses a chain of interceptor objects to set up and process the results of network requests.Note that this is going to be a 🎉 Spectacularly 🎉 breaking change - while very surface level APIs will remain basically the same, if you're doing anything remotely advanced, this will necessitate some changes, but the idea is to break it now so we don't have to break it way worse later.
I would REALLY love feedback on this before I start working towards making this the default option. You can see the code changes in-place in this PR. I will be updating this RFC with feedback as it is received.
Why The Change?
HTTPNetworkTransport
allows you to hook into various delegates to accomplish various things. There are several limitations to this approach:The other major issue driving this update is that the current networking stack is deeply tied to the current cache architecture. This isn't ideal for many reasons, the biggest of which is that the cache likely to change in relation to the Swift Codegen Rewrite.
What is proposed?
The proposed new architecture uses the Interceptor pattern to create a customizable request chain. This means users can hook into the system at any point during the request creation or data processing process.
This also means that the pieces which will need to be swapped out for the Swift Codegen Rewrite are more clearly defined, and less tied to the actual parsing operation.
Finally, this also opens the opportunity for different patterns than we already support, such as writing to the cache after returning data to the UI instead of before, or creating an array of interceptors which hit the network first, then hit the cache if nothing was returned.
New Protocols
FlexibleDecoder
: This is mostly going to be helpful for theCodable
implementation down the line, but this will allow anything conforming toDecoder
to be used to decode data.Parseable
: This is a wrapper that allows us to continue to support non-Codable
parsing alongsideCodable
parsing, while keeping us able to constrain and construct things generically. A default implementation forCodable
will be provided.ApolloInterceptor
: This is an interface which allows you to add an asynchronous handler to perform any necessary work, such as fetching credentials and reading or writing from the cache, asynchronously.Default implementations of
ApolloInterceptor
for both Legacy (ie, non Swift Codegen) networking and Swift Codegen networking will be provided.InterceptorProvider
This protocol will be used to quickly create a new array of interceptors for a given request:This design allows for both flexibility (you can return different interceptors for different types of requests, for instance) and isolation (each request will have its own unique set of interceptors, reducing the possibility of different requests stomping on each other).
Two default interceptor providers are set up:
LegacyInterceptorProvider
will provide interceptors mimicking the current stackCodableInterceptorProvider
will provide interceptors for the forthcoming Swift Codegen Rewrite's network stack.ApolloErrorInterceptor
will allow you to have additional checks whenever an error is about to be returned. This will be optional to implement, and no default implementation is provided.New Classes
HTTPRequest
This object will hold all the information related to a request before it hits the network, with thetoURLRequest()
method creating an actualURLRequest
based on all the information in the request. This is subclass-able (and will mostly be using subclasses).JSONRequest
subclass ofHTTPRequest
will handle creating requests with JSON, which will be the vast majority of requests with operations. This is where handling of auto-persisted queries is also layered in:UploadRequest
subclass ofHTTPRequest
will handle multipart file uploads:HTTPResponse
will represent the objects returned and/or parsed from the server:
RequestChain
will handle the interaction with the network for a single operation.RequestChainNetworkTransport
provides an implementation ofNetworkTransport
which uses anInterceptorProvider
to create a request chain for each request.Changes to existing Protocols and Classes
ApolloStore
will no longer require aGraphQLQuery
explicitly for fetching data from the store. It will instead return an error if theGraphQLOperationType
is not.query
. This change is necessary to avoid going down an enormous rabbit hole with generics sinceGraphQLOperation
has an associated type.The
NetworkTransport
protocol will get a new method to be implemented:This will avoid the double-wrapping of
GraphQLResponse
around theGraphQLResult
so that only theGraphQLResult
is actually returned. Thesend
method will eventually be deprecated and removed.ApolloClient
will get a newsendForResult
method which calls into thesendForResult
method added toNetworkTransport
.How will this work in practice?
Instantiating a new legacy client manually will look like this:
Ideally I'll be able to transparently swap out the existing
HTTPNetworkTransport
for this so that this would be the under-the-hood setup onApolloClient
, but this may involve a transition period.Calls to the client will look like this:
Note that this is VERY similar to how they look on the surface at the moment, which is intentional.
The text was updated successfully, but these errors were encountered: