Front-End Web & Mobile
Enhancing Live Sports with the new AWS AppSync filtering capabilities
This article was written by Stefano Sandrini, Principal Solutions Architect, AWS
With AWS AppSync you can create serverless GraphQL APIs that simplify application development by providing a single endpoint to securely access data from multiple data sources, and leverage GraphQL subscriptions to implement engaging real-time application experiences by automatically publishing data updates to subscribed API clients via serverless WebSockets connections.
Taking advantage of GraphQL subscriptions to perform real-time operations, AppSync pushes data to clients that choose to listen to specific events from the backend. This means that you can easily and effortlessly make any supported data source in AppSync real-time. Connection management between clients and your API endpoint is handled automatically. Real-time data, connections, scalability, fan-out and broadcasting are all handled by AppSync, allowing you to focus on your business use cases and requirements instead of dealing with the complex infrastructure required to manage WebSockets connections at scale.
When it comes to real-time with GraphQL, subscriptions filtering is an important capability as there are use cases that require restricting or filtering the data specific groups of subscribed clients receive. AWS AppSync recently released new filtering capabilities, enabling developers to build a broader range of real-time experiences in their applications by leveraging new logical operators, server-side filtering, and the ability to trigger subscription invalidations to unsubscribe clients.
filter
argument while invoking the subscription operation. This enables clients to define their own filters dynamically, based on the different use cases depending on the application requirements.
GraphQL subscriptions are particularly important in the Media and Entertainment industry, where companies offer applications that enable their customers to view sports scores as they happen, track live game/match information and statistics, receive fantasy sports updates, and interact with fellow subscribers. Delivering this sort of data in real-time is critical in the media business. Hence, in order to help enable entertainment companies deliver sports information in real-time at high scale, AWS released the Real-Time Live Sports Updates Using AWS AppSync solution.
The solution creates an easily deployable reference architecture implemented with best practices in mind, aiming to address challenges commonly found in the media and entertainment industry specifically related to live sports real-time updates. The reference architecture addresses common use cases in sports such as delivering live game updates in a second screen application, fantasy score updates, additional statistics and information on-demand (e.g. via OTT, over-the-top services).
Data from a data feed provider is ingested into Amazon Kinesis Data Streams, then a Lambda function transforms and enriches the data using configuration information provided by an Amazon DynamoDB table, adapting data records received from a third-party data provider to the expected GraphQL data type format. The Lambda function calls AppSync to invoke a GraphQL mutation that save game events data to DynamoDB tables. Once the mutation is completed, AppSync automatically notifies multiple subscribers in real-time about a new event. The real-time message is delivered via secure WebSockets, as described in the AppSync documentation.
With AppSync GraphQL subscriptions a media company can address real-time use cases and scale automatically to handle peak usage and reach millions of connected clients with real-time notifications. With the launch of enhanced subscriptions filtering capabilities in AppSync, customers using the solution can now add additional features and further enhance the user experience during live games.
How AppSync’s enhanced filtering simplifies and enables new use cases
Let’s use a Fantasy League application to highlight the value of AppSync’s enhanced filtering capabilities. In Fantasy League games, participants assemble an imaginary virtual team of real life players and score points based on the players’ actual statistical performance. For example, in Fantasy Football you can score points when the quarterback of your team scores a touchdown, a wide receiver receive 10+ yards or a running back runs for 10+ yards. Equally, in basketball you may score 3 points any time one of your virtual team players scores a 3 points field goal or 1 point for each rebound. Same for soccer, 3 points for a goal or 1 point for an assist made by one of your players.
A common challenge with the fantasy league scenario is that if you want to provide live fantasy scores updates then you must collect scores and stats in real-time from multiple games at the same time, from each game played by at least one member of your team.
Before the introduction of enhanced filtering capabilities in AWS AppSync, this use case could be implemented by subscribing to all games, using the onCreateGameEvent
subscription defined in the Real-Time Live Sports Updates solution:
The gameId
argument is optional, therefore it is possible for clients to subscribe to all games by removing the argument when performing the subscription operation.
Once subscribed, our clients must parse the payload of all game events received in real-time, discard all events unrelated to our team’s players and aggregate the remaining events stats and scores. This approach may add complexity to the implementation and bandwidth usage inefficiency, an important aspect if clients are running on mobile devices such as tablets or phones.
Thanks to the new enhanced filtering capabilities, a more efficient implementation is now possible. The following table lists the new logical filter operators available with AppSync server-side filtering:
Operator | Description |
---|---|
eq | Equal |
ne | Not equal |
le | Less than or equal |
lt | Less than |
ge | Greater than or equal |
gt | Greater than |
contains | Checks for a subsequence, or value in a set |
notContains | Checks for absence of a subsequence, or absence of a value in a set |
beginsWith | Checks for a prefix |
in | Checks for matching elements in a list |
notIn | Checks for matching elements not in a list |
between | Between two values |
The GameEvent
type defined in the solution reflects a specific sports event and it is implemented with multiple fields:
There are different fields related to the concept of a player as the solution provides a way to evaluate what is the impact of each player in the game event. For example, this design allows you to identify who is the player that scored the goal and what player made the assist. In fantasy league scenarios, this is important because scoring a goal and making an assist differ in terms of points for your team.
To define our filtering option we could leverage the contains
operator to check for matching elements in a list of players, however currently the enhanced filtering feature doesn’t support nested fields. Therefore we can’t use the players: [Player]
field in the GameEvent
type. For the same reason, we can’t inspect all fields of type Player
and look for a specific ID.
As a workaround we can modify slightly the GraphQL schema, create a new top level field as a list of player IDs, and populate the list with references to players that are involved in the game event, using what it is already part of the GameEvent
type. We can use fields such as scorer: Player
, assist: Player
, playerIn: Player
, playerOut: Player
.
The modified GameEvent
type now includes a new playerIds
field, as follows:
As the type was modified we need to reflect the change in the createGameEvent
mutation resolver response template accordingly:
## [Start] Copy result into a new map. **
#set( $returnMap = $ctx.result )
#set ( $returnMap.playerIds = [] )
## Check players and copy IDs in the playerIds array *
#if( $ctx.result.playerIn)
$util.qr($returnMap.playerIds.add($ctx.result.playerIn.id))
#end
#if( $ctx.result.playerOut)
$util.qr($returnMap.playerIds.add($ctx.result.playerOut.id))
#end
#if( $ctx.result.scorer)
$util.qr($returnMap.playerIds.add($ctx.result.scorer.id))
#end
#if( $ctx.result.assist)
$util.qr($returnMap.playerIds.add($ctx.result.assist.id))
#end
$util.toJson($returnMap)
## [End] **
The response template for the resolver populates the playerIds
field by copying into the array the value of the id
from the fields playerIn
, playerOut
, scorer
, and assist
.
Now we move on to define enhanced filters from the client side, as we need to implement these filters dynamically based on the roster of our own fantasy team.
Dynamic client-defined enhanced filters can be configured using a filter
argument in a new subscription we create called onCreateGameEventForMyPlayers
:
A new resolver utility $util.transform.toSubscriptionFilter()
generates dynamic enhanced filters based on the filter definition passed in the filter
argument. We can use this resolver utility in the response mapping template for the onCreateGameEventForMyPlayers
subscription resolver.
We create the subscription resolver and attach it to a Local/None data source, creating what is called a Local Resolver. This type of resolver it’s not attached to an external data source, and it executes in AppSync itself and just forwards the result of the request mapping template to the response mapping template.
We can create the Local/None data source from the Data Sources section in the AppSync console.
Leveraging the new utility $util.transform.toSubscriptionFilter()
, the response mapping template for the new subscription resolver can be configured as follows:
## Response Mapping Template - onCreateGameEventForMyPlayers subscription
$extensions.setSubscriptionFilter($util.transform.toSubscriptionFilter($util.parseJson($ctx.args.filter)))
## In case of subscription with custom response template you must provide a valid payload that respects any mandatory fields defined in the GameEvent type
$util.toJson({"id": "","gameId": "", "createdAt":"2022-04-26T22:11:46.703Z", "updatedAt":"2022-04-26T22:11:46.703Z"} )
Note that we return a JSON object that provides all mandatory fields for the GameEvent
type, as we’re executing a custom response template for a subscription that has GameEvent
as data type.
As described in the documentation for transformation helpers, the input object for the utility method is a Map
with the following key values for a simplified filter definition:
- field_names
- “and”
- “or”
In our fantasy league scenario, we must use an or
filter with a contains
operator for each playerId
in our team.
As an example, let’s assume we are managing a soccer fantasy league and our team is based on players from different clubs playing different games at the same time, such as:
"player": {
"id": "123",
"name": "Thiago Silva"
}
"player": {
"id": "456",
"name": "Neymar"
}
"player": {
"id": "789",
"name": "Lucas Paqueta"
}
In order to listen for events related to our players, we need to use a filter such as:
"or" : [
{
"playerIds" : {
"contains" : "123"
}
},
{
"playerIds" : {
"contains" : "456"
}
},
{
"playerIds" : {
"contains" : "789"
}
}
]
The the filter evaluates to true
if the subscription notification has the playerIds
field with an Array value that contains at least of one of the playerIds
provided.
With this configuration, a client would then execute the following query to subscribe to data changes based on the filter criteria dynamically defined at client side based on the playerIds
of the user’s fantasy team:
The subscription filter is evaluated against the notification payload, based on the mutation response payload. Therefore, it is important to remember that in order to use this approach the createGameEvent
mutation should have the playerIds
within the mutation response fields:
With this new configuration, our clients can be notified only when a new GameEvent
is related to one of our fantasy league player. For instance, clients subscribed to events from a single player player Messi would have data related to Neymar filtered in the AppSync backend:
Subscription invalidation when a sport event ends
During live sports main events, like a motorsports race or the championship game of the year, media and entertainment companies may have millions of connected devices subscribed and listening for real-time event data. To avoid inefficiency and unnecessary costs, it is important to disconnect clients as soon as the event ends when no more real-time data needs to be published to subscribers.
Initially this could be achieved by listening to specific events notified through a subscription, and then implement the unsubscribe logic at client side. This approach can be tricky and may cause problems if, for example, a client misses the notification due to lack of connectivity or due to a bug in the client-side business logic.
AWS AppSync now supports the ability to invalidate subscriptions, allowing to trigger the automatic closure of multiple WebSocket client connections from the service side. Invalidation allows to simplify application development and reduces data sent to clients with improved authorization logic over data.
Subscriptions Invalidation in AppSync is executed in response to a special payload or event defined in a GraphQL mutation. When the mutation is invoked, the invalidation payload defined in the resolver is forwarded to a linked subscription that has a related invalidation filter configured to unsubscribe the connection. If the invalidation payload sent as argument(s) in the mutation match the invalidation filter criteria in the subscription, invalidation takes place and the WebSocket connection is closed. In our use case, we configure the match_ended
type in the GameEvent
as the mutation invalidation payload. If the type
sent in the createGameEvent
mutation request matches the type
defined in the invalidation filter, the WebSocket connection related to the onCreateGameEvent
subscription is invalidated and clients are unsubscribed.
To implement this scenario, we first need to change the response mapping template for the createGameEvent
mutation resolver by adding the following code snippet at the beginning of the template:
#if ($ctx.result.type == "match_ended")
$extensions.invalidateSubscriptions({
"subscriptionField": "onCreateGameEvent",
"payload": {
"gameId": $context.result.gameId
}
})
#end
The extension defines that when the resulting type is match_ended
, the subscription invalidation process is initiated for the linked subscrioption onCreateGameEvent
. The subscription invalidation operation has a specific payload that refers to the gameId
in the GameEvent
, so we can invalidate only subscriptions for the game that is ended.
We attach a new resolver to the OnCreateGameEvent
subscription with the Local/None data source configured earlier. We must change the response mapping template to provide the subscription invalidation filter that describes what are the conditions to be matched to invalidate clients subscriptions, as follows:
#if( $ctx.args.gameId )
$extensions.setSubscriptionInvalidationFilter({
"filterGroup": [
{
"filters" : [
{
"fieldName" : "gameId",
"operator" : "eq",
"value" : $ctx.args.gameId
}
]
}
]
})
#end
## In case of subscription with custom response template you must provide a valid payload that respects any mandatory fields defined in the GameEvent type
$util.toJson({"id": "","gameId": "", "createdAt":"2022-04-26T22:11:46.703Z", "updatedAt":"2022-04-26T22:11:46.703Z"} )
Now, let’s assume we subscribe to the OnCreateGameEvent
subscription related to the gameId
so0102sim:match:01 .
We start receiving game events in real-time for that specific game. If the game ends and we receive the game event match_ended
from the feed, the solution invokes a mutation similar to:
The WebSocket connection is closed and the client avoids keeping an idle connection alive for no reason. You can test this scenario in the AppSync console by invoking the subscription operation in one tab of your web browser.
We invoke the following mutation on another tab in the web browser (note the type
field of the input
with value match_ended
):
After invoking the mutation with
type:“match_ended”
, we receive a
Subscription complete
message as response. The WebSocket connection is closed, the client unsubscribed and does not receive new upcoming messages related to the specific event.
Conclusion
Enhanced filtering in AppSync allows for more flexibility when developing real-time applications such as the ones our media and entertainment customers are building. Using the Real-Time Live Sports Updates Using AWS AppSync solution now in conjunction with enhanced filtering enables advanced use cases and an easier implementation for existing use cases with less business logic code to write on the client side and more efficiency in bandwidth usage, subscriptions connection time, and data transfer.
AppSync helps you improve your real-time user experience with no need to worry about managing WebSockets connections and the infrastructure required to provide real-time data at scale. For more information on enhanced GraphQL subscriptions filtering, refer to the AppSync documentation.