Skip to content

Commit 3f125f5

Browse files
feat!: Actor conversion and Swift Testing
Client and Server became actors to ensure sendability, and Messenger was marked sendable
1 parent a726bd3 commit 3f125f5

4 files changed

Lines changed: 158 additions & 208 deletions

File tree

Sources/GraphQLTransportWS/Client.swift

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ 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>: @unchecked Sendable {
5+
public actor Client<InitPayload: Equatable & Codable> {
66
let messenger: Messenger
77

8-
var onConnectionAck: (ConnectionAckResponse, Client) async throws -> Void = { _, _ in }
9-
var onNext: (NextResponse, Client) async throws -> Void = { _, _ in }
10-
var onError: (ErrorResponse, Client) async throws -> Void = { _, _ in }
11-
var onComplete: (CompleteResponse, Client) async throws -> Void = { _, _ in }
12-
var onMessage: (String, Client) async throws -> Void = { _, _ in }
8+
let onConnectionAck: (ConnectionAckResponse, Client) async throws -> Void
9+
let onNext: (NextResponse, Client) async throws -> Void
10+
let onError: (ErrorResponse, Client) async throws -> Void
11+
let onComplete: (CompleteResponse, Client) async throws -> Void
12+
let onMessage: (String, Client) async throws -> Void
1313

1414
let encoder = GraphQLJSONEncoder()
1515
let decoder = JSONDecoder()
@@ -19,9 +19,19 @@ public class Client<InitPayload: Equatable & Codable>: @unchecked Sendable {
1919
/// - Parameters:
2020
/// - messenger: The messenger to bind the client to.
2121
public init(
22-
messenger: Messenger
22+
messenger: Messenger,
23+
onConnectionAck: @escaping (ConnectionAckResponse, Client) async throws -> Void = { _, _ in },
24+
onNext: @escaping (NextResponse, Client) async throws -> Void = { _, _ in },
25+
onError: @escaping (ErrorResponse, Client) async throws -> Void = { _, _ in },
26+
onComplete: @escaping (CompleteResponse, Client) async throws -> Void = { _, _ in },
27+
onMessage: @escaping (String, Client) async throws -> Void = { _, _ in }
2328
) {
2429
self.messenger = messenger
30+
self.onConnectionAck = onConnectionAck
31+
self.onNext = onNext
32+
self.onError = onError
33+
self.onComplete = onComplete
34+
self.onMessage = onMessage
2535
}
2636

2737
/// Listen and react to the provided async sequence of server messages. This function will block until the stream is completed.
@@ -80,36 +90,6 @@ public class Client<InitPayload: Equatable & Codable>: @unchecked Sendable {
8090
}
8191
}
8292

83-
/// Define the callback run on receipt of a `connection_ack` message
84-
/// - Parameter callback: The callback to assign
85-
public func onConnectionAck(_ callback: @escaping (ConnectionAckResponse, Client) async throws -> Void) {
86-
onConnectionAck = callback
87-
}
88-
89-
/// Define the callback run on receipt of a `next` message
90-
/// - Parameter callback: The callback to assign
91-
public func onNext(_ callback: @escaping (NextResponse, Client) async throws -> Void) {
92-
onNext = callback
93-
}
94-
95-
/// Define the callback run on receipt of an `error` message
96-
/// - Parameter callback: The callback to assign
97-
public func onError(_ callback: @escaping (ErrorResponse, Client) async throws -> Void) {
98-
onError = callback
99-
}
100-
101-
/// Define the callback run on receipt of a `complete` message
102-
/// - Parameter callback: The callback to assign
103-
public func onComplete(_ callback: @escaping (CompleteResponse, Client) async throws -> Void) {
104-
onComplete = callback
105-
}
106-
107-
/// Define the callback run on receipt of any message
108-
/// - Parameter callback: The callback to assign
109-
public func onMessage(_ callback: @escaping (String, Client) async throws -> Void) {
110-
onMessage = callback
111-
}
112-
11393
/// Send a `connection_init` request through the messenger
11494
public func sendConnectionInit(payload: InitPayload) async throws {
11595
try await messenger.send(

Sources/GraphQLTransportWS/Messenger.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

3-
/// Protocol for an object that can send messages. This allows mocking in tests
4-
public protocol Messenger {
3+
/// Protocol for an object that can send messages. This allows mocking in tests.
4+
public protocol Messenger: Sendable {
55
/// Send a message through this messenger
66
/// - Parameter message: The message to send
77
func send<S: Sendable>(_ message: S) async throws -> Void where S: Collection, S.Element == Character

Sources/GraphQLTransportWS/Server.swift

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,28 @@ import GraphQL
44
/// Server implements the server-side portion of the protocol, allowing a few callbacks for customization.
55
///
66
/// By default, there are no authorization checks
7-
public class Server<
7+
public actor Server<
88
InitPayload: Equatable & Codable & Sendable,
99
InitPayloadResult: Sendable,
1010
SubscriptionSequenceType: AsyncSequence & Sendable
11-
>: @unchecked Sendable where
11+
> where
1212
SubscriptionSequenceType.Element == GraphQLResult
1313
{
14-
// We keep this weak because we strongly inject this object into the messenger callback
1514
let messenger: Messenger
1615

1716
let onInit: (InitPayload) async throws -> InitPayloadResult
1817
let onExecute: (GraphQLRequest, InitPayloadResult) async throws -> GraphQLResult
1918
let onSubscribe: (GraphQLRequest, InitPayloadResult) async throws -> SubscriptionSequenceType
20-
21-
var onExit: () async throws -> Void = {}
22-
var onMessage: (String) async throws -> Void = { _ in }
23-
var onOperationComplete: (String) async throws -> Void = { _ in }
24-
var onOperationError: (String, [Error]) async throws -> Void = { _, _ in }
25-
26-
var initialized = false
27-
var initResult: InitPayloadResult?
19+
20+
let onMessage: (String) async throws -> Void
21+
let onOperationComplete: (String) async throws -> Void
22+
let onOperationError: (String, [Error]) async throws -> Void
2823

2924
let decoder = JSONDecoder()
3025
let encoder = GraphQLJSONEncoder()
3126

27+
private var initialized = false
28+
private var initResult: InitPayloadResult?
3229
private var subscriptionTasks = [String: Task<Void, any Error>]()
3330

3431
/// Create a new server
@@ -37,16 +34,25 @@ public class Server<
3734
/// - messenger: The messenger to bind the server to.
3835
/// - onExecute: Callback run during `start` resolution for non-streaming queries. Typically this is `API.execute`.
3936
/// - onSubscribe: Callback run during `start` resolution for streaming queries. Typically this is `API.subscribe`.
37+
/// - onMessage: Optional callback run on every message event
38+
/// - onOperationComplete: Optional callback run when an operation completes
39+
/// - onOperationError: Optional callback run when an operation errors
4040
public init(
4141
messenger: Messenger,
4242
onInit: @escaping (InitPayload) async throws -> InitPayloadResult,
4343
onExecute: @escaping (GraphQLRequest, InitPayloadResult) async throws -> GraphQLResult,
44-
onSubscribe: @escaping (GraphQLRequest, InitPayloadResult) async throws -> SubscriptionSequenceType
44+
onSubscribe: @escaping (GraphQLRequest, InitPayloadResult) async throws -> SubscriptionSequenceType,
45+
onMessage: @escaping (String) async throws -> Void = { _ in },
46+
onOperationComplete: @escaping (String) async throws -> Void = { _ in },
47+
onOperationError: @escaping (String, [Error]) async throws -> Void = { _, _ in },
4548
) {
4649
self.messenger = messenger
4750
self.onInit = onInit
4851
self.onExecute = onExecute
4952
self.onSubscribe = onSubscribe
53+
self.onMessage = onMessage
54+
self.onOperationComplete = onOperationComplete
55+
self.onOperationError = onOperationError
5056
}
5157

5258
/// Listen and react to the provided async sequence of client messages. This function will block until the stream is completed.
@@ -104,30 +110,6 @@ public class Server<
104110
subscriptionTasks.values.forEach { $0.cancel() }
105111
}
106112

107-
/// Define the callback run when the communication is shut down, either by the client or server
108-
/// - Parameter callback: The callback to assign
109-
public func onExit(_ callback: @escaping () -> Void) {
110-
onExit = callback
111-
}
112-
113-
/// Define the callback run on receipt of any message
114-
/// - Parameter callback: The callback to assign
115-
public func onMessage(_ callback: @escaping (String) -> Void) {
116-
onMessage = callback
117-
}
118-
119-
/// Define the callback run on the completion a full operation (query/mutation, end of subscription)
120-
/// - Parameter callback: The callback to assign
121-
public func onOperationComplete(_ callback: @escaping (String) -> Void) {
122-
onOperationComplete = callback
123-
}
124-
125-
/// Define the callback to run on error of any full operation (failed query, interrupted subscription)
126-
/// - Parameter callback: The callback to assign
127-
public func onOperationError(_ callback: @escaping (String, [Error]) -> Void) {
128-
onOperationError = callback
129-
}
130-
131113
private func onConnectionInit(_ connectionInitRequest: ConnectionInitRequest<InitPayload>, _: Messenger) async throws {
132114
guard !initialized else {
133115
try await error(.tooManyInitializations())

0 commit comments

Comments
 (0)