This implements the graphql-transport-ws WebSocket subprotocol. It is mainly intended for server support, but there is a basic client implementation included.
Features:
- Server implementation that implements defined protocol conversations
- Client and Server types that wrap messengers
- Codable Server and Client message structures
- Custom authentication support
To use this package, include it in your Package.swift
dependencies:
.package(url: "git@gitlab.com:PassiveLogic/platform/GraphQLTransportWS.git", from: "<version>")
Then create a class to implement the Messenger
protocol. Here's an example using
WebSocketKit
:
import WebSocketKit
import GraphQLTransportWS
/// Messenger wrapper for WebSockets
class WebSocketMessenger: Messenger {
private weak var websocket: WebSocket?
private var onReceive: (String) -> Void = { _ in }
init(websocket: WebSocket) {
self.websocket = websocket
websocket.onText { _, message in
self.onReceive(message)
}
}
func send<S>(_ message: S) where S: Collection, S.Element == Character {
guard let websocket = websocket else { return }
websocket.send(message)
}
func onReceive(callback: @escaping (String) -> Void) {
self.onReceive = callback
}
func error(_ message: String, code: Int) {
guard let websocket = websocket else { return }
websocket.send("\(code): \(message)")
}
func close() {
guard let websocket = websocket else { return }
_ = websocket.close()
}
}
Next create a Server
, provide the messenger you just defined, and wrap the API execute
and subscribe
commands:
routes.webSocket(
"graphqlSubscribe",
onUpgrade: { request, websocket in
let messenger = WebSocketMessenger(websocket: websocket)
let server = GraphQLTransportWS.Server<EmptyInitPayload?>(
messenger: messenger,
onExecute: { graphQLRequest in
api.execute(
request: graphQLRequest.query,
context: context,
on: self.eventLoop,
variables: graphQLRequest.variables,
operationName: graphQLRequest.operationName
)
},
onSubscribe: { graphQLRequest in
api.subscribe(
request: graphQLRequest.query,
context: context,
on: self.eventLoop,
variables: graphQLRequest.variables,
operationName: graphQLRequest.operationName
)
}
)
}
)
This package exposes authentication hooks on the connection_init
message. To perform custom authentication,
provide a codable type to the Server init and define an auth
callback on the server. For example:
struct UsernameAndPasswordInitPayload: Equatable & Codable {
let username: String
let password: String
}
let server = GraphQLTransportWS.Server<UsernameAndPasswordInitPayload>(
messenger: messenger,
onExecute: { ... },
onSubscribe: { ... }
)
server.auth { payload in
guard payload.username == "admin" else {
throw Abort(.unauthorized)
}
}
This example would require connection_init
message from the client to look like this:
{
"type": "connection_init",
"payload": {
"username": "admin",
"password": "supersafe"
}
}
If the payload
field is not required on your server, you may make Server's generic declaration optional like Server<Payload?>
Memory ownership among the Server, Client, and Messenger may seem a little backwards. This is because the Swift/Vapor WebSocket implementation persists WebSocket objects long after their callback and they are expected to retain strong memory references to the objects required for responses. In order to align cleanly and avoid memory cycles, Server and Client are injected strongly into Messenger callbacks, and only hold weak references to their Messenger. This means that Messenger objects (or their enclosing WebSocket) must be persisted to have the connected Server or Client objects function. That is, if a Server's Messenger falls out of scope and deinitializes, the Server will no longer respond to messages.