9. Synchronizing graph-wide updates
15m

Overview

Now that our is working (and has been filtered), let's broaden our focus. So far, we've been working with just a single : so, do we get any actually benefit from using a federated , rather than a single ? Let's dive in!

In this lesson, we will:

  • Talk about the process in a federated
  • Look at that involve from multiple s
  • "Subscribe" to updates across the by using from a different

Updates from across the graph

Let's review what our 's doing when we send it a . Recall that the router first devises its —the set of instructions it follows to resolve the query. This can involve making multiple smaller requests to multiple different , but so far we see that our router's sending a request to just one: the messages .

So why don't we just send our client directly to the messages ? After all, the looks like just another stop on the way to data.

Well, it's important to remember that when we use the to execute against our federated graph, we get the benefit of our entire API at our fingertips. So we might instead build a to data from across our : we'll subscribe to one single aspect of our schema (each message as it's added to a conversation), but we can bring other data from across our other along for the ride.

A diagram showing the router retrieving subscription data from one subgraph, and additional data from others

Here's one way we might build out our to include from other .

subscription SubscribeToMessagesInConversation(
$listenForMessageInConversationId: ID!
) {
listenForMessageInConversation(id: $listenForMessageInConversationId) {
text
sentTime
sentTo {
id
name
profileDescription
}
sentFrom {
id
name
}
}
}

We've modified our to include the additional Message type sentTo and sentFrom. Both of these return a User type, from which we can select additional sub. But if we jump into our messages , we'll find that most of these (name, profileDescription) are not resolved here; they're actually the responsibility of our accounts . Because the User type is an , it has in different .

If we ran this new , the would have two stops to make for every new event: it would receive the details of the new message submitted, and could provide the values right away for the text and sentTime . But to provide the data for sentFrom and sentTo, it would need to take both user IDs and make an additional request to accounts. Once all the details are collected, the would bundle the data together and return it to the client.

A diagram showing where each field is fetched from, on every new message event

This is pretty cool, because it means that every time our picks up a new message, the makes sure that we request the data for each user involved in the conversation.

But—there's a flipside. Do we want to re-request each user's id, name, and sometimes even profileDescription each time we get a new message? Well... probably not. Those are unlikely to change frequently enough to warrant refreshing the data with each new message event. Instead, we might want to focus on other fields in our that do change more frequently. Let's explore a better use case for our next.

Adding the isOnline field

Rather than filling up our with all sorts of static from across our , we'll keep it focused.

We want this to be responsible for notifying us of each new message as soon as it's sent and saved in the database. Focusing on the "realtime" nature of our messages , we might also be interested to know whether our message recipient is online. This is a status that can change from moment to moment, which makes it a good candidate to include in our .

subscription SubscribeToMessagesInConversation(
$listenForMessageInConversationId: ID!
) {
listenForMessageInConversation(id: $listenForMessageInConversationId) {
text
sentTime
sentTo {
id
isOnline
}
}
}

There's just one problem: our User type doesn't actually have an isOnline that we can use. Let's add one!

Jump into your messages , and open up src/schema.graphql. Let's add this to our User type, with a return type of non-nullable Boolean.

messages/src/schema.graphql
type User @key(fields: "id") {
id: ID!
"The status indicating whether a user is online"
isOnline: Boolean!
}

The big question is: what determines whether someone is online?

Considerations for isOnline status

When deciding whether someone is "online", we'll use two factors: whether they're logged in and whether they have been active within some recent time period.

There are two that can help us here: isLoggedIn and lastActiveTime. Both of these exist on the User type, but they exist in the accounts ! And because the isOnline is relevant to our chat feature, it makes sense to keep it within the messages .

# User definition in MESSAGES
type User @key(fields: "id") {
id: ID!
"The status indicating whether a user is online"
isOnline: Boolean! # ⬅️ How do we determine true/false?
}
# User definition in ACCOUNTS
type User @key(fields: "id") {
id: ID!
# ... other fields
"The last recorded activity timestamp of the user"
lastActiveTime: String
"Whether or not a user is logged in"
isLoggedIn: Boolean!
}

So how exactly can we use lastActiveTime and isLoggedIn (provided by a completely separate ) to determine the value of isOnline? to the rescue!

@requires and @external

Federation are special instructions we can include in our schema to tell the to do something specific. We've already seen one of these directives, @key, which is used to indicate the primary key(s) that we can use when referring to an instance across our .

There are two additional that we'll put to work in our messages : @requires and @external.

Let's scroll to the top of our schema.graphql file and add them to our federation import.

messages/src/schema.graphql
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.8"
import: ["@key", "@requires", "@external"]
)

