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: Networking Updates #1340

Closed
designatednerd opened this issue Jul 31, 2020 · 27 comments
Closed

RFC: Networking Updates #1340

designatednerd opened this issue Jul 31, 2020 · 27 comments
Labels
discussion Requests for comment or other discussions networking-stack

Comments

@designatednerd
Copy link
Contributor

designatednerd commented Jul 31, 2020

This is the technical outline of a proposal to make major changes to our primary networking interface from the current HTTPNetworkTransport to a RequestChainNetworkTransport 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:

  • Users can only do things that are specifically supported by delegates.
  • Asynchronous use of delegates without callbacks is basically impossible.
  • Any time we want to add a new feature, we need to add a new delegate method and handle it, creating additional complexity.
  • There is no flexibility in terms of order of operations, particularly around whether data should be returned to the UI before being written to the cache.

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 the Codable implementation down the line, but this will allow anything conforming to Decoder to be used to decode data.

  • Parseable: This is a wrapper that allows us to continue to support non-Codable parsing alongside Codable parsing, while keeping us able to constrain and construct things generically. A default implementation for Codable 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.

    public protocol ApolloInterceptor: class {
    
      /// Called when this interceptor should do its work.
      ///
      /// - Parameters:
      ///   - chain: The chain the interceptor is a part of.
      ///   - request: The request, as far as it has been constructed
      ///   - response: [optional] The response, if received
      ///   - completion: The completion block to fire when data needs to be returned to the UI.
      func interceptAsync<Operation: GraphQLOperation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void)
    }

    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:

    public protocol InterceptorProvider {
    
      /// Creates a new array of interceptors when called
      ///
      /// - Parameter operation: The operation to provide interceptors for
      func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor]
    }

    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 stack
    • CodableInterceptorProvider 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.

    /// Asynchronously handles the receipt of an error at any point in the chain.
    ///
    /// - Parameters:
    ///   - error: The received error
    ///   - chain: The chain the error was received on
    ///   - request: The request, as far as it was constructed
    ///   - response: [optional] The response, if received
    ///   - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method.
    func handleErrorAsync<Operation: GraphQLOperation>(
        error: Error,
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void)

