Supscriptions
Subscriptions allow clients to receive updates in real time from the server. It works by exposing a Websocket endpoint allowing real-time communication.
This documentation might use some vocabulary you never had to deal with before:
Subscription:
A subscription is a way to listen to real time events. In our case the clients will subscribe
to events coming from our backend.
Topic:
A topic (also called channel) is an identifier used to subscribe to specific events. For
example clients that will subscribe to the topic recipe:created will only receive the events after
a recipe has been created.
PubSub:
A PubSub system is a way to Publish and Subscribe to different topics. For example you
could use pubsub.subscribe('recipe:created', callback) to subscribe to the recipe:created topic
and pubsub.publish('recipe:created', recipe) to publish a created recipe.
You can find more information about Subscriptions on the Official TypeGraphQL documentation
Configuration
First you will need to install the @graphql-yoga/subscription package.
npm i @graphql-yoga/subscriptionPubSub instance
Let's now create a new PubSub instance that will be used by our GraphQL server to Subscribe and us to Publish.
import { createPubSub } from '@graphql-yoga/subscription'
export default createPubSub()Register it in your GraphQL configuration:
import pubsub from '#graphql/pubsub'
import { defineConfig } from '@foadonis/graphql'
export default defineConfig({
pubSub: pubsub,
})In production, you might have multiple instances of your Adonis Application running behind a Load Balancer. Events published on one instance will not be broadcasted to other instances. Please check Distributed PubSub documentation.
Typing the PubSub
Our PubSub can already be used as it is but to have proper autocompletion and ensure we always forward proper data it is useful to define what are the different topics.
import { createPubSub } from '@graphql-yoga/subscription'
export default createPubSub<{
'recipe:created': [Recipe]
'recipe:deleted': [Recipe]
}>()Creating Subscriptions
Subscription resolvers are similar to queries and mutation resolvers. In this example we will allow clients to receive real-time updates every time a new Recipe is created.
In a Resolver class, create a new method decorated with @Subscription.
Single Topic
import { Resolver, Subscription } from '@foadonis/graphql'
import Recipe from '#graphql/recipe'
@Resolver()
export default class RecipeResolver {
@Subscription({
topics: 'recipe:created',
})
recipeCreated(@Root() payload: Recipe): Recipe {
return payload
}
}Multiple Topics
The topics option accepts a list of topics allowing you to subscribe to multiple topics.
import { Resolver, Subscription } from '@foadonis/graphql'
import Recipe from '#models/recipe'
import RecipeEvent from '#graphql/schemas/recipe_event'
@Resolver()
export default class RecipeResolver {
@Subscription({
topics: ['recipe:created', 'recipe:deleted'],
})
recipe(@Root() payload: RecipeEvent): RecipeEvent {
return payload
}
}Dynamic Topics
The topics option also accept a function that receive the context allowing you to dynamically subscribe to topics.
import { Resolver, Subscription } from '@foadonis/graphql'
import Recipe from '#models/recipe'
import RecipeEvent from '#graphql/schemas/recipe_event'
@Resolver()
export default class RecipeResolver {
@Subscription({
topics: ({ args }) => args.topic,
})
recipe(@Root() payload: RecipeEvent): RecipeEvent {
return payload
}
}Custom Subscription
The @Subscription decorator accepts a subscribe parameter allowing you to subscribe to any events using Async Iterators.
For example a common scenario used for real-time applications is to emit an initial empty event and subscribe to different topics to re-send values to the client.
@Resolver()
export default class RecipeResolver {
@Subscription(() => [Recipe], {
subscribe: () =>
Repeater.merge([
undefined,
pubsub.subscribe('recipe:created'),
pubsub.subscribe('recipe:updated'),
]),
})
@Query(() => [Recipe])
recipes() {
return Recipe.all()
}
}This solution makes it easy to build real-time applications but it comes with a big performance trade-off as recipes will be re-fetched everytime a new one is updated or created.
Triggering Subscription Topics
Now that we have create our subscriptions, we can use the PubSub system to broadcast our events. This will usually be done inside a mutation but you can use it wherever you want inside your application.
Inside a resolver
import { Resolver, Subscription } from '@foadonis/graphql'
import Recipe from '#models/recipe'
import RecipeEvent from '#graphql/schemas/recipe_event'
import pubsub from '#graphql/pubsub'
@Resolver()
export default class RecipeResolver {
@Mutation(() => Recipe)
createRecipe(): RecipeEvent {
const recipe = Recipe.create()
pubsub.publish('recipe:created', new RecipeEvent(recipe, 'created'))
return recipe
}
}Outside a resolver
import Recipe from '#models/recipe'
import RecipeEvent from '#graphql/schemas/recipe_event'
import pubsub from '#graphql/pubsub'
router.post('/api/recipes', () => {
const recipe = Recipe.create()
pubsub.publish('recipe:created', new RecipeEvent(recipe, 'created'))
return recipe
})Distributed PubSub
When running multiple instances of your Adonis Application in a distributed environment you need a way to distribute the published event so every subscription will receive them.
This feature is not officially supported. In a near future you will be able to use your already configured Adonis Redis.
npm install @graphql-yoga/redis-event-target ioredisimport { createPubSub } from 'graphql-yoga'
import { Redis } from 'ioredis'
import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'
const publishClient = new Redis()
const subscribeClient = new Redis()
const eventTarget = createRedisEventTarget({
publishClient,
subscribeClient,
})
const pubSub = createPubSub({ eventTarget })More information on the official Yoga documentation.