Let's walk through what both of these can do for us.

@requires

The @requires is used to indicate that the requires one or more values from other before it can return its own data. It tells the that a particular field cannot return a value until it has the values of another field (or fields) first.

Let's take a look at the syntax by applying it to our isOnline .

type User @key(fields: "id") {
id: ID!
isOnline: Boolean! @requires
}

After @requires, we pass a set of parentheses (()) where we include the names of the that are "required". In our case, we want to first know a user's lastActiveTime and isLoggedIn values before we make any determination about whether they're online or not. We'll pass these field names as a single string (separated with a space) to a property called fields.

type User @key(fields: "id") {
id: ID!
isOnline: Boolean! @requires(fields: "lastActiveTime isLoggedIn")
}

That's it for @requires—but we have a few more schema updates to make. Our messages still has no idea what these required are, or where they come from. This is where @external shines!

@external

We use the @external when we need a to reference a it's not actually responsible for resolving. Our current situation is a great example of this: one of our fields requires the values of two other fields before it can return data. But those fields don't exist within the messages ; they're external .

To fix this problem, and keep our from getting confused, we'll update our User definition with both of these required . They should be exactly the same as they're defined in the accounts , with one difference: we'll tack on the @external to both.

type User @key(fields: "id") {
id: ID!
isOnline: Boolean! @requires(fields: "lastActiveTime isLoggedIn")
"The last recorded activity timestamp of the user"
lastActiveTime: String @external
"Whether or not a user is logged in"
isLoggedIn: Boolean! @external
}

Walking through the operation

So how exactly does this change the way the resolves our ? Let's walk through it step-by-step.

Subscription operation
subscription SubscribeToMessagesInConversation(
$listenForMessageInConversationId: ID!
) {
listenForMessageInConversation(id: $listenForMessageInConversationId) {
text
sentTime
sentTo {
id
isOnline
}
}
}

To resolve this , the establishes the with messages via the Subscription.listenForMessageInConversation . With every new message event, it returns the message's text and sentTime .

A diagram showing the query's entrypoint provided by the messages subgraph, along with the first two fields immediately available on the returned message

But when it gets to the sentTo , the sees that there's additional information requested; we want to know whether the recipient of the message is online. In order to fetch this field from messages, the sees that it first needs to go and fetch the lastActiveTime and isLoggedIn from the accounts ! (The isOnline , after all, requires them to do its job!)

A diagram showing how resolving the sentTo user's isOnline field first requires values from accounts

