Supscriptions
Subscriptions allow clients to receive updates in real time from the server. You can either us Websocket or Server-Sent-Events protocol.
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.
Configuration
First you will need to install the @graphql-yoga/subscription package.
npm i @graphql-yoga/subscriptionThen configure both the PubSubDriver and Subscription driver in config/graphql.ts.
import { , } from '@foadonis/graphql'
export default ({
: ..(),
: ..({
: '/graphql',
}),
})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.
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 { , } from '@foadonis/graphql'
import from '#graphql/recipe'
@()
export default class {
@({
: 'recipe:created',
})
(@Root() : ): {
return
}
}Multiple Topics
The topics option accepts a list of topics allowing you to subscribe to multiple topics.
import { , } from '@foadonis/graphql'
import from '#models/recipe'
import from '#graphql/schemas/recipe_event'
@()
export default class {
@({
: ['recipe:created', 'recipe:deleted'],
})
(@Root() : ): {
return
}
}Dynamic Topics
The topics option also accept a function that receive the context allowing you to dynamically subscribe to topics.
import { , } from '@foadonis/graphql'
import from '#models/recipe'
import from '#graphql/schemas/recipe_event'
@()
export default class {
@({
: ({ }) => .topic,
})
(@Root() : ): {
return
}
}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.
import from '@foadonis/graphql/services/main'
import { , } from '@foadonis/graphql'
import { } from '@graphql-yoga/subscription'
@()
export default class {
@(() => [Recipe], {
: () =>
.([
,
pubsub.subscribe('recipe:created'),
pubsub.subscribe('recipe:updated'),
]),
})
@Query(() => [Recipe])
() {
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 { , } from '@foadonis/graphql'
import from '#models/recipe'
import from '#graphql/schemas/recipe_event'
import from '@foadonis/graphql/services/main'
@()
export default class {
@(() => )
(): {
const = .create()
..('recipe:created', new (, 'created'))
return
}
}Outside a resolver
import from '@adonisjs/core/services/router'
import from '#models/recipe'
import from '#graphql/schemas/recipe_event'
import from '@foadonis/graphql/services/main'
.('/api/recipes', () => {
const = .create()
..('recipe:created', new (, 'created'))
return
})Typing PubSub Events
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.
declare module '@foadonis/graphql/types' {
interface PubSubEvents {
'recipe:created': [Recipe]
'recipe:deleted': [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 instance will notify their subscribers.
npm install @graphql-yoga/redis-event-target ioredisimport { defineConfig, drivers } from '@foadonis/graphql'
export default defineConfig({
pubSub: drivers.pubsub.redis({
publish: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD'),
},
subscribe: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD'),
},
}),
})Context
When doing Subscriptions over Websocket there is no ServerResponse as once the Weboscket handshake has been performed there is no way to write to the response.
We still create an HttpContext containing the initial Request and a fake Response allowing you to access it inside your resolvers. Be aware that you will not be able to use the response.
import { Resolver, Subscription, Ctx } from '@foadonis/graphql'
import { HttpContext } from '@adonisjs/core/http'
export default class RecipesResolver {
@Subscription({
topics: ['recipe:created', 'recipe:deleted'],
})
recipe(@Root() payload: RecipeEvent, @Ctx() ctx: HttpContext): RecipeEvent {
ctx.request.cookie('my-cookie') // ✅ Works
ctx.response.cookie('my-cookie', 'new-value') // ❌ Does not work
return payload
}
}Middlewares & Authentication
As we re-create an HttpContext you can use the same middlewares you would use on the HttpRouter (minus the response capabilities).
import graphql from '@foadonis/graphql/services/main'
graphql.subscription.use([
() => import('@adonisjs/auth/initialize_auth_middleware'),
() => import('#middleware/silent_auth_middleware'),
])