New Classes

  • HTTPRequest This object will hold all the information related to a request before it hits the network, with the toURLRequest() method creating an actual URLRequest based on all the information in the request. This is subclass-able (and will mostly be using subclasses).

    open class HTTPRequest<Operation: GraphQLOperation> {
    
      open var graphQLEndpoint: URL
      open var operation: Operation
      open var contentType: String
      open var additionalHeaders: [String: String]
      open var clientName: String? = nil
      open var clientVersion: String? = nil
      open var retryCount: Int = 0
      public let cachePolicy: CachePolicy
    
      public init(graphQLEndpoint: URL,
                  operation: Operation,
                  contentType: String,
                  additionalHeaders: [String: String],
                  cachePolicy: CachePolicy = .default)
                  
      open func toURLRequest() throws -> URLRequest
    
      open func addHeader(name: String, value: String)
    }
    • JSONRequest subclass of HTTPRequest 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:
    public class JSONRequest<Operation: GraphQLOperation>: HTTPRequest<Operation> {
    
      public let requestCreator: RequestCreator
    
      public let autoPersistQueries: Bool
      public let useGETForQueries: Bool
      public let useGETForPersistedQueryRetry: Bool
      public var isPersistedQueryRetry = false
    
      public let serializationFormat = JSONSerializationFormat.self
    
      public init(operation: Operation,
                  graphQLEndpoint: URL,
                  additionalHeaders: [String: String] = [:],
                  cachePolicy: CachePolicy = .default,
                  autoPersistQueries: Bool = false,
                  useGETForQueries: Bool = false,
                  useGETForPersistedQueryRetry: Bool = false,
                  requestCreator: RequestCreator = ApolloRequestCreator())
    }
    • UploadRequest subclass of HTTPRequest will handle multipart file uploads:
    public class UploadRequest<Operation: GraphQLOperation>: HTTPRequest<Operation> {
    
      public let requestCreator: RequestCreator
      public let files: [GraphQLFile]
      public let manualBoundary: String?  
      public let serializationFormat = JSONSerializationFormat.self
    
      public init(graphQLEndpoint: URL,
              operation: Operation,
              additionalHeaders: [String: String] = [:],
              files: [GraphQLFile],
              manualBoundary: String? = nil,
              requestCreator: RequestCreator = ApolloRequestCreator()) 
    }
  • HTTPResponse will represent the objects returned and/or parsed from the server:

    /// Designated initializer
    ///
    /// - Parameters:
    ///   - response: The `HTTPURLResponse` received from the server.
    ///   - rawData: The raw, unparsed data received from the server.
    ///   - parsedResponse: [optional] The response parsed into the `ParsedValue` type. Will be nil if not yet parsed, or if parsing failed.
    public class HTTPResponse<Operation: GraphQLOperation> {
      public var httpResponse: HTTPURLResponse
      public var rawData: Data
      public var parsedResponse: GraphQLResult<Operation.Data>?
    }
  • RequestChain will handle the interaction with the network for a single operation.

    public class RequestChain: Cancellable {
    
    /// Creates a chain with the given interceptor array
    public init(interceptors: [ApolloInterceptor])
    
    /// Kicks off the request from the beginning of the interceptor array.
    ///
    /// - Parameters:
    ///   - request: The request to send.
    ///   - completion: The completion closure to call when the request has completed.
    public func kickoff<ParsedValue: Parseable, Operation: GraphQLOperation>(
        request: HTTPRequest<Operation>, 
        completion: @escaping (Result<ParsedValue, Error>) -> Void)
    
    /// Proceeds to the next interceptor in the array.
    /// 
    /// - Parameters:
    ///   - request: The in-progress request object
    ///   - response: [optional] The in-progress response object, if received yet
    ///   - completion: The completion closure to call when data has been processed and should be returned to the UI.
    public func proceedAsync<ParsedValue: Parseable, Operation: GraphQLOperation>(
        request: HTTPRequest<Operation>,               
        response: HTTPResponse<ParsedValue>?,
        completion: @escaping (Result<ParsedValue, Error>) -> Void)
    
    /// Cancels the entire chain of interceptors.
    public func cancel()
    
    /// Restarts the request starting from the first inteceptor.
    ///
    /// - Parameters:
    ///   - request: The request to retry
    ///   - completion: The completion closure to call when the request has completed.
    public func retry<ParsedValue: Parseable, Operation: GraphQLOperation>(
        request: HTTPRequest<Operation>,
        completion: @escaping (Result<ParsedValue, Error>) -> Void)
    }
  • RequestChainNetworkTransport provides an implementation of NetworkTransport which uses an InterceptorProvider to create a request chain for each request.

    public class RequestChainNetworkTransport: NetworkTransport {
    
    public init(interceptorProvider: InterceptorProvider,
      endpointURL: URL,
      additionalHeaders: [String: String] = [:],
      autoPersistQueries: Bool = false,
      cachePolicy: CachePolicy = .default,
      requestCreator: RequestCreator = ApolloRequestCreator(),
      useGETForQueries: Bool = false,
      useGETForPersistedQueryRetry: Bool = false)
    }

Changes to existing Protocols and Classes

  • ApolloStore will no longer require a GraphQLQuery explicitly for fetching data from the store. It will instead return an error if the GraphQLOperationType is not .query. This change is necessary to avoid going down an enormous rabbit hole with generics since GraphQLOperation has an associated type.

  • The NetworkTransport protocol will get a new method to be implemented:

      func sendForResult<Operation: GraphQLOperation>(operation: Operation,
                                                      completionHandler: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) -> Cancellable

    This will avoid the double-wrapping of GraphQLResponse around the GraphQLResult so that only the GraphQLResult is actually returned. The send method will eventually be deprecated and removed.

  • ApolloClient will get a new sendForResult method which calls into the sendForResult method added to NetworkTransport.

How will this work in practice?

Instantiating a new legacy client manually will look like this:

lazy var legacyClient: ApolloClient = {
    let url = URL(string: "http://localhost:8080/graphql")!
    
    let store = ApolloStore(cache: InMemoryNormalizedCache())
    let provider = LegacyInterceptorProvider(store: store)
    let transport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url)
    
    return ApolloClient(networkTransport: transport)
}()

Ideally I'll be able to transparently swap out the existing HTTPNetworkTransport for this so that this would be the under-the-hood setup on ApolloClient, but this may involve a transition period.

Calls to the client will look like this:

legacyClient.fetchForResult(query: HeroNameQuery()) { result in
    switch result {
    case .success(let graphQLResult):
        print(graphQLResult.data?.hero?.name ?? "Name not found")
    case .failure(let error):
        print("Unexpected error: \(error)")
    }
}

Note that this is VERY similar to how they look on the surface at the moment, which is intentional.

