Skip to content

Commit 5a3b62e

Browse files
Merge pull request #8 from GraphQLSwift/feat/pass-init-result
Major cleanup
2 parents 66b2570 + 0c07dd7 commit 5a3b62e

7 files changed

Lines changed: 299 additions & 321 deletions

File tree

README.md

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# GraphQLWS
22

3+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FGraphQLSwift%2FGraphQLWS%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/GraphQLSwift/GraphQLWS)
4+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FGraphQLSwift%2FGraphQLWS%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/GraphQLSwift/GraphQLWS)
5+
36
This implements the [graphql-ws WebSocket subprotocol](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md).
47
It is mainly intended for server support, but there is a basic client implementation included.
58

@@ -14,7 +17,7 @@ Features:
1417
To use this package, include it in your `Package.swift` dependencies:
1518

1619
```swift
17-
.package(url: "git@gitlab.com:PassiveLogic/platform/GraphQLWS.git", from: "<version>"),
20+
.package(url: "https://github.com/GraphQLSwift/GraphQLWS", from: "<version>"),
1821
```
1922

2023
Then create a class to implement the `Messenger` protocol. Here's an example using
@@ -25,33 +28,18 @@ import WebSocketKit
2528
import GraphQLWS
2629

2730
/// Messenger wrapper for WebSockets
28-
class WebSocketMessenger: Messenger {
29-
private weak var websocket: WebSocket?
30-
private var onReceive: (String) -> Void = { _ in }
31-
32-
init(websocket: WebSocket) {
33-
self.websocket = websocket
34-
websocket.onText { _, message in
35-
try await self.onReceive(message)
36-
}
37-
}
31+
struct WebSocketMessenger: Messenger {
32+
let websocket: WebSocket
3833

3934
func send<S>(_ message: S) async throws where S: Collection, S.Element == Character async throws {
40-
guard let websocket = websocket else { return }
4135
try await websocket.send(message)
4236
}
4337

44-
func onReceive(callback: @escaping (String) async throws -> Void) {
45-
self.onReceive = callback
46-
}
47-
4838
func error(_ message: String, code: Int) async throws {
49-
guard let websocket = websocket else { return }
5039
try await websocket.send("\(code): \(message)")
5140
}
5241

5342
func close() async throws {
54-
guard let websocket = websocket else { return }
5543
try await websocket.close()
5644
}
5745
}
@@ -85,6 +73,12 @@ routes.webSocket(
8573
)
8674
}
8775
)
76+
let incoming = AsyncStream<String> { continuation in
77+
websocket.onText { _, message in
78+
continuation.yield(message)
79+
}
80+
}
81+
try await server.listen(to: incoming)
8882
}
8983
)
9084
```
@@ -125,12 +119,3 @@ This example would require `connection_init` message from the client to look lik
125119
```
126120

127121
If the `payload` field is not required on your server, you may make Server's generic declaration optional like `Server<Payload?>`
128-
129-
## Memory Management
130-
131-
Memory ownership among the Server, Client, and Messenger may seem a little backwards. This is because the Swift/Vapor WebSocket
132-
implementation persists WebSocket objects long after their callback and they are expected to retain strong memory references to the
133-
objects required for responses. In order to align cleanly and avoid memory cycles, Server and Client are injected strongly into Messenger
134-
callbacks, and only hold weak references to their Messenger. This means that Messenger objects (or their enclosing WebSocket) must
135-
be persisted to have the connected Server or Client objects function. That is, if a Server's Messenger falls out of scope and deinitializes,
136-
the Server will no longer respond to messages.

Sources/GraphQLWS/Client.swift

Lines changed: 53 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ import Foundation
22
import GraphQL
33

