Conversation
Instead, a protocol `GraphQLOperationBase` with no associated type has been extracted from `GraphQLOperation`.
| /// The `ApolloClient` class provides the core API for Apollo. This API provides methods to fetch and watch queries, and to perform mutations. | ||
| public class ApolloClient { | ||
| let networkTransport: NetworkTransport | ||
| let link: TerminatingLink |
There was a problem hiding this comment.
I'm a little confused here because the terminating link would be the last link in the chain, so if you add any other links in front of it, the link you set on the client would be non-terminating.
There was a problem hiding this comment.
I agree, it could be confusing, but actually it makes quite a lot of sense.
The confusion comes from the belief that there is a chain of links, but there isn't.
The only way to combine links is by composition, which produces a new link.
Consider concatenation:
concat(NT, T) -> T and concat(NT, NT) -> NT where NT is a non-terminating link and T is a terminating link.
And split:
split(NT, NT) -> NT and split(T, T) -> T
Then there is also forced termination:
makeTerminating(NT) -> T
As you can see, no matter how many links go into the composition the end result is always a single link.
It makes sense to give the client a terminating link, because it is what produces a result in the end.
Note that this very terminating link may be a single link (e.g. HTTPLink) or a link that was composed from (multiple) other links (e.g. concat(AuthLink, split(HTTPLink, WebSocketLink))).
Does that make sense to you?
There was a problem hiding this comment.
Ah, you're right. I was thinking of this as the first link in the chain, but it's function composition. If we can get this working reliably that would be great, but I don't think it's absolutely necessary to make termination part of the type system.
However, it makes it difficult to define a split of a terminating and a non-terminating link or vice-versa, because what should the return type be? SchrödingersLink?
Have you thought more about this yet?
And what does makeTerminating do?
| let mutationLink = SetContextLink.incrementCounter(by: -1) | ||
|
|
||
| let splitLink = passthroughLink.split(first: queryLink, second: mutationLink) { op, _ in | ||
| type(of: op).rootCacheKey == "QUERY_ROOT" |
There was a problem hiding this comment.
We shouldn't rely on rootCacheKey to get the operation type (and probably not expose it). Instead, maybe we could have a GraphQLOperationType enum and operationType property.
There was a problem hiding this comment.
I would also prefer a way to avoid having to go through type(of: op), making the properties non-static. (They can still be static in the generated code, but maybe we can define instance properties that forward to the static ones in a protocol extension.)
| self.second = second | ||
| } | ||
|
|
||
| func request<Operation>(operation: Operation, context: LinkContext) -> Promise<GraphQLResponse<Operation>> { |
There was a problem hiding this comment.
We really can't rely on Promise here, because links need the ability to return multiple responses. That's important for subscriptions support, but also for something like a polling link. I'd rather not implement Observable (or rely on RxSwift), but maybe a response handler callback would work? On the other hand, promises make the link code a lot easier to write, and so would observables, so maybe it is worth implementing something minimal.
There was a problem hiding this comment.
Yeah, I know :(.
Observables would be nice to have and it would probably be fun to implement, but difficult to get right.
Here's another idea: Similar to the cancel signal, we could add a onNextResponse callback to the context which is called whenever a link produces a new response. What do you think?
There was a problem hiding this comment.
Can you give an example of how onNextResponse would work? Would this replace returning a promise?
| @@ -1,12 +1,14 @@ | |||
| public protocol GraphQLOperation: class { | |||
| public protocol GraphQLOperationBase { | |||
There was a problem hiding this comment.
We may want to keep the : class because it makes code using it slightly more efficient, and there is no reason to have structs conform to this protocol.
| @@ -1,12 +1,14 @@ | |||
| public protocol GraphQLOperation: class { | |||
There was a problem hiding this comment.
Hmmm, actually, I don't think links ever need knowledge of the associated type. Here is an alternative idea:
We keep GraphQLOperation as is (so without GraphQLOperationBase), but we define a new GraphQLRequest protocol and make it the basis of links. So instead of request(operation:context:) it would now be perform(request:).
That means GraphQLRequest also contains the context, basically everything needed to perform a request. Something like:
protocol GraphQLRequest {
var context: GraphQLRequestContext { get }
var operationType: GraphQLOperationType { get }
var requestString: String { get }
var variables: GraphQLMap? { get }
}I like the idea that it makes links GraphQLRequest -> GraphQLResponse, and it cleanly differentiates between operations (reusable across requests) and requests (containing everything that is needed fora single request, including context)
We could then have a GraphQLOperationRequest class that initialises itself based on a GraphQLOperation. So similar to your LinkOperationDescriptor, but with instance properties and also containing the context.
An added benefit is that it also allows you to use links without generated code, or even without the rest of Apollo iOS by implementing something like a GraphQLDynamicRequest class that isn't initialised from a GraphQLOperation.
We do need to make GraphQLResponse depend on GraphQLRequest instead of GraphQLOperation as well, but that might be a good idea anyway because it no longer has to be generic and that way we can more easily store responses.
What do you think?
|
How is this going on @nubbel @martijnwalraven ? This is really helpful! |
| @@ -0,0 +1,37 @@ | |||
| public final class CancelController: Cancellable { | |||
There was a problem hiding this comment.
@ziccardi seems that wrapping Cancellable is the common approach
|
Hi there, what's the status of this PR? |
|
What's the status of this PR? |
|
@nubbel Do you have any interest in continuing this PR? If not, I can close this out for you. |
|
@designatednerd I'm so excited, but I first want to make sure. By closing, you mean resolving and merging this PR? This PR is a really awesome way to enable people to build advanced customization on top of Apollo IOS. |
|
@wtrocki Closing would mean closing without merging. We'd like to add this functionality in general, but this PR is pretty old and has a lot of outstanding comments, and I want to determine if this is something I should take the time to review here, or if that time would be better used looking into a new solution. |
|
@designatednerd well understood thank you. |
|
We're definitely still interested in making request and response handling more composable but at this point, this PR is super out of date and I haven't heard back from the original author for a week about whether they want to proceed. I'll close this one out but definitely keeping #173 open until I'm able to get a better line on this. Thanks very much for your work and sorry this wound up getting neglected! |
|
I'm sorry I haven't been able to finish this. @wtrocki If you intend to work on this issue and anything in this PR is useful, please feel free to reuse as much as you want. |
Also see the discussion in #173.
Terminating vs. Non-Terminating Links
ApolloLink has a concept of terminating and non-terminating links. In the Javascript implementation, the link type is determined at runtime.
I thought we could de better and determine it at compile time by using two distinct types TerminatingLink and NonTerminatingLink. This eliminates some compile time checks. For example, concat is only defined on NonTerminatingLink and returns a terminating or non-terminating link, depending on the type of the second link.
However, it makes it difficult to define a split of a terminating and a non-terminating link or vice-versa, because what should the return type be? SchrödingersLink?
Cancellation
Requests need to be cancellable, that means links need to be cancellable. I considered three options to achieve that:
Using Observables. Observables are inherently cancellable, but non-trivial to implement.
Adding a Cancelled state to Promise.
Using a cancellable object, that us to register onCancel handlers. This was inspired by an experimental Javascript feature, AbortController.
I chose the first third option and created an CancelController and CancelSignal similar to AbortController/AbortSignal in Javascript. The signal is passed in the context.
It seemed to be the less invasive solution and I actually really like the design.
Caveats
The current design has a few caveats. Most notably, links can only return one result, which renders them unsuitable for subscriptions.
Next steps
As a next step, I would like to implement an HTTPLink that pulls configuration from the context and makes a network request.
Then, I'd like to see how we could implement a RetryLink.