Real-time updates with GraphQL subscriptions¶
A GraphQL subscription lets the server push a message to every browser that has a page open, for example when another user changes the record on screen. The browser opens a websocket (Action Cable) and the page reacts to each message.
The examples below use a hypothetical MyEntity model in a plugin. For the subscription base class and naming rules, see GraphQL.
Prerequisites¶
- Action Cable must work.
config/cable.ymluses theasyncadapter in development and Redis in production. - The frontend Apollo client (
app/frontend/src/shared/plugins/apolloClient.ts) already sends subscription operations over Action Cable; you do not configure it.
Send a signal, not data¶
Design the message so it only says that something changed, and let the client refetch with its ordinary query. A broadcast payload is built once and read by every subscriber, so anything in it bypasses per-user authorisation. A refetch also fixes ordering: messages can arrive out of order, and a refetch always converges on the current state.
A payload may carry identifiers and the state needed to re-authorise the subscriber. It must not carry attribute values, rendered text or anything whose visibility depends on who reads it.
Define the subscription¶
Create the result type, the subscription and the trigger in your plugin's app/api/easy_graphql/.
| app/api/easy_graphql/subscriptions/results/my_entity_changed_result.rb | |
|---|---|
Return a result type instead of naming fields on the subscription. It is easier to extend and reuse.
Re-authorise on every event in update, not only in subscribe: a connection outlives the permissions it was opened with. Return NO_UPDATE to stay silent.
When access is lost, the subscription goes quiet; there is no "you can no longer see this" event.
Register the subscription¶
Add a field to the root Subscription type from a file your plugin requires in an initialiser (see Where the code goes):
| lib/my_plugin/easy_graphql.rb | |
|---|---|
Trigger the subscription¶
Call EasyGraphql::AppSchema.subscriptions.trigger after the change is committed, typically from an after_commit callback or at the end of a mutation:
- The first argument is the subscription field name, the second the arguments the client subscribed with (all must match exactly), the third the object passed to
update; its structure must match the result type. - Always rescue and log. An error raised in
after_commitpropagates into the request that saved the record, so a hiccup in the cable transport must not fail the save. - Never broadcast an empty change. Compute what changed first and return early when nothing did, otherwise every save wakes every subscriber.
Subscribe from the frontend¶
Define the subscription in a graphql/subscriptions/ file and generate its types (see GraphQL services and definitions):
| graphql/subscriptions/myEntityChanged.ts | |
|---|---|
Use the useLiveUpdates composable (app/frontend/src/shared/composables/useLiveUpdates.ts). It turns each message into one debounced refetch and defers the refetch while the tab is hidden:
- Subscribe once per view, from its root component, never inside a component that repeats.
- The user who made the change refetches too. Do not suppress the echo: the server's answer can differ from what the client expects, and a second tab of the same user would never update.
- Ignore out-of-order responses in the store you refresh, for example by applying only the response with the newest request counter.
- Do not reload a field under a user who has its editor open; warn instead.
Test¶
At minimum, assert that the trigger was called: