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/subscription

Then configure both the PubSubDriver and Subscription driver in config/graphql.ts.

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

app/graphql/resolvers/recipe_resolver.ts
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.

app/graphql/resolvers/recipe_resolver.ts
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.

app/graphql/resolvers/recipe_resolver.ts
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.

app/graphql/resolvers/recipe_resolver.ts
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

app/graphql/resolvers/recipe_resolver.ts
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

start/routes.ts
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.

types/pubsub.ts
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 ioredis
config/graphql.ts
import { 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'),
])

On this page