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

New GraphQLWsLink for graphql-ws subscriptions library #9369

Merged
merged 10 commits into from
Feb 2, 2022

Conversation

glasser
Copy link
Member

@glasser glasser commented Jan 28, 2022

Apollo Client currently contains WebSocketLink in
@apollo/client/link/ws which uses the subscriptions-transport-ws
library. That library is no longer actively maintained, and there is an
improved fork called graphql-ws.

The two libraries use different protocols so a different client link is
required for graphql-ws. (While the WebSocket protocol does allow for
subprotocol negotiation, neither server implementation supports this in
a practical way.)

This PR adds a new link GraphQLWsLink in
@apollo/client/link/subscriptions. Its constructor arguments are the
same as the createClient function in graphql-ws (or it can take a
Client object returned from that function), and you need to install
the optional peer dep graphql-ws instead of
subscriptions-transport-ws. Once you've created the link, it works
exactly like the old WebSocketLink.

This PR changes the main subscriptions doc page to mostly document the
new link, with an extra section at the bottom for the old link.

The core GraphQLWsLink code is based on MIT-licensed code from the
README of the graphql-ws repository.

Fixes #8345.
Part of apollographql/apollo-server#6058

@glasser glasser changed the title [wip for graphql-ws support] New GraphQLWsLink for graphql-ws subscriptions library Jan 28, 2022
@glasser glasser marked this pull request as ready for review January 28, 2022 21:14
@glasser glasser force-pushed the glasser/graphql-ws branch 2 times, most recently from 76dc22a to 3f32357 Compare January 28, 2022 22:10
@glasser
Copy link
Member Author

glasser commented Jan 28, 2022

(fixed accidental prettier applications)

Copy link
Contributor

@brainkim brainkim left a comment

Choose a reason for hiding this comment

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

Thanks for doing this @glasser. Some thoughts, but nothing blocking.

src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
src/link/subscriptions/index.ts Show resolved Hide resolved
src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
export class GraphQLWsLink extends ApolloLink {
private client: Client;

constructor(optionsOrClient: ClientOptions | Client) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is very little benefit to this sort of polymorphism, except maybe consistency and users not having to import graphql-ws directly. However, I think the latter pro can also be considered a a con, as people are likely to improperly add graphql-ws to their dependencies, or improperly configure the client and then open up an issue with us. Also, the type-sniffing of a dependency creeps me out because this would break if graphql-ws ever added a subscribe property directly to the client object. Therefore, I think it might be better to BYOWSC (bring your own websocket client).

But it’s not that big a deal.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's my general philosophy as well. I was echoing the existing link here, but very happy to just require you to use graphql-ws directly. It simplifies the doc too. @benjamn Any thoughts or shall I make this change?

Copy link
Member

@benjamn benjamn Feb 1, 2022

Choose a reason for hiding this comment

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

Since Client is just an interface in the graphql-ws library, I agree it's important to preserve the ability to provide your own Client, and we should not assume createClient will always be the source of the implementation. In other words, I agree with @brainkim that the constructor should take a Client and not ClientOptions (so it doesn't have to do any shape sniffing).

I'm sympathetic to the argument that new GraphQLWsLink({ url }) seems more ergonomic in the common case than new GraphQLWsLink(createClient({ url })), but the first version forces us to call (and bundle!) createClient (a rather large function—333 lines) behind the scenes, much like new ApolloClient({ resolvers }) forces us to use/bundle our old LocalState implementation, whereas something more verbose like

new ApolloClient({
  localState: new LocalState({ resolvers }),
})

would allow developers to completely omit our LocalState implementation, or provide their own. In other words, the simplicity of new GraphQLWsLink({ url }) has some large hidden costs.

With all that said, I believe we could safely re-export createClient from this module, without impacting bundle sizes when it's not used, just so folks don't have to import it from graphql-ws directly if they don't want to.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I think I want to move towards just taking the client (not creation options) and letting you import it yourself from graphql-ws rather than re-exporting it. I don't think importing a function from a package you're required to install yourself anyway (optional peer dep) is that big a deal.

At this point, the only non-test dependency on graphql-ws will be for the Client type (which is an exposed part of the GraphQLWsLink API). Should we still have this as an optional peer dep? Or is there a simpler alternative? It would be nice if there was a mechanism in npm/ts for "type-only dep" but I don't think there really is one (devDependencies aren't appropriate for types that are actually used in the depending package's API, I think). I guess we could also just copy the Client interface into the file or something but that's not great either.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I think the optional peer dep is still the way to go. Better to reuse the original Client type than copy it over.