@designatednerd designatednerd added discussion Requests for comment or other discussions networking-stack labels Jul 31, 2020
@TizianoCoroneo
Copy link
Contributor

Overall this looks great. I only have 2 small questions:

  • Do you think it is possible to add an ErrorType: Error generic type to the signature of handleErrorAsync in the ApolloErrorInterceptor? I would love to be able to return typed errors, and this generic parameter could simply default to Error itself if not specified.
  • Do you have any plan to add an official ApolloCombine module?

@designatednerd
Copy link
Contributor Author

  1. I'll have to mess around with this and see how it works - my recollection is that it would cause much more complex typing issues, but I'll see what we can do.
  2. Eventually, but it's going to be after all the codegen rewrites. There is a community Combine package if you're looking for something immediately

@eliperkins
Copy link
Contributor

Instantiating a new legacy client

Heh, this was kinda funny to me.


public let requestCreator: RequestCreator

Should the request creator live on all requests? Said another way, what's the rationale behind only allowing JSONRequests to have this property?

Do you think it is possible to add an ErrorType: Error generic type to the signature of handleErrorAsync

+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!

ApolloStore will no longer require a GraphQLQuery explicitly for fetching data from the store. It will instead return an error if the GraphQLOperationType is not .query. This change is necessary to avoid going down an enormous rabbit hole with generics since GraphQLOperation has an associated type.

I'm a little bit fuzzy on what this change is. If not a GraphQLQuery, what will the input for data fetching from the store be? Can you clarify for me?


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!

@designatednerd
Copy link
Contributor Author

Instantiating a new legacy client

Heh, this was kinda funny to me.

Ha, that's what I get for working on this doc for so long, I completely glossed that over 🙃

public let requestCreator: RequestCreator

Should the request creator live on all requests? Said another way, what's the rationale behind only allowing JSONRequests to have this property?

My thought that was only HTTPRequest subclasses that actually need to use it should have access to it - JSONRequest and a forthcoming UploadRequest would be the places I'm thinking. At this point both of those subclasses would need it, but I don't know that it makes sense to tie that to the base class at this time. Would be interesting to hear your thoughts on that.

ApolloStore will no longer require a GraphQLQuery explicitly for fetching data from the store. It will instead return an error if the GraphQLOperationType is not .query. This change is necessary to avoid going down an enormous rabbit hole with generics since GraphQLOperation has an associated type.

I'm a little bit fuzzy on what this change is. If not a GraphQLQuery, what will the input for data fetching from the store be? Can you clarify for me?

Basically, we have the GraphQLOperation protocol, with an associated type of Data, and its three sub-protocols, GraphQLQuery, GraphQLMutation, and GraphQLSubscription. Previously, we were limiting what operations could read from the cache to GraphQLQuery, since in theory that's the only place getting something from the cache would matter.

HTTPRequest only requires a specification of GraphQLOperation, but when making a call into the cache, we can't use the Blah is GraphQLQuery method of figuring out whether a GraphQLOperation is a query because of the associatedType on GraphQLOperation.

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.

@designatednerd
Copy link
Contributor Author

designatednerd commented Aug 4, 2020

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)

@designatednerd
Copy link
Contributor Author

OK, I've done some poking around on the request for a typed error parameter, and I don't think it's going to be doable at the interceptor level.

When I tried to add this in, any existing Error I tried to return would cause a "Cannot convert value" build failure:
Screen Shot 2020-08-05 at 1 44 07 PM
Screen Shot 2020-08-05 at 1 46 15 PM

It'd make the interceptors a lot harder to keep independent if they all had to have exactly the same type of error. It also wouldn't be possible for me to have default implementations return errors without having TypedError conform to some other protocol that allows me to return an underlying arbitrary error. Ultimately, I think that adds too much complexity for general use cases.

I think if you want to write your own typed wrapper that takes whatever's returned and feeds it into something with an underlying error that's easier to switch on, that could work. But I think requiring everything have to be the same type at a protocol level is going to lead to a hell of a lot more confusion than it solves.

Would love to hear feedback on this finding.

@benjamn
Copy link
Member

benjamn commented Aug 6, 2020

One of the things that's nice about the ApolloLink abstraction used for the web version of Apollo Client is that the link chain is more like a linked list, rather than an array, with each link deciding how to forward the request on to the rest of the chain (and how to process the response), without needing an overarching RequestChain to manage the list of links.

Concretely, this kind of approach would probably mean the InterceptorProvider would return a single ApolloInterceptor (analogous to ApolloLink), representing the head of the linked list, rather than a [ApolloInterceptor] array. The last interceptor in the chain (the tail of the list) would be responsible for performing the actual HTTP request, in most cases—but not always!

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 ApolloLink system, even as I've changed large portions of the rest of the library.

For reference, here's an overview of ApolloLink concepts that I've found useful in the past: https://www.apollographql.com/docs/link/overview/

@designatednerd
Copy link
Contributor Author

I think one huge, huge difference is that things like cancellation, retry, and thread management can be handled through javascript's Observable reactive system.

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 HTTPResponse.

Will futz with this tomorrow.

@benjamn
Copy link
Member

benjamn commented Aug 6, 2020

@designatednerd I'm glad you brought that up!

While I agree that Observables are an easier pill to swallow in JavaScript, and they do figure prominently in the ApolloLink API, I specifically do not think it's necessary to replicate their behavior here.

I should also mention that Observable is not implemented natively in JS, so you still need some sort of library, and (if I'm being honest) the API doesn't fully deliver on any of those benefits you mentioned, so I think it's fair to say the acceptance of Observable in JS (compared to the resistance to Combine in iOS) is a matter of culture/taste/evangelism.

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. Observables don't provide any useful notion of context, even if you wanted it, but you can design a pipeline/interceptor/chain system like this to provide a strongly typed context object everywhere it's needed, I believe.

On the topic of retrying, the RetryLink subclass of ApolloLink has been successful in part because the retry logic can be hidden behind the same abstraction that any other ApolloLink provides. From the perspective of links earlier in the chain, a request that succeeded after several retries looks exactly like one that succeeded on the first try, except that it might have taken a bit longer. If retrying was something you could implement in a perfectly generic way for all interceptors, it might make sense to hoist it to a higher layer of the system (maybe into RequestChain), but in practice retrying tends to be sensitive to application concerns (different logic for different queries, even), so I think it makes sense to push it down into a part of the system that can be customized by application developers.

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 Observable and Combine provide that kind of API, but they are both probably overkill, or at the very least they would need to earn their way into a system like this. I believe you that Combine is not worth it for iOS, but then again I'm not sure Observable is totally defensible for JS applications, either.

@TizianoCoroneo
Copy link
Contributor

TizianoCoroneo commented Aug 6, 2020

When I tried to add this in, any existing Error I tried to return would cause a "Cannot convert value" build failure:

from this error it looks like you changed the signature of handleErrorAsync to:

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!

+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!

@designatednerd would you prefer to move this conversation in a separate issue?

@designatednerd
Copy link
Contributor Author

designatednerd commented Aug 6, 2020

@TizianoCoroneo Yeah if I left it with Error as the incoming error, I got that same Cannot convert value problem in the method in question - I'll take a look at your suggestion later today though

@designatednerd
Copy link
Contributor Author

@benjamn I think one thing that's helpful context to understand is that the basis for this architecture is the ApolloInterceptor type in Android. This itself is based on OkHttp library's Interceptor type that's used very widely throughout the Android ecosystem for networking.

In OkHttp, there's a chain that takes an array of interceptors. Each interceptor in the array needs to call proceed before it can keep going, and then each one calls intercept to handle the result. There's a few issues with that architecture as-is (mostly around things being implicitly asynchronous instead of explicitly asynchronous), which are generally dealt with by ApolloInterceptor's changes to the architecture.

Still futzing around, but I thought that'd be useful context.

@designatednerd
Copy link
Contributor Author

designatednerd commented Aug 7, 2020

@TizianoCoroneo I've pushed some stuff to a branch ominously named nope/typed-errors if you want to take a look - basically, what you're looking for is not possible without associated types which sends this wholllllllllle mess down a huge rabbit hole. This is a huge piece of why the generic constraint is on the function rather than set up as an associatedType in the first place. If you see a better way please definitely feel free to open a PR to that branch!

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 Parseable and moved to GraphQLResult<Operation.Data> as the type being returned in the completion closure), I'll update the RFC tomorrow or Monday to match the updated implementation details.

@TizianoCoroneo
Copy link
Contributor

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 TypedError bubble up to the RequestChain, and from there going everywhere else.
Thanks for trying this anyway 😄

@designatednerd
Copy link
Contributor Author

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 Operation.Data on GraphQLOperation instead of to the more generic Parseable. In the end, the result of a GraphQL operation is actually what we handle, so making it even more generic didn't really anything (and in fact made it harder to real with errors that are within GraphQLResult, which is part of what a lot of folks want to be able to do.

