#Subscriptions
Subscriptions are long-lasting GraphQL read operations that can update their result whenever a particular server-side event occurs. Subscriptions are most commonly used tp pushed updated results from the server to subscribing clients.
#Resolving a subscription
Resolvers for Subscription differ from resolvers for fields of other types. Specifically, Subscription resolvers require to return an implementation of EventStream
.
For the subscription to work under Pioneer the resolver function must return an EventStream
of type:
- AsyncEventStream
- Can be from any
AsyncSequence
, a standard protocol in Swift 5.5 for asynchronous, sequential, iterated elements
- Can be from any
- ConcurrentEventStream
- Must be from a
AsyncThrowingStream
- Must be from a
#AsyncEventStream
Swift 5.5 brought in a reactive stream like feature in the form of a protocol named AsyncSequence
.
Pioneer provide an implementation of EventStream
named AsyncEventStream that takes a generic AsyncSequence
. This mean you can create an event stream using this class from any AsyncSequence
.
#Extensions for AsyncSequence
Converting can be done as well with using the extended method for all AsyncSequence
. This method also allow the AsyncEventStream to emit an initial and/or an ending value.
#Termination callback
By default, AsyncEventStream will cancel the task consuming the provided AsyncSequence
when converting to an AsyncStream
of a different type. For something like AsyncStream
, this cancellation will trigger its termination callback so resources can be deallocated and prevent memory leaks of any kind.
However, a custom AsyncSequence
might have a different trigger and approach in termination. Hence, .toEventStream
alllow an explicit termination callback when converting to EventStream
.
In the termination callback, you are provided with an enum that specify the two cases where termination can occur.
Cases where stream is no longer consumed / stopped and termination will require to be triggered:
- Stream ended itself
- Client send a explicit stop request to end the subscription (might be before stream ended)
- Client disconnect and implicitly stop any running subscription
Termination callback can be implicitly inferred built-in AsyncSequence
and ones created by a PubSub.
#AsyncPubSub
Pioneer provide an in memory publish-subscribe (Pub/Sub) model named, AsyncPubSub, to concurrent safely track events and update all active subscribers.
AsyncPubSub conforms to PubSub which enables your server code to both publish events to a particular topic/trigger/string and listen for events associated with a particular topic.
#Publishing an event
You can publish an event using the .publish method:
- The first parameter is the trigger of the event you're publishing to, as a string.
- You don't need to register a trigger name before publishing to it.
- The second parameter is the payload associated with the event.
As an example, let's say our GraphQL API supports a createPost
mutation. A basic resolver for that might look this:
After we successfully persist the new post into the database, we can publish it to the pubsub as an event.
Next, we can listen for this event in our Subscription
resolver.
#Listening for events
An AsyncStream
asynchronously iterate over events, and if that stream comes from a PubSub, it will be associated with a particular trigger and will receive the events published under that trigger.
You can create an AsyncStream
by calling the .asyncStream method and passing in a the event trigger that this stream should listen for and the type.
Which would looke this in a subscription resolver:
#Custom Pub/Sub
As mentioned before, AsyncPubSub is an in memory pub-sub implementation that is limited to a single server instance, which may become an issue on production environments where there are multiple distributed server instances.
In which case, you likely want to either use or implement a custom pub-sub system that is backed by an external datastore.
#PubSub as protocol
Pub/Sub implementation conform to this protocol is enforced to have the same API to AsyncPubSub, which make easy to switch between.
However, it is not necessary to use PubSub for your subscription resolver and to build a custom Pub/Sub implementation.
Pioneer exported the PubSub protocol which allow different implementation with the same API AsyncPubSub notably implementation backed by popular event-publishing systems (i.e. Redis) with similar API which allow user of this library to prototype with the in memory AsyncPubSub and easily migrate to a distributed PubSub implementation without very little changes.
The basic rules to implement A PubSub are as follow:
The method .asyncStream should return an AsyncStream
for a single subscriber where it can be unsubscribed without closing the topic entirely.
- The type of
DataType
should conform toSendable
andDecodabble
to help make sure it is safe to pass around and be able to decoded if necessary (since it is likely to come from a network call). - Recommended to create a new
AsyncStream
on each method call. - If you are having trouble with broadcasting a publisher to multiple consumer/subscriber, recommended taking a look at Broadcast.
The method .publish should publish events to all subscriber that associated with the trigger.
- The
DataType
conform toSendable
andEncodable
to help make sure it is safe to pass around and be able to encoded if necessary (since it is likely to be send on a network call).
The method .close should dispose and shutdown all subscriber that associated with the trigger.
The implementation should be free of data races and be working safely under asynchronous scopes.
If you are having trouble with data-race safe state management, recommended use Swift's Actor.
#Broadcast
Additionally, common client libraries for popular event-publishing systems usually only provide a function that to subscribe to a specific publisher, but
- No option of unsubscribing without closing the publisher entirely
- Only allow 1 subscriber for each publisher / channel
- Usually because subscription is its own new network connection and multiple of those can be resource intensive.
In this case, the actor, Broadcast, is provided which can broadcast any events from a publisher to multiple different downstream where each downstream share the same upstream and can be unsubscribed / disposed (to prevent leaks) without closing the upstream and publisher.
Broadcast provide the methods:
- .downstream to create a new subscriber stream that will receive events broadcasted
- .publish to broadcast the events to all subscriber
- .close to close the broadcast and shutdown all subscriber
Essentially, it will be applied on an event publisher to create multiple downstream(s) and handle distribution of events, where:
- Different consumer can subscribe to the same upstream and all of them get the same messages
- Usually to prevent making multiple subscription might be resource intensive
- Downstream(s) can be disposed, stopped, or cancelled individually to prevent leaks
- Disposed by cancelling
Task
used to consume it
- Disposed by cancelling
- Closing any downstream(s) does not close other downstream(s), broadcast, and the upstream
- Other downstream(s) will continue receiving broadcasted events
- Closing broadcast dispose all downstream(s), but not necessarily the upstream
#Redis Example
As an example, say we want to build a redis backed PubSub.
A package, PioneerRedisPubSub, provide a Redis implemention of PubSub where it has been optimised and tested.
Here we create an example implementation of PubSub using Redis, that utilize Redis channel for Pub/Sub. We also make use of Broadcast to not open multiple connection and use the 1 redis subscription connection for all GraphQL subscription of the same topic.
Now we can have the Resolver to have a property pubsub
of type PubSub instead of AsyncPubSub, while still being able to use AsyncPubSub during development.
So now, if we can use the RedisPubSub
on a production environment.