Copy link
Member

@benjamn benjamn left a comment

Choose a reason for hiding this comment

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

Looking good, with a few questions/comments/responses. Thanks especially for all this documentation @glasser!

docs/source/api/link/apollo-link-ws.md Outdated Show resolved Hide resolved
docs/source/data/subscriptions.mdx Outdated Show resolved Hide resolved
docs/source/data/subscriptions.mdx Outdated Show resolved Hide resolved
export class GraphQLWsLink extends ApolloLink {
private client: Client;

constructor(optionsOrClient: ClientOptions | Client) {
Copy link
Member

@benjamn benjamn Feb 1, 2022

Choose a reason for hiding this comment

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

Since Client is just an interface in the graphql-ws library, I agree it's important to preserve the ability to provide your own Client, and we should not assume createClient will always be the source of the implementation. In other words, I agree with @brainkim that the constructor should take a Client and not ClientOptions (so it doesn't have to do any shape sniffing).

I'm sympathetic to the argument that new GraphQLWsLink({ url }) seems more ergonomic in the common case than new GraphQLWsLink(createClient({ url })), but the first version forces us to call (and bundle!) createClient (a rather large function—333 lines) behind the scenes, much like new ApolloClient({ resolvers }) forces us to use/bundle our old LocalState implementation, whereas something more verbose like

new ApolloClient({
  localState: new LocalState({ resolvers }),
})

would allow developers to completely omit our LocalState implementation, or provide their own. In other words, the simplicity of new GraphQLWsLink({ url }) has some large hidden costs.

With all that said, I believe we could safely re-export createClient from this module, without impacting bundle sizes when it's not used, just so folks don't have to import it from graphql-ws directly if they don't want to.

src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
@glasser
Copy link
Member Author

glasser commented Feb 1, 2022

@brainkim @benjamn I believe all feedback has been addressed.

glasser and others added 9 commits February 1, 2022 15:31
Apollo Client currently contains `WebSocketLink` in
`@apollo/client/link/ws` which uses the `subscriptions-transport-ws`
library. That library is no longer actively maintained, and there is an
improved fork called `graphql-ws`.

The two libraries use different protocols so a different client link is
required for `graphql-ws`. (While the WebSocket protocol does allow for
subprotocol negotiation, neither server implementation supports this in
a practical way.)

This PR adds a new link `GraphQLWsLink` in
`@apollo/client/link/subscriptions`. Its constructor arguments are the
same as the `createClient` function in `graphql-ws` (or it can take a
`Client` object returned from that function), and you need to install
the optional peer dep `graphql-ws` instead of
`subscriptions-transport-ws`. Once you've created the link, it works
exactly like the old `WebSocketLink`.

This PR changes the main subscriptions doc page to mostly document the
new link, with an extra section at the bottom for the old link.

The core GraphQLWsLink code is based on MIT-licensed code from the
README of the graphql-ws repository.

Fixes #8345.
Part of apollographql/apollo-server#6058
@glasser glasser changed the base branch from main to release-3.6 February 1, 2022 23:32
@glasser
Copy link
Member Author

glasser commented Feb 1, 2022

@benjamn Rebased and retargeted to release-3.6, with CHANGELOG added.

Copy link
Contributor

@brainkim brainkim left a comment

Choose a reason for hiding this comment

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

Thank you for bearing with my nits!

@benjamn benjamn merged commit d98f1de into release-3.6 Feb 2, 2022
@benjamn benjamn deleted the glasser/graphql-ws branch February 2, 2022 16:22
@benjamn benjamn restored the glasser/graphql-ws branch February 2, 2022 16:22
@darkbasic
Copy link

Would it be possible to release a new beta with this?

benjamn added a commit that referenced this pull request Feb 24, 2022
New GraphQLWsLink for graphql-ws subscriptions library
benjamn added a commit that referenced this pull request Feb 24, 2022
New `GraphQLWsLink` and `@apollo/client/link/subscriptions` sub-package
entry point for `graphql-ws` subscriptions library.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Why use WebSocketLink from '@apollo/client/link/ws'
5 participants