@designatednerd
Copy link
Contributor Author

More updates:

  • Moved HTTPResponse to being optional on the interceptor and related methods rather than having its properties be optional. This makes it way clearer whether a response has been received from the network or not.
  • Moved error declaration within each individual interceptor for clarity

@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 proceed if you feel like it."

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.

@designatednerd
Copy link
Contributor Author

designatednerd commented Aug 18, 2020

More updates are up - I don't think any fundamentally change anything already in here, but I do have a couple questions:

  1. Is there anyone using the context: UnsafeMutableRawPointer? on the various send/fetch methods for ApolloClient in a way that could not be replaced by this change? I took it out for now but I figured It's worth asking.

  2. I'm extremely tempted to rip out the current HTTPNetworkTransport that RequestChainNetworkTransport is replacing altogether, largely because I would need to keep a whole bunch of knotted code in ApolloClient around that facilitates caching longer than I'd like to. However, I'd like to hear y'all's thoughts about ripping the band-aid in a single release (with a migration guide) vs. doing a bridge release that still has HTTPNetworkTransport and its associated ick with a bunch of deprecation warnings.

    For what it's worth I've updated all the tests that aren't specifically for the HTTPNetworkTransport to use either a RequestChainNetworkTransport, or a MockNetworkTransport which inherits from it, and everything is back to passing. I don't think that fully captures the extent to which this would create work for developers using the more advanced delegate features, but I do think for people using less advanced features it shows that everything should work the same.

@eliperkins
Copy link
Contributor

  1. Is there anyone using the context: UnsafeMutableRawPointer? on the various send/fetch methods for ApolloClient in a way that could not be replaced by this change? I took it out for now but I figured It's worth asking.

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'm extremely tempted to rip out the current ` that RequestChainNetworkTransport is replacing altogether, largely because I would need to keep a whole bunch of knotted code in ApolloClient around that facilitates caching longer than I'd like to. However, I'd like to hear y'all's thoughts about ripping the band-aid in a single release (with a migration guide) vs. doing a bridge release that still has HTTPNetworkTransport and its associated ick with a bunch of deprecation warnings.

I think it's alright to remove this in favor of RequestChainNetworkTransport. Is there a way to deprecate it, and replace it's impl with RequestChainNetworkTransport? If it feels like more overhead to maintain, API-wise, I think it's fine to force consumers over to use RequestChainNetworkTransport instead, since it solves the same problems that supplying a custom HTTPNetworkTransport was doing.

@designatednerd
Copy link
Contributor Author

designatednerd commented Aug 18, 2020

feels like a pretty cumbersome API to use effectively right now,

Wholeheartedly agreed, I want to come up with something way better (and that does not involve the word Unsafe)

replace it's impl withRequestChainNetworkTransport

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.

@designatednerd
Copy link
Contributor Author

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 HTTPRequest itself and onto the MaxRetryInterceptor.

Further comments should be left on #1341 - thank you all for the feedback!

@wuf810
Copy link

wuf810 commented Oct 13, 2021

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?

@designatednerd
Copy link
Contributor Author

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.

@calvincestari
Copy link
Member

Further to this @wuf810, the networking stack will be the focus of our attention in future 2.x releases.

@wuf810
Copy link

wuf810 commented Oct 14, 2021

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.

@wuf810
Copy link

wuf810 commented Oct 14, 2021

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.

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.

@designatednerd
Copy link
Contributor Author

So the preflight delegate can basically be replaced by anything prior to the NetworkFetchInterceptor, I would look at the order in the DefaultInterceptorProvider and see the order in which that's set up to see if just shoving your custom interceptor at the front of the line would work or if you need to pass your own array of interceptors.

There's a MaxRetryInterceptor provided that you can use to help handle how many times you want to retry.

There isn't really a shouldSend other than calling a completion block with before you hit NetworkFetchInterceptor.

@wuf810
Copy link

wuf810 commented Oct 15, 2021

So the preflight delegate can basically be replaced by anything prior to the NetworkFetchInterceptor, I would look at the order in the DefaultInterceptorProvider and see the order in which that's set up to see if just shoving your custom interceptor at the front of the line would work or if you need to pass your own array of interceptors.

There's a MaxRetryInterceptor provided that you can use to help handle how many times you want to retry.

There isn't really a shouldSend other than calling a completion block with before you hit NetworkFetchInterceptor.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Requests for comment or other discussions networking-stack
Projects
None yet
Development

No branches or pull requests

6 participants