Multiple subscriptions overriding each other

I have been searching for days to find the answer to this problem and I can’t find any mention of it anywhere.

I have a React application that uses both Queries and Subscriptions.
Queries are hitting /api
Subscriptions are hitting /api/subscriptions

The flow is 1 Query for filter options to put in variables, 3 Queries for initial data, then 3 Subscriptions for updates to that data. Let’s call them People, Books, and Cars.
Query Filters /api
…then
Query People /api
Query Books /api
Query Cars /api
…then
Subscribe People /subscriptions
Subscribe Books /subscriptions
Subscribe Cars /subscriptions

The problem is that the last Subscribe (in this case Cars) is the only one that will receive updates. If i change the order to Cars, People, Books, then Books receives the updates.

Checking Chrome dev tools I can see the messages all have different ids under the /subscriptions endpoint.

The server is is managed by a different team and it’s written in Go with the github.com/gorilla/websocket package. I THINK that is due to no Apollo server package available for Go. Not sure if this is a client side or server side issue

Here is an example of the client side implementation

Apollo setup

import { ApolloClient, ApolloProvider, split, HttpLink, InMemoryCache } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';

const httpLink = new HttpLink({
  uri: process.env.REACT_APP_SERVICE_API,
  credentials: 'include'
});

const wsLink = new WebSocketLink({
  uri: `wss://${process.env.REACT_APP_SERVICE_API.replace('https://', '')}/subscriptions`,
  credentials: 'include',
  options: {
    reconnect: true,
    wsOptionArguments: []
  }
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link: splitLink,
  cache
});

Component

import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { FILTERS_QUERY } from '../../graphql/filters.query';
import { PEOPLE_SUBSCRIPTION, PEOPLE_QUERY } from '../../graphql/people.operation';
import { BOOKS_SUBSCRIPTION, BOOKS_QUERY } from '../../graphql/books.operation';
import { CARS_SUBSCRIPTION, CARS_QUERY } from '../../graphql/cars.operation';
import { FilterBar, FilterChip, LoadingSpinner } from '../';
import { useLayoutContext, useStateCallback } from '../../hooks';
import { isNotEmpty, Logger } from '../../utils';
import OverviewSections from './OverviewSections/OverviewSections';

const Overview = () => {
  const { isLoading, setIsLoading } = useLayoutContext();
  const [filters, setFilters] = useStateCallback({ people: [] });

  const { loading: filtersLoading, data: filtersData } = useQuery(FILTERS_QUERY, {
    onError: (error) => Logger('filtersError: ', error.message),
    onCompleted: (data) => {
      setFilters({
        people: data.filters.people
      });
    }
  });

  const { data: peopleData, loading: peopleLoading, subscribeToMore: subscribeToMorePeople } = useQuery(PEOPLE_QUERY, {
    variables: filters,
    skip: !isNotEmpty(filters.people),
    errorPolicy: 'all',
    onCompleted: (data) => Logger('peopleData', data),
    onError: (error) => Logger('peopleError', error.message)
  });

  useEffect(() => {
    if (isNotEmpty(filters.people)) {
      subscribeToMorePeople({
        document: PEOPLE_SUBSCRIPTION,
        variables: filters,
        updateQuery: (prev, { subscriptionData }) => {
          Logger('PEOPLE_SUBSCRIPTION: ', subscriptionData);

          if (!subscriptionData.data) {
            return prev;
          }

          return Object.assign({}, prev, {
            people: subscriptionData.data.people
          });
        }
      });
    }
  }, [subscribeToMorePeople, filters]);

  const { data: booksData, loading: booksLoading, subscribeToMore: subscribeToMoreBooks } = useQuery(BOOKS_QUERY, {
    variables: filters,
    skip: !isNotEmpty(filters.people),
    errorPolicy: 'all',
    onCompleted: (data) => Logger('booksData', data),
    onError: (error) => Logger('booksError', error.message)
  });

  useEffect(() => {
    if (isNotEmpty(filters.people)) {
      subscribeToMoreBooks({
        document: BOOKS_SUBSCRIPTION,
        variables: filters,
        updateQuery: (prev, { subscriptionData }) => {
          Logger('BOOKS_SUBSCRIPTION', subscriptionData);

          if (!subscriptionData.data) {
            return prev;
          }

          return Object.assign({}, prev, {
            books: subscriptionData.data.books
          });
        }
      });
    }
  }, [subscribeToMoreBooks, filters]);

  const { data: carsData, loading: carsLoading, subscribeToMore: subscribeToMoreCars } = useQuery(CARS_QUERY, {
    variables: filters,
    skip: !isNotEmpty(filters.people),
    errorPolicy: 'all',
    onCompleted: (data) => Logger('carsData', data),
    onError: (error) => Logger('carsError', error.message)
  });

  useEffect(() => {
    if (isNotEmpty(filters.people)) {
      subscribeToMoreCars({
        document: CARS_SUBSCRIPTION,
        variables: filters,
        updateQuery: (prev, { subscriptionData }) => {
          if (!subscriptionData.data) {
            return prev;
          }

          if (isNotEmpty(prev.cars)) {
            return Object.assign({}, prev, {
              cars: [subscriptionData.data.cars, ...prev.cars]
            });
          } else {
            return {
              cars: [subscriptionData.data.cars]
            };
          }
        }
      });
    }
  }, [filters, subscribeToMoreCars]);

  useEffect(() => {
    setIsLoading([ filtersLoading, booksLoading, peopleLoading, carsLoading].includes(true));
  }, [setIsLoading, filtersLoading, booksLoading, peopleLoading, carsLoading]);

  function handleApply(list, filterKey) {
    setFilters((prevState) => ({
      ...prevState,
      [filterKey]: list
    }));
  }

  return (
    <div id="overview">
      <section className="top-action-bar">
        {isNotEmpty(filtersData) && (
          <FilterBar>
            <FilterChip
              label="People"
              id="overview-people-filter"
              data={filtersData.filters.people}
              filterKey="people"
              onApply={handleApply}
            />
          </FilterBar>
        )}
      </section>
      <OverviewSections
        books={booksData?.books}
        cars={carsData?.cars}
        people={peopleData?.people}
        filters={filtersData?.filters}
      />
      <LoadingSpinner show={isLoading} delay={1000} />
    </div>
  );
};

export default Overview;

UPDATE:
It looks like this is actually an issue with the server side implementation rather than the client. The server isn’t currently accepting multiple channels so each new subscription is overriding the previous one. Not an issue with Apollo.

How did you manage to fix the issue on the server side ?