npm package: @livesubscriptions/node-server
Description: Subscription middleware and utilities for GQL Live Subscriptions
Author: David Hardy
Latest version: 0.6.16

# Live Subscriptions

[![pipeline status](https://gitlab.com/livesubscriptions/monorepo/badges/develop/pipeline.svg)](https://gitlab.com/livesubscriptions/monorepo/commits/master)
[![coverage report](https://gitlab.com/livesubscriptions/monorepo/badges/develop/coverage.svg)](https://gitlab.com/livesubscriptions/monorepo/-/commits/master)
[![licence](https://img.shields.io/npm/l/@livesubscriptions/node-server.svg?style=flat)](https://opensource.org/licenses/MIT)
[![npm version](https://img.shields.io/npm/v/@livesubscriptions/node-server.svg?style=flat)](https://www.npmjs.com/package/@livesubscriptions/node-server)

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](https://www.npmjs.com/package/@livesubscriptions/apollo-client) and
the [server](https://www.npmjs.com/package/@livesubscriptions/node-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

```bash
yarn add @livesubscriptions/apollo-client
```

Assuming you already know how to set up [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/)
for Apollo Client, adding support for Live Subscriptions is straightforward.

```typescript
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

```bash
yarn add @livesubscriptions/node-server
```

When running an [Express GraphQL Server](https://www.apollographql.com/docs/graphql-subscriptions/express/) installing
Live Subscriptions is can be done as follows:

```typescript
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.

```graphql
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.

```graphql
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)](http://tools.ietf.org/html/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.

```typescript
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`.

```typescript
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.

```json
[
  {
    "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` exposes `minifyLiveData`.

## Keywords

GraphQL, Apollo, Live Queries, Live Subscriptions

## License

By [David Hardy](http://www.davidhardy.nl) and [codecentric](https://www.codecentric.nl):

```text
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.
```