Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to Pause and Resume Apollo GraphQL Subscription using The Guild GraphQL CodeGen

Posted on: 2023-01-12

Context and Benefits of Autogeneration

Using The Guild GraphQL CodeGen is my first instinct when consuming a GraphQL on the front end. It works flawlessly by producing strongly typed TypeScript code to perform your custom GraphQL queries. There is a plugin to generate the operation called typescript-operation. In the past, I decided to avoid but recently been a fan since it creates your React Hook from your .graphql files. It gives you the advantage of not having to import the file on each component but instead import the generated code file, which is a .ts file. The two benefits are avoiding needing to import a file extension that is not native with React CRA. Second, it creates for your a hook that is strongly typed.

Here is a snippet of the configuration of the codegen.yml.

generates:
  Autogenerated.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

Why Pausing?

There are many reasons surrounding the viability of pausing a subscription. The WebSocket might be pushing a lot of messages, and the user may want to freeze the user interface of changes. Or, maybe you want to keep the application operational but do not want to receive any further notice of new data. You may want to do it when the user is not active on the tab of your application. Regardless of the reason, being able to pause the stream of data and unpause it is a scenario that occurs.

How to Pause?

Results on the Internet on the topic of pausing a subscription are very scarce. I stumbled upon the following solution by avoiding requests on the query using the skip option on the query. It happens that the subscription has the same property. The generated hook opens access to the choice and next to the variables property, you will see a skip option.

When the skip option is true, the WebSocket closes its connection. When the value is false, a new connection is established. Hence, we can toggle between pause and unpause by having a React state that is a boolean.

Pseudo-Code

In a nutshell, your component looks like this:

export const YourComponent = (props: YourComponentProps) => {
  const [isPaused, setIsPaused] = useState<boolean>(false); // By default, we have the connection pushing data
  // ...
  useYourGeneratedFunctionSubscription({
    onData(options) {
      // Do something with the data in options.data
    },
    skip: isPaused
  });
  //
  return <div>...</div>;
};

Considerations of Missing Data

Going further on the path of pausing, you will get into the situation where the user un-pause. The pattern described works by switching the isPaused to false, a new WebSocket connection is created, and the data starts to flow again. However, there needs to be more data. For example, if you are fetching data that is pushed into a grid and you pause for 1 minute, on resume, you will have the data from the time before the pause and after the minute. Where is the data from the paused minute? Hence, when the user resumes the subscription, it is wise to perform a GraphQL Query to fetch the data.

In my cases, I fetch all the data as if the user were to start the application again and start with a fresh data slate. However, keep in mind that there is still a case of missing or duplicate data. For example, if you are fetching the data and then opening the WebSocket connection, there might be missing data. If it takes 5 seconds for the response to come back, then new data was created but not in the response nor the WebSocket. The reason is that the connection opened once the response was received, leaving a window where new data was never transmitted.

There might be duplicate data if you are starting the WebSocket at the same time you start the request to fetch the data, as the WebSocket may push some data that will be present in the query response. In the example below, the server pushes data twice while building the response. Hence, the query response will contain the data already pushed. However, on the third push, the data is not from the initial response. So, upon reception of the initial query, filtering out the information already received from the WebSocket is desired. In many scenarios, the amount of data filtered out is small as most of the data comes from prior to the WebSocket connection and is unavailable in the few seconds of the GraphQL query request and response.

A native solution does not exist using Apollo Client. However, leaning toward duplication is reasonable if your data has a unique identifier (or is timestamped), as it is easy to filter out. On the contrary, it is only possible to substitute present information. Thus, opening the WebSocket upon receiving the initial data is suboptimal for your user because it cannot guarantee that all the data is available.

Optimization

Depending on how your data is structured, you may resume the data by fetching only the missing delta. Data with a sequential number or based on time can persist the last receive data in memory. When the action of resuming streaming the data starts, a GraphQL Query is performed with the last data id (or timestamp) and the response containt only the information since this last piece of information. The optimization adds complexity to support querying from a specific point in time but is suitable for an architecture based on cursor like GraphQL Relay. For example, in the illustration below, the user pauses at T2 and resumes at T6. At T6, the subscription turns skip: false, creating a new WebSocket connection and getting the latest data. At the same time, a GraphQL Query fetching data from T2 to T6 is triggered.

Thus, my recommendation is that if you are not using a cursor your data can be fetched affordly from without delta to fetch everything. That should be many scenarios. However, if you have infinite data, like a news feed, and that this data is already provided using cursor that getting delta might be the best solution. The main issue with delta fetching during resuming is that it might fetch from a long time ago. For example, if someone goes out to vacation for one week and unpause, the data sent will be dated from a week ago. Fetching everything does not prevent over fetching but rely on the same logic that you have when the user enter the application meaning that it must already have a logic in place. As long as the delta logic on the backend has similar limit on data being fetched, it will work fine.

Conclusion

Whether you are autogenerating or not, the skip value is available if you use the Apollo Client. The surprising news is that it works well with the Apollo Client Subscription and behaves as a way to pause the data pushed into your application from the WebSocket beneath the GraphQL Subscription mechanism.