Odyssey

Federated subscriptions with TypeScript & Apollo Server

Course overview and setupFederated subscriptionsSetting up subscriptionsSubscriptions over HTTP callbacksPubSub and resolversThe Subscription resolverReturning subscription dataUsing withFilterSynchronizing graph-wide updates
6. The Subscription resolver
2m

Overview

Our mutation is responsible for publishing events; it's our subscription resolver's job to subscribe to them!

In this lesson, we will:

  • Write our subscription resolver and discuss its requirements

The Subscription resolver

In the messages/src/resolvers folder, create a new file called Subscription.ts.

📂 resolvers
┣ 📄 Conversation.ts
┣ 📄 index.ts
┣ 📄 Message.ts
┣ 📄 Mutation.ts
┣ 📄 Subscription.ts
┣ 📄 Query.ts
┗ 📄 User.ts

We'll start by adding some boilerplate. Paste the following code into your file.

src/resolvers/Subscription.ts
import { Resolvers } from "../__generated__/resolvers-types";
export const Subscription: Resolvers = {
Subscription: {
// TODO
},
};

We'll also need to include this resolver in our full resolvers object. Jump over to src/resolvers/index.ts and uncomment a couple of lines involving the Subscription resolver.

src/resolvers/index.ts
import { Query } from "./Query";
import { Mutation } from "./Mutation";
import { Message } from "./Message";
import { Conversation } from "./Conversation";
import { User } from "./User";
import { Subscription } from "./Subscription";
const resolvers = {
...Query,
...Mutation,
...Conversation,
...Message,
...User,
...Subscription,
};
export default resolvers;

Back in Subscription.ts, we'll finish up our resolver object. As you might expect, we'll provide a key that matches our Subscription type's field: messageInConversation.

src/resolvers/Subscription.ts
Subscription: {
messageInConversation: // TODO
}

Unlike our other resolvers, however, we won't define a function as this key's value. Instead, we'll open up another object—and define a subscribe key inside of it. The subscribe key is where we'll put our actual resolver function, which has access to the same resolver arguments we're accustomed to using (parent, args, contextValue, and info.)

src/resolvers/Subscription.ts
// ... other resolvers
Subscription: {
messageInConversation: {
subscribe: () => {},
}
}

Note: Our messageInConversation object can define an additional property: resolve. We can use the resolve function to further drill into the payload that we receive from each emitted event. We won't use resolve in this course.

Great! Now what should this subscribe function return? Consulting our schema, we're expected to return a Message type. However, there's one catch: subscriptions deal with asynchronous data. The most important requirement for the subscribe function is that it must return an AsyncIterator type.

AsyncIterator

AsyncIterator is an interface that allows us to iterate over asynchronous results—exactly the kind of data we'd expect to get from a subscription! We won't have to define a new class to make this work; the PubSub library has us covered with a special method that handles all the details.

Note: Want to learn more about the AsyncIterator interface? Check out the MDN Web Docs.

The pubsub.asyncIterableIterator method accepts an array, where we can specify the events that the subscription should be listening for, and whose results it should iterate over.

Let's set this up! Back in Subscription.ts, we'll start by accessing pubsub inside of the subscribe resolver by destructuring its third positional arguemnt. Then we'll call the asyncIterableIterator method.

src/resolvers/Subscription.ts
messageInConversation: {
subscribe: (_, __, { pubsub }) => {
return pubsub.asyncIterableIterator();
},
}

Inside the asyncIterableIterator method, we'll define the array of events we want to listen for. We have just a single event—the one we published in our mutation—called "NEW_MESSAGE_SENT".

src/resolvers/Subscription.ts
subscribe: (_, __, { pubsub }) => {
return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"])
},
import { Resolvers } from "../__generated__/resolvers-types";
export const Subscription: Resolvers = {
Subscription: {
messageInConversation: {
subscribe: (_, __, { pubsub }) => {
return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"])
}
},
},
};

Great! Our subscribe function is returning a PubSubAsyncIterableIterator type (a type that satisfies the required AsyncIterator interface), and we've configured it for our specific "NEW_MESSAGE_SENT" event. But...how does this subscribe function actually return data—specifically, each new message sent to a conversation?

Publishing an event with payload

Let's take our mutation and subscription resolvers and look at them side-by-side.

Mutation: {
sendMessage: () => {
// ... resolver logic
await pubsub.publish("NEW_MESSAGE_SENT", {}); 1️⃣
// ... return message
},
},
Subscription: {
messageInConversation: {
subscribe: (_, __, { pubsub }) => {
return pubsub.asyncIterableIterator(["NEW_MESSAGE_SENT"])
},
}
}

Here we can see 1) where an event is published, and 2) where the event is being subscribed to.

But is there actually any message data getting passed along with it? In other words—will our subscription operation actually return anything remotely like the Message type in our schema?

Well...not yet. There's an important piece that we're missing here, and that's the event payload. Inside our mutation resolver, we passed an empty object ({}) as the payload to our publish call. So anytime a "NEW_MESSAGE_SENT" event is published, an empty object is all that gets passed along as the actual event data!

What we need to do instead is pass along the message that was submitted as part of the mutation. We'll tackle that in the next lesson.

Practice

Which of the following are differences between subscription resolvers and other resolvers?

Refer to the following schema to complete the code challenge below.

type Subscription {
"Subscribe to changes in a mission status"
newMissionStatus(id: ID!): Mission
}
type Mutation {
"Update the status for a particular mission"
changeMissionStatus(missionId: ID!, status: String!): Mission
}
Code Challenge!

Implement the resolver function for Subscription.newMissionStatus. The resolver listens to the STATUS_UPDATE event.

Key takeaways

  • Subscription resolvers can be defined by providing an object with a subscribe key. The subscribe function has access to all of the usual resolver arguments (parent, args, contextValue, and info).
  • An important requirement for any subscribe function is that it should return an AsyncIterator type (or a type that satisfies the AsyncIterator interface, such as PubSubAsyncIterableIterator.)
  • The PubSub class gives us a method called asyncIterableIterator, which lets us iterate over asynchronous events (such as sending a new message) as they're dispatched in our system.

Up next

Nearly there! Let's wrap up our publish call and pass along the actual data we want our subscription to return.

Previous
Next

Share your questions and comments about this lesson

Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              arguments

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              subscriptions

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              operation

              A single query, mutation, or subscription that clients send to a GraphQL server to request or manipulate data.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              Subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              arguments

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              subscription

              A long-lived, real-time GraphQL operation that enables real-time communication by allowing clients to receive data updates from the server when specific events or changes occur.

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course