44
/// Client is an open-ended implementation of the client side of the protocol. It parses and adds callbacks for each type of server respose.
5-
public class Client<InitPayload: Equatable & Codable> {
6-
// We keep this weak because we strongly inject this object into the messenger callback
7-
weak var messenger: Messenger?
5+
public actor Client<InitPayload: Equatable & Codable> {
6+
let messenger: Messenger
87

9-
var onConnectionError: (ConnectionErrorResponse, Client) async throws -> Void = { _, _ in }
10-
var onConnectionAck: (ConnectionAckResponse, Client) async throws -> Void = { _, _ in }
11-
var onConnectionKeepAlive: (ConnectionKeepAliveResponse, Client) async throws -> Void = { _, _ in }
12-
var onData: (DataResponse, Client) async throws -> Void = { _, _ in }
13-
var onError: (ErrorResponse, Client) async throws -> Void = { _, _ in }
14-
var onComplete: (CompleteResponse, Client) async throws -> Void = { _, _ in }
15-
var onMessage: (String, Client) async throws -> Void = { _, _ in }
8+
let onConnectionError: (ConnectionErrorResponse, Client) async throws -> Void
9+
let onConnectionAck: (ConnectionAckResponse, Client) async throws -> Void
10+
let onConnectionKeepAlive: (ConnectionKeepAliveResponse, Client) async throws -> Void
11+
let onData: (DataResponse, Client) async throws -> Void
12+
let onError: (ErrorResponse, Client) async throws -> Void
13+
let onComplete: (CompleteResponse, Client) async throws -> Void
1614

1715
let encoder = GraphQLJSONEncoder()
1816
let decoder = JSONDecoder()
@@ -21,120 +19,98 @@ public class Client<InitPayload: Equatable & Codable> {
2119
///
2220
/// - Parameters:
2321
/// - messenger: The messenger to bind the client to.
22+
/// - onConnectionError: The callback run on receipt of a `connection_error` message
23+
/// - onConnectionAck: The callback run on receipt of a `connection_ack` message
24+
/// - onConnectionKeepAlive: The callback run on receipt of a `connection_ka` message
25+
/// - onData: The callback run on receipt of a `data` message
26+
/// - onError: The callback run on receipt of an `error` message
27+
/// - onComplete: The callback run on receipt of a `complete` message
2428
public init(
25-
messenger: Messenger
29+
messenger: Messenger,
30+
onConnectionError: @escaping (ConnectionErrorResponse, Client) async throws -> Void = { _, _ in },
31+
onConnectionAck: @escaping (ConnectionAckResponse, Client) async throws -> Void = { _, _ in },
32+
onConnectionKeepAlive: @escaping (ConnectionKeepAliveResponse, Client) async throws -> Void = { _, _ in },
33+
onData: @escaping (DataResponse, Client) async throws -> Void = { _, _ in },
34+
onError: @escaping (ErrorResponse, Client) async throws -> Void = { _, _ in },
35+
onComplete: @escaping (CompleteResponse, Client) async throws -> Void = { _, _ in }
2636
) {
2737
self.messenger = messenger
28-
messenger.onReceive { message in
29-
try await self.onMessage(message, self)
38+
self.onConnectionError = onConnectionError
39+
self.onConnectionAck = onConnectionAck
40+
self.onConnectionKeepAlive = onConnectionKeepAlive
41+
self.onData = onData
42+
self.onError = onError
43+
self.onComplete = onComplete
44+
}
3045

46+
/// Listen and react to the provided async sequence of server messages. This function will block until the stream is completed.
47+
/// - Parameter incoming: The server message sequence that the client should react to.
48+
public func listen<A: AsyncSequence & Sendable>(to incoming: A) async throws -> Void where A.Element == String {
49+
for try await message in incoming {
3150
// Detect and ignore error responses.
3251
if message.starts(with: "44") {
3352
// TODO: Determine what to do with returned error messages
3453
return
3554
}
3655

3756
guard let json = message.data(using: .utf8) else {
38-
try await self.error(.invalidEncoding())
57+
try await error(.invalidEncoding())
3958
return
4059
}
4160

4261
let response: Response
4362
do {
44-
response = try self.decoder.decode(Response.self, from: json)
63+
response = try decoder.decode(Response.self, from: json)
4564
} catch {
4665
try await self.error(.noType())
4766
return
4867
}
4968

5069
switch response.type {
5170
case .GQL_CONNECTION_ERROR:
52-
guard let connectionErrorResponse = try? self.decoder.decode(ConnectionErrorResponse.self, from: json) else {
53-
try await self.error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR))
71+
guard let connectionErrorResponse = try? decoder.decode(ConnectionErrorResponse.self, from: json) else {
72+
try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR))
5473
return
5574
}
56-
try await self.onConnectionError(connectionErrorResponse, self)
75+
try await onConnectionError(connectionErrorResponse, self)
5776
case .GQL_CONNECTION_ACK:
58-
guard let connectionAckResponse = try? self.decoder.decode(ConnectionAckResponse.self, from: json) else {
59-
try await self.error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR))
77+
guard let connectionAckResponse = try? decoder.decode(ConnectionAckResponse.self, from: json) else {
78+
try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR))
6079
return
6180
}
62-
try await self.onConnectionAck(connectionAckResponse, self)
81+
try await onConnectionAck(connectionAckResponse, self)
6382
case .GQL_CONNECTION_KEEP_ALIVE:
64-
guard let connectionKeepAliveResponse = try? self.decoder.decode(ConnectionKeepAliveResponse.self, from: json) else {
65-
try await self.error(.invalidResponseFormat(messageType: .GQL_CONNECTION_KEEP_ALIVE))
83+
guard let connectionKeepAliveResponse = try? decoder.decode(ConnectionKeepAliveResponse.self, from: json) else {
84+
try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_KEEP_ALIVE))
6685
return
6786
}
68-
try await self.onConnectionKeepAlive(connectionKeepAliveResponse, self)
87+
try await onConnectionKeepAlive(connectionKeepAliveResponse, self)
6988
case .GQL_DATA:
70-
guard let nextResponse = try? self.decoder.decode(DataResponse.self, from: json) else {
71-
try await self.error(.invalidResponseFormat(messageType: .GQL_DATA))
89+
guard let nextResponse = try? decoder.decode(DataResponse.self, from: json) else {
90+
try await error(.invalidResponseFormat(messageType: .GQL_DATA))
7291
return
7392
}
74-
try await self.onData(nextResponse, self)
93+
try await onData(nextResponse, self)
7594
case .GQL_ERROR:
76-
guard let errorResponse = try? self.decoder.decode(ErrorResponse.self, from: json) else {
77-
try await self.error(.invalidResponseFormat(messageType: .GQL_ERROR))
95+
guard let errorResponse = try? decoder.decode(ErrorResponse.self, from: json) else {
96+
try await error(.invalidResponseFormat(messageType: .GQL_ERROR))
7897
return
7998
}
80-
try await self.onError(errorResponse, self)
99+
try await onError(errorResponse, self)
81100
case .GQL_COMPLETE:
82-
guard let completeResponse = try? self.decoder.decode(CompleteResponse.self, from: json) else {
83-
try await self.error(.invalidResponseFormat(messageType: .GQL_COMPLETE))
101+
guard let completeResponse = try? decoder.decode(CompleteResponse.self, from: json) else {
102+
try await error(.invalidResponseFormat(messageType: .GQL_COMPLETE))
84103
return
85104
}
86-
try await self.onComplete(completeResponse, self)
105+
try await onComplete(completeResponse, self)
87106
default:
88-
try await self.error(.invalidType())
107+
try await error(.invalidType())
89108
}
90109
}
91110
}
92111

