Advice on Connecting Apollo Subscriptions via Shared Worker

Hi everyone,

We’re currently using Apollo GraphQL Client in our frontend React.js application, and we subscribe to backend events using GraphQL subscription queries over WebSocket. We’re now exploring ways to reduce the number of WebSocket connections across multiple browser tabs.

To achieve this, we plan to introduce a Shared Worker, which will manage a single WebSocket connection to our backend (Apollo GraphQL Server), and expose communication hooks for each tab to interact with the Shared Worker. Each tab will then subscribe to GraphQL updates through the Shared Worker, rather than establishing its own WebSocket connection.

We’re wondering if there are any best practices, patterns, or recommendations from the Apollo community for integrating Shared Workers with Apollo subscriptions, especially in terms of client setup, caching, reconnection logic, or handling subscription multiplexing.

Any guidance or references would be greatly appreciated!

Thanks in advance :folded_hands:

I also see apollo-link-webworker on this page Community links - Apollo GraphQL Docs, but the last commit was 7 years ago. I don’t know if still worth to try it.

Hey @Bobby_Chao :waving_hand:

I can’t speak quite to the worker side of it since I haven’t tried doing this myself but I can at least give you a few tips from the ApolloLink side of things.

Apollo Client core expects to receive separate observable notifications for each subscription requested by your components. If you do any sort of multiplexing so that you maintain a single connection for multiple subscriptions, you’ll need to make sure that each subscription owns a separate Observable. Apollo Client core will call the link chain for each subscription that wants data from the network so its easy to know when you get new requests.

It probably isn’t too difficult to track each observable if you do it via a Map or something similar. Be aware that you will likely need to track the combination of the query + variables and not just the query itself (“query” being the GraphQL document for the observable, not a query operation). If you only dedupe only on the query itself, you might report results for the wrong variables (for example, imagine reporting updates to a user with ID 1 for a subscription that requests variables for user 2. That could be problematic!).

For variables, you can use the canonicalStringify function exported from @apollo/client/utilities which should provide a stable string value. It sorts keys in the object so that the order is deterministic and you get the same string back. We use this utility inside Apollo Client in quite a few places :slightly_smiling_face:.

caching

Caching is not something you have to worry about assuming you follow the steps above. Apollo Client core doesn’t care what transport is used to provide data from the queries, just that its emitted from the source observable. The core will handle caching that data and notifying existing components that a cache update has happened. You just have to make sure the terminating link sends events from your subscription on the right observable and Apollo Client will take care of the rest.

reconnection logic

The biggest thing here is determining where you want the reconnection to happen. You can put it in your terminating link to provide automatic reconnections as long as your terminating link doesn’t call observer.complete() or observer.error() to terminate the observable. Once the observable terminates, you’d need the component/code responsible for starting the subscription to handle reconnecting again (hopefully that makes sense). I’d play around with this and see what kind of experience you want. If you go the route of automatically reconnecting in the terminating link, I’d recommend adding some kind of reconnect limit or exponential backup (in case the network is down or your backend is overwhelmed). See the RetryLink for prior art.

In fact you might try RetryLink. Its biggest downside is that it only handles network errors instead of GraphQL errors, but that retry logic might still work. Definitely play around with it, but if not, you might be able to use that for inspiration.

Let me know if you have any questions, but hopefully this gets you on the right track!