Table of Contents
Subscription Operations
subscription { user { id status } }
Subscription is one of the three operations that you can run on GraphQL. Subscriptions’ purpose is to fetch an initial installment of data and keep the result up-to-date with your API. Subscriptions are handled by requests for the first fetch and a provider for subsequent updates.
Definition
Subscriptions can be composed of 5 different elements:
type
subscription
[name]
- An optional name
[variables]
- An optional set of variables
[directives]
- An optional set of directives
selection
- One field from the
schema query fields
# type name variables directives selection
subscription FirstSubscription($var: String!) @directive { welcome }
Fields
The top-level fields in the selection of your subscriptions are called entry points
,
and they must exist in the subscription fields of your schema.
Subscription work similarly to queries, but you can only request a
single entry point. Subsequent results will have the same structure.
subscription {
user # entry point GraphQL::AppSchema[:subscription][:user]
{ # one GraphQL::User < GraphQL::Object
id # GraphQL::User[:id]
status # GraphQL::User[:status]
}
}
Here is an example of a response from the above:
{ "data": { "user": { "id": 1, "status": "PENDING" } } }
Read more about fields subscription.
Extra Definitions
Subscription fields accept some additional settings when they are being defined.
scope
A scope allows you to define an internal condition before delivering subsequent results. You can set this up using a named argument or the chaining definition, giving one or more values to it. Symbols and Procs receive special treatment, for example:
:current_user
- A symbol indicates that such value should be taken from the request context;->(subscription) { }
- A proc will be called with the subscription object, and its result will be added to the scope.
You will have full access to the request and operation installing the subscription using a Proc.
# app/graphql/app_schema.rb
field(:user, 'User', scope: :current_user)
field(:user, 'User') { scope :current_user }
field(:user, 'User').scope(System.version, ->(subscription) {
subscription.request.context.current_user
})
subscribed
You can set a callback for when a subscription is successfully installed for that given field. This can be done using the chaining definition or inside the block definition.
# app/graphql/app_schema.rb
field(:user, 'User').subscribed { puts subscription.inspect }
field(:user, 'User') do
subscribed { puts subscription.inspect }
end
Broadcasting
By default, if there are several subscriptions to the same field, with the same document, same scope, and same arguments, only one payload will be generated and then transmitted to every subscriber.
The default_subscription_broadcastable
setting and the fields’ broadcastable option control this behavior,
and if a false
value is encountered, then a request per subscriber will be executed.
In this example, we are subscribing to updates to a user, constantly checking if the current
user is following the other one. We mark the isFollowing
as not broadcastable because
different users can receive different results, which prevents subscriptions from
being broadcasted.
# app/graphql/objects/user.rb
field(:is_following, :boolean, broadcastable: false)
# app/graphql/app_schema.rb
field(:user, 'User', arguments: id_argument)
subscription { user(id: 2) { name isFollowing } }
Triggering
To trigger an update, you need to call one of the following methods from the field. If you are using a standalone definition, you can call using the class instead.
This process will happen asynchronously.
trigger(args: nil, scope: nil, **)
This method will search for all the subscriptions with matching arguments and scope, then trigger an update on all of them. You can pass an array of arguments and an array of scopes to update multiples at once.
field = GraphQL::AppSchema[:subscription][:user]
# A regular trigger
field.trigger
# Will update subscriptions that `user(id: 1)`
field.trigger(args: { id: 1 })
# Will update subscriptions `user(id: 1)` and `user(id: 2)`
field.trigger(args: [{ id: 1 }, { id: 2 }])
# Will update subscriptions of the current user
field.trigger(scope: current_user)
trigger_for(object, and_prepare: true, **)
This method does two things before calling the method above:
- It attempts to extract arguments values from the provided object;
- Set up a prepared data as the given object
unless
and_prepare
is false.
In this example, we can trigger one or several updates fairly easily using this method. Plus, the GraphQL request won’t have to load the users because that will be prepared for it directly:
# app/graphql/app_schema.rb
field(:user, 'User') { argument(:id, null: false) }
# somewhere else
field = GraphQL::AppSchema[:subscription][:user]
field.trigger_for(User.all) # Produces args: [{ id: 1 }, { id: 2 }]
field.trigger_for(User.first) # Produces args: [{ id: 1 }]
# The above is the same as
field.trigger(args: [{ id: 1 }], data_for: {
"subscription.user" => User.first,
})
Read more about prepared data.
The Scope
The scope
parameter has similar behavior to the arguments above. It combines
multiple values to produce all the matching possibilities. One interesting thing about the
scope is that it supports pseud-instances:
field.trigger(scope: current_user)
# Is similar to
field.trigger(scope: { User => 1 })
Read more about it here.
Unsubscribing
You can force subscriptions from being removed by calling unsubscribe
or unsubscribe_from
,
which works similarly to trigger
and trigger_for
, respectively.
Clients will receive one last update informing them that there won’t be any more updates.
Subsequent Results
There are two parts involved in the process of providing the subsequent results.
Store
Stores are the places where all subscriptions are saved, and they are responsible for handling the addition, removal, and search of subscriptions. These subscription objects are like receipts, holding the necessary information for it to be re-evaluated.
During a request, you can access that object from the subscribed
callback
or any other place by calling operation.subscription
. Here is a list of things that are
stored in these objects:
id
/sid
- The unique identifier of the subscription
schema
- The namespace of the schema
context
- The context of the request when the subscription was created
scope
- The scope of the subscription
operation_id
- The id of the operation, which points to a cached document
origin
- The origin of the subscription
field
- The field of the subscription
args
- A hash of the arguments provided to the field
broadcastable
- An indicator of whether the subscription can be broadcasted
created_at
- The timestamp of when the subscription was created
updated_at
- The timestamp of when the subscription was last updated
Important Different stores may manipulate these attributes so that they can be serialized and deserialized.
Available Stores
Provider
Providers are the ones capable of running asynchronously, executing subscriptions, and streaming subsequent results. They are the ones with the “Pub-Sub” architecture.
Providers can share the same store or have one of their own. Regardless, it is important to keep these two parts separated so that you can choose which combination best fits your application.