93-
/// Define the callback run on receipt of a `connection_error` message
94-
/// - Parameter callback: The callback to assign
95-
public func onConnectionError(_ callback: @escaping (ConnectionErrorResponse, Client) async throws -> Void) {
96-
onConnectionError = callback
97-
}
98-
99-
/// Define the callback run on receipt of a `connection_ack` message
100-
/// - Parameter callback: The callback to assign
101-
public func onConnectionAck(_ callback: @escaping (ConnectionAckResponse, Client) async throws -> Void) {
102-
onConnectionAck = callback
103-
}
104-
105-
/// Define the callback run on receipt of a `connection_ka` message
106-
/// - Parameter callback: The callback to assign
107-
public func onConnectionKeepAlive(_ callback: @escaping (ConnectionKeepAliveResponse, Client) async throws -> Void) {
108-
onConnectionKeepAlive = callback
109-
}
110-
111-
/// Define the callback run on receipt of a `data` message
112-
/// - Parameter callback: The callback to assign
113-
public func onData(_ callback: @escaping (DataResponse, Client) async throws -> Void) {
114-
onData = callback
115-
}
116-
117-
/// Define the callback run on receipt of an `error` message
118-
/// - Parameter callback: The callback to assign
119-
public func onError(_ callback: @escaping (ErrorResponse, Client) async throws -> Void) {
120-
onError = callback
121-
}
122-
123-
/// Define the callback run on receipt of any message
124-
/// - Parameter callback: The callback to assign
125-
public func onComplete(_ callback: @escaping (CompleteResponse, Client) async throws -> Void) {
126-
onComplete = callback
127-
}
128-
129-
/// Define the callback run on receipt of a `complete` message
130-
/// - Parameter callback: The callback to assign
131-
public func onMessage(_ callback: @escaping (String, Client) async throws -> Void) {
132-
onMessage = callback
133-
}
134-
135112
/// Send a `connection_init` request through the messenger
136113
public func sendConnectionInit(payload: InitPayload) async throws {
137-
guard let messenger = messenger else { return }
138114
try await messenger.send(
139115
ConnectionInitRequest(
140116
payload: payload
@@ -144,7 +120,6 @@ public class Client<InitPayload: Equatable & Codable> {
144120

145121
/// Send a `start` request through the messenger
146122
public func sendStart(payload: GraphQLRequest, id: String) async throws {
147-
guard let messenger = messenger else { return }
148123
try await messenger.send(
149124
StartRequest(
150125
payload: payload,
@@ -155,7 +130,6 @@ public class Client<InitPayload: Equatable & Codable> {
155130

156131
/// Send a `stop` request through the messenger
157132
public func sendStop(id: String) async throws {
158-
guard let messenger = messenger else { return }
159133
try await messenger.send(
160134
StopRequest(
161135
id: id
@@ -165,15 +139,13 @@ public class Client<InitPayload: Equatable & Codable> {
165139

166140
/// Send a `connection_terminate` request through the messenger
167141
public func sendConnectionTerminate() async throws {
168-
guard let messenger = messenger else { return }
169142
try await messenger.send(
170143
ConnectionTerminateRequest().toJSON(encoder)
171144
)
172145
}
173146

174147
/// Send an error through the messenger and close the connection
175148
private func error(_ error: GraphQLWSError) async throws {
176-
guard let messenger = messenger else { return }
177149
try await messenger.error(error.message, code: error.code.rawValue)
178150
}
179151
}

Sources/GraphQLWS/GraphQLWSError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ struct GraphQLWSError: Error {
8989

9090
/// Error codes for miscellaneous issues
9191
public enum ErrorCode: Int, CustomStringConvertible, Sendable {
92-
// Miscellaneous
92+
/// Miscellaneous
9393
case miscellaneous = 4400
9494

9595
// Internal errors

Sources/GraphQLWS/Messenger.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import Foundation
22

3-
/// Protocol for an object that can send and recieve messages. This allows mocking in tests
4-
public protocol Messenger: AnyObject {
5-
// AnyObject compliance requires that the implementing object is a class and we can reference it weakly
6-
3+
/// Protocol for an object that can send messages.
4+
public protocol Messenger: Sendable {
75
/// Send a message through this messenger
86
/// - Parameter message: The message to send
9-
func send<S>(_ message: S) async throws -> Void where S: Collection, S.Element == Character
10-
11-
/// Set the callback that should be run when a message is recieved
12-
func onReceive(callback: @escaping (String) async throws -> Void)
7+
func send<S: Sendable & Collection>(_ message: S) async throws -> Void where S.Element == Character
138

149
/// Close the messenger
1510
func close() async throws

0 commit comments

Comments
 (0)