3
votes

I am trying to configure subscriptions with Apollo 2 and NEXT.js. I can get the client to connect to the server and they are working in the GraphQL playground, so the bad configuration must be in the withData file, or the component that handles the subscription.

When inspecting the socket connection on the network panel in chrome, the subscription payload does not get added as a frame, like it does in the GraphQL playground.

withData:

import { ApolloLink, Observable } from 'apollo-link';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';

import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { onError } from 'apollo-link-error';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';

function createClient({ headers }) {
  const cache = new InMemoryCache();

  const request = async (operation) => {
    operation.setContext({
      http: {
        includeExtensions: true,
        includeQuery: false
      },
      headers
    });
  };

  const requestLink = new ApolloLink(
    (operation, forward) => new Observable((observer) => {
      let handle;
      Promise.resolve(operation)
        .then(oper => request(oper))
        .then(() => {
          handle = forward(operation).subscribe({
            next: observer.next.bind(observer),
            error: observer.error.bind(observer),
            complete: observer.complete.bind(observer)
          });
        })
        .catch(observer.error.bind(observer));

      return () => {
        if (handle) handle.unsubscribe();
      };
    })
  );

  return new ApolloClient({
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors) {
          console.log({ graphQLErrors });
        }
        if (networkError) {
          console.log('Logout user');
        }
      }),
      requestLink,
      // link,
      withClientState({
        defaults: {
          isConnected: true
        },
        resolvers: {
          Mutation: {
            updateNetworkStatus: (_, { isConnected }, { cache }) => {
              cache.writeData({ data: { isConnected } });
              return null;
            }
          }
        },
        cache
      }),
      createPersistedQueryLink().concat(
        new BatchHttpLink({
          uri: GRAPHQL_ENDPOINT,
          credentials: 'include'
        }),
        process.browser
          ? new WebSocketLink({
            uri: WS_PATH,
            options: {
              reconnect: true
            }
          })
          : null
      )
    ]),
    cache
  });
}

export default withApollo(createClient);

Subscription component:

import { CONVERSATION_QUERY } from '../../constants/queries';
import { CONVERSATION_SUBSCRIPTION } from '../../constants/subscriptions';
import PropTypes from 'prop-types';
import { Query } from 'react-apollo';

const Conversation = props => (
  <Query
    {...props}
    query={CONVERSATION_QUERY}
    variables={{ input: { _id: props._id } }}
  >
    {(payload) => {
      const more = () => payload.subscribeToMore({
        document: CONVERSATION_SUBSCRIPTION,
        variables: { input: { conversation: props._id } },
        updateQuery: (prev, { subscriptionData }) => {
          console.log({ subscriptionData });

          if (!subscriptionData.data.messageSent) return prev;

          const data = subscriptionData;

          console.log({ data });

          return Object.assign({}, prev, {});
        },
        onError(error) {
          console.log(error);
        },
        onSubscriptionData: (data) => {
          console.log('onSubscriptionData ', data);
        }
      });

      return props.children({ ...payload, more });
    }}
  </Query>
);

Conversation.propTypes = {
  children: PropTypes.func.isRequired
};

export default Conversation;

The subscription that has been tested in the GraphQL playground:

import gql from 'graphql-tag';

export const CONVERSATION_SUBSCRIPTION = gql`
  subscription messageSent($input: messageSentInput) {
    messageSent(input: $input) {
      _id
      users {
        _id
        profile {
          firstName
          lastName
          jobTitle
          company
          picture
        }
      }
      messages {
        _id
        body
        createdAt
        read
        sender {
          _id
          profile {
            firstName
            lastName
            jobTitle
            company
            picture
          }
        }
      }
    }
  }
`;

The more function is then executed in componentDidMount:

componentDidMount() {
    this.props.subscribeToMore();
  }

The result in the console from the log in updateQuery is:

{"data":{"messageSent":null}}
1

1 Answers

3
votes

I hadn't configured my withData file properly. You need to use split from the apollo-link package to let Apollo determine if the request should be handled with http or ws. Here is my working configuration file.

import { ApolloLink, Observable } from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { getMainDefinition } from 'apollo-utilities';
import { onError } from 'apollo-link-error';
import { split } from 'apollo-link';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';

function createClient({ headers }) {
  const cache = new InMemoryCache();

  const request = async (operation) => {
    operation.setContext({
      http: {
        includeExtensions: true,
        includeQuery: false
      },
      headers
    });
  };

  const requestLink = new ApolloLink(
    (operation, forward) => new Observable((observer) => {
      let handle;
      Promise.resolve(operation)
        .then(oper => request(oper))
        .then(() => {
          handle = forward(operation).subscribe({
            next: observer.next.bind(observer),
            error: observer.error.bind(observer),
            complete: observer.complete.bind(observer)
          });
        })
        .catch(observer.error.bind(observer));

      return () => {
        if (handle) handle.unsubscribe();
      };
    })
  );

  const httpLink = new BatchHttpLink({
    uri: GRAPHQL_ENDPOINT
  });

  // Make sure the wsLink is only created on the browser. The server doesn't have a native implemention for websockets
  const wsLink = process.browser
    ? new WebSocketLink({
      uri: WS_PATH,
      options: {
        reconnect: true
      }
    })
    : () => {
      console.log('SSR');
    };

  // Let Apollo figure out if the request is over ws or http
  const terminatingLink = split(
    ({ query }) => {
      const { kind, operation } = getMainDefinition(query);
      return (
        kind === 'OperationDefinition'
        && operation === 'subscription'
        && process.browser
      );
    },
    wsLink,
    httpLink
  );

  return new ApolloClient({
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors) {
          console.error({ graphQLErrors });
        }
        if (networkError) {
          console.error({ networkError});
        }
      }),
      requestLink,
      // link,
      withClientState({
        defaults: {
          isConnected: true
        },
        resolvers: {
          Mutation: {
            updateNetworkStatus: (_, { isConnected }, { cache }) => {
              cache.writeData({ data: { isConnected } });
              return null;
            }
          }
        },
        cache
      }),

      // Push the links into the Apollo client
      createPersistedQueryLink().concat(
        // New config
        terminatingLink
        // Old config
        // new BatchHttpLink({
        //   uri: GRAPHQL_ENDPOINT,
        //   credentials: 'include'
        // })
      )
    ]),
    cache
  });
}

export default withApollo(createClient);