@livesubscriptions/node-server
v0.6.16
Published
Subscription middleware and utilities for GQL Live Subscriptions
Downloads
15
Maintainers
Readme
Live Subscriptions
Live Subscriptions for GraphQL. Client state easily managed by the server, only send patches on updates.
Installation
To integrate Live Subscriptions in your system you need to insert middleware in both the client and the server.
TLDR
- Obtain the packages via npm.
- Install middleware on client and server, both a single line change.
- Introduce two small keywords in your schema
- Prefix subscription with live; livePosts
- Add
liveId: String!
to the toplevel type.
Apollo Client
yarn add @livesubscriptions/apollo-client
Assuming you already know how to set up Subscriptions for Apollo Client, adding support for Live Subscriptions is straightforward.
import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client';
import { liveRequestHandlerBuilder } from '@livesubscriptions/apollo-client';
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([liveRequestHandlerBuilder(), splitLink]), // It's just this line (and the import)
});
Server
yarn add @livesubscriptions/node-server
When running an Express GraphQL Server installing Live Subscriptions is can be done as follows:
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import { liveSubscribeBuilder } from '@livesubscriptions/node-server';
server.listen(PORT, () => {
new SubscriptionServer(
{
execute,
subscribe: liveSubscribeBuilder(subscribe), // It's just this line (and the import)
schema: myGraphQLSchema,
},
{
server: server,
path: '/subscriptions',
},
);
});
Usage
Client side there are no additional requirements to start using Live Subscriptions. Server side some minor changes are needed.
Schema
Without Live Subscriptions a regular schema looks like the schema below. A list with posts, and each Post
has
an Author
. Suppose our backend has all capabilities to notify all clients whenever new data is present. Whenever
something changes all clients receives all Posts
and all Authors
. So when the name
of an Author
changes,
all Posts
and all nested Authors
are send to all client.
type Subscription {
posts: [Post!]!
}
type Post {
author: Author!
content: string!
}
type Author {
name: string!
}
Therefore typically this is not done with a Subscription
, but with a Query
and several Subscription
. Merging data
is left to the client.
Now see the Live Subscriptions implementation. The root of the subscription has been changed, and prefixed with live . Also a new object is introduced, with the field liveId. Everything else is the same. With just these two minor changes to the schema Live Subscriptions can start working.
type Subscription {
livePosts: LivePosts!
}
type LivePosts {
liveId: string!
posts: [Post!]!
}
type Post {
author: Author!
content: string!
}
type Author {
name: string!
}
When ever a part of the data changes, only the changed data is send to all clients. For this the middleware keeps track of state for each client, and uses JSON-Patch (RFC6902) standard format to create diffs. State is automatically cleaned when the websocket closes. This keeps working, even when using multiple instance of GraphQL, since websockets are tied to a single instance.
The schema requirements for Live Subscriptions can be summarized in 2 bullets:
- The subscription must be prefixed with live;
livePosts
. - The root object must have a
liveId
field op type string.
Implementation
There are no additional requirements for the client, besides installing middleware.
const { data, error, loading } = useSubscription(gql`
subscription livePosts {
livePosts {
liveId
posts {
content
author {
name
}
}
}
}
`);
Server side mostly relies on the middleware as well, and regular GraphQL resolvers. However, a utility
class LiveManager
is provided to simply managing the AsyncIterator
.
export const liveManager = new LiveManager(pubSub);
liveManager.addTopic('livePosts');
pubSub.subscribe('PostUpdateEvent', () => liveManager.publish('livePosts'), {});
pubSub.subscribe('AuthorUpdateEvent', () => liveManager.publish('livePosts'), {});
Subscription: {
livePosts: {
// The name of the topic in the LiveManager must be the name of this field; livePosts.
subscribe: async (parent: unknown, args: unknown, context: any) => {
return liveManager.addSubscription({ topic: 'livePosts' }, context.user.id);
};
}
}
LivePosts: {
posts: async (root: GqlLivePosts, args: unknown, context: any) => {
/* Resolver implementations */
};
}
Now whenever the backend inform GraphQL on new data, the resolvers will reconstruct the entire data structure per client that might be interested. The middleware will create patch files with only the diffs per client, and the middleware client side will reconstruct the data structure.
[
{
"OP": {
"path": "xxx",
"value": "yyy"
}
}
]
Caveats
There are some caveats to Live Subscriptions, where the main one is that your server becomes statefull. This however is without risk, since the middleware is responsible for cleaning this state once it becomes obsolete, and websockets guarentee connection to a single server. Next to that there are some more:
- Subscribe not accessible in all version Apollo GQL, some copy code is needed
- useSubscription can create issues with strict mode, during development.
- useSubscription can remain open on live reload, during development
- https://github.com/apollographql/apollo-client/issues/6405
- Sockets need custom dataloader init script and auth, see upcoming blogposts
- https://github.com/apollographql/apollo-link/issues/197
- https://www.apollographql.com/docs/react/data/subscriptions/#authentication-over-websocket
- https://github.com/apollographql/apollo-server/issues/1526
- useSubscription can stays active after unmount
- Read more
Misc
- For testability in - for example - e2e tests,
liveSubscribe
exposesminifyLiveData
.
Keywords
GraphQL, Apollo, Live Queries, Live Subscriptions
License
By David Hardy and codecentric:
The MIT License
Copyright (c) 2022 David Hardy
Copyright (c) 2022 codecentric nl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.