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

[WIP] Add Apollo Link #186

Closed
wants to merge 4 commits into from
Closed

Conversation

nubbel
Copy link

@nubbel nubbel commented Nov 22, 2017

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.

Instead, a protocol `GraphQLOperationBase` with no associated type has been extracted from `GraphQLOperation`.
@@ -28,7 +28,7 @@ public typealias OperationResultHandler<Operation: GraphQLOperation> = (_ result

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

Copy link
Author

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

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"
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Interesting

@@ -1,12 +1,14 @@
public protocol GraphQLOperation: class {
Copy link
Contributor

@martijnwalraven martijnwalraven Nov 23, 2017

Choose a reason for hiding this comment

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

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?

Copy link
Author

Choose a reason for hiding this comment

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

Sound great!

Copy link
Contributor

Choose a reason for hiding this comment

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

Glad you like it!

@dangthaison91
Copy link

How is this going on @nubbel @martijnwalraven ? This is really helpful!

@@ -0,0 +1,37 @@
public final class CancelController: Cancellable {
Copy link

@wtrocki wtrocki Oct 10, 2018

Choose a reason for hiding this comment

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

@ziccardi seems that wrapping Cancellable is the common approach

@koenpunt
Copy link
Contributor

Hi there, what's the status of this PR?

@LeeWong
Copy link

LeeWong commented Apr 26, 2019

What's the status of this PR?

@designatednerd
Copy link
Contributor

@nubbel Do you have any interest in continuing this PR? If not, I can close this out for you.

@wtrocki
Copy link

wtrocki commented Jul 10, 2019

@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.
There is a lot of work done already. It may require very little effort now to get it merged, but IMHO it will be the game changer that may really help the community to customize their clients without pushing some specific use cases to the core.

@designatednerd
Copy link
Contributor

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

@wtrocki
Copy link

wtrocki commented Jul 10, 2019

@designatednerd well understood thank you.

@designatednerd
Copy link
Contributor

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!

@nubbel
Copy link
Author

nubbel commented Jul 19, 2019

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.

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.

7 participants