Subscriptions
A subscription type inherits GraphQL::BaseSubscription. Its fields return a
Channel whose element type is the field's GraphQL type, and every value sent
on the channel becomes one response. GraphQL::Broadcast fans values out to
any number of subscribers, which is the usual way to publish from a mutation:
MESSAGES = GraphQL::Broadcast(Message).new
@[GraphQL::Object]
class Mutation < GraphQL::BaseMutation
@[GraphQL::Field]
def post(text : String) : Message
message = Message.new(text)
MESSAGES.publish(message)
message
end
end
@[GraphQL::Object]
class Subscription < GraphQL::BaseSubscription
@[GraphQL::Field]
def message_added : Channel(Message)
MESSAGES.subscribe
end
end
schema = GraphQL::Schema.new(Query.new, Mutation.new, Subscription.new)
schema.subscribe takes the same arguments as schema.execute and returns a
GraphQL::Subscription that yields response documents. It ends when the
resolver's channel closes. Call close to unsubscribe, which also closes the
resolver's channel:
subscription = schema.subscribe(%(subscription { messageAdded { text } }))
spawn do
subscription.each do |response|
puts response # {"data":{"messageAdded":{"text":"hi"}}}
end
end
A subscription operation must select exactly one root field. A request that
cannot be started, for example because it selects two root fields, yields a
single error response and is already closed. Running a subscription through
schema.execute is answered with an error pointing at schema.subscribe.
Each event is rendered like a query result: field errors, null for failed
fields and locations all work per event, see Errors. The
context passed to schema.subscribe is reused for every event.
Broadcast
GraphQL::Broadcast(T) keeps one buffered channel per subscriber.
subscribe returns a new channel, publish sends a value to every
subscriber, and unsubscribe or closing the channel removes it. Every
subscriber channel buffers 16 values by default (Broadcast(T).new(buffer)
changes that). A subscriber that has not drained its buffer misses the next
value rather than blocking the publisher and every other subscriber.
Over WebSockets
GraphQL::Transport::WebSocket speaks the graphql-transport-ws protocol
used by graphql-ws, Apollo Client, urql and Relay. It is not loaded by
require "graphql". With Kemal:
require "graphql/transport/ws"
ws "/graphql" do |socket, env|
GraphQL::Transport::WebSocket.new(schema, socket) { MyContext.new(env) }
end
Browsers insist that the server echoes the graphql-transport-ws subprotocol
during the handshake, which Kemal's ws helper cannot do. For browser
clients, mount the standard library handler instead and pass the protocol
name so the handshake negotiates it. The subprotocol argument needs Crystal
1.20 or later. With Kemal's path-scoped use, requests that are not upgrades
fall through to the HTTP route on the same path:
subscriptions = HTTP::WebSocketHandler.new([GraphQL::Transport::WebSocket::PROTOCOL]) do |socket, http|
GraphQL::Transport::WebSocket.new(schema, socket) { MyContext.new(http.request) }
end
use "/graphql", subscriptions
The block builds the context for every operation on the connection. Queries
and mutations sent over the socket are executed once and answered with
next and complete. Subscriptions stream until the client sends complete
or disconnects.
The GraphiQL example in the repository wires all of this up with a live subscription in the browser.