The sentTo returns a User type, and the uses the id (the User type's primary key) to make a request to the accounts for the same user's lastActiveTime and isLoggedIn values.

A diagram showing the User representation being sent to the accounts subgraph to retrieve additional fields

The accounts uses that provided id to look up the corresponding User, and it resolves the values for lastActiveTime and isLoggedIn, returning them to the .

A diagram showing the accounts subgraph using the entity representation to find the right user object and return a new representation

Now the has the required values in hand! It returns to the messages , passing along not just the User type's id , but lastActiveTime and isLoggedIn as well. (This is called the representation, the object that the uses to associate objects between .)

A diagram showing the retrieved values now available to the messages subgraph, and can be used in the isOnline resolver

Now the messages has all the data it needs to resolve isOnline. Inside the isOnline , we can pluck off the lastActiveTime and isLoggedIn and write the logic to determine conclusively whether a user should appear as online or not.

Now that we've seen how we get access to lastActiveTime and isLoggedIn, let's use those values in the isOnline to return true or false!

Updating resolvers

In the resolvers/User.ts file, make some space for a new function called isOnline.

resolvers/User.ts
User: {
__resolveReference: async ({ id, ...attributes }, { dataSources }) => {
const user = await dataSources.db.getUserDetails(id)
return { ...attributes, ...user, id: user.username }
},
isOnline: () => {}
}

We'll start by destructuring the first positional , parent, for those two that we required from the accounts : lastActiveTime and isLoggedIn.

isOnline: ({ lastActiveTime, isLoggedIn }) => {
// TODO
},

Next we'll paste in some logic. This checks whether the user is logged in AND if they've been active in the last five minutes.

const now = Date.now();
const lastActiveDate = new Date(lastActiveTime).getTime();
const difference = now - lastActiveDate;
if (isLoggedIn && difference < 300000) {
return true;
}
return false;

Try it out!

Let's see our in action: this time, with data coming from two parts of our . Open up http://localhost:4000 where our rover dev process should still be running the local .

We'll set up our to the same conversation, and send a message.

Step 1: Start the subscription

Open up a new tab and paste in the following :

subscription SubscribeToMessagesInConversation(
$listenForMessageInConversationId: ID!
) {
listenForMessageInConversation(id: $listenForMessageInConversationId) {
text
sentTime
sentTo {
id
isOnline
}
}
}

And in the Variables tab:

Variables
{
"listenForMessageInConversationId": "wardy-eves-chat"
}

Also, make sure that your Headers tab reflects the following:

Headers
Authorization: Bearer eves

Step 2: Send a message

Next let's send a message to that conversation. You know the drill! Open up a new tab, add the from your Operation Collection, or paste the operation below.

mutation SendMessageToConversationIncludeOnline($message: NewMessageInput!) {
sendMessage(message: $message) {
text
sentTo {
id
name
isOnline
}
}
}

And in the Variables panel:

Variables
{
"message": {
"text": "Hey there, thanks so much for booking!",
"conversationId": "wardy-eves-chat"
}
}

Make sure that your Headers tab includes the values specified previously. Submit the ! We should see the new event, along with the usual response.

Step 3: Change the recipient's online status!

Now let's toggle the online status of our recipient. Open up a new tab; this time we'll "log in" as our recipient, wardy.

Add the following :

Toggle the user's logged in status
mutation ToggleUserLoggedIn {
changeLoggedInStatus {
time
success
message
}
}

And in the Headers tab, make sure that your Authorization header now specifies wardy. (We want to log the recipient of our message in/out!)

Headers
Authorization: Bearer wardy

Step 4: Send another message, toggle status, and repeat!

Return to the tab where you've built the SendMessageToConversationIncludeOnline —and send another message! (Here, you should be authorized as eves.)

This time, you should see in the Subscriptions box that the recipient's isOnline status has changed! Jump back to the tab with ToggleUserLoggedIn and toggle their status once more. When you go back to send another message, you should see that their status has changed again!

http://localhost:4000

A screenshot of Sandbox, the user was toggled online

With every new message we send or receive, we're getting a fresh update of the user's "online" status; all through using schema , and the power of the !

Practice

Use the following code snippets to answer the multiple choice question.

Example reviews subgraph
type Subscription {
listenForNewReviewOnListing(id: ID!): Review
}
type Review {
review: String!
rating: Int!
author: User!
listing: Listing!
}
# Other fields exist in the `listings` subgraph
type Listing @key(fields: "id") {
id: ID!
}
# Other fields exist in the `accounts` subgraph
type User @key(fields: "id") {
id: ID!
}
An example operation
subscription SubscribeToNewReviews($listenForNewReviewOnListingId: ID!) {
listenForNewReviewOnListing(id: $listenForNewReviewOnListingId) {
review
rating
author {
id
name
}
listing {
title
}
}
}
Which of the following describes how the router would handle resolving the SubscribeToNewReviews operation with each new subscription event?

Key takeaways

  • can include not only the single "realtime" that frequently changes, but other fields from across our as well.
  • Each time a new event occurs, the will refetch the data for all included in the , and return it all at once.
  • We can use federation , such as @requires and @external, to indicate that one requires the value (or values) of another field before it can return data.

Journey's end

And with that, you've made it to the end! You've implemented federated from start to finish, moving from development to production without mising a beat. Excellent job!

That's not all though: we've barely scratched the surface of the tooling available to you when you run a through and the . Keep an eye out for the second part in this series, Observability, coming soon on ! Thanks for joining us in this one—we're looking forward to our next journey together.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. 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.