Creating Custom Exchanges
SwiftGraphQLClient is centered around the idea of exchanges. Exchanges are a natural way to model the flow of continuous data. Multiple exchanges form a pipeline where each exchange only does one job and leaves the rest to other exchanges.
An exchange may either modify an operation (e.g. AuthExchange
, LogExchange
), process an operation (e.g. FetchExchange
, WebSocketExchange
) or both (e.g. CacheExchange
).
Understanding the Client
When you create a client, it connects all exchanges into a single pipeline. The easiest way to imagine the pipeline is as a ladder with requests going down on the right side and coming up on the left side.
We call the right side of the ladder downstream and the left side upstream.
When a new opeartion request is created by the client, the operation starts going down the ladder stopping at each exchange. As mentioned, each exchange may either
- modify the operation and push it further down the stream,
- process the operation and stop its way down,
- modify the operation and push it down as well as start processing it.
Once the exchange processes an operation, it sends it back up the ladder as operation result. The result then again stops at each exchange - in reverse order - and each exchange again may modify, filter or process the operation result. Once it reaches the top of the ladder, the client emits it to the source where it reaches the application.
The Structure of an Exchange
Each exchange has to follow the Exchange
protocol spec.
typealias ExchangeIO = (AnyPublisher<Operation, Never>) -> AnyPublisher<OperationResult, Never>
protocol Exchange {
func register(
client: GraphQLClient,
operations: AnyPublisher<Operation, Never>,
next: @escaping ExchangeIO
) -> AnyPublisher<OperationResult, Never>
}
The only requirement of an exchange is that it implements register
method. register
method lets the exchange hook itself to the stream of operations and call generic methods on client
. It should return the stream of operation results.
Do Nothing Exchange
The simplest exchange is an exchange that does nothing. It forwards the operations to the next exchange and returns the result stream of that exchange.
struct DoNothingExchange: Exchange {
func register(
client: GraphQLClient,
operations: AnyPublisher<Operation, Never>,
next: @escaping ExchangeIO
) -> AnyPublisher<OperationResult, Never> {
next(operations)
}
}
Logging Exchange
A slightly more complex example of an exchange is a logging exchange that logs all operations and results that go up and down the stream.
struct LoggingExchange: Exchange {
func register(
client: GraphQLClient,
operations: AnyPublisher<Operation, Never>,
next: @escaping ExchangeIO
) -> AnyPublisher<OperationResult, Never> {
let downstream = operations
.print()
.eraseToAnyPublisher()
let upstream = next(downstream)
.print()
.eraseToAnyPublisher()
return upstream
}
}
We could easily imagine also modifying the values instead of simply printing them.
Operation Processing Exchange
Lastly, we are going to observe an example of a more complex exchange - an exchange that processes operations. Such an exchange should take care of
- creating new streams,
- merging their results into the result upstream, and
- dismantling each pipeline when the application stops listening to events or the server has stopped sending them.
As a general guideline, your exchange should
- create a shared stream,
- filter the operations it’s going to process and forward the rest downstream,
- create a new result stream for a new operation,
- emit events until the client sends a
teardown
event with the same operation ID, - manage the “dangling” stream appropriately.
You should check out FetchExchange
and WebSocketExchange
source